openstudio-standards 0.8.2 → 0.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. checksums.yaml +4 -4
  2. data/data/standards/OpenStudio_Standards-ashrae_90_1-ALL-comstock(space_types).xlsx +0 -0
  3. data/data/standards/openstudio_standards_duplicates_log.csv +7962 -0
  4. data/lib/openstudio-standards/btap/costing/README.md +502 -0
  5. data/lib/openstudio-standards/btap/costing/btap_costing.rb +473 -0
  6. data/lib/openstudio-standards/btap/costing/btap_measure_helper.rb +359 -0
  7. data/lib/openstudio-standards/btap/costing/btap_workflow.rb +117 -0
  8. data/lib/openstudio-standards/btap/costing/common_paths.rb +78 -0
  9. data/lib/openstudio-standards/btap/costing/common_resources/ConstructionProperties.csv +52 -0
  10. data/lib/openstudio-standards/btap/costing/common_resources/Constructions.csv +37 -0
  11. data/lib/openstudio-standards/btap/costing/common_resources/construction_sets.csv +1270 -0
  12. data/lib/openstudio-standards/btap/costing/common_resources/constructions_glazing.csv +61 -0
  13. data/lib/openstudio-standards/btap/costing/common_resources/constructions_opaque.csv +2256 -0
  14. data/lib/openstudio-standards/btap/costing/common_resources/costs.csv +1904 -0
  15. data/lib/openstudio-standards/btap/costing/common_resources/costs_local_factors.csv +2315 -0
  16. data/lib/openstudio-standards/btap/costing/common_resources/hvac_vent_ahu.csv +925 -0
  17. data/lib/openstudio-standards/btap/costing/common_resources/lighting.csv +364 -0
  18. data/lib/openstudio-standards/btap/costing/common_resources/lighting_sets.csv +2667 -0
  19. data/lib/openstudio-standards/btap/costing/common_resources/locations.csv +75 -0
  20. data/lib/openstudio-standards/btap/costing/common_resources/materials_glazing.csv +35 -0
  21. data/lib/openstudio-standards/btap/costing/common_resources/materials_hvac.csv +1699 -0
  22. data/lib/openstudio-standards/btap/costing/common_resources/materials_lighting.csv +267 -0
  23. data/lib/openstudio-standards/btap/costing/common_resources/materials_opaque.csv +164 -0
  24. data/lib/openstudio-standards/btap/costing/copy_test_results_files_to_expected_results.rb +11 -0
  25. data/lib/openstudio-standards/btap/costing/cost_building_from_file.rb +136 -0
  26. data/lib/openstudio-standards/btap/costing/costing_database_wrapper.rb +177 -0
  27. data/lib/openstudio-standards/btap/costing/daylighting_sensor_control_costing.rb +353 -0
  28. data/lib/openstudio-standards/btap/costing/dcv_costing.rb +314 -0
  29. data/lib/openstudio-standards/btap/costing/dummy.epw +8768 -0
  30. data/lib/openstudio-standards/btap/costing/dummy.osm +5320 -0
  31. data/lib/openstudio-standards/btap/costing/envelope_costing.rb +284 -0
  32. data/lib/openstudio-standards/btap/costing/heating_cooling_costing.rb +2584 -0
  33. data/lib/openstudio-standards/btap/costing/led_lighting_costing.rb +155 -0
  34. data/lib/openstudio-standards/btap/costing/lighting_costing.rb +209 -0
  35. data/lib/openstudio-standards/btap/costing/mech_sizing.json +502 -0
  36. data/lib/openstudio-standards/btap/costing/neb_end_use_prices.csv +42 -0
  37. data/lib/openstudio-standards/btap/costing/necb_2011_spacetype_info.csv +225 -0
  38. data/lib/openstudio-standards/btap/costing/necb_reference_runs.csv +28705 -0
  39. data/lib/openstudio-standards/btap/costing/nv_costing.rb +547 -0
  40. data/lib/openstudio-standards/btap/costing/parallel_tests.rb +92 -0
  41. data/lib/openstudio-standards/btap/costing/pv_ground_costing.rb +687 -0
  42. data/lib/openstudio-standards/btap/costing/shw_costing.rb +705 -0
  43. data/lib/openstudio-standards/btap/costing/test_list.txt +17 -0
  44. data/lib/openstudio-standards/btap/costing/test_run_all_test_locally.rb +26 -0
  45. data/lib/openstudio-standards/btap/costing/test_run_costing_tests.rb +80 -0
  46. data/lib/openstudio-standards/btap/costing/ventilation_costing.rb +2616 -0
  47. data/lib/openstudio-standards/constructions/modify.rb +2 -1
  48. data/lib/openstudio-standards/refrigeration/create_case.rb +58 -21
  49. data/lib/openstudio-standards/refrigeration/create_typical_refrigeration.rb +4 -2
  50. data/lib/openstudio-standards/refrigeration/create_walkin.rb +57 -22
  51. data/lib/openstudio-standards/refrigeration/data/refrigerated_cases.csv +31 -31
  52. data/lib/openstudio-standards/refrigeration/data/refrigerated_walkins.csv +76 -76
  53. data/lib/openstudio-standards/service_water_heating/create_typical.rb +10 -10
  54. data/lib/openstudio-standards/service_water_heating/create_water_heater.rb +10 -0
  55. data/lib/openstudio-standards/service_water_heating/create_water_heating_loop.rb +16 -3
  56. data/lib/openstudio-standards/service_water_heating/data/convert_data.rb +9 -9
  57. data/lib/openstudio-standards/service_water_heating/data/typical_water_use_equipment.csv +49 -49
  58. data/lib/openstudio-standards/service_water_heating/data/typical_water_use_equipment.json +159 -159
  59. data/lib/openstudio-standards/standards/Standards.CoilCoolingDXMultiSpeed.rb +7 -18
  60. data/lib/openstudio-standards/standards/Standards.CoilCoolingDXSingleSpeed.rb +10 -20
  61. data/lib/openstudio-standards/standards/Standards.CoilCoolingDXTwoSpeed.rb +6 -15
  62. data/lib/openstudio-standards/standards/Standards.CoilCoolingWaterToAirHeatPumpEquationFit.rb +5 -6
  63. data/lib/openstudio-standards/standards/Standards.CoilDX.rb +93 -43
  64. data/lib/openstudio-standards/standards/Standards.CoilHeatingDXMultiSpeed.rb +5 -5
  65. data/lib/openstudio-standards/standards/Standards.CoilHeatingDXSingleSpeed.rb +135 -37
  66. data/lib/openstudio-standards/standards/Standards.CoilHeatingWaterToAirHeatPumpEquationFit.rb +2 -2
  67. data/lib/openstudio-standards/standards/Standards.Model.rb +48 -13
  68. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2004/data/ashrae_90_1_2004.computer_room_acs.json +302 -140
  69. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2004/data/ashrae_90_1_2004.heat_pumps.json +648 -326
  70. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2004/data/ashrae_90_1_2004.heat_pumps_heating.json +371 -90
  71. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2004/data/ashrae_90_1_2004.water_heaters.json +66 -22
  72. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2007/data/ashrae_90_1_2007.computer_room_acs.json +302 -140
  73. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2007/data/ashrae_90_1_2007.heat_pumps.json +1012 -296
  74. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2007/data/ashrae_90_1_2007.heat_pumps_heating.json +443 -79
  75. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2007/data/ashrae_90_1_2007.water_heaters.json +66 -22
  76. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2010/data/ashrae_90_1_2010.computer_room_acs.json +302 -140
  77. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2010/data/ashrae_90_1_2010.heat_pumps.json +672 -306
  78. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2010/data/ashrae_90_1_2010.heat_pumps_heating.json +354 -74
  79. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2010/data/ashrae_90_1_2010.water_heaters.json +72 -24
  80. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2013/data/ashrae_90_1_2013.computer_room_acs.json +302 -140
  81. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2013/data/ashrae_90_1_2013.energy_recovery.json +8 -8
  82. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2013/data/ashrae_90_1_2013.heat_pumps.json +930 -604
  83. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2013/data/ashrae_90_1_2013.heat_pumps_heating.json +415 -111
  84. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2013/data/ashrae_90_1_2013.water_heaters.json +72 -24
  85. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2016/data/ashrae_90_1_2016.computer_room_acs.json +602 -140
  86. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2016/data/ashrae_90_1_2016.heat_pumps.json +1005 -333
  87. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2016/data/ashrae_90_1_2016.heat_pumps_heating.json +642 -88
  88. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2016/data/ashrae_90_1_2016.water_heaters.json +78 -26
  89. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2019/data/ashrae_90_1_2019.computer_room_acs.json +722 -140
  90. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2019/data/ashrae_90_1_2019.heat_pumps.json +1741 -426
  91. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2019/data/ashrae_90_1_2019.heat_pumps_heating.json +1108 -111
  92. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2019/data/ashrae_90_1_2019.water_heaters.json +186 -62
  93. data/lib/openstudio-standards/standards/ashrae_90_1/data/ashrae_90_1.schedules.json +62 -232
  94. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.CoilCoolingDXSingleSpeed.rb +2 -3
  95. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.CoilCoolingDXTwoSpeed.rb +1 -1
  96. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.CoilDX.rb +7 -18
  97. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.CoilHeatingDXSingleSpeed.rb +9 -7
  98. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.CoilHeatingGas.rb +1 -1
  99. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.Model.rb +2 -2
  100. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm_2019/data/ashrae_90_1_prm_2019.heat_pumps.json +154 -69
  101. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm_2019/data/ashrae_90_1_prm_2019.heat_pumps_heating.json +72 -72
  102. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm_2019/data/ashrae_90_1_prm_2019.water_heaters.json +382 -295
  103. data/lib/openstudio-standards/standards/ashrae_90_1_prm/userdata_csv/ashrae_90_1_prm.UserData.rb +6 -1
  104. data/lib/openstudio-standards/standards/deer/data/deer.schedules.json +62 -232
  105. data/lib/openstudio-standards/standards/necb/BTAPPRE1980/btap_pre1980.rb +2 -27
  106. data/lib/openstudio-standards/standards/necb/BTAPPRE1980/data/heat_pumps.json +16 -0
  107. data/lib/openstudio-standards/standards/necb/BTAPPRE1980/data/heat_pumps_heating.json +6 -0
  108. data/lib/openstudio-standards/standards/necb/BTAPPRE1980/hvac_system_3_and_8_single_speed.rb +68 -27
  109. data/lib/openstudio-standards/standards/necb/BTAPPRE1980/hvac_system_4.rb +64 -25
  110. data/lib/openstudio-standards/standards/necb/BTAPPRE1980/hvac_system_6.rb +9 -14
  111. data/lib/openstudio-standards/standards/necb/ECMS/hvac_systems.rb +46 -20
  112. data/lib/openstudio-standards/standards/necb/NECB2011/autozone.rb +635 -248
  113. data/lib/openstudio-standards/standards/necb/NECB2011/data/constants.json +43 -7
  114. data/lib/openstudio-standards/standards/necb/NECB2011/data/fuel_type_sets.json +7 -1
  115. data/lib/openstudio-standards/standards/necb/NECB2011/data/geometry/HighriseApartmentMult.osm +14272 -0
  116. data/lib/openstudio-standards/standards/necb/NECB2011/data/heat_pumps.json +16 -0
  117. data/lib/openstudio-standards/standards/necb/NECB2011/data/heat_pumps_heating.json +6 -0
  118. data/lib/openstudio-standards/standards/necb/NECB2011/data/necb_2015_table_c1.json +1 -1
  119. data/lib/openstudio-standards/standards/necb/NECB2011/data/space_types.json +437 -437
  120. data/lib/openstudio-standards/standards/necb/NECB2011/data/systems.json +516 -0
  121. data/lib/openstudio-standards/standards/necb/NECB2011/data/systems_including_sys5.json +588 -0
  122. data/lib/openstudio-standards/standards/necb/NECB2011/hvac_namer.rb +489 -0
  123. data/lib/openstudio-standards/standards/necb/NECB2011/hvac_system_1_single_speed.rb +16 -6
  124. data/lib/openstudio-standards/standards/necb/NECB2011/hvac_system_2_and_5.rb +48 -5
  125. data/lib/openstudio-standards/standards/necb/NECB2011/hvac_system_3_and_8_multi_speed.rb +2 -2
  126. data/lib/openstudio-standards/standards/necb/NECB2011/hvac_system_3_and_8_single_speed.rb +35 -27
  127. data/lib/openstudio-standards/standards/necb/NECB2011/hvac_system_4.rb +34 -23
  128. data/lib/openstudio-standards/standards/necb/NECB2011/hvac_system_6.rb +8 -6
  129. data/lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb +43 -14
  130. data/lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb +214 -25
  131. data/lib/openstudio-standards/standards/necb/NECB2011/system_fuels.rb +61 -1
  132. data/lib/openstudio-standards/standards/necb/NECB2015/data/heat_pumps.json +16 -0
  133. data/lib/openstudio-standards/standards/necb/NECB2015/data/heat_pumps_heating.json +8 -0
  134. data/lib/openstudio-standards/standards/necb/NECB2015/data/space_types.json +636 -636
  135. data/lib/openstudio-standards/standards/necb/NECB2015/data/unitary_acs.json +38 -38
  136. data/lib/openstudio-standards/standards/necb/NECB2015/hvac_systems.rb +15 -6
  137. data/lib/openstudio-standards/standards/necb/NECB2017/data/space_types.json +636 -636
  138. data/lib/openstudio-standards/standards/necb/NECB2020/data/chillers.json +71 -71
  139. data/lib/openstudio-standards/standards/necb/NECB2020/data/heat_pumps.json +20 -0
  140. data/lib/openstudio-standards/standards/necb/NECB2020/data/heat_pumps_heating.json +10 -0
  141. data/lib/openstudio-standards/standards/necb/README.md +343 -0
  142. data/lib/openstudio-standards/standards/necb/common/btap_data.rb +190 -28
  143. data/lib/openstudio-standards/standards/necb/common/btap_datapoint.rb +14 -5
  144. data/lib/openstudio-standards/standards/necb/common/eccc_electric_grid_intensity_20250311.csv +14 -0
  145. data/lib/openstudio-standards/standards/necb/common/nir_gas_grid_intensity_20250311.csv +14 -0
  146. data/lib/openstudio-standards/standards/necb/common/system_types.yaml +0 -0
  147. data/lib/openstudio-standards/utilities/logging.rb +18 -14
  148. data/lib/openstudio-standards/utilities/simulation.rb +3 -2
  149. data/lib/openstudio-standards/version.rb +1 -1
  150. data/lib/openstudio-standards/weather/modify.rb +2 -2
  151. data/lib/openstudio-standards.rb +12 -0
  152. metadata +56 -3
@@ -0,0 +1,2616 @@
1
+ class BTAPCosting
2
+ def ventilation_costing(model, prototype_creator, template_type, mech_room, cond_spaces)
3
+ # Set up reporting hash
4
+ @costing_report['ventilation'] = {system_1: [], system_2: [], system_3: [], system_4: [], system_5: [], system_6: [], system_7: [], mech_to_roof: [], trunk_duct: [], floor_trunk_ducts: [], tz_distribution: [], hrv_return_ducting: [], natural_ventilation: [], demand_controlled_ventilation: []}
5
+ # Get mechanical sizing for costing information from mech_sizing.json
6
+ mech_sizing_info = read_mech_sizing()
7
+ # Find the mechanical room in the model and conditioned spaces - moved to btap_costing.rb
8
+ # mech_room, cond_spaces = prototype_creator.find_mech_room(model)
9
+ # Find the center of the highest roof in the model (this will be surrounded by roof top mechancial equipment and is where utility lines will be sent)
10
+ roof_cent = prototype_creator.find_highest_roof_centre(model)
11
+ # Find the lowest space in the building (trunk duct runs from here to the highest space).
12
+ min_space = get_lowest_space(spaces: cond_spaces)
13
+ vent_cost = 0
14
+ # Start ventilation costing
15
+ vent_cost += ahu_costing(model: model, prototype_creator: prototype_creator, template_type: template_type, mech_room: mech_room, roof_cent: roof_cent, mech_sizing_info: mech_sizing_info, min_space: min_space)
16
+ # natural ventilation costing
17
+ nv_total_cost = cost_audit_nv(model: model, prototype_creator: prototype_creator)
18
+ # demand-controlled ventilation costing
19
+ dcv_cost_total = cost_audit_dcv(model: model, prototype_creator: prototype_creator)
20
+ # total ventilation cost
21
+ vent_cost += nv_total_cost + dcv_cost_total
22
+ return vent_cost
23
+ end
24
+
25
+ def ahu_costing(model:, prototype_creator:, template_type:, mech_room:, roof_cent:, mech_sizing_info:, min_space:)
26
+ ahu_cost = 0
27
+ hrv_total_cost = 0
28
+ heat_type = {
29
+ 'HP' => 0,
30
+ 'elec' => 0,
31
+ 'Gas' => 0,
32
+ 'HW' => 0,
33
+ }
34
+ cool_type = {
35
+ 'DX' => 0,
36
+ 'CHW' => 0,
37
+ }
38
+
39
+ rt_unit_num = 0
40
+ total_vent_flow_m3_per_s = 0
41
+ sys_1_4 = true
42
+ hvac_floors = []
43
+ # Go through each air loop in the model and cost it
44
+ model.getAirLoopHVACs.sort.each do |airloop|
45
+ @airloop_info = nil
46
+ airloop_name = airloop.nameString
47
+ # Look for the system type from the name of the air loop
48
+ sys_name_loc = airloop_name.to_s.upcase.index("SYS_")
49
+ if sys_name_loc.nil?
50
+ puts "The name of airloop #{airloop_name} does not start with a valid NECB system type described as \"Sys_\" and then an NECB system number."
51
+ puts "Please rename the airloop appropriately or do not cost the ventilation system until ventilation costing can handle non-NECB ventilation systems."
52
+ next
53
+ else
54
+ sys_type = airloop_name[(sys_name_loc+4)].to_i
55
+ sys_type_real = sys_type
56
+ # For costing, treat system types 1 and 4 the same (treat both as system 1)
57
+ sys_type = 1 if sys_type == 4
58
+ next if sys_type == 2
59
+ end
60
+ ahu_tags = [
61
+ "ventilation",
62
+ airloop_name,
63
+ "system #{sys_type_real}"
64
+ ]
65
+ rt_unit_num += 1
66
+
67
+ @airloop_info = {sys_type: sys_type}
68
+ @airloop_info[:name] = airloop_name
69
+
70
+ # Get the air loop supply airflow rate (used for sizing the ahu for costing)
71
+ if airloop.isDesignSupplyAirFlowRateAutosized
72
+ airloop_flow_m3_per_s = airloop.autosizedDesignSupplyAirFlowRate.to_f
73
+ else
74
+ airloop_flow_m3_per_s = airloop.designSupplyAirFlowRate.to_f
75
+ end
76
+ airloop_flow_cfm = (OpenStudio.convert(airloop_flow_m3_per_s, 'm^3/s', 'cfm').get)
77
+ airloop_flow_lps = (OpenStudio.convert(airloop_flow_m3_per_s, 'm^3/s', 'L/s').get)
78
+ total_vent_flow_m3_per_s += airloop_flow_m3_per_s
79
+ # Set up hash to record heating and cooling capacities. If more than one heating or cooling source is present this will be used to determine which is predominant one since ahu costing is done based on one heating fuel and cooling type
80
+ heat_cap = {
81
+ 'HP' => 0,
82
+ 'elec' => 0,
83
+ 'Gas' => 0,
84
+ 'HW' => 0,
85
+ 'CCASHP' => 0
86
+ }
87
+ cool_cap = {
88
+ 'DX' => 0,
89
+ 'CHW' => 0,
90
+ }
91
+ @airloop_info[:airloop_flow_m3_per_s] = airloop_flow_m3_per_s.round(3)
92
+ total_heat_cool_cost = 0
93
+ airloop_equipment = []
94
+ #@airloop_info[:equipment_info] = []
95
+ # Find HRVs in the air loop so they can be costed if present
96
+ hrv_info = get_hrv_info(airloop: airloop, model: model)
97
+ # Sort through all of the supply components in the air loop and collect heating and cooling equipment
98
+ airloop.supplyComponents.sort.each do |supplycomp|
99
+ # Get the OS object type of the supply component
100
+ obj_type = supplycomp.iddObjectType.valueName.to_s
101
+ mech_capacity = 0
102
+ heating_fuel = 'none'
103
+ cooling_type = 'none'
104
+ adv_dx_clg_eqpt = false
105
+ cat_search = nil
106
+ # Based on the object type determine how to handle it.
107
+ case obj_type
108
+ # Determine what to do (if anything) with a piece of air loop heating/cooling equipment. Note the comment for the first type applies to the rest.
109
+ when /OS_Coil_Heating_DX_VariableSpeed/
110
+ # Get the object and make sure it is cast correctly
111
+ suppcomp = supplycomp.to_CoilHeatingDXVariableSpeed.get
112
+ # Determine the size of the object if either autosized or manualy sized
113
+ if suppcomp.isRatedHeatingCapacityAtSelectedNominalSpeedLevelAutosized
114
+ mech_capacity = suppcomp.autosizedRatedHeatingCapacityAtSelectedNominalSpeedLevel.to_f/1000.0
115
+ else
116
+ mech_capacity = suppcomp.ratedHeatingCapacityAtSelectedNominalSpeedLevel.to_f/1000.0
117
+ end
118
+ # Determine from the name if it is a CCASHP
119
+ if suppcomp.name.to_s.upcase.include?("CCASHP")
120
+ # Set the heating equipment type (used to determine how to cost the equipment)
121
+ heating_fuel = 'CCASHP'
122
+ # Set the term used to search the 'hvac_costing' sheet in the costing spreadsheet to get costing information
123
+ cat_search = 'coils'
124
+ # Set the heating capacity (used to determine the predominant heating type for the air loop)
125
+ heat_cap['CCASHP'] += mech_capacity
126
+ else
127
+ heating_fuel = 'HP'
128
+ cat_search = 'ashp'
129
+ heat_cap['HP'] += mech_capacity
130
+ end
131
+ when /OS_Coil_Heating_DX_SingleSpeed/
132
+ suppcomp = supplycomp.to_CoilHeatingDXSingleSpeed.get
133
+ if suppcomp.isRatedTotalHeatingCapacityAutosized
134
+ mech_capacity = suppcomp.autosizedRatedTotalHeatingCapacity.to_f/1000.0
135
+ else
136
+ mech_capacity = suppcomp.ratedTotalHeatingCapacity.to_f/1000.0
137
+ end
138
+ if suppcomp.name.to_s.upcase.include?("CCASHP")
139
+ heating_fuel = 'CCASHP'
140
+ # There is a separate method which costs additional CCASHP cost information. The 'coils' category is only
141
+ # one of the pieces of equipment that goes into CCASHP costing.
142
+ cat_search = 'coils'
143
+ heat_cap['CCASHP'] += mech_capacity
144
+ else
145
+ heating_fuel = 'HP'
146
+ cat_search = 'ashp'
147
+ heat_cap['HP'] += mech_capacity
148
+ end
149
+ when 'OS_Coil_Heating_Electric'
150
+ heating_fuel = 'elec'
151
+ suppcomp = supplycomp.to_CoilHeatingElectric.get
152
+ if suppcomp.isNominalCapacityAutosized
153
+ mech_capacity = suppcomp.autosizedNominalCapacity.to_f/1000.0
154
+ else
155
+ mech_capacity = suppcomp.nominalCapacity.to_f/1000.0
156
+ end
157
+ cat_search = 'elecheat'
158
+ heat_cap['elec'] += mech_capacity
159
+ when /OS_Coil_Heating_Gas/
160
+ heating_fuel = 'Gas'
161
+ suppcomp = supplycomp.to_CoilHeatingGas.get
162
+ if suppcomp.isNominalCapacityAutosized
163
+ mech_capacity = suppcomp.autosizedNominalCapacity.to_f/1000.0
164
+ else
165
+ mech_capacity = suppcomp.nominalCapacity.to_f/1000.0
166
+ end
167
+ cat_search = 'FurnaceGas'
168
+ heat_cap['Gas'] += mech_capacity
169
+ when /OS_Coil_Heating_Water/
170
+ heating_fuel = 'HW'
171
+ suppcomp = supplycomp.to_CoilHeatingWater.get
172
+ if suppcomp.isRatedCapacityAutosized
173
+ mech_capacity = suppcomp.autosizedRatedCapacity.to_f/1000.0
174
+ else
175
+ suppcomp.ratedCapacity.to_f/1000.0
176
+ end
177
+ cat_search = 'coils'
178
+ heat_cap['HW'] += mech_capacity
179
+ when /OS_Coil_Cooling_DX_SingleSpeed/
180
+ suppcomp = supplycomp.to_CoilCoolingDXSingleSpeed.get
181
+ if suppcomp.isRatedTotalCoolingCapacityAutosized
182
+ mech_capacity = suppcomp.autosizedRatedTotalCoolingCapacity.to_f/1000.0
183
+ else
184
+ mech_capacity = suppcomp.ratedTotalCoolingCapacity.to_f/1000.0
185
+ end
186
+ if suppcomp.name.to_s.upcase.include?('DX-ADV')
187
+ cooling_type = 'DX-adv'
188
+ cat_search = 'coils'
189
+ cool_cap['DX'] += mech_capacity
190
+ else
191
+ cooling_type = 'DX'
192
+ cat_search = 'coils'
193
+ cool_cap['DX'] += mech_capacity
194
+ end
195
+ when /OS_Coil_Cooling_DX_VariableSpeed/
196
+ suppcomp = supplycomp.to_CoilCoolingDXVariableSpeed.get
197
+ if suppcomp.isGrossRatedTotalCoolingCapacityAtSelectedNominalSpeedLevelAutosized
198
+ mech_capacity = suppcomp.autosizedGrossRatedTotalCoolingCapacityAtSelectedNominalSpeedLevel.to_f/1000.0
199
+ else
200
+ mech_capacity = suppcomp.grossRatedTotalCoolingCapacityAtSelectedNominalSpeedLevel.to_f/1000.0
201
+ end
202
+ if suppcomp.name.to_s.upcase.include?('DX-ADV')
203
+ cooling_type = 'DX-adv'
204
+ cat_search = 'coils'
205
+ cool_cap['DX'] += mech_capacity
206
+ else
207
+ cooling_type = 'DX'
208
+ cat_search = 'coils'
209
+ cool_cap['DX'] += mech_capacity
210
+ end
211
+ when /Coil_Cooling_Water/
212
+ cooling_type = 'CHW'
213
+ suppcomp = supplycomp.to_CoilCoolingWater.get
214
+ mech_capacity = suppcomp.autosizedDesignCoilLoad.to_f/1000.0
215
+ cat_search = 'coils'
216
+ cool_cap['CHW'] += mech_capacity
217
+ when /OS_AirLoopHVAC_UnitaryHeatPump_AirToAir/
218
+ suppcomp = supplycomp.to_AirLoopHVACUnitaryHeatPumpAirToAir.get
219
+ htg_coil = suppcomp.heatingCoil
220
+ if htg_coil.to_CoilHeatingDXSingleSpeed.is_initialized
221
+ htg_coil = htg_coil.to_CoilHeatingDXSingleSpeed.get
222
+ if htg_coil.isRatedTotalHeatingCapacityAutosized
223
+ mech_capacity = htg_coil.autosizedRatedTotalHeatingCapacity.to_f/1000.0
224
+ else
225
+ mech_capacity = htg_coil.ratedTotalHeatingCapacity.to_f/1000.0
226
+ end
227
+ heating_fuel = 'HP'
228
+ cat_search = 'ashp'
229
+ heat_cap['HP'] += mech_capacity
230
+ end
231
+ clg_coil = suppcomp.coolingCoil
232
+ if clg_coil.to_CoilCoolingDXSingleSpeed.is_initialized
233
+ clg_coil = clg_coil.to_CoilCoolingDXSingleSpeed.get
234
+ if clg_coil.isRatedTotalCoolingCapacityAutosized
235
+ mech_capacity = clg_coil.autosizedRatedTotalCoolingCapacity.to_f/1000.0
236
+ else
237
+ mech_capacity = clg_coil.ratedTotalCoolingCapacity.to_f/1000.0
238
+ end
239
+ cooling_type = 'DX'
240
+ cat_search = 'coils'
241
+ cool_cap['DX'] += mech_capacity
242
+ end
243
+ supp_htg_coil = suppcomp.supplementalHeatingCoil
244
+ if supp_htg_coil.to_CoilHeatingElectric.is_initialized
245
+ supp_htg_coil = supp_htg_coil.to_CoilHeatingElectric.get
246
+ elsif supp_htg_coil.to_CoilHeatingGas.is_initialized
247
+ supp_htg_coil = supp_htg_coil.to_CoilHeatingGas.get
248
+ end
249
+ if supp_htg_coil.isNominalCapacityAutosized
250
+ mech_capacity = supp_htg_coil.autosizedNominalCapacity.to_f/1000.0
251
+ else
252
+ mech_capacity = supp_htg_coil.nominalCapacity.to_f/1000.0
253
+ end
254
+ if supp_htg_coil.class.name.include? 'CoilHeatingElectric'
255
+ cat_search = 'elecheat'
256
+ heat_cap['elec'] += mech_capacity
257
+ elsif supp_htg_coil.class.name.include? 'CoilHeatingGas'
258
+ cat_search = 'FurnaceGas'
259
+ heat_cap['Gas'] += mech_capacity
260
+ end
261
+ end
262
+ # This hash contains all of the pertinent information required for costing a piece of air loop heating/cooling equipment
263
+ equipment_info = {
264
+ sys_type: sys_type,
265
+ obj_type: obj_type,
266
+ supply_comp: supplycomp,
267
+ heating_fuel: heating_fuel,
268
+ cooling_type: cooling_type,
269
+ adv_dx_clg_eqpt: adv_dx_clg_eqpt,
270
+ mech_capacity_kw: mech_capacity,
271
+ cat_search: cat_search
272
+ }
273
+ unless equipment_info[:mech_capacity_kw].to_f <= 0
274
+ # Add the piece of air loop equipment to an array for costing if the equipment does something (that is has a size larger than 0)
275
+ airloop_equipment << equipment_info
276
+ end
277
+ end
278
+
279
+ # Determine the predominant heating and cooling fuel type.
280
+ ahu_heat_cool_info = determine_ahu_htg_clg_fuel(heat_cap: heat_cap, cool_cap: cool_cap, heat_type: heat_type, cool_type: cool_type)
281
+ heat_type = ahu_heat_cool_info[:heat_type]
282
+ cool_type = ahu_heat_cool_info[:cool_type]
283
+ # Cost rooftop ventilation unit.
284
+ costed_ahu_info = cost_ahu(sys_type: sys_type, airloop_flow_lps: airloop_flow_lps, airloop_flow_cfm: airloop_flow_cfm, mech_sizing_info: mech_sizing_info, heating_fuel: ahu_heat_cool_info[:heating_fuel], cooling_type: ahu_heat_cool_info[:cooling_type], airloop_name: airloop_name, vent_tags: ahu_tags)
285
+ # Get ventilation heating and cooling equipment costs.
286
+ air_loop_equip_return_info = airloop_equipment_costing(airloop_equipment: airloop_equipment, ahu_mult: costed_ahu_info[:mult].to_f, vent_tags: ahu_tags)
287
+ # Get the air loop equipment reporting information from the air loop equipment costing method return hash
288
+ al_eq_reporting_info = air_loop_equip_return_info[:al_eq_reporting_info]
289
+ # Add the air loop equipment costing to the total air loop cost
290
+ total_heat_cool_cost += air_loop_equip_return_info[:heat_cool_cost]
291
+
292
+ # Determine information about thermal zones supplied by this air loop and sort it by building floor
293
+ hvac_floors = gen_hvac_info_by_floor(hvac_floors: hvac_floors, model: model, prototype_creator: prototype_creator, airloop: airloop, sys_type: sys_type, hrv_info: hrv_info)
294
+ sys_1_4 = false unless (sys_type == 1 || sys_type == 4)
295
+
296
+ reheat_cost, reheat_array = reheat_recool_cost(airloop: airloop, prototype_creator: prototype_creator, model: model, roof_cent: roof_cent, mech_sizing_info: mech_sizing_info, vent_tags: ahu_tags, report_mult: 1.0)
297
+
298
+ if hrv_info[:hrv_present]
299
+ hrv_rep = hrv_cost(hrv_info: hrv_info, airloop: airloop, vent_tags: ahu_tags, report_mult: 1.0)
300
+ hrv_total_cost += hrv_rep[:revised_hrv_cost].to_f
301
+ else
302
+ hrv_rep = {}
303
+ end
304
+
305
+ @airloop_info[:hrv] = hrv_rep
306
+ ahu_cost += costed_ahu_info[:adjusted_base_ahu_cost] + reheat_cost + total_heat_cool_cost
307
+ @airloop_info[:equipment_info] = al_eq_reporting_info
308
+ @airloop_info[:reheat_recool] = reheat_array
309
+ @costing_report['ventilation'].each {|key, value| value << @airloop_info if key.to_s == ('system_' + sys_type.to_s)}
310
+ end
311
+ if total_vent_flow_m3_per_s == 0 || total_vent_flow_m3_per_s.nil?
312
+ puts "No ventilation system is present which can currently be costed."
313
+ @costing_report['ventilation'] = {
314
+ error: "No ventilation system is present which can currently be costed."
315
+ }
316
+ return 0
317
+ end
318
+ @costing_report['ventilation'][:hrv_total_cost] = hrv_total_cost.round(2)
319
+ mech_roof_cost, mech_roof_rep = mech_to_roof_cost(heat_type: heat_type, cool_type: cool_type, mech_room: mech_room, roof_cent: roof_cent, rt_unit_num: rt_unit_num)
320
+ @costing_report['ventilation'][:mech_to_roof] = mech_roof_rep
321
+ trunk_duct_cost, trunk_duct_info = vent_trunk_duct_cost(tot_air_m3pers: total_vent_flow_m3_per_s, min_space: min_space, roof_cent: roof_cent, mech_sizing_info: mech_sizing_info, sys_1_4: sys_1_4)
322
+ @costing_report['ventilation'][:trunk_duct] << trunk_duct_info
323
+ floor_dist_cost, build_floor_trunk_info = floor_vent_dist_cost(hvac_floors: hvac_floors, prototype_creator: prototype_creator, roof_cent: roof_cent, mech_sizing_info: mech_sizing_info)
324
+ @costing_report['ventilation'][:floor_trunk_ducts] << build_floor_trunk_info
325
+ tz_dist_cost, duct_dist_rep = tz_vent_dist_cost(hvac_floors: hvac_floors, mech_sizing_info: mech_sizing_info)
326
+ @costing_report['ventilation'][:tz_distribution] << duct_dist_rep
327
+ hrv_ducting_cost, hrv_ret_duct_report = hrv_duct_cost(prototype_creator: prototype_creator, roof_cent: roof_cent, mech_sizing_info: mech_sizing_info, hvac_floors: hvac_floors)
328
+ @costing_report['ventilation'][:hrv_return_ducting] = hrv_ret_duct_report
329
+ ahu_cost += tz_dist_cost + trunk_duct_cost + floor_dist_cost + hrv_ducting_cost + hrv_total_cost + mech_roof_cost
330
+ return ahu_cost.round(2)
331
+ end
332
+
333
+ # This method determines the main heating fuel and cooling type used by an air handling unit (a given model's air
334
+ # loop). The method also determines the ahu's supplementary heating type (if any) if the primary heater is a heat
335
+ # pump. All capacities are in KW.
336
+ #
337
+ # Inputs:
338
+ #
339
+ # heat_cap: The capacity of heaters in the supply side of the air loop. This is used to determine the main heating
340
+ # type used by the ahu. They can be the following types:
341
+ # HP (Heat Pump)
342
+ # elec (Electricity)
343
+ # Gas
344
+ # HW (Hot Water)
345
+ # CCASHP (Cold Climate Air Source Heat Pump)
346
+ # cool_cap: The capacity of cooling units in the supply side of the air loop. This is used to determine the main
347
+ # cooling type used by the ahu. They can be the following types:
348
+ # DX (Direct Expansion)
349
+ # CHW (Chilled Water)
350
+ # Note that HP and CCASHP are not inculded. If the the main heating type is a HP or CCASHP and the main
351
+ # cooling type is DX then the the main cooling type will be reported as being the same as the main heating
352
+ # type.
353
+ # heat_type: This is a hash of counters used to determine what services (electrical lines, hot water pipes, chilled
354
+ # water pipes, etc.) need to be run from the main mechanical room (where they are assumed to originate) to
355
+ # the roof of the building (where the ahu's are located). The following tpes are used:
356
+ # HP: Heat pump (also used for CCASHP, esentially just an electircal line is needed which is always
357
+ # inculded anyway)
358
+ # elec: Electricity (an electrical line is needed which is always inculded anyway)
359
+ # Gas: Gas (a gas line is needed)
360
+ # HW: Hot water (a hot water line is needed)
361
+ # cool_type: This is the same as heat_type only for cooling. This is really just used to determine if a chilled water
362
+ # line is needed since an electrical line is always inculded.
363
+ # DX: Direct Exchange (also used for HP and CCASHP since only the defaul electrical line is needed)
364
+ # CHW: Chilled water (a chilled water pipe is required)
365
+ #
366
+ # Outputs:
367
+ # heat_cool_info: This is a hash that contains the return information which inculdes:
368
+ # heating_fuel: The primary heating fuel used by the ahu (and supplemental heating fuel if used by a HP
369
+ # or CCASHP). This is used when searching the 'hvac_vent_ahu' sheet in the costing
370
+ # spreasheet when costing the ahu.
371
+ # cooling_type: The primary cooling type used by the ahu. This is used when searching the
372
+ # 'hvac_vent_ahu' sheet in the costing spreadsheet when costing the ahu.
373
+ # heat_type: See above (only counters adjusted)
374
+ # cool_type: See above (only counters adjusted)
375
+ #
376
+ def determine_ahu_htg_clg_fuel(heat_cap:, cool_cap:, heat_type:, cool_type:)
377
+ # Determine the predominant heating and cooling type by looking for the key associated with the largest value in the
378
+ # heat_cap and cool_cap hashes. For heating it returns HP, elec, Gas, HW or CCASHP and for cooling it returns CHW
379
+ # or DX.
380
+ heating_fuel = heat_cap.max_by{|key, value| value}[0]
381
+ cooling_type = cool_cap.max_by{|key, value| value}[0]
382
+
383
+ # Increase the counter of the associated cooling type by 1
384
+ cool_type[cooling_type] += 1
385
+
386
+
387
+ # If a variety of heat pump (regular HP or CCASHP) is present then, for costing, it is assumed to be the primary
388
+ # heating type for the ahu.
389
+ if heat_cap['HP'] > 0 || heat_cap['CCASHP'] > 0
390
+ # Increase the heat_type counter for heat pump by 1.
391
+ heat_type['HP'] += 1
392
+
393
+ # Get the capacities of just the HP and CCASHP.
394
+ pri_hp_type = {
395
+ 'HP' => heat_cap['HP'],
396
+ 'CCASHP' => heat_cap['CCASHP']
397
+ }
398
+ # Use the same technique for heating_fuel and cooling_type to determine which type of heat pump has the largest
399
+ # capacity. This is used in the off chance that more than one heat pump type is present (I'm not even sure that
400
+ # is possible in OpenStudio air loops but I include this little bit of edge case handling anyway).
401
+ hp_type = pri_hp_type.max_by{|key, value| value}[0].to_s
402
+ heating_fuel = hp_type
403
+ # It is possible to heat your building with an ASHP and use chilled water to cool your building. I don't know why
404
+ # you would do that but we can cost the ahu if you do. If the main cooling type is DX (which is highly likely
405
+ # if you are heating your air loop with an ASHP) then the main cooling type is set to be the main heating type
406
+ # of the air loop (either regular HP or fancy CCAHP).
407
+ if cooling_type == 'DX'
408
+ cooling_type = hp_type
409
+ end
410
+ # This determines if supplemental heating is used with your heat pump (very likely in most of Canada if you heat
411
+ # with a HP).
412
+ unless (heat_cap['elec'] == 0) && (heat_cap['Gas'] == 0) && (heat_cap['HW'] == 0)
413
+ # Create a hash of just the fuel heating in the air loop and change the hash key to match what we will look for
414
+ # in the hvac_vent_ahu sheet in the costing speadsheet
415
+ hp_supp_cap = {
416
+ '-e' => heat_cap['elec'],
417
+ '-g' => heat_cap['Gas'],
418
+ '-hw' => heat_cap['HW'],
419
+ }
420
+ # Look for the key (which is the fuel type) with the largest associated value (which is the capacity).
421
+ hp_supp = hp_supp_cap.max_by{|key, value| value}[0].to_s
422
+ # Increase the heat_type count for the associated supplement heat type. This is necessary since if gas heating
423
+ # is used as supplemental heatnig for a heat pump then a gas line will be required between the mechanical room
424
+ # and the roof.
425
+ case hp_supp
426
+ when '-e'
427
+ heat_type['elec'] += 1
428
+ when '-g'
429
+ heat_type['Gas'] += 1
430
+ when 'hw'
431
+ heat_type['HW'] += 1
432
+ end
433
+ end
434
+ # Get the heating fuel by appending the supplementary heating type just determined (if any) to the heat pump type
435
+ heating_fuel = hp_type
436
+ heating_fuel += hp_supp unless hp_supp.nil?
437
+ else
438
+ # If you do not use a heat pump then increase the heat_type counter for whatever fuel you use to heat the air loop
439
+ # by 1.
440
+ heat_type[heating_fuel] += 1
441
+ end
442
+ # Create the hash with the results and return it (I use a hash to return a bunch of results because it seems
443
+ # cleaner).
444
+ heat_cool_info = {
445
+ heating_fuel: heating_fuel,
446
+ cooling_type: cooling_type,
447
+ heat_type: heat_type,
448
+ cool_type: cool_type,
449
+ }
450
+ return heat_cool_info
451
+ end
452
+ # This method tokes in:
453
+ # ids: The list of material ids to look for in the 'material_id' column of the materials_hvac sheet.
454
+ # The number of ids should match the number of id_quants (this is checked earlier).
455
+ # id_quants: The number of the piece of equipment defined by the ids above required. Like ids this should be an
456
+ # array taken from the 'id_layers_quantity_multipliers' column of the 'hvac_vent_ahu' sheet for the air handler that
457
+ # matches the required criteria. The number of ids should match the number of id_quants (this is checked earlier).
458
+ # overall_mult: An multiplier to apply to all ids and id_quants (I'm not sure if this is used anymore).
459
+ # This method cycles through each of the ids and searches for it in the 'material_id' column of the 'material_hvac'
460
+ # sheet in the costing spreadsheet. The equipment information found in the 'materials_hvac' is then costed. The cost
461
+ # is then multiplied by the associated id_quants. For example, if the ids contains 5 elements the method searches for
462
+ # each one. In our example, when we get ot the 4th element of the ids we multiply its associated cost by the 4th
463
+ # element of the id_quants array. The total cost is then summed and multiplied by the 'overall_mult' and returned.
464
+ def vent_assembly_cost(ids:, id_quants:, overall_mult: 1.0, vent_tags: [], report_mult: 1.0)
465
+ assembly_tags = vent_tags.clone
466
+ total_cost = 0
467
+ # Cycle through each of the ids. The index is used to select the correct element of the id_quants array.
468
+ ids.each_with_index do |id, index|
469
+ # Get the equipment information from the costing spreadsheet's 'material_hvac' sheet whose 'material_id' matches
470
+ # the id.
471
+ mat_cost_info = @costing_database['raw']['materials_hvac'].select {|data|
472
+ data['material_id'].to_f.round(0) == id.to_f.round(0)
473
+ }.first
474
+ # If it cannot find it there is an issue with either the 'materials_hvac' sheet or the 'hvac_vent_ahu' sheet which
475
+ # the user has to deal with.
476
+ if mat_cost_info.nil?
477
+ raise "Error: no assembly information available for material id #{id}!"
478
+ end
479
+ # Get the cost for the piece of equipment, multiply it by the associated id_quants element and add to the total
480
+ total_cost += get_vent_mat_cost(mat_cost_info: mat_cost_info, report_mult: (overall_mult*id_quants[index].to_f*report_mult), vent_tags: assembly_tags)*id_quants[index].to_f
481
+ end
482
+ # multiply the total by the overal_mult (which is probably always 1.0 now but I'm not sure) and return the cost.
483
+ return (total_cost*overall_mult)
484
+ end
485
+
486
+ # This method finds how many pieces of costed equipment are required to meet a given load if no one piece of costed
487
+ # equipment can do it. It takes in two hashes:
488
+ # mult_floor: This should probably be mult_ceiling. It is the maximum size of mechanical equipment that should be
489
+ # selected. This is used if you really want to make sure that a given piece of mechanical equipment does not exceed
490
+ # this size. It is normally not used.
491
+ # loop_equip: This hash must have the following information in it:
492
+ # cat_search: The category or type of the mechanical equipment that is being searched for in the 'Material' column
493
+ # of the 'materials_hvac' sheet in the costing spreadsheet.
494
+ # supply_comp: This is the oir loop supply component from the OpenStudio model. It is really just used to give a
495
+ # name in any error messages.
496
+ # mech_capacity: This is the capacity of the piece of the supply component being costed.
497
+ #
498
+ # The method first looks for all of the items in the 'materials_hvac' sheet whose 'Material' match the 'cat_search'
499
+ # criteria. If none are found then something has gone wrong so an error is generated telling the user what happened.
500
+ # Assuming it found some items it then finds the largest one (or the largest one that does not exceed the mult_floor
501
+ # category). It then divides the mech_capacity by the size of the costed equipment it found to determine the minimum
502
+ # number of pieces of costed equipment meets the model equipment capacity (the multiplier). With this information it
503
+ # then rounds the multiplier to the next largest whole number and divides the modeled equipment capacity by this size
504
+ # to determine the revised size of equipment (this may be smaller than the largest piece of equipment). It then looks
505
+ # for the smallest piece of costed equipment that meets this requirement and returns the result.
506
+ def get_vent_system_mult(loop_equip:, mult_floor: nil)
507
+ # Look for all of the equipment in the materials_hvac sheet that has a 'Material' that matches the cat_search
508
+ # criteria.
509
+ heat_cool_cost = @costing_database['raw']['materials_hvac'].select {|data|
510
+ data['Material'].to_s.upcase == loop_equip[:cat_search].to_s.upcase
511
+ }
512
+
513
+ # In some cases loop_equip[:supply_comp] may not be an object but a string. If this is the case then the string
514
+ # should be given rather than a message that nameString does not exist.
515
+ equip_name = loop_equip[:supply_comp].nameString rescue equip_name = loop_equip[:supply_comp].to_s
516
+
517
+ # If it cannot find any then return an error telling the user what happened. This is likely the result of a
518
+ # spelling mistake somewhere but it is something the user will have to deal with.
519
+ if heat_cool_cost.nil? || heat_cool_cost.empty?
520
+ raise "Error: no equipment could be found whose type matches the name #{loop_equip[:cat_search]} for the #{equip_name} air loop supply component!"
521
+ end
522
+ # Set the maximum size to be a really large number if it is not defined.
523
+ mult_floor.nil? ? max_eq_size = 99999999999999999999.0 : max_eq_size = mult_floor.to_f
524
+ # Find the largest piece of equipment that is smaller than the size ceiling.
525
+ max_size = heat_cool_cost.select {|element| element['Size'].to_f <= max_eq_size}.max_by{|data| data['Size'].to_f}
526
+ # If you cannot find any then the size ceiling is too small. Raise an error telling the user
527
+ if max_size.nil? || max_size.empty?
528
+ raise "Error no equipment of the type #{loop_equip[:cat_search]} could be found with a size less than #{max_eq_size} for the #{equip_name} air loop supply component!"
529
+ end
530
+ # Make sure the piece of equipment has a capacity larger than 0.
531
+ if max_size['Size'].to_f <= 0
532
+ raise "Error: #{loop_equip[:cat_search]} has a size of 0 or less. Please check that the correct costing_database.json file is being used or check the costing spreadsheet!"
533
+ end
534
+ # Find the revised number of pieces of equipment and round to the next largest whole number.
535
+ mult = (loop_equip[:mech_capacity_kw].to_f) / (max_size['Size'].to_f)
536
+ # This is to handle the small possibility that the revised capacity is a whole number.
537
+ mult > (mult.to_i).to_f.round(0) ? multiplier = (mult.to_i).to_f.round(0) + 1 : multiplier = mult.round(0)
538
+ # Find the new capacity of the pieces of equipment
539
+ new_cap = loop_equip[:mech_capacity_kw].to_f/multiplier.to_f
540
+ # Find the smallest piece of costed equimpent that meets the new size requirement.
541
+ return_equip = heat_cool_cost.select{|data| data['Size'].to_f >= new_cap}.min_by{|element| element['Size'.to_f]}
542
+ # If no costed equipment can be found that matches this new size then something is wrong and use the largest piece
543
+ # you found before.
544
+ return_equip = max_size if (return_equip.nil? || return_equip.empty?)
545
+ return return_equip, multiplier.to_f
546
+ end
547
+
548
+ # This method finds ahu with the largest supply air capacity based on the heating and cooling characteristics defined
549
+ # by loop_equip. Loop_equip is a hash which includes:
550
+ # sys_type: the NECB HVAC system type (one of 1, 3, 4, or 6)
551
+ # heating_fuel: The predominant heating fuel
552
+ # cooling_type: The predominant cooling type
553
+ # airloop_flow_lps: The air loop flow rate (L/s)
554
+ # airloop_name: The name of the air loop (used in an error message)
555
+ #
556
+ # If no air handler is found that meets the above requirements raise an error telling the user that something is
557
+ # wrong. If one or more air handlers are found choose the one with the larges 'Supply_air'. This defines the largest
558
+ # air handler of the given type. Then divide the air loop air flow by the maximum air flow available. Round up and
559
+ # this number defines how many air handlers are required to meet the load.
560
+ #
561
+ # In some cases, the air loop flow rate is only a little larger than that available by the largest air handler. For
562
+ # example, an air loop may have a flow rate of 16000 L/s but the largest available air handler is 15000 L/s. Rather
563
+ # than costing two 15000 L/s air handlers it would be cheaper to cost two 8000 L/s air handlers. To do this, the
564
+ # method divides the air_loop_flow_lps by the number of required air handlers. It then looks for air handlers which
565
+ # meet the required characteristics and revised air flow rate. If more than one are found it selects the smallest one
566
+ # available. It then returns this new air handler along with the raw number of air handlens (which may be a fraction)
567
+ # and the maximum number (which will be an integer).
568
+ def get_ahu_mult(loop_equip:)
569
+ # Look for the largest air handler that matches the system type, heating fuel, and cooling type requirements
570
+ ahu = @costing_database['raw']['hvac_vent_ahu'].select {|data|
571
+ data['Sys_type'].to_f.round(0) == loop_equip[:sys_type].to_f.round(0) and
572
+ data['Htg'].to_s.upcase == loop_equip[:heating_fuel].to_s.upcase and
573
+ data['Clg'].to_s.upcase == loop_equip[:cooling_type].to_s.upcase
574
+ }.max_by {|element| element['Supply_air'].to_f}
575
+ # If none are found something has gone wrong. Tell the user.
576
+ if ahu.nil? || ahu.empty?
577
+ raise "Error: no ahu information available for equipment #{loop_equip[:airloop_name]}!"
578
+ end
579
+ # I probably don't need to check this but make sure that the air handler has a size larger than 0.
580
+ if ahu['Supply_air'].to_f <= 0
581
+ raise "Error: #{loop_equip[:airloop_name]} has a size of 0 or less. Please check that the correct costing_database.json file is being used or check the costing spreadsheet!"
582
+ end
583
+ # Determine the number of air handlers to be the air loop flow rate divided by the maximum air handler size. This
584
+ # will likely not be a whole number.
585
+ mult = (loop_equip[:airloop_flow_lps].to_f) / (ahu['Supply_air'].to_f)
586
+ # Since air handlers only come in integer numbers (half and air handler would not be too useful) round up to the
587
+ # next whole number (the if statement is for the off chance that the required number ended up being an integer).
588
+ mult > (mult.to_i).to_f.round(0) ? multiplier = (mult.to_i).to_f.round(0) + 1 : multiplier = mult.round(0)
589
+ # Get the revised required air flow rate by dividing the air loop air flow by the number of air handlers
590
+ rev_air_flow = loop_equip[:airloop_flow_lps].to_f / multiplier
591
+ # Find air handlers that can meet that air flow and choose the smallest one that meets the requirement.
592
+ rev_ahu = @costing_database['raw']['hvac_vent_ahu'].select {|data|
593
+ data['Sys_type'].to_f.round(0) == loop_equip[:sys_type].to_f.round(0) and
594
+ data['Htg'].to_s.upcase == loop_equip[:heating_fuel].to_s.upcase and
595
+ data['Clg'].to_s.upcase == loop_equip[:cooling_type].to_s.upcase and
596
+ data['Supply_air'].to_f >= rev_air_flow
597
+ }.min_by{|info| info['Supply_air'].to_f}
598
+ # If none are found something weird is happening so keep the one you already found.
599
+ if rev_ahu.nil? || rev_ahu.empty?
600
+ # Something weird happened, keep the ahu you found before.
601
+ else
602
+ ahu = rev_ahu
603
+ end
604
+ return ahu, multiplier, rev_air_flow
605
+ end
606
+
607
+ # This method costs a piece of mechanical equipment. The mat_cost_info is a hash that contains the information for the
608
+ # piece of equipment from the 'materials_hvac' sheet of the costing spreadsheet. It contains:
609
+ # material_id: An index sometimes used to refer to find a specific piece of equipment
610
+ # material: The type of equipment.
611
+ # description: A description of the piece of equipment.
612
+ # Size: The size of the piece of equipment (see units for the unit this is in).
613
+ # Fuel: Sometimes this is indicates the fuel type, sometimes it is an additional size criteria.
614
+ # source: The source to look for the costing information. This can be placeholder or custom.
615
+ # id: The unique id of the costing information associated with this piece of equipment.
616
+ # unit: The units of the given Size (can be one of many units).
617
+ # province_state: For custom costing data, this is the province or state that the costing data is given for (used to
618
+ # adjust the costing data so it can be used nationally).
619
+ # city: For custom costing data, this is the city that the costing data is given for (used to adjust the costing so
620
+ # it can be used nationally).
621
+ # year: The year the costing information is provided for (it should be the same for everything but some costs are
622
+ # only available in some years and not others).
623
+ # material_cost: The custom cost for material (e.g. the cost of a pipe). Not used for placeholder costs.
624
+ # labour_cost: The custom cost for labour (e.g. the labour to install the pipe). Not used for placeholder costs.
625
+ # equipment_cost: The custom cost of equipment required (e.g. the cost of any machinery required to install the pipe,
626
+ # often this is 0). This is not used for placeholder costs.
627
+ # material_op_factor: Ask Mike or Phylroy. Probably not for placeholder costs.
628
+ # labour_op_factor: Ask Mike or Phylroy. Probably not for placeholder costs.
629
+ # equipment_op_factor: Ask Mike or Phylroy. Probably not for placeholder costs.
630
+ # comments: comments.
631
+ # material_mult: A fixed multiplier to multiply the material cost by.
632
+ # labour_mult: A fixed multiplier to multiply the labour costs by.
633
+ # The method uses the id from 'mat_cost_info' to find costing information for the piece of equipment in the costing
634
+ # database. It then adjusts the material and equipment cost by the regional cost factor for the location the model
635
+ # is supposed to be in. The resulting adjusted equipment and material costs are then multiplied by any associated
636
+ # multipliers and the total amount is returned.
637
+ def get_vent_mat_cost(mat_cost_info:, vent_tags: [], report_mult: 1.0)
638
+ cost_tags =vent_tags.clone
639
+ if mat_cost_info.nil?
640
+ raise("Error: no assembly information available for material!")
641
+ end
642
+ # Look for the costing information for the piece of equipment in the costing database.
643
+ costing_data = @costing_database['costs'].detect {|data| data['id'].to_s.upcase == mat_cost_info['id'].to_s.upcase}
644
+ # If no costing information is found then return an error.
645
+ if costing_data.nil?
646
+ raise "Error: no costing information available for material id #{mat_cost_info['id']}!"
647
+ elsif costing_data['baseCosts']['materialOpCost'].nil? || costing_data['baseCosts']['laborOpCost'].nil?
648
+ #This is a stub for some work that needs to be done to account for equipment costing. For now this is zeroed out.
649
+ # A similar test is done on reading the data from the database and collected in the error file when the
650
+ # costing database is generated.
651
+ puts("Error: costing information for material id #{mat_cost_info['id']} is nil. Please check costing data.")
652
+ return 0.0
653
+ end
654
+ # The costs from the costing database are US national average costs (for placeholder costs) or whatever is in the
655
+ # 'province_state' and 'city' fieleds (for custom costs). These costs need to be adjusted to reflect the costs
656
+ # expected in the location of interest. The 'get_regional_cost_factors' method finds the appropriate cost
657
+ # adjustment factors.
658
+ mat_mult, inst_mult = get_regional_cost_factors(@costing_report['province_state'], @costing_report['city'], mat_cost_info)
659
+ if mat_mult.nil? || inst_mult.nil?
660
+ raise("Error: no localization information available for material id #{id}!")
661
+ end
662
+ # Get any associated material or labour multiplier for the equipment present in the 'materials_hvac' sheet in the
663
+ # costing spreadsheet.
664
+ mat_cost_info['material_mult'].to_f == 0 ? mat_quant = 1.0 : mat_quant = mat_cost_info['material_mult'].to_f
665
+ mat_cost_info['labour_mult'].to_f == 0 ? lab_quant = 1.0 : lab_quant = mat_cost_info['labour_mult'].to_f
666
+ # Calculate the adjusted material and labour costs.
667
+ mat_cost = costing_data['baseCosts']['materialOpCost']*(mat_mult/100.0)*mat_quant
668
+ lab_cost = costing_data['baseCosts']['laborOpCost']*(inst_mult/100.0)*lab_quant
669
+ # Add information to report output if tags provided.
670
+ unless cost_tags.empty?
671
+ cost_tags << mat_cost_info['Material'].to_s
672
+ cost_tags << mat_cost_info['description'].to_s
673
+ # Add support for equipment_multiplier (if used in the future).
674
+ mat_cost_info['equipment_mult'].nil? || mat_cost_info['equipment_mult'].to_f == 0 ? equip_quant = 1.0 : equip_quant = mat_cost_info['equipment_mult'].to_f
675
+ add_costed_item(material_id: mat_cost_info['id'], quantity: report_mult.to_f, material_mult: mat_quant, labour_mult: lab_quant, equip_mult: equip_quant, tags: cost_tags)
676
+ end
677
+ # Return the total.
678
+ return (mat_cost+lab_cost)
679
+ end
680
+
681
+ def cost_heat_cool_equip(equipment_info:, vent_tags: [], report_mult: 1.0)
682
+ equip_tags = vent_tags.clone
683
+ total_cost = 0
684
+ multiplier, heat_cool_cost_info = get_vent_cost_data(equipment_info: equipment_info)
685
+ total_cost += (get_vent_mat_cost(mat_cost_info: heat_cool_cost_info, vent_tags: equip_tags, report_mult: (report_mult*multiplier)))*multiplier
686
+ if equipment_info[:cooling_type] == 'DX' || equipment_info[:cooling_type] == 'DX-adv'
687
+ equipment_info[:cooling_type].include?("-adv") ? search_suff = "-adv" : search_suff = ""
688
+ equipment_info[:cat_search] = "CondensingUnit" + search_suff
689
+ equip_tags << equipment_info[:cat_search] unless equip_tags.empty?
690
+ multiplier, heat_cool_cost_info = get_vent_cost_data(equipment_info: equipment_info)
691
+ total_cost += get_vent_mat_cost(mat_cost_info: heat_cool_cost_info, vent_tags: equip_tags, report_mult: (report_mult*multiplier))*multiplier
692
+ equip_tags << "piping" unless equip_tags.empty?
693
+ piping_search = []
694
+
695
+ piping_search << {
696
+ mat: 'SteelPipe',
697
+ unit: 'L.F.',
698
+ size: 1.25,
699
+ mult: 32.8
700
+ }
701
+ piping_search << {
702
+ mat: 'PipeInsulationsilica',
703
+ unit: 'L.F.',
704
+ size: 1.25,
705
+ mult: 32.8
706
+ }
707
+ piping_search << {
708
+ mat: 'SteelPipeElbow',
709
+ unit: 'each',
710
+ size: 1.25,
711
+ mult: 8
712
+ }
713
+ total_cost += get_comp_cost(cost_info: piping_search, vent_tags: equip_tags, report_mult: (report_mult*multiplier))*multiplier
714
+ return total_cost
715
+ end
716
+ # This needs to be revised as currently the costing spreadsheet may not inculde heating and cooling coil costs in
717
+ # the ahu definition sheet. This is commented out for now but will need to be revisited. See btap_tasks issue 156.
718
+ =begin
719
+ if equipment_info[:heating_fuel] == 'HP'
720
+ if sys_type == 3 || sys_type == 6
721
+ # Remove the DX cooling unit for ashp in type 3 and 6 systems
722
+ heat_cool_cost = @costing_database['raw']['materials_hvac'].select {|data|
723
+ data['Material'].to_s.upcase == 'DX' and
724
+ data['Size'].to_f.round(8) >= equipment_info[:mech_capacity_kw].to_f
725
+ }.first
726
+ if heat_cool_cost.nil?
727
+ heat_cool_cost, multiplier = get_vent_system_mult(loop_equip: equipment_info)
728
+ end
729
+ total_cost -= (get_vent_mat_cost(mat_cost_info: heat_cool_cost))*multiplier
730
+
731
+ # Remove the heating coil for ashp in type 3 and 6 systems
732
+ heat_cool_cost = @costing_database['raw']['materials_hvac'].select {|data|
733
+ data['Material'].to_s.upcase == 'COILS' and
734
+ data['Size'].to_f.round(8) >= equipment_info[:mech_capacity_kw].to_f
735
+ }.first
736
+ if heat_cool_cost.nil?
737
+ heat_cool_cost, multiplier = get_vent_system_mult(loop_equip: equipment_info)
738
+ end
739
+ total_cost -= (get_vent_mat_cost(mat_cost_info: heat_cool_cost))*multiplier
740
+ puts 'hello'
741
+ end
742
+ # Add pre-heat for ashp in all cases
743
+ # This needs to be refined as well. Only add the cost of an electric heat if a heater (presumably of any type) if
744
+ # one is not already explicitly modeled in the air loop (and thus costed already as part of this method). This is
745
+ # also part of btap_tasks issue 156.
746
+ heat_cool_cost = @costing_database['raw']['materials_hvac'].select {|data|
747
+ data['Material'].to_s.upcase == 'ELECHEAT' and
748
+ data['Size'].to_f.round(8) >= equipment_info[:mech_capacity_kw].to_f
749
+ }.first
750
+ if heat_cool_cost.nil?
751
+ heat_cool_cost, multiplier = get_vent_system_mult(loop_equip: equipment_info)
752
+ end
753
+ total_cost += (get_vent_mat_cost(mat_cost_info: heat_cool_cost))*multiplier
754
+ end
755
+ =end
756
+ return total_cost
757
+ end
758
+
759
+ # This method collects information related a piece of equipment from the 'materials_hvac' sheet in the costing
760
+ # spreadsheet. This information is then used to determine the cost of a piece of equipment. It takes in the
761
+ # equipment_info hash. This hash contains the following information:
762
+ # equipment_info = {
763
+ # cat_search: This is the category or type of mechanical equipment that is being costed. It is used to match items in
764
+ # the 'Material' column of the 'materials_hvac' sheet.
765
+ # mech_capacity_kw: This is the capacity of the piece of mechanical equipment being costed. Although it has kw in
766
+ # the name this is not always the case. It is compared against information in the 'Size' column of the
767
+ # 'materials_hvac' sheet.
768
+ # supply_comp: This is the OpenStudio object being costed. If there is an error this is used to tell which piece
769
+ # of the model had the issue.
770
+ #
771
+ # The method tries to find the smallest piece of equipment that matches the equipment type and that can satisfy the
772
+ # capacity requirements. If it cannot find one then it then it assumes that the largest matching piece of equipment
773
+ # cannot meet the required capacity and tries to determine how many would be need to meet the required capacity. It
774
+ # then returns the information it found in the costing spreadsheet and the number of piece of equipment would be
775
+ # required (if applicable)
776
+ def get_vent_cost_data(equipment_info:)
777
+ # Assume one piece of equipment is enough.
778
+ multiplier = 1.0
779
+ # Find the smallest piece of equipment in 'materials_hvac' sheet that matches the equipment type and meets the
780
+ # capacity requirement.
781
+ heat_cool_cost_data = @costing_database['raw']['materials_hvac'].select {|data|
782
+ data['Material'].to_s.upcase == equipment_info[:cat_search].to_s.upcase and
783
+ data['Size'].to_f.round(8) >= equipment_info[:mech_capacity_kw].to_f
784
+ }.min_by{|heat_cool| heat_cool[:mech_capcity_kw].to_f}
785
+ # If it cannot find any then assume the largest piece of equipment in the costing spreadsheet is too small and
786
+ # figure out how many of a smaller piece of equipment are required and what the smaller piece of equipment would be.
787
+ if heat_cool_cost_data.nil? || heat_cool_cost_data.empty?
788
+ heat_cool_cost_data, multiplier = get_vent_system_mult(loop_equip: equipment_info)
789
+ end
790
+ # Return the number of equipment necessary and the informatino required to find the piece of equipment in the
791
+ # costing database.
792
+ return multiplier, heat_cool_cost_data
793
+ end
794
+
795
+ def gas_burner_cost(heating_fuel:, sys_type:, airloop_flow_cfm:, mech_sizing_info:, costed_ahu_info:, vent_tags: [], report_mult: 1.0)
796
+ ahu_airflow_lps = costed_ahu_info[:ahu]["Supply_air"].to_f
797
+ report_mult_mod = report_mult*(-1.0)
798
+ burner_tags = vent_tags.clone
799
+ if (sys_type == 3 || sys_type == 6)
800
+ return 0
801
+ mech_table = get_mech_table(mech_size_info: mech_sizing_info, table_name: 'ahu_airflow')
802
+ coil_sizing_info = mech_table.select{|data| (data['ahu_airflow_range_lps'][0].to_f <= ahu_airflow_lps) && (data['ahu_airflow_range_lps'][1].to_f > ahu_airflow_lps) }
803
+ if coil_sizing_info.empty?
804
+ coil_sizing_kW = mech_table.max_by{|data| data['ahu_airflow_range_lps'][1]}
805
+ else
806
+ coil_sizing_kW = coil_sizing_info[0]
807
+ end
808
+ heating_kw = coil_sizing_kW['htg_coil_sizing_kW'].to_f
809
+ cooling_kw = coil_sizing_kW['DX_coil_sizing_kW'].to_f
810
+ heat_mech_eq_mult, heat_cost_info = get_vent_cost_data(equipment_info: {cat_search: 'coils', mech_capacity_kw: heating_kw})
811
+ cool_mech_eq_mult, cool_cost_info = get_vent_cost_data(equipment_info: {cat_search: 'coils', mech_capacity_kw: cooling_kw})
812
+ heating_coil_cost = heat_mech_eq_mult*get_vent_mat_cost(mat_cost_info: heat_cost_info, vent_tags: burner_tags, report_mult: (report_mult_mod*heat_mech_eq_mult))
813
+ dx_coil_cost = cool_mech_eq_mult*get_vent_mat_cost(mat_cost_info: cool_cost_info, vent_tags: burner_tags, report_mult: (report_mult_mod*cool_mech_eq_mult))
814
+ return heating_coil_cost + dx_coil_cost
815
+ else
816
+ if airloop_flow_cfm >= 1000 && airloop_flow_cfm <= 1500
817
+ mult, mech_info = get_vent_cost_data(equipment_info: {cat_search: 'DuctFurGasExt', mech_capacity_kw: 88})
818
+ return get_vent_mat_cost(mat_cost_info: mech_info, vent_tags: burner_tags, report_mult: (report_mult_mod*mult))*mult
819
+ elsif airloop_flow_cfm > 1500
820
+ mult, mech_info = get_vent_cost_data(equipment_info: {cat_search: 'DuctFurGasExt', mech_capacity_kw: 132})
821
+ return get_vent_mat_cost(mat_cost_info: mech_info, vent_tags: burner_tags, report_mult: (report_mult_mod*mult))*mult
822
+ end
823
+ end
824
+ return 0.0
825
+ end
826
+
827
+ # This method looks for an air handler in the 'hvac_vent_ahu' sheet of the costing spreadsheet. The inputs it uses
828
+ # to find the air handler are:
829
+ # sys_type: HVAC system type (can handle NECB systems 1, 3, 4 or 6)
830
+ # airloop_flow_lps: Air loop design air flow rate (L/s)
831
+ # heating_fuel: The predominant heating fuel used by the air loop (HP, CCASHP, HW, Gas, Propane, Oil)
832
+ # cooling_type: The predominant cooling type used by the air loop (DX, HP, CCASHP, CHW)
833
+ # airloop_name: The name of the air loop (only used for error messages)
834
+ #
835
+ # If no air handler with matching characteristics are found it assumes that all of the ones in the 'hvac_vent_ahu' ore
836
+ # too small. I then calls get_ahu_mult to find the largest air handler with the appropriate characteristics and finds
837
+ # how many of those are required to meet the load (see get_ahu_mult for more information). Once the appropriate air
838
+ # handler is selected from the 'hvac_vent_ahu' the method then reads the numbers in column K (id_layers) and column N
839
+ # (id_layers_quantity_multipliers). The numbers in 'id_layers' are indexes that match column A (material_id) in the
840
+ # 'material_hvac' costing spreadsheet sheet. The numbers in 'id_layers_quantity_multipliers' define how many pieces
841
+ # of equipment defined in the id_layer. The method then calls the 'vent_assembly_cost' method which takes the set of
842
+ # id_layers, the 'id_layers_quantity_multipliers' and the overall_mult. This costs each item in 'id_layers',
843
+ # multiplies the cost by the number in 'id_layers_quantity_multipliers' and multiplies everything by 'overall_mult'.
844
+ # The returned cast is then multiplied by the number of air handlers present (mult) and returns the cost.
845
+ #
846
+ # The method now also also includes the call to the 'gas_burner_cost' method to adjust for burner costs. It also
847
+ # includes the ahu size adjustement previously done in the main 'ahu_costing' method.
848
+ def cost_ahu(sys_type:, airloop_flow_lps:, airloop_flow_cfm:, mech_sizing_info:, heating_fuel:, cooling_type:, airloop_name:, vent_tags: [])
849
+ # Assmue one air handler to start
850
+ mult = 1.0
851
+ # Find an air handler in the 'hvac_vent_ahu' sheet that matches the system_type, air flow rate, heating type and
852
+ # cooling type.
853
+ ahu = @costing_database['raw']['hvac_vent_ahu'].select {|data|
854
+ data['Sys_type'].to_f.round(0) == sys_type.to_f.round(0) and
855
+ data['Supply_air'].to_f >= airloop_flow_lps and
856
+ data['Htg'].to_s == heating_fuel and
857
+ data['Clg'].to_s == cooling_type
858
+ }.min_by{|info| info['Supply_air'].to_f}
859
+ # If none are there assume that none had a big enough air flow rate. Create a data structure with the pertinent
860
+ # air handler information.
861
+ if ahu.nil? || ahu.empty?
862
+ loop_equip = {
863
+ sys_type: sys_type,
864
+ heating_fuel: heating_fuel,
865
+ cooling_type: cooling_type,
866
+ airloop_flow_lps: airloop_flow_lps,
867
+ airloop_name: airloop_name
868
+ }
869
+ # Find send the air handler information to the 'get_ahu_mult' method which returns the air handler information and
870
+ # the number which will meet the supply air rate.
871
+ ahu, mult, rev_airloop_flow_lps = get_ahu_mult(loop_equip: loop_equip)
872
+ # If one air handler which meets the requirements is found then use that one.
873
+ else
874
+ rev_airloop_flow_lps = airloop_flow_lps
875
+ end
876
+ # set the number of air hondlers in @airloop_info which is included in the ventilation costing report.
877
+ @airloop_info[:num_rooftop_units] = mult.to_i
878
+ # Calculate the ahu cost modifier for systems other than the largest (recreation of modifier originally applied in
879
+ # the 'ahu_costing' method).
880
+ ahu['Supply_air'].to_f.round(0) == 15000 ? ahu_cost_mod = 1.0 : ahu_cost_mod = (rev_airloop_flow_lps/(ahu['Supply_air'].to_f))
881
+ # Get the 'id_layers' from the 'hvac_vent_ahu' sheet and put them into an array
882
+ ids = ahu['id_layers'].to_s.split(',')
883
+ # Get the quantity of each of the preceding 'id_layers'. To do this, get the 'id_layers_quantity_multipliers'
884
+ # numbers from the 'hvac_vent_ahu' and convert them into an array
885
+ id_quants = ahu['Id_layers_quantity_multipliers'].to_s.split(',')
886
+ # Check that the number of ids is the same as the number of id_quants. If it isn't something is wrong and raise an
887
+ # error.
888
+ raise "The number of id_layers does not match the number of id_layer_quantity_multipliers in the hvac_vent_auh sheet of the costing spreadsheet. Please look for the air handler in the costing spreadsheet and check the appropriate columns. The air handler characteristics are: #{ahu}" if ids.size != id_quants.size
889
+ # Get the overall_mult. This used to be used but does not seem to be used anymore. I left it in just in case
890
+ # (probably a bad idea).
891
+ overall_mult = ahu['material_mult'].to_f
892
+ overall_mult = 1.0 if overall_mult == 0
893
+
894
+ # Create tags that will be added to the cost list output
895
+ new_tags = vent_tags.clone
896
+ new_tags << heating_fuel
897
+ new_tags << cooling_type
898
+ new_tags << "Required Air Flow (L/s): #{airloop_flow_lps.to_f.round(2)}"
899
+ new_tags << "Total AHU Air Flow with Multipliers(L/s): #{(ahu['Supply_air'].to_f*mult).to_f.round(2)}"
900
+ new_tags << "AHU Equipment"
901
+
902
+ # Cost the ids (multiplied by the number associated id_quants) and maltiply everything by the number of air handlers
903
+ # (if one was too small).
904
+ ind_ahu_cost = vent_assembly_cost(ids: ids, id_quants: id_quants, overall_mult: overall_mult, vent_tags: new_tags, report_mult: (overall_mult*ahu_cost_mod*mult))
905
+ # This is the total ahu cost without adjusting cost with airflow
906
+ calc_ahu_cost = ind_ahu_cost*mult
907
+ # Create the start of the return hash (done here because it is used in the 'gas_burner_cost' method)
908
+ costed_ahu_info = {
909
+ ahu: ahu,
910
+ mult: mult,
911
+ air_loop_flow_lps: airloop_flow_lps,
912
+ ind_ahu_cost: ind_ahu_cost
913
+ }
914
+ new_tags.pop
915
+ # Remove gas burner cost from ahu cost because it is accounted for in the heating and cooling equipment calculated later.
916
+ new_tags << "AHU Cost Adjustment"
917
+ ahu_mech_adj = gas_burner_cost(heating_fuel: heating_fuel, sys_type: sys_type, airloop_flow_cfm: airloop_flow_cfm, mech_sizing_info: mech_sizing_info, costed_ahu_info: costed_ahu_info, vent_tags: new_tags, report_mult: ahu_cost_mod)
918
+ base_ahu_cost = calc_ahu_cost - ahu_mech_adj
919
+ # Caclculate the adjusted ahu cost
920
+ adj_ahu_cost = (ind_ahu_cost*mult- ahu_mech_adj)*ahu_cost_mod
921
+ # Add costs to costing output
922
+ @airloop_info[:ind_ahu_max_airflow_l_per_s] = ahu['Supply_air'].to_f.round(0)
923
+ @airloop_info[:base_ahu_cost] = base_ahu_cost.round(2)
924
+ @airloop_info[:revised_base_ahu_cost] = adj_ahu_cost.round(2)
925
+
926
+ # Add ahu costs to return hash
927
+ costed_ahu_info[:base_ahu_cost] = base_ahu_cost
928
+ costed_ahu_info[:adjusted_base_ahu_cost] = adj_ahu_cost
929
+
930
+ return costed_ahu_info
931
+ end
932
+
933
+ def mech_to_roof_cost(heat_type:, cool_type:, mech_room:, roof_cent:, rt_unit_num:)
934
+ mech_to_roof_rep = {
935
+ Gas_Line_m: 0.0,
936
+ HW_Line_m: 0.0,
937
+ CHW_Line_m: 0.0,
938
+ Elec_Line_m: 0.0,
939
+ Total_cost: 0.0
940
+ }
941
+ mech_dist = [(roof_cent[:roof_centroid][0] - mech_room['space_centroid'][0]), (roof_cent[:roof_centroid][1] - mech_room['space_centroid'][1]), (roof_cent[:roof_centroid][2] - mech_room['space_centroid'][2])]
942
+ utility_dist = 0
943
+ ut_search = []
944
+ rt_roof_dist = OpenStudio.convert(10, 'm', 'ft').get
945
+ mech_dist.each{|dist| utility_dist+= dist.abs}
946
+ utility_dist = OpenStudio.convert(utility_dist, 'm', 'ft').get
947
+ heat_type.each do |key, value|
948
+ if value >= 1
949
+ case key
950
+ when 'HP'
951
+ next
952
+ when 'elec'
953
+ next
954
+ when 'Gas'
955
+ ut_search << {
956
+ mat: 'GasLine',
957
+ unit: 'L.F.',
958
+ size: 0,
959
+ mult: utility_dist + rt_roof_dist*value
960
+ }
961
+ heat_type['Gas'] = 0
962
+ mech_to_roof_rep[:Gas_Line_m] == (utility_dist + rt_roof_dist*value).round(1)
963
+ when 'HW'
964
+ ut_search << {
965
+ mat: 'SteelPipe',
966
+ unit: 'L.F.',
967
+ size: 4,
968
+ mult: 2*utility_dist + 2*rt_roof_dist*value
969
+ }
970
+ mech_to_roof_rep[:HW_Line_m] = (2*utility_dist + 2*rt_roof_dist*value).round(1)
971
+ ut_search << {
972
+ mat: 'PipeInsulation',
973
+ unit: 'none',
974
+ size: 4,
975
+ mult: 2*utility_dist + 2*rt_roof_dist*value
976
+ }
977
+ ut_search << {
978
+ mat: 'PipeJacket',
979
+ unit: 'none',
980
+ size: 4,
981
+ mult: 2*utility_dist + 2*rt_roof_dist*value
982
+ }
983
+ end
984
+ end
985
+ end
986
+
987
+ cool_type.each do |key, value|
988
+ if value >= 1
989
+ case key
990
+ when 'DX'
991
+ next
992
+ when 'CHW'
993
+ ut_search << {
994
+ mat: 'SteelPipe',
995
+ unit: 'L.F.',
996
+ size: 4,
997
+ mult: 2*utility_dist + 2*rt_roof_dist*value
998
+ }
999
+ mech_to_roof_rep[:CHW_Line_m] = (2*utility_dist + 2*rt_roof_dist*value).round(1)
1000
+ ut_search << {
1001
+ mat: 'PipeInsulation',
1002
+ unit: 'none',
1003
+ size: 4,
1004
+ mult: 2*utility_dist + 2*rt_roof_dist*value
1005
+ }
1006
+ ut_search << {
1007
+ mat: 'PipeJacket',
1008
+ unit: 'none',
1009
+ size: 4,
1010
+ mult: 2*utility_dist + 2*rt_roof_dist*value
1011
+ }
1012
+ end
1013
+ end
1014
+ end
1015
+ mech_to_roof_rep[:Elec_Line_m] = (utility_dist + rt_unit_num*rt_roof_dist).round(1)
1016
+ ut_search << {
1017
+ mat: 'Wiring',
1018
+ unit: 'CLF',
1019
+ size: 10,
1020
+ mult: (utility_dist + rt_unit_num*rt_roof_dist)/100
1021
+ }
1022
+ ut_search << {
1023
+ mat: 'Conduit',
1024
+ unit: 'L.F.',
1025
+ size: 0,
1026
+ mult: utility_dist + rt_unit_num*rt_roof_dist
1027
+ }
1028
+ total_comp_cost = get_comp_cost(cost_info: ut_search)
1029
+ mech_to_roof_rep[:Total_cost] = total_comp_cost.round(2)
1030
+ return total_comp_cost, mech_to_roof_rep
1031
+ end
1032
+
1033
+ def reheat_recool_cost(airloop:, prototype_creator:, model:, roof_cent:, mech_sizing_info:, vent_tags: [], report_mult: 1.0)
1034
+ reheat_recool_tags = vent_tags.clone
1035
+ heat_cost = 0
1036
+ out_reheat_array = []
1037
+ airloop.thermalZones.sort.each do |thermalzone|
1038
+ tz_mult = thermalzone.multiplier.to_f
1039
+ thermalzone.equipment.sort.each do |eq|
1040
+ tz_eq_cost = 0
1041
+ terminal, box_name = get_airloop_terminal_type(eq: eq)
1042
+ next if box_name.nil?
1043
+ if terminal.isMaximumAirFlowRateAutosized.to_bool
1044
+ query = "SELECT Value FROM ComponentSizes WHERE CompName='#{eq.name.to_s.upcase}' AND Description='Design Size Maximum Air Flow Rate'"
1045
+ air_m3_per_s = model.sqlFile().get().execAndReturnFirstDouble(query).to_f/tz_mult
1046
+ else
1047
+ air_m3_per_s = terminal.maximumAirFlowRate.to_f/tz_mult
1048
+ end
1049
+ tz_centroids = prototype_creator.thermal_zone_get_centroid_per_floor(thermalzone)
1050
+ reheat_recool_tags << thermalzone.name.to_s
1051
+ if box_name == 'CVMixingBoxes'
1052
+ reheat_recool_tags << "Contant Volume Mixing Box" unless vent_tags.empty?
1053
+ tz_eq_cost, box_info = reheat_coil_costing(terminal: terminal, tz_centroids: tz_centroids, model: model, tz: thermalzone, roof_cent: roof_cent, tz_mult: tz_mult, mech_sizing_info: mech_sizing_info, air_m3_per_s: air_m3_per_s, box_name: box_name, vent_tags: reheat_recool_tags, report_mult: (tz_mult*report_mult))
1054
+ reheat_recool_tags.pop()
1055
+ else
1056
+ reheat_recool_tags << "VAV" unless vent_tags.empty?
1057
+ tz_eq_cost, box_info = vav_cost(terminal: terminal, tz_centroids: tz_centroids, tz: thermalzone, roof_cent: roof_cent, mech_sizing_info: mech_sizing_info, air_flow_m3_per_s: air_m3_per_s, box_name: box_name, vent_tags: reheat_recool_tags, report_mult: (tz_mult*report_mult))
1058
+ reheat_recool_tags.pop()
1059
+ end
1060
+ reheat_recool_tags.pop()
1061
+ heat_cost += tz_mult*tz_eq_cost
1062
+ out_reheat_array << {
1063
+ terminal: (terminal.iddObjectType.valueName.to_s)[3..-1],
1064
+ zone_mult: tz_mult,
1065
+ box_type: box_name,
1066
+ box_name: terminal.nameString,
1067
+ unit_info: box_info,
1068
+ cost: tz_eq_cost.round(2)
1069
+ }
1070
+ end
1071
+ end
1072
+ return heat_cost, out_reheat_array
1073
+ end
1074
+
1075
+ def get_airloop_terminal_type(eq:)
1076
+ case eq.iddObject.name
1077
+ when /OS:AirTerminal:SingleDuct:ConstantVolume:Reheat/
1078
+ terminal = eq.to_AirTerminalSingleDuctConstantVolumeReheat.get
1079
+ box_name = 'CVMixingBoxes'
1080
+ when /OS:AirTerminal:SingleDuct:VAV:NoReheat/
1081
+ terminal = eq.to_AirTerminalSingleDuctVavNoReheat.get
1082
+ box_name = 'VAVFanMixingBoxesClg'
1083
+ when /OS:AirTerminal:SingleDuct:VAV:Reheat/
1084
+ terminal = eq.to_AirTerminalSingleDuctVAVReheat.get
1085
+ box_name = 'VAVFanMixingBoxesHtg'
1086
+ when /OS:AirTerminal:SingleDuct:ConstantVolume:NoReheat/
1087
+ terminal = eq.to_AirTerminalSingleDuctConstantVolumeNoReheat.get
1088
+ box_nam = nil
1089
+ else
1090
+ terminal = nil
1091
+ box_name = nil
1092
+ end
1093
+ return terminal, box_name
1094
+ end
1095
+
1096
+ def reheat_coil_costing(terminal:, tz_centroids:, model:, tz:, roof_cent:, tz_mult:, mech_sizing_info:, air_m3_per_s:, box_name:, vent_tags: [], report_mult: 1.0)
1097
+ coil_tags = vent_tags.clone
1098
+ coil_mat = 'none'
1099
+ coil_cost = 0
1100
+ coil = terminal.reheatCoil
1101
+ case coil.iddObject.name
1102
+ when /Water/
1103
+ coil = coil.to_CoilHeatingWater.get
1104
+ if coil.isRatedCapacityAutosized
1105
+ capacity = coil.autosizedRatedCapacity.to_f/(1000.0*tz_mult)
1106
+ else
1107
+ capacity = coil.ratedCapacity.to_f/(1000.0*tz_mult)
1108
+ end
1109
+ coil_mat = 'Coils'
1110
+ coil_tags << "water coil" unless coil_tags.empty?
1111
+ when /Electric/
1112
+ coil = coil.to_CoilHeatingElectric.get
1113
+ if coil.isNominalCapacityAutosized.to_bool
1114
+ capacity = (coil.autosizedNominalCapacity.to_f)/(1000.0*tz_mult)
1115
+ else
1116
+ capacity = (coil.nominalCapacity.to_f)/(1000.0*tz_mult)
1117
+ end
1118
+ coil_mat = 'ElecDuct'
1119
+ coil_tags << "electric duct heater" unless coil_tags.empty?
1120
+ end
1121
+ return 0, {size_kw: 0.0, air_flow_m3_per_s: 0.0, pipe_dist_m: 0.0, elect_dist_m: 0.0, num_units: 0} if coil_mat == 'none'
1122
+ pipe_length_m = 0
1123
+ elect_length_m = 0
1124
+ num_coils = 0
1125
+ tz_centroids.sort.each do |tz_cent|
1126
+ coil_tags << tz_cent[:story_name]
1127
+ story_floor_area = 0
1128
+ num_coils += 1
1129
+ tz_cent[:spaces].each { |space| story_floor_area += space.floorArea.to_f }
1130
+ floor_area_frac = (story_floor_area/tz.floorArea).round(2)
1131
+ floor_cap = floor_area_frac*capacity
1132
+ coil_cost += get_mech_costing(mech_name: coil_mat, size: floor_cap, terminal: terminal, vent_tags: coil_tags, report_mult: report_mult)
1133
+ coil_cost += get_mech_costing(mech_name: box_name, size: floor_area_frac*(OpenStudio.convert(air_m3_per_s, 'm^3/s', 'cfm').get), terminal: terminal, vent_tags: coil_tags, report_mult: report_mult)
1134
+ ut_dist = (tz_cent[:centroid][0].to_f - roof_cent[:roof_centroid][0].to_f).abs + (tz_cent[:centroid][1].to_f - roof_cent[:roof_centroid][1].to_f).abs
1135
+ if coil_mat == 'Coils'
1136
+ pipe_length_m += ut_dist
1137
+ coil_cost += piping_cost(pipe_dist_m: ut_dist, mech_sizing_info: mech_sizing_info, air_m3_per_s: air_m3_per_s, vent_tags: coil_tags, report_mult: report_mult)
1138
+ end
1139
+ elect_length_m += ut_dist
1140
+ coil_cost += vent_box_elec_cost(cond_dist_m: ut_dist, vent_tags: coil_tags, report_mult: report_mult)
1141
+ coil_tags.pop()
1142
+ end
1143
+ box_info = {size_kw: capacity.round(3), air_flow_m3_per_s: air_m3_per_s.round(3), pipe_dist_m: pipe_length_m.round(1), elect_dist_m: elect_length_m.round(1), num_units: num_coils}
1144
+ return coil_cost, box_info
1145
+ end
1146
+
1147
+ def vav_cost(terminal:, tz_centroids:, tz:, roof_cent:, mech_sizing_info:, air_flow_m3_per_s:, box_name:, vent_tags: [], report_mult: 1.0)
1148
+ cost = 0
1149
+ pipe_length_m = 0
1150
+ elect_length_m = 0
1151
+ num_coils = 0
1152
+ tz_centroids.sort.each do |tz_cent|
1153
+ vav_tags = vent_tags.clone
1154
+ vav_tags << tz_cent[:story_name] unless vav_tags.empty?
1155
+ num_coils += 1
1156
+ story_floor_area = 0
1157
+ tz_cent[:spaces].each { |space| story_floor_area += space.floorArea.to_f }
1158
+ floor_area_frac = (story_floor_area/tz.floorArea).round(2)
1159
+ cost += get_mech_costing(mech_name: box_name, size: floor_area_frac*(OpenStudio.convert(air_flow_m3_per_s, 'm^3/s', 'cfm').get), terminal: terminal, vent_tags: vav_tags, report_mult: report_mult)
1160
+ ut_dist = (tz_cent[:centroid][0].to_f - roof_cent[:roof_centroid][0].to_f).abs + (tz_cent[:centroid][1].to_f - roof_cent[:roof_centroid][1].to_f).abs
1161
+ if /Htg/.match(box_name)
1162
+ pipe_length_m += ut_dist
1163
+ cost += piping_cost(pipe_dist_m: ut_dist, mech_sizing_info: mech_sizing_info, air_m3_per_s: floor_area_frac*air_flow_m3_per_s, vent_tags: vav_tags, report_mult: report_mult)
1164
+ end
1165
+ elect_length_m += ut_dist
1166
+ cost += vent_box_elec_cost(cond_dist_m: ut_dist, vent_tags: vav_tags, report_mult: report_mult)
1167
+ end
1168
+ box_info = {size_kw: 0.0, air_flow_m3_per_s: air_flow_m3_per_s.round(3), pipe_dist_m: pipe_length_m.round(1), elect_dist_m: elect_length_m.round(1), num_units: num_coils}
1169
+ return cost, box_info
1170
+ end
1171
+
1172
+ # This method gets the cost of a piece of equipment. I takes the following in:
1173
+ # mech_name: The category or type of equipment that is being searched for in the 'Material' column of the
1174
+ # 'materials_hvac' sheet of the costing spreadsheet.
1175
+ # size: The size of the piece of equipment being searched for.
1176
+ # terminal: The openstudio object being costed (used to let the user know if there is an issue finding costing info).
1177
+ # mult: A switch which is used to determine if you want to cost multiple pieces of equipment. If it is set to true
1178
+ # (the default) then if a piece of equipment is too large to be costed, then multiple smaller pieces of equipment will
1179
+ # be costed. If it is set to false, then only 1 of the largest piece of equipment will be costed.
1180
+ def get_mech_costing(mech_name:, size:, terminal:, use_mult: true, vent_tags: [], report_mult: 1.0)
1181
+ mech_cost_tags = vent_tags.clone
1182
+ # Turn the input into something that the get_vent_cost_data method can use.
1183
+ mech_info = {
1184
+ cat_search: mech_name,
1185
+ mech_capacity_kw: size,
1186
+ supply_component: terminal
1187
+ }
1188
+ # Get the costing information and multiplier (if the piece of equipment is too large) for the equipment.
1189
+ mech_mult, cost_info = get_vent_cost_data(equipment_info: mech_info)
1190
+ # Use only one piece of equipment if use_mult is set to false
1191
+ mech_mult = 1.0 unless use_mult
1192
+ # Return the total cost for the piece of equipment.
1193
+ return get_vent_mat_cost(mat_cost_info: cost_info, vent_tags: mech_cost_tags, report_mult: (mech_mult*report_mult))*mech_mult
1194
+ end
1195
+
1196
+ def piping_cost(pipe_dist_m:, mech_sizing_info:, air_m3_per_s:, is_cool: false, vent_tags: [], report_mult: 1.0)
1197
+ piping_tags = vent_tags.clone
1198
+ piping_tags << "piping" unless piping_tags.nil?
1199
+ pipe_dist = OpenStudio.convert(pipe_dist_m, 'm', 'ft').get
1200
+ air_flow = (OpenStudio.convert(air_m3_per_s, 'm^3/s', 'L/s').get)
1201
+ air_flow = 15000 if air_flow > 15000
1202
+ mech_table = get_mech_table(mech_size_info: mech_sizing_info, table_name: 'piping')
1203
+ pipe_sz_info = mech_table.select {|pipe_choice|
1204
+ pipe_choice['ahu_airflow_range_Literpers'][0].to_f.round(0) < air_flow.round(0) and
1205
+ pipe_choice['ahu_airflow_range_Literpers'][1].to_f.round(0) >= air_flow.round(0)
1206
+ }.first
1207
+ pipe_dia = pipe_sz_info['heat_valve_pipe_dia_inch'].to_f.round(2)
1208
+ pipe_dia = pipe_sz_info['cool_valve_pipe_dia_inch'].to_f.round(2) if is_cool == true
1209
+ pipe_cost_search = []
1210
+ pipe_cost_search << {
1211
+ mat: 'Steelpipe',
1212
+ unit: 'L.F.',
1213
+ size: pipe_dia,
1214
+ mult: 2*pipe_dist
1215
+ }
1216
+ pipe_cost_search << {
1217
+ mat: 'SteelPipeElbow',
1218
+ unit: 'none',
1219
+ size: pipe_dia,
1220
+ mult: 2
1221
+ }
1222
+ pipe_cost_search << {
1223
+ mat: 'SteelPipeTee',
1224
+ unit: 'none',
1225
+ size: pipe_dia,
1226
+ mult: 2
1227
+ }
1228
+ pipe_cost_search << {
1229
+ mat: 'SteelPipeTeeRed',
1230
+ unit: 'none',
1231
+ size: pipe_dia,
1232
+ mult: 2
1233
+ }
1234
+ pipe_cost_search << {
1235
+ mat: 'SteelPipeRed',
1236
+ unit: 'none',
1237
+ size: pipe_dia,
1238
+ mult: 2
1239
+ }
1240
+ pipe_dia > 3 ? pipe_dia_union = 3 : pipe_dia_union = pipe_dia
1241
+ pipe_cost_search << {
1242
+ mat: 'SteelPipeUnion',
1243
+ unit: 'none',
1244
+ size: pipe_dia_union,
1245
+ mult: 2
1246
+ }
1247
+ return get_comp_cost(cost_info: pipe_cost_search, vent_tags: piping_tags, report_mult: report_mult)
1248
+ end
1249
+
1250
+ def vent_box_elec_cost(cond_dist_m:, vent_tags: [], report_mult: 1.0)
1251
+ elec_tags = vent_tags.clone
1252
+ elec_tags << "electrical" unless elec_tags.empty?
1253
+ cond_dist = OpenStudio.convert(cond_dist_m, 'm', 'ft').get
1254
+ elec_cost_search = []
1255
+ elec_cost_search << {
1256
+ mat: 'Wiring',
1257
+ unit: 'CLF',
1258
+ size: 14,
1259
+ mult: cond_dist/100
1260
+ }
1261
+ elec_cost_search << {
1262
+ mat: 'Conduit',
1263
+ unit: 'L.F.',
1264
+ size: 0,
1265
+ mult: cond_dist
1266
+ }
1267
+ elec_cost_search << {
1268
+ mat: 'Box',
1269
+ unit: 'none',
1270
+ size: 4,
1271
+ mult: 1
1272
+ }
1273
+ elec_cost_search << {
1274
+ mat: 'Box',
1275
+ unit: 'none',
1276
+ size: 1,
1277
+ mult: 1
1278
+ }
1279
+ return get_comp_cost(cost_info: elec_cost_search, vent_tags: elec_tags, report_mult: report_mult)
1280
+ end
1281
+
1282
+ def get_comp_cost(cost_info:, vent_tags: [], report_mult: 1.0)
1283
+ vent_comp_tags = vent_tags.clone
1284
+ cost = 0
1285
+ cost_info.each do |comp|
1286
+ comp_info = nil
1287
+ if comp[:unit].to_s == 'none'
1288
+ comp_info = @costing_database['raw']['materials_hvac'].select {|data|
1289
+ data['Material'].to_s.upcase == comp[:mat].to_s.upcase and
1290
+ data['Size'].to_f.round(2) == comp[:size].to_f.round(2)
1291
+ }.first
1292
+ elsif comp[:size].to_f == 0
1293
+ comp_info = @costing_database['raw']['materials_hvac'].select {|data|
1294
+ data['Material'].to_s.upcase == comp[:mat].to_s.upcase and
1295
+ data['unit'].to_s.upcase == comp[:unit].to_s.upcase
1296
+ }.first
1297
+ else
1298
+ comp_info = @costing_database['raw']['materials_hvac'].select {|data|
1299
+ data['Material'].to_s.upcase == comp[:mat].to_s.upcase and
1300
+ data['Size'].to_f.round(2) == comp[:size].to_f.round(2) and
1301
+ data['unit'].to_s.upcase == comp[:unit].to_s.upcase
1302
+ }.first
1303
+ end
1304
+ if comp_info.nil?
1305
+ puts("No data found for #{comp}!")
1306
+ raise
1307
+ end
1308
+ # report_mult included for cost list output.
1309
+ cost += get_vent_mat_cost(mat_cost_info: comp_info, vent_tags: vent_comp_tags, report_mult: (comp[:mult].to_f*report_mult))*(comp[:mult].to_f)
1310
+ end
1311
+ return cost
1312
+ end
1313
+
1314
+ def get_mech_table(mech_size_info:, table_name:)
1315
+ table = mech_size_info.select {|hash|
1316
+ hash['component'].to_s.upcase == table_name.to_s.upcase
1317
+ }.first
1318
+ return table['table']
1319
+ end
1320
+
1321
+ # This method finds the centroid of the ceiling line on a given story furthest from the specified point. It only
1322
+ # takes into account ceilings that above conditioned spaces that are not plenums. A line can be defined between the
1323
+ # supplied point (we'll call it point O) and the ceiling line centroid furthest from that point(we'll call it point A).
1324
+ # We will call this line AO. If the full_length input argument is set to true the method will also return the point
1325
+ # where line AO intercepts the ceiling line on the other side of the building. Note that the method only looks at x
1326
+ # and y coordinates and ignores the z coordinate of the point you pass it. The method assumes that the ceilings of
1327
+ # all of the spaces on the floor you pass it are flat so generally ignores their z components as well. This was done
1328
+ # to avoid further complicating things with 3D geometry. If the ceilings of all of the spaces in the building story
1329
+ # you pass the method are not flat it will still work but pretend as though the ceilings are flat by ignoring the z
1330
+ # coordinate.
1331
+ #
1332
+ # The method works by going through each space in the supplied building story and finding the ones which are
1333
+ # conditioned (either heated or cooled) and which are not considered plenums. It then goes through the surfaces of
1334
+ # the conditioned spaces and finds the ones which have an OpenStudio SurfaceType of 'RoofCeiling'. It then goes
1335
+ # through each point on that surface and makes lines going from the current point (CP) to the previous point (PP). It
1336
+ # calculates the centroid (LC) of the line formed between PP and CP by averaging each coordinate of PP and CP. It then
1337
+ # determines which LC is furthest from the supplied point (point O) and this becomes point A. Note that point A is not
1338
+ # necessarily on the outside of a building since no checks are made on where line P lies in the building (only that it
1339
+ # is on a RoofCeiling above a conditioned space that is not a plenum). For example in the LargeOffice building
1340
+ # archetype point P generally lies on one of the short edges of the trapezoids forming the perimeter spaces. This is
1341
+ # if this reference point (O) is the center of the building.
1342
+ #
1343
+ # The inputs arguments are are:
1344
+ # building_story: OpenStudio BuildingStory object. A building story defined in OpenStudio.
1345
+ # prototype_creator: The Openstudio-standards object, containing all of the methods etc. in the nrcan branch of
1346
+ # Openstudio-standards.
1347
+ # target_cent: Array. The point you supply from which you want to find the furthest ceiling line centroid (point O
1348
+ # in the description above). This point should be a one dimensional array containing at least two
1349
+ # elements target_cent[0] = x, target_cent[1] = y. The array can have more points but they will be
1350
+ # ignored. This point should be inside the building.
1351
+ # tol: Float. The tolerence used by the method when rounding geometry (default is 8 digits after decimal).
1352
+ # full_length: Boolean true/false
1353
+ # The switch which tells the method whether or not it should find, and supply, the point where line AO (
1354
+ # as defined above) intercepts the other side of the building. It is defaulted to false, meaning it
1355
+ # will only return points A and O. If it set to 'true' it will return the point where line AO
1356
+ # intercepts the other side of the building. It does this by going through all of the ceiling lines
1357
+ # in the specified building story and determining if any intercept line AO (let us call each intercepts
1358
+ # point C). It then runs through each intercept (point C) and determines which C makes line AOC the
1359
+ # longest.
1360
+ #
1361
+ # The output is the following hash.
1362
+ #
1363
+ # {
1364
+ # start_point: Hash. A hash which defines point A and provides a bunch of other information (see below),
1365
+ # mid_point: Hash. This is a hash containing the array defining the point you passed the method in the first
1366
+ # place.,
1367
+ # end_point: Hash. If full_length was set to true then this defines point C and provides a bunch of other
1368
+ # information (see below). If full_length was not set to false or undefined then this is set to nil.
1369
+ #
1370
+ # The structure of the hashes start_point and end_point are identical. I will only define the hash start_point below
1371
+ # noting differences for end_point.
1372
+ #
1373
+ # start_point: {
1374
+ # space: OpenStudio Space object. The space that contains point A (or point C if in the end_point hash).,
1375
+ # surface: OpenStudio Surface object. The surface in space that contains point A (should have a RoofCeiling
1376
+ # SpaceType). In the case of the end_point hash this is the surface that contains point C.,
1377
+ # verts: Two dimmensional array. The points defining ':surface'. These points are in the building coordinate
1378
+ # system (rather than the space coordinate system). These points are ordered clockwise when viewed with the
1379
+ # surface normal pointed towards the viewer. The array would be structured as follows:
1380
+ # [1st point, 2nd point, ..., last point]. Each point is an array as follows: [x coord, y coord, z coord].
1381
+ # The points are in meters.,
1382
+ # line: Hash. A hash defining the line containing point A (point C if this is in the 'end_point' hash). See
1383
+ # definition below.
1384
+ # }
1385
+ #
1386
+ # 'line' has the identical structure in the start_point and end_point hashes. I will define it once but note any
1387
+ # differences for when it is containing in the start_point and end_point hashes.
1388
+ #
1389
+ # line: {
1390
+ # verta: Array. The end point of the line containing point A (when in the start_point hash) or point C (when in
1391
+ # the end_point hash). It is formed as [x, y, z]. It is in the building coordinate system, in meters.
1392
+ # ventb: Array. The start point of the line containing point A (when in the start_point hash) or point C (when in
1393
+ # the end_point hash). It is formed as [x, y, z]. It is in the building coordinate system, in meters.
1394
+ # int: Array. If this is in the start_point hash then this is the centre of the line from vertb to verta. If this
1395
+ # is in the end_point hash then this is the intercept of the line AO with the line starting with vertb and
1396
+ # ending with verta. It is formed as [x, y, z]. It is in the building coordinate system, in meters. If in
1397
+ # the start_point hash then the z coordinate is the average of the z coordinates of verta and vertb. If in
1398
+ # the end_point hash then the z coordinate is calculated by first determining of the distance of the line
1399
+ # between vertb and verta when only using their x and y coordinates (we will call it the xy_dist). Then the
1400
+ # distance from just the x and y coordinates of ventb to the x and y coordinates (the only ones provided) of
1401
+ # point C is determined (we will call it the c_dist). The fraction c_dist/xy_dist is then found and added to
1402
+ # the z coordinate of ventb thus providing the z coordinate of point C.
1403
+ # i: Integer. The index of verta in the verts array.
1404
+ # ip: Integer. The index of vertb in the verts array.
1405
+ # dist: If in the start_point hash this is the distance between point A and point O using only the x and y
1406
+ # coordinates of the respective points. If in the end_point hash this is the distance between point A and
1407
+ # point C using only the x and y coordinates of the respective points. In meters.
1408
+ # }
1409
+ #
1410
+ def get_story_cent_to_edge(building_story:, prototype_creator:, target_cent:, tol: 8, full_length: false)
1411
+ ceiling_start = []
1412
+ space_mod = OpenstudioStandards::Space
1413
+ building_story.spaces.sort.each do |space|
1414
+ if (space_mod.space_heated?(space) || space_mod.space_cooled?(space)) && !space_mod.space_plenum?(space)
1415
+ origin = [space.xOrigin.to_f, space.yOrigin.to_f, space.zOrigin.to_f]
1416
+ space.surfaces.each do |surface|
1417
+ if surface.surfaceType.to_s.upcase == 'ROOFCEILING'
1418
+ verts = surface.vertices
1419
+ dists = []
1420
+ surf_verts = []
1421
+ for index in 1..verts.length
1422
+ index == verts.length ? i = 0 : i = index
1423
+ i == 0 ? ip = verts.length - 1 : ip = i - 1
1424
+ verta = [verts[i].x.to_f + origin[0], verts[i].y.to_f + origin[1], verts[i].z.to_f + origin[2]]
1425
+ vertb = [verts[ip].x.to_f + origin[0], verts[ip].y.to_f + origin[1], verts[ip].z.to_f + origin[2]]
1426
+ cent = [(verta[0] + vertb[0])/2.0 , (verta[1] + vertb[1])/2.0, (verta[2] + vertb[2])/2.0]
1427
+ dist = Math.sqrt((target_cent[0].to_f - cent[0])**2 + (target_cent[1].to_f - cent[1])**2)
1428
+ dists << {
1429
+ verta: verta,
1430
+ vertb: vertb,
1431
+ int: cent,
1432
+ i: i,
1433
+ ip: ip,
1434
+ dist: dist
1435
+ }
1436
+ surf_verts << vertb
1437
+ end
1438
+ max_dist = dists.max_by{|dist_el| dist_el[:dist].to_f}
1439
+ ceiling_start << {
1440
+ space: space,
1441
+ surface: surface,
1442
+ verts: surf_verts,
1443
+ line: max_dist
1444
+ }
1445
+ end
1446
+ end
1447
+ end
1448
+ end
1449
+
1450
+ return nil if ceiling_start.empty?
1451
+
1452
+ furthest_line = ceiling_start.max_by{|wall| wall[:line][:dist].to_f}
1453
+
1454
+ return {start_point: furthest_line, mid_point: target_cent, end_point: nil} unless full_length
1455
+
1456
+ x_dist_ref = (furthest_line[:line][:int][0].round(tol) - target_cent[0].round(tol))
1457
+ x_dist_ref == 1 if x_dist_ref == 0
1458
+ y_dist_ref = (furthest_line[:line][:int][1].round(tol) - target_cent[1].round(tol))
1459
+ y_dist_ref == 1 if y_dist_ref == 0
1460
+ x_side_ref = x_dist_ref/x_dist_ref.abs
1461
+ y_side_ref = y_dist_ref/y_dist_ref.abs
1462
+ linea_eq = get_line_eq(a: target_cent, b: furthest_line[:line][:int], tol: tol)
1463
+ ints = []
1464
+ ceiling_start.each do |side|
1465
+ verts = side[:verts]
1466
+ for index in 1..(verts.length)
1467
+ index == verts.length ? i = 0 : i = index
1468
+ i == 0 ? ip = verts.length-1 : ip = i - 1
1469
+ lineb = [verts[i], verts[ip]]
1470
+ int = line_int(line_seg: lineb, line: linea_eq, tol: tol)
1471
+ next if int.nil?
1472
+ x_dist = (int[0].round(tol) - target_cent[0].round(tol))
1473
+ x_dist = 1 if x_dist == 0
1474
+ y_dist = (int[1].round(tol) - target_cent[1].round(tol))
1475
+ y_dist = 1 if y_dist == 0
1476
+ x_side = x_dist/x_dist.abs
1477
+ y_side = y_dist/y_dist.abs
1478
+ next if x_side == x_side_ref && y_side == y_side_ref
1479
+ ceil_dist = Math.sqrt((furthest_line[:line][:int][0] - int[0])**2 + (furthest_line[:line][:int][1] - int[1])**2)
1480
+ int_dist = Math.sqrt((int[0] - verts[ip][0])**2 + (int[1] - verts[ip][1])**2)
1481
+ line_dist = Math.sqrt((verts[i][0] - verts[ip][0])**2 + (verts[i][1] - verts[ip][1])**2)
1482
+ z_coord = verts[ip][2] + ((verts[i][2] - verts[ip][2])*int_dist/line_dist)
1483
+ ints << {
1484
+ ceiling_info: side,
1485
+ line: lineb,
1486
+ int: [int[0], int[1], z_coord],
1487
+ i: i,
1488
+ ip: ip,
1489
+ dist: ceil_dist
1490
+ }
1491
+ end
1492
+ end
1493
+
1494
+ return nil if ints.empty?
1495
+ end_wall = ints.max_by{|wall| wall[:dist].to_f}
1496
+ return {
1497
+ start_point: furthest_line,
1498
+ mid_point: target_cent,
1499
+ end_point: {
1500
+ space: end_wall[:ceiling_info][:space],
1501
+ surface: end_wall[:ceiling_info][:surface],
1502
+ verts: end_wall[:ceiling_info][:verts],
1503
+ line: {
1504
+ verta: end_wall[:line][0],
1505
+ vertb: end_wall[:line][1],
1506
+ int: end_wall[:int],
1507
+ i: end_wall[:i],
1508
+ ip: end_wall[:ip],
1509
+ dist: end_wall[:dist]
1510
+ },
1511
+ }
1512
+ }
1513
+ end
1514
+
1515
+ def get_line_eq(a:, b:, tol: 8)
1516
+ if a[0].round(tol) == b[0].round(tol) and a[1].round(tol) == b[1].round(tol)
1517
+ return {
1518
+ slope: 0,
1519
+ int: 0,
1520
+ inf: true
1521
+ }
1522
+ elsif a[0].round(tol) == b[0].round(tol)
1523
+ return {
1524
+ slope: a[0].round(tol),
1525
+ int: 1,
1526
+ inf: true
1527
+ }
1528
+ else
1529
+ slope = (b[1].round(tol) - a[1].round(tol))/(b[0].round(tol) - a[0].round(tol))
1530
+ int = a[1].round(tol) - (slope*a[0].round(tol))
1531
+ end
1532
+ return {
1533
+ slope: slope,
1534
+ int: int,
1535
+ inf: false
1536
+ }
1537
+ end
1538
+
1539
+ def line_int(line_seg:, line:, tol: 8)
1540
+ line[:inf] == true && line[:int] == 1 ? x_cross = line[:slope] : x_cross = nil
1541
+ if line_seg[0][0].round(tol) == line_seg[1][0].round(tol) && line_seg[0][1].round(tol) == line_seg[1][1].round(tol)
1542
+ if x_cross.nil?
1543
+ y_val = line[:slope]*line_seg[0][0] + line[:int]
1544
+ y_val.round(tol) == line_seg[0][1].round(tol) ? (return line_seg[0]) : (return nil)
1545
+ else
1546
+ x_cross.round(tol) == line_seg[0][0].round(tol) ? (return line_seg[0]) : (return nil)
1547
+ end
1548
+ elsif line_seg[0][0].round(tol) == line_seg[1][0]
1549
+ if x_cross.nil?
1550
+ y_val = line[:slope]*line_seg[0][0] + line[:int]
1551
+ if (line_seg[0][1].round(tol) >= y_val.round(tol) && y_val.round(tol) >= line_seg[1][1].round(tol)) ||
1552
+ (line_seg[0][1].round(tol) <= y_val.round(tol) && y_val.round(tol) <= line_seg[1][1].round(tol))
1553
+ return [line_seg[0][0] , y_val, line_seg[0][2]]
1554
+ else
1555
+ return nil
1556
+ end
1557
+ else
1558
+ if x_cross.round(tol) == line_seg[0][0]
1559
+ y_val = (line_seg[0][1] + line_seg[1][1])/2
1560
+ return [line_seg[0][0] , y_val, line_seg[0][2]]
1561
+ else
1562
+ return nil
1563
+ end
1564
+ end
1565
+ end
1566
+ lineb = get_line_eq(a: line_seg[0], b: line_seg[1], tol: tol)
1567
+ if lineb[:slope].round(tol) == 0 && line[:slope].round(tol) == 0
1568
+ if x_cross.nil?
1569
+ if lineb[:int].round(tol) == line[:int].round(tol)
1570
+ x_val = (line_seg[0][0] + line_seg[1][0])/2
1571
+ return [x_val, lineb[:slope], line_seg[0][2]]
1572
+ else
1573
+ return nil
1574
+ end
1575
+ else
1576
+ if (line_seg[0][0].round(tol) <= x_cross.round(tol) && x_cross.round(tol) <= line_seg[1][0].round(tol)) ||
1577
+ (line_seg[0][0].round(tol) >= x_cross.round(tol) && x_cross.round(tol) >= line_seg[1][0].round(tol))
1578
+ [x_cross, lineb[:slope]]
1579
+ else
1580
+ return nil
1581
+ end
1582
+ end
1583
+ end
1584
+ unless x_cross.nil?
1585
+ if (line_seg[0][0].round(tol) <= x_cross.round(tol) && x_cross.round(tol) <= line_seg[1][0].round(tol)) ||
1586
+ (line_seg[0][0].round(tol) >= x_cross.round(tol) && x_cross.round(tol) >= line_seg[1][0].round(tol))
1587
+ y_val = lineb[:slope]*x_cross + lineb[:int]
1588
+ return [x_cross , y_val, line_seg[0][2]]
1589
+ else
1590
+ return nil
1591
+ end
1592
+ end
1593
+ if lineb[:inf] == true && lineb[:int] == 1
1594
+ x_int = lineb[:slope]
1595
+ y_int = line[:slope].to_f*x_int + line[:int].to_f
1596
+ else
1597
+ x_int = (lineb[:int].to_f - line[:int].to_f)/(line[:slope].to_f - lineb[:slope].to_f)
1598
+ y_int = lineb[:slope].to_f*x_int + lineb[:int].to_f
1599
+ end
1600
+ if (line_seg[0][0].round(tol) <= x_int.round(tol) && x_int.round(tol) <= line_seg[1][0].round(tol)) ||
1601
+ (line_seg[0][0].round(tol) >= x_int.round(tol) && x_int.round(tol) >= line_seg[1][0].round(tol))
1602
+ if (line_seg[0][1].round(tol) >= y_int.round(tol) && y_int.round(tol) >= line_seg[1][1].round(tol)) ||
1603
+ (line_seg[0][1].round(tol) <= y_int.round(tol) && y_int.round(tol) <= line_seg[1][1].round(tol))
1604
+ return [x_int, y_int, line_seg[0][2]]
1605
+ end
1606
+ end
1607
+ return nil
1608
+ end
1609
+
1610
+ def line_seg_int(linea:, lineb:, tol: 8)
1611
+ if linea[0][0].round(tol) == lineb[0][0].round(tol) && linea[0][1].round(tol) == lineb[0][1].round(tol) &&
1612
+ linea[1][0].round(tol) == lineb[1][0].round(tol) && linea[1][1].round(tol) == lineb[1][1].round(tol)
1613
+ return [(linea[0][0] + linea[1][0])/2 , (linea[0][1] + linea[1][1])/2]
1614
+ elsif linea[0][0].round(tol) == linea[1][0].round(tol) && linea[0][1].round(tol) == linea[1][1].round(tol)
1615
+ return linea[0]
1616
+ elsif lineb[0][0].round(tol) == lineb[1][0].round(tol) && lineb[0][1].round(tol) == lineb[1][1].round(tol)
1617
+ return lineb[0]
1618
+ end
1619
+
1620
+ o1 = get_orient(p: linea[0], q: linea[1], r: lineb[0], tol: tol)
1621
+ o2 = get_orient(p: linea[0], q: linea[1], r: lineb[1], tol: tol)
1622
+ o3 = get_orient(p: lineb[0], q: lineb[1], r: linea[0], tol: tol)
1623
+ o4 = get_orient(p: lineb[0], q: lineb[1], r: linea[1], tol: tol)
1624
+
1625
+ int_sect = 0
1626
+ int_sect = 1 if o1 != o2 && o3 != o4
1627
+ return lineb[0] if o1 == 0 && point_on_line(p: linea[0], q: lineb[0], r: linea[1], tol: tol)
1628
+ return lineb[1] if o2 == 0 && point_on_line(p: linea[0], q: lineb[1], r: linea[1], tol: tol)
1629
+ return linea[0] if o3 == 0 && point_on_line(p: lineb[0], q: linea[0], r: lineb[1], tol: tol)
1630
+ return linea[1] if o4 == 0 && point_on_line(p: lineb[0], q: linea[1], r: lineb[1], tol: tol)
1631
+
1632
+ return nil if int_sect == 0
1633
+
1634
+ eq_linea = get_line_eq(a: linea[0], b: linea[1], tol: tol)
1635
+ eq_lineb = get_line_eq(a: lineb[0], b: lineb[1], tol: tol)
1636
+ if eq_linea[:inf] == true && eq_linea[:slope].to_f == 1
1637
+ x_int = linea[0][0]
1638
+ y_int = eq_lineb[:slope].to_f*x_int + eq_lineb[:int].to_f
1639
+ return [x_int, y_int]
1640
+ elsif eq_lineb[:inf] == true && eq_lineb[:slope].to_f == 1
1641
+ x_int = lineb[0][0]
1642
+ y_int = eq_linea[:slope].to_f*x_int + eq_linea[:int].to_f
1643
+ return [x_int, y_int]
1644
+ else
1645
+ x_int = (eq_lineb[:int].to_f - eq_linea[:int].to_f) / (eq_linea[:slope].to_f - eq_lineb[:slope].to_f)
1646
+ y_int = eq_lineb[:slope].to_f*x_int + eq_lineb[:int].to_f
1647
+ return [x_int, y_int]
1648
+ end
1649
+ end
1650
+
1651
+ def get_orient(p:, q:, r:, tol: 8)
1652
+ orient = (q[1].round(tol) - p[1].round(tol))*(r[0].round(tol) - q[0].round(tol)) - (q[0].round(tol) - p[0].round(tol))*(r[1].round(tol) - q[1].round(tol))
1653
+ return 0 if orient == 0
1654
+ orient > 0 ? (return 1) : (return 2)
1655
+ end
1656
+
1657
+ def point_on_line(p:, q:, r:, tol: 8)
1658
+ q[0].round(tol) <= [p[0].round(tol), r[0].round(tol)].max ? crita = true : crita = false
1659
+ q[0].round(tol) >= [p[0].round(tol), r[0].round(tol)].min ? critb = true : critb = false
1660
+ q[1].round(tol) <= [p[1].round(tol), r[1].round(tol)].max ? critc = true : critc = false
1661
+ q[1].round(tol) >= [p[1].round(tol), r[1].round(tol)].min ? critd = true : critd = false
1662
+ return true if crita && critb && critc && critd
1663
+ return false
1664
+ end
1665
+
1666
+ def get_lowest_space(spaces:)
1667
+ cents = []
1668
+ spaces.each do |space|
1669
+ test = space['space']
1670
+ origin = [space['space'].xOrigin.to_f, space['space'].yOrigin.to_f, space['space'].zOrigin.to_f]
1671
+ space['space'].surfaces.each do |surface|
1672
+ if surface.surfaceType.to_s.upcase == 'ROOFCEILING'
1673
+ cents <<{
1674
+ space: space['space'],
1675
+ roof_cent: [surface.centroid.x.to_f + origin[0], surface.centroid.y.to_f + origin[1], surface.centroid.z.to_f + origin[2]]
1676
+ }
1677
+ end
1678
+ end
1679
+ end
1680
+ min_space = cents.min_by{|cent| cent[:roof_cent][2]}
1681
+ return min_space
1682
+ end
1683
+
1684
+ def vent_trunk_duct_cost(tot_air_m3pers:, min_space:, roof_cent:, mech_sizing_info:, sys_1_4:)
1685
+ sys_1_4 ? overall_mult = 1 : overall_mult = 2
1686
+ duct_cost_search = []
1687
+ mech_table = get_mech_table(mech_size_info: mech_sizing_info, table_name: 'trunk')
1688
+ max_trunk_line = mech_table.max_by {|entry| entry['max_flow_range_m3pers'][0]}
1689
+ tot_air_m3pers = max_trunk_line['max_flow_range_m3pers'][0].to_f.round(2) if tot_air_m3pers.round(2) > max_trunk_line['max_flow_range_m3pers'][1].to_f.round(2)
1690
+ trunk_sz_info = mech_table.select {|trunk_choice|
1691
+ trunk_choice['max_flow_range_m3pers'][0].to_f.round(2) < tot_air_m3pers.round(2) and
1692
+ trunk_choice['max_flow_range_m3pers'][1].to_f.round(2) >= tot_air_m3pers.round(2)
1693
+ }.first
1694
+ duct_dia = trunk_sz_info['duct_dia_inch']
1695
+ duct_length_m = (roof_cent[:roof_centroid][2].to_f - min_space[:roof_cent][2].to_f).abs
1696
+ duct_length = (OpenStudio.convert(duct_length_m, 'm', 'ft').get)
1697
+ duct_cost_search << {
1698
+ mat: 'Ductwork-S',
1699
+ unit: 'L.F.',
1700
+ size: duct_dia,
1701
+ mult: duct_length*overall_mult
1702
+ }
1703
+ duct_area = (duct_dia/12)*Math::PI*duct_length*overall_mult
1704
+ duct_cost_search << {
1705
+ mat: 'Ductinsulation',
1706
+ unit: 'ft2',
1707
+ size: 1.5,
1708
+ mult: duct_area
1709
+ }
1710
+ duct_cost = get_comp_cost(cost_info: duct_cost_search)
1711
+ trunk_duct_info = {
1712
+ DuctSize_in: duct_dia.round(1),
1713
+ DuctLength_m: duct_length_m.round(1),
1714
+ NumberRuns: overall_mult,
1715
+ DuctCost: duct_cost.round(2)
1716
+ }
1717
+ return duct_cost, trunk_duct_info
1718
+ end
1719
+
1720
+ def gen_hvac_info_by_floor(hvac_floors:, model:, prototype_creator:, airloop:, sys_type:, hrv_info:)
1721
+ airloop.thermalZones.sort.each do |tz|
1722
+ tz.equipment.sort.each do |eq|
1723
+ tz_mult = tz.multiplier.to_f
1724
+ terminal, box_name = get_airloop_terminal_type(eq: eq)
1725
+ next if terminal.nil?
1726
+ if terminal.isMaximumAirFlowRateAutosized.to_bool
1727
+ query = "SELECT Value FROM ComponentSizes WHERE CompName='#{eq.name.to_s.upcase}' AND Description='Design Size Maximum Air Flow Rate'"
1728
+ tz_air = model.sqlFile().get().execAndReturnFirstDouble(query).to_f/tz_mult
1729
+ else
1730
+ tz_air = terminal.maximumAirFlowRate.to_f/tz_mult
1731
+ end
1732
+ tz_cents = prototype_creator.thermal_zone_get_centroid_per_floor(tz)
1733
+ tz_cents.each do |tz_cent|
1734
+ story_floor_area = 0
1735
+ tz_outdoor_air_m3ps = 0
1736
+ tz_cent[:spaces].each do |space|
1737
+ # Note that space.floorArea gets the floor area for the space only and does not include a thermal zone multiplier.
1738
+ # Thus the outdoor air flow rate totaled here will be for only one thermal zone and will not include thermal zone multipliers.
1739
+ story_floor_area += space.floorArea.to_f
1740
+ outdoor_air_obj = space.designSpecificationOutdoorAir
1741
+ outdoor_air_obj.is_initialized ? outdoor_air_m3ps = (outdoor_air_obj.get.outdoorAirFlowperFloorArea)*(space.floorArea.to_f) : outdoor_air_m3ps = 0
1742
+ tz_outdoor_air_m3ps += outdoor_air_m3ps
1743
+ end
1744
+ story_obj = tz_cent[:spaces][0].buildingStory.get
1745
+ floor_area_frac = (story_floor_area/tz.floorArea).round(2)
1746
+ tz_floor_air = floor_area_frac*tz_air
1747
+ (sys_type == 1 || sys_type == 4) ? tz_floor_return = 0 : tz_floor_return = tz_floor_air
1748
+ tz_floor_system = {
1749
+ story_name: tz_cent[:story_name],
1750
+ story: story_obj,
1751
+ sys_name: airloop.nameString,
1752
+ sys_type: sys_type,
1753
+ sys_info: airloop,
1754
+ tz: tz,
1755
+ tz_mult: tz_mult,
1756
+ terminal: terminal,
1757
+ floor_area_frac: floor_area_frac,
1758
+ tz_floor_area: story_floor_area,
1759
+ tz_floor_supp_air_m3ps: tz_floor_air,
1760
+ tz_floor_ret_air_m3ps: tz_floor_return,
1761
+ tz_floor_outdoor_air_m3ps: tz_outdoor_air_m3ps,
1762
+ hrv_info: hrv_info,
1763
+ tz_cent: tz_cent
1764
+ }
1765
+ hvac_floors = add_floor_sys(hvac_floors: hvac_floors, tz_floor_sys: tz_floor_system)
1766
+ end
1767
+ end
1768
+ end
1769
+ return hvac_floors
1770
+ end
1771
+
1772
+ def add_floor_sys(hvac_floors:, tz_floor_sys:)
1773
+ if hvac_floors.empty?
1774
+ hvac_floors << {
1775
+ story_name: tz_floor_sys[:story_name],
1776
+ story: tz_floor_sys[:story],
1777
+ supply_air_m3ps: tz_floor_sys[:tz_floor_supp_air_m3ps],
1778
+ return_air_m3ps: tz_floor_sys[:tz_floor_ret_air_m3ps],
1779
+ tz_mult: tz_floor_sys[:tz_mult],
1780
+ tz_num: 1,
1781
+ floor_tz: [tz_floor_sys]
1782
+ }
1783
+ else
1784
+ found_story = false
1785
+ hvac_floors.each do |hvac_floor|
1786
+ if hvac_floor[:story_name].to_s.upcase == tz_floor_sys[:story_name].to_s.upcase
1787
+ hvac_floor[:supply_air_m3ps] += tz_floor_sys[:tz_floor_supp_air_m3ps]
1788
+ hvac_floor[:return_air_m3ps] += tz_floor_sys[:tz_floor_ret_air_m3ps]
1789
+ hvac_floor[:tz_mult] += tz_floor_sys[:tz_mult]
1790
+ hvac_floor[:tz_num] += 1
1791
+ hvac_floor[:floor_tz] << tz_floor_sys
1792
+ found_story = true
1793
+ end
1794
+ end
1795
+ if found_story == false
1796
+ hvac_floors << {
1797
+ story_name: tz_floor_sys[:story_name],
1798
+ story: tz_floor_sys[:story],
1799
+ supply_air_m3ps: tz_floor_sys[:tz_floor_supp_air_m3ps],
1800
+ return_air_m3ps: tz_floor_sys[:tz_floor_ret_air_m3ps],
1801
+ tz_mult: tz_floor_sys[:tz_mult],
1802
+ tz_num: 1,
1803
+ floor_tz: [tz_floor_sys]
1804
+ }
1805
+ end
1806
+ end
1807
+ return hvac_floors
1808
+ end
1809
+
1810
+ def floor_vent_dist_cost(hvac_floors:, prototype_creator:, roof_cent:, mech_sizing_info:)
1811
+ floor_duct_cost = 0
1812
+ build_floor_trunk_info = []
1813
+ mech_table = get_mech_table(mech_size_info: mech_sizing_info, table_name: 'vel_prof')
1814
+ hvac_floors.each do |hvac_floor|
1815
+ next if hvac_floor[:tz_num] < 2 && hvac_floor[:floor_tz][0][:sys_type] == 3
1816
+ tz_floor_mult = (hvac_floor[:tz_mult].to_f)/(hvac_floor[:tz_num].to_f)
1817
+ floor_trunk_line = get_story_cent_to_edge(building_story: hvac_floor[:story], prototype_creator: prototype_creator, target_cent: roof_cent[:roof_centroid], full_length: true)
1818
+ current_floor_duct_cost, floor_trunk_info = get_floor_trunk_cost(mech_table: mech_table, hvac_floor: hvac_floor, prototype_creator: prototype_creator, floor_trunk_dist_m: floor_trunk_line[:end_point][:line][:dist])
1819
+ floor_duct_cost += current_floor_duct_cost*tz_floor_mult
1820
+ floor_trunk_info[:Floor] = hvac_floor[:story_name]
1821
+ floor_trunk_info[:Multiplier] = tz_floor_mult
1822
+ build_floor_trunk_info << floor_trunk_info
1823
+ end
1824
+ return floor_duct_cost, build_floor_trunk_info
1825
+ end
1826
+
1827
+ def get_floor_trunk_cost(mech_table:, hvac_floor:, prototype_creator:, floor_trunk_dist_m:, fric_allow: 1)
1828
+ floor_trunk_info = {
1829
+ Floor: '',
1830
+ Predominant_space_type: 0,
1831
+ SupplyDuctSize_in: 0,
1832
+ SupplyDuctLength_m: 0,
1833
+ ReturnDuctSize_in: 0,
1834
+ ReturnDuctLength_m: 0,
1835
+ TotalDuctCost: 0,
1836
+ Multiplier: 1
1837
+ }
1838
+ floor_trunk_cost = 0
1839
+ duct_comp_search = []
1840
+ floor_trunk_dist = (OpenStudio.convert(floor_trunk_dist_m, 'm', 'ft').get)
1841
+ space_type = get_predominant_floor_space_type_area(hvac_floor: hvac_floor, prototype_creator: prototype_creator)
1842
+ floor_trunk_info[:Predominant_space_type] = space_type[:space_type]
1843
+ loor_vel_fpm = nil
1844
+ mech_table.each do |vel_prof|
1845
+ spc_type_name = nil
1846
+ spc_type_name = vel_prof['space_types'].select {|spc_type|
1847
+ spc_type.to_s.upcase == space_type[:space_type].to_s.upcase
1848
+ }.first
1849
+ floor_vel_fpm = vel_prof['vel_fpm'].to_f unless spc_type_name.nil?
1850
+ end
1851
+ floor_vel_fpm = mech_table[mech_table.size - 1]['vel_fpm'].to_f if floor_vel_fpm.nil?
1852
+ supply_flow_cfm = (OpenStudio.convert(hvac_floor[:supply_air_m3ps], 'm^3/s', 'cfm').get)
1853
+ sup_cross_in2 = ((supply_flow_cfm*fric_allow)/floor_vel_fpm)*144
1854
+ sup_dia_in = 2*Math.sqrt(sup_cross_in2/Math::PI)
1855
+ duct_cost_search = {
1856
+ mat: 'Ductwork-S',
1857
+ unit: 'L.F.',
1858
+ size: sup_dia_in,
1859
+ mult: floor_trunk_dist
1860
+ }
1861
+ duct_cost, comp_info = get_duct_cost(cost_info: duct_cost_search)
1862
+ floor_trunk_info[:SupplyDuctSize_in] = sup_dia_in.round(2)
1863
+ floor_trunk_info[:SupplyDuctLength_m] = floor_trunk_dist_m.round(1)
1864
+ floor_trunk_cost += duct_cost
1865
+ sup_area_sqrft = (comp_info['Size'].to_f/12)*Math::PI*floor_trunk_dist
1866
+ duct_comp_search << {
1867
+ mat: 'Ductinsulation',
1868
+ unit: 'ft2',
1869
+ size: 1.5,
1870
+ mult: sup_area_sqrft
1871
+ }
1872
+ if hvac_floor[:return_air_m3ps] == hvac_floor[:supply_air_m3ps]
1873
+ floor_trunk_cost += duct_cost
1874
+ duct_comp_search[0][:mult] = sup_area_sqrft*2
1875
+ floor_trunk_info[:ReturnDuctSize_in] = floor_trunk_info[:SupplyDuctSize_in]
1876
+ floor_trunk_info[:ReturnDuctLength_m] = floor_trunk_info[:SupplyDuctLength_m]
1877
+ elsif hvac_floor[:return_air_m3ps].to_f > 0
1878
+ return_flow_cfm = (OpenStudio.convert(hvac_floor[:return_air_m3ps], 'm^3/s', 'cfm').get)
1879
+ ret_cross_in2 = ((return_flow_cfm*fric_allow)/floor_vel_fpm)*144
1880
+ ret_dia_in = 2*Math.sqrt(ret_cross_in2/Math::PI)
1881
+ duct_cost_search = {
1882
+ mat: 'Ductwork-S',
1883
+ unit: 'L.F.',
1884
+ size: ret_dia_in,
1885
+ mult: floor_trunk_dist
1886
+ }
1887
+ duct_cost, comp_info = get_duct_cost(cost_info: duct_cost_search)
1888
+ floor_trunk_cost += duct_cost
1889
+ ret_area_sqrft = (comp_info['Size'].to_f/12)*Math::PI*floor_trunk_dist
1890
+ duct_comp_search << {
1891
+ mat: 'Ductinsulation',
1892
+ unit: 'ft2',
1893
+ size: 1.5,
1894
+ mult: ret_area_sqrft
1895
+ }
1896
+ floor_trunk_info[:ReturnDuctSize_in] = ret_dia_in.round(2)
1897
+ floor_trunk_info[:ReturnDuctLength_m] = floor_trunk_dist_m.round(1)
1898
+ end
1899
+ floor_trunk_cost += get_comp_cost(cost_info: duct_comp_search)
1900
+ floor_trunk_info[:TotalDuctCost] = floor_trunk_cost.round(2)
1901
+ return floor_trunk_cost, floor_trunk_info
1902
+ end
1903
+
1904
+ def get_duct_cost(cost_info:)
1905
+ comp_info = nil
1906
+ comp_info_all = @costing_database['raw']['materials_hvac'].select {|data|
1907
+ data['Material'].to_s.upcase == cost_info[:mat].to_s.upcase and
1908
+ data['Size'].to_f.round(1) >= cost_info[:size].to_f.round(1) and
1909
+ data['unit'].to_s.upcase == cost_info[:unit].to_s.upcase
1910
+ }
1911
+ if comp_info_all.nil? || comp_info_all.empty?
1912
+ max_size_info = @costing_database['raw']['materials_hvac'].select {|data|
1913
+ data['Material'].to_s.upcase == cost_info[:mat].to_s.upcase
1914
+ }
1915
+ if max_size_info.nil?
1916
+ puts("No data found for #{cost_info}!")
1917
+ raise
1918
+ end
1919
+ comp_info = max_size_info.max_by {|element| element['Size'].to_f}
1920
+ elsif comp_info_all.size == 1
1921
+ comp_info = comp_info_all[0]
1922
+ else
1923
+ comp_info = comp_info_all.min_by{|data| data['Size'].to_f}
1924
+ end
1925
+ cost = get_vent_mat_cost(mat_cost_info: comp_info)*cost_info[:mult].to_f
1926
+ return cost, comp_info
1927
+ end
1928
+
1929
+ def get_predominant_floor_space_type_area(hvac_floor:, prototype_creator:)
1930
+ spaces = hvac_floor[:story].spaces
1931
+ space_list = []
1932
+ space_mod = OpenstudioStandards::Space
1933
+ spaces.sort.each do |space|
1934
+ if (space_mod.space_cooled?(space) || space_mod.space_heated?(space)) && !space_mod.space_plenum?(space)
1935
+ space_type = space.spaceType.get.nameString[15..-1]
1936
+ if space_list.empty?
1937
+ space_list << {
1938
+ space_type: space_type,
1939
+ floor_area: space.floorArea
1940
+ }
1941
+ else
1942
+ new_space = nil
1943
+ space_list.each do |spc_lst|
1944
+ if space_type.upcase == spc_lst[:space_type]
1945
+ spc_lst[:floor_area] += space.floorArea
1946
+ else
1947
+ new_space = {
1948
+ space_type: space_type,
1949
+ floor_area: space.floorArea
1950
+ }
1951
+ end
1952
+ end
1953
+ unless new_space.nil?
1954
+ space_list << new_space
1955
+ end
1956
+ end
1957
+ end
1958
+ end
1959
+ max_space_type = space_list.max_by {|spc_lst| spc_lst[:floor_area]}
1960
+ return max_space_type
1961
+ end
1962
+
1963
+ def tz_vent_dist_cost(hvac_floors:, mech_sizing_info:)
1964
+ dist_reporting = []
1965
+ vent_dist_cost = 0
1966
+ mech_table = get_mech_table(mech_size_info: mech_sizing_info, table_name: 'tz_dist_info')
1967
+ flexduct_table = get_mech_table(mech_size_info: mech_sizing_info, table_name: 'flex_duct')
1968
+ hvac_floors.each_with_index do |hvac_floor, index|
1969
+ dist_reporting << {
1970
+ Story: hvac_floor[:story_name],
1971
+ thermal_zones: []
1972
+ }
1973
+ hvac_floor[:floor_tz].each do |floor_tz|
1974
+ floor_vent_cost = 0
1975
+ airflow_m3ps = []
1976
+ airflow_m3ps << floor_tz[:tz_floor_supp_air_m3ps]*floor_tz[:floor_area_frac]
1977
+ airflow_m3ps << floor_tz[:tz_floor_ret_air_m3ps]*floor_tz[:floor_area_frac] if floor_tz[:tz_floor_ret_air_m3ps].to_f.round(6) > 0.0
1978
+ airflow_m3ps.each_with_index do |max_air_m3ps, flow_index|
1979
+ # Using max supply air flow rather than breathing zone outdoor airflow. Keep breathing zone outdoor airflow in
1980
+ # case we change our minds.
1981
+ # breathing_zone_outdoor_airflow_vbz= model.sqlFile().get().execAndReturnFirstDouble("SELECT Value FROM TabularDataWithStrings WHERE ReportName='Standard62.1Summary' AND ReportForString='Entire Facility' AND TableName='Zone Ventilation Parameters' AND ColumnName='Breathing Zone Outdoor Airflow - Vbz' AND Units='m3/s' AND RowName='#{tz.nameString.to_s.upcase}' ")
1982
+ # bz_outdoor_airflow_m3_s = breathing_zone_outdoor_airflow_vbz.get unless breathing_zone_outdoor_airflow_vbz.empty?
1983
+ tz_dist_sz = mech_table.select {|size_range|
1984
+ max_air_m3ps > size_range['airflow_m3ps'][0] && max_air_m3ps <= size_range['airflow_m3ps'][1]
1985
+ }
1986
+ if tz_dist_sz.empty?
1987
+ size_range = mech_table[mech_table.size - 1]
1988
+ diffusers = (max_air_m3ps/size_range["diffusers"]).round(0)
1989
+ tz_dist_sz << {
1990
+ "airflow_m3ps" => size_range['airflow_m3ps'],
1991
+ "diffusers" => diffusers,
1992
+ "ducting_lbs" => (diffusers*size_range["ducting_lbs"]).round(0),
1993
+ "duct_insulation_ft2" => (diffusers*size_range["duct_insulation_ft2"]).round(0),
1994
+ "flex_duct_ft" => (diffusers*size_range["flex_duct_ft"]).round(0)
1995
+ }
1996
+ elsif tz_dist_sz[0] == mech_table[mech_table.size - 1]
1997
+ diffusers = (max_air_m3ps/tz_dist_sz[0]['diffusers']).round(0)
1998
+ tz_dist_sz[0] = {
1999
+ "airflow_m3ps" => tz_dist_sz[0]['airflow_m3ps'],
2000
+ "diffusers" => diffusers,
2001
+ "ducting_lbs" => (diffusers*tz_dist_sz[0]['ducting_lbs']).round(0),
2002
+ "duct_insulation_ft2" => (diffusers*tz_dist_sz[0]['duct_insulation_ft2']).round(0),
2003
+ "flex_duct_ft" => (diffusers*tz_dist_sz[0]['flex_duct_ft']).round(0)
2004
+ }
2005
+ end
2006
+ duct_cost_search = []
2007
+ duct_cost_search << {
2008
+ mat: 'Diffusers',
2009
+ unit: 'each',
2010
+ size: 36,
2011
+ mult: tz_dist_sz[0]['diffusers']
2012
+ }
2013
+ if tz_dist_sz[0]["ducting_lbs"] < 200
2014
+ duct_cost_search << {
2015
+ mat: 'Ductwork',
2016
+ unit: 'lb.',
2017
+ size: 199,
2018
+ mult: tz_dist_sz[0]['ducting_lbs']
2019
+ }
2020
+ else
2021
+ duct_cost_search << {
2022
+ mat: 'Ductwork',
2023
+ unit: 'lb.',
2024
+ size: 200,
2025
+ mult: tz_dist_sz[0]['ducting_lbs']
2026
+ }
2027
+ end
2028
+ duct_cost_search << {
2029
+ mat: 'DuctInsulation',
2030
+ unit: 'ft2',
2031
+ size: 1.5,
2032
+ mult: tz_dist_sz[0]['duct_insulation_ft2']
2033
+ }
2034
+ floor_vent_cost = get_comp_cost(cost_info: duct_cost_search)*floor_tz[:tz_mult]
2035
+ flex_duct_sz = flexduct_table.select {|flex_duct|
2036
+ max_air_m3ps > flex_duct['airflow_m3ps'][0] && max_air_m3ps <= flex_duct['airflow_m3ps'][1]
2037
+ }
2038
+ flex_duct_sz << flexduct_table[flexduct_table.size-1] if flex_duct_sz.empty?
2039
+ duct_cost_search = {
2040
+ mat: 'Ductwork-M',
2041
+ unit: 'L.F.',
2042
+ size: flex_duct_sz[0]['diameter_in'],
2043
+ mult: tz_dist_sz[0]['flex_duct_ft']
2044
+ }
2045
+ duct_cost, comp_info = get_duct_cost(cost_info: duct_cost_search)
2046
+ floor_vent_cost += duct_cost*floor_tz[:tz_mult]
2047
+ vent_dist_cost += floor_vent_cost
2048
+ if flow_index == 0
2049
+ flow_dir = 'Supply'
2050
+ else
2051
+ flow_dir = 'Return'
2052
+ end
2053
+ dist_reporting[index][:thermal_zones] << {
2054
+ ThermalZone: floor_tz[:tz].nameString,
2055
+ ducting_direction: flow_dir,
2056
+ tz_mult: floor_tz[:tz_mult],
2057
+ airflow_m3ps: max_air_m3ps.round(3),
2058
+ num_diff: tz_dist_sz[0]['diffusers'],
2059
+ ducting_lbs: tz_dist_sz[0]['ducting_lbs'],
2060
+ duct_insulation_ft2: tz_dist_sz[0]['duct_insulation_ft2'],
2061
+ flex_duct_sz_in: flex_duct_sz[0]['diameter_in'],
2062
+ flex_duct_length_ft: tz_dist_sz[0]['flex_duct_ft'],
2063
+ cost: floor_vent_cost.round(2)
2064
+ }
2065
+ end
2066
+ end
2067
+ end
2068
+ return vent_dist_cost, dist_reporting
2069
+ end
2070
+
2071
+ def get_hrv_info(airloop:, model:)
2072
+ hrv_present = false
2073
+ hrv_data = nil
2074
+ hrv_design_flow_m3ps = 0
2075
+ airloop.oaComponents.each do |oaComp|
2076
+ if oaComp.iddObjectType.valueName.to_s == 'OS_HeatExchanger_AirToAir_SensibleAndLatent'
2077
+ hrv_present = true
2078
+ hrv_data = oaComp.to_HeatExchangerAirToAirSensibleAndLatent.get
2079
+ if hrv_data.isNominalSupplyAirFlowRateAutosized
2080
+ hrv_design_flow_m3ps = hrv_data.autosizedNominalSupplyAirFlowRate.to_f
2081
+ else
2082
+ hrv_design_flow_m3ps = hrv_data.nominalSupplyAirFlowRate.to_f
2083
+ end
2084
+ end
2085
+ end
2086
+ return {
2087
+ hrv_present: hrv_present,
2088
+ hrv_data: hrv_data,
2089
+ hrv_size_m3ps: hrv_design_flow_m3ps,
2090
+ supply_cap_m3ps: 0,
2091
+ return_cap_m3ps: 0
2092
+ } unless hrv_present
2093
+ airloop.supplyFan.is_initialized ? supply_fan_cap = get_fan_cap(fan: airloop.supplyFan.get, model: model) : supply_fan_cap = 0
2094
+ airloop.returnFan.is_initialized ? return_fan_cap = get_fan_cap(fan: airloop.returnFan.get, model: model) : return_fan_cap = 0
2095
+ return {
2096
+ hrv_present: hrv_present,
2097
+ hrv_data: hrv_data,
2098
+ hrv_size_m3ps: hrv_design_flow_m3ps,
2099
+ supply_cap_m3ps: supply_fan_cap,
2100
+ return_cap_m3ps: return_fan_cap
2101
+ }
2102
+ end
2103
+
2104
+ def get_fan_cap(fan:, model:)
2105
+ fan_type = fan.iddObjectType.valueName.to_s
2106
+ case fan_type
2107
+ when /OS_Fan_VariableVolume/
2108
+ fan_obj = fan.to_FanVariableVolume.get
2109
+ if fan_obj.isMaximumFlowRateAutosized
2110
+ fan_cap_m3ps = fan_obj.autosizedMaximumFlowRate.to_f
2111
+ else
2112
+ fan_cap_m3ps = fan_obj.maximumFlowRate.to_f
2113
+ end
2114
+ when /OS_Fan_ConstantVolume/
2115
+ fan_obj = fan.to_FanConstantVolume.get
2116
+ if fan_obj.isMaximumFlowRateAutosized
2117
+ fan_cap_m3ps = fan_obj.autosizedMaximumFlowRate.to_f
2118
+ else
2119
+ fan_cap_m3ps = fan_obj.maximumFlowRate.to_f
2120
+ end
2121
+ else
2122
+ fan_cap_m3ps = 0
2123
+ end
2124
+ return fan_cap_m3ps
2125
+ end
2126
+
2127
+ def hrv_duct_cost(prototype_creator:, roof_cent:, mech_sizing_info:, hvac_floors:)
2128
+ hrv_cost_tot = 0
2129
+ mech_table = get_mech_table(mech_size_info: mech_sizing_info, table_name: 'trunk')
2130
+ air_system_totals = []
2131
+ hrv_dist_rep = []
2132
+ hvac_floors.each_with_index do |hvac_floor, floor_index|
2133
+ hrv_dist_rep << {
2134
+ floor: hvac_floor[:story_name],
2135
+ air_systems: []
2136
+ }
2137
+ floor_systems = sort_tzs_by_air_system(hvac_floor: hvac_floor)
2138
+ floor_systems.each_with_index do |air_system, air_index|
2139
+ next if air_system[:sys_hrv_flow_m3ps].round(2) == 0.0 || air_system[:hrv_info][:hrv_present] == false
2140
+ floor_trunk_line = nil
2141
+ floor_air_sys = {
2142
+ air_system: air_system[:air_sys].nameString,
2143
+ hrv: air_system[:hrv_info][:hrv_data].nameString,
2144
+ floor_mult: 1,
2145
+ hrv_ret_trunk: {},
2146
+ tz_dist: [],
2147
+ }
2148
+ if air_system[:num_tz] > 1
2149
+ sys_floor_mult = air_system[:tz_mult]/(air_system[:num_tz])
2150
+ floor_trunk_line = get_story_cent_to_edge(building_story: hvac_floor[:story], prototype_creator: prototype_creator, target_cent: roof_cent[:roof_centroid], full_length: true)
2151
+ hrv_trunk_cost, floor_air_sys[:hrv_ret_trunk] = get_hrv_floor_trunk_cost(mech_table: mech_table, air_system: air_system, floor_trunk_dist_m: floor_trunk_line[:end_point][:line][:dist])
2152
+ hrv_cost_tot += hrv_trunk_cost*sys_floor_mult
2153
+ floor_air_sys[:floor_mult] = sys_floor_mult
2154
+ end
2155
+ air_system[:floor_tz].each do |floor_tz|
2156
+ floor_tz[:tz_floor_ret_air_m3ps] >= floor_tz[:tz_floor_outdoor_air_m3ps] ? hrv_air = 0 : hrv_air = (floor_tz[:tz_floor_outdoor_air_m3ps] - floor_tz[:tz_floor_ret_air_m3ps]).abs
2157
+ next if hrv_air.round(2) == 0.0
2158
+ air_system_total = {
2159
+ dist_to_roof_m: (roof_cent[:roof_centroid][2] - floor_tz[:tz_cent][:centroid][2]).abs,
2160
+ hrv_air_m3ps: hrv_air*floor_tz[:tz_mult],
2161
+ num_systems: floor_tz[:tz_mult]
2162
+ }
2163
+ if floor_trunk_line.nil?
2164
+ floor_duct_coords = [roof_cent[:roof_centroid][0] - floor_tz[:tz_cent][:centroid][0], roof_cent[:roof_centroid][1] - floor_tz[:tz_cent][:centroid][1], roof_cent[:roof_centroid][2] - floor_tz[:tz_cent][:centroid][2]]
2165
+ floor_duct_dist_m = floor_duct_coords[0].abs + floor_duct_coords[1].abs
2166
+ else
2167
+ line = {
2168
+ start: floor_trunk_line[:start_point][:line][:int],
2169
+ end: floor_trunk_line[:end_point][:line][:int]
2170
+ }
2171
+ floor_duct_dist_m = short_dist_point_and_line(point: floor_tz[:tz_cent][:centroid], line: line).abs
2172
+ if floor_duct_dist_m.nil?
2173
+ floor_duct_dist_m = (line[:start][0] - floor_tz[:tz_cent][:centroid][0]).abs + (line[:start][1] - floor_tz[:tz_cent][:centroid][1]).abs
2174
+ end
2175
+ end
2176
+ if floor_duct_dist_m.round(2) > 0.1
2177
+ floor_duct_dist_ft = (OpenStudio.convert(floor_duct_dist_m, 'm', 'ft').get)
2178
+ branch_duct_sz = mech_table.select {|sz_range|
2179
+ hrv_air > sz_range['max_flow_range_m3pers'][0] && hrv_air <= sz_range['max_flow_range_m3pers'][1]
2180
+ }
2181
+ branch_duct_sz << mech_table[mech_table.size-1] if branch_duct_sz.empty?
2182
+ duct_comp_search = []
2183
+ duct_dia_in = branch_duct_sz[0]['duct_dia_inch']
2184
+ duct_surface_area = floor_duct_dist_ft*(duct_dia_in.to_f/12)*Math::PI
2185
+ duct_comp_search << {
2186
+ mat: 'Ductinsulation',
2187
+ unit: 'ft2',
2188
+ size: 1.5,
2189
+ mult: duct_surface_area
2190
+ }
2191
+ duct_comp_search << {
2192
+ mat: 'Ductwork-S',
2193
+ unit: 'L.F.',
2194
+ size: duct_dia_in,
2195
+ mult: floor_duct_dist_ft
2196
+ }
2197
+ hrv_branch_cost = get_comp_cost(cost_info: duct_comp_search)
2198
+ hrv_cost_tot += hrv_branch_cost*floor_tz[:tz_mult]
2199
+ floor_air_sys[:tz_dist] << {
2200
+ tz: floor_tz[:tz].nameString,
2201
+ tz_mult: floor_tz[:tz_mult],
2202
+ hrv_ret_dist_m: floor_duct_dist_m.round(1),
2203
+ hrv_ret_size_in: duct_dia_in.round(2),
2204
+ cost: hrv_branch_cost.round(2)
2205
+ }
2206
+ end
2207
+ air_system_totals = add_tz_to_air_sys(air_system: air_system, air_system_total: air_system_total, air_system_totals: air_system_totals, floor_tz: floor_tz)
2208
+ end
2209
+ hrv_dist_rep[floor_index][:air_systems] << floor_air_sys
2210
+ end
2211
+ end
2212
+ unless air_system_totals.empty?
2213
+ air_system_totals.each do |air_system|
2214
+ next if air_system[:hrv_air_m3ps].round(2) == 0
2215
+ # In addition to distance from floor to roof add 20' of duct from roof centre to box
2216
+ main_trunk_dist_ft = (OpenStudio.convert(air_system[:dist_to_roof_m], 'm', 'ft').get) + 20
2217
+ main_trunk_sz = mech_table.select {|sz_range|
2218
+ air_system[:hrv_air_m3ps] > sz_range['max_flow_range_m3pers'][0] && air_system[:hrv_air_m3ps] <= sz_range['max_flow_range_m3pers'][1]
2219
+ }
2220
+ main_trunk_sz << mech_table[mech_table.size-1] if main_trunk_sz.empty?
2221
+ duct_comp_search = []
2222
+ duct_dia_in = main_trunk_sz[0]['duct_dia_inch']
2223
+ duct_surf_area_ft2 = main_trunk_dist_ft*(duct_dia_in.to_f/12)*Math::PI
2224
+ duct_comp_search << {
2225
+ mat: 'Ductinsulation',
2226
+ unit: 'ft2',
2227
+ size: 1.5,
2228
+ mult: duct_surf_area_ft2
2229
+ }
2230
+ duct_comp_search << {
2231
+ mat: 'Ductwork-S',
2232
+ unit: 'L.F.',
2233
+ size: duct_dia_in,
2234
+ mult: main_trunk_dist_ft
2235
+ }
2236
+ main_trunk_cost = get_comp_cost(cost_info: duct_comp_search)
2237
+ hrv_cost_tot += main_trunk_cost
2238
+ hrv_dist_rep << {
2239
+ air_system: air_system[:air_system].nameString,
2240
+ hrv: air_system[:hrv_info][:hrv_data].nameString,
2241
+ hrv_building_trunk_length_m: air_system[:dist_to_roof_m].round(1),
2242
+ hrv_building_trunk_dia_in: duct_dia_in.round(2),
2243
+ cost: main_trunk_cost.round(2)
2244
+ }
2245
+ end
2246
+ end
2247
+ return hrv_cost_tot, hrv_dist_rep
2248
+ end
2249
+
2250
+ def sort_tzs_by_air_system(hvac_floor:)
2251
+ floor_systems = []
2252
+ hvac_floor[:floor_tz].each do |floor_tz|
2253
+ air_sys = floor_tz[:sys_info]
2254
+ next if floor_tz[:hrv_info][:hrv_present] == false
2255
+ floor_tz[:tz_floor_ret_air_m3ps] >= floor_tz[:tz_floor_outdoor_air_m3ps] ? hrv_ret_air_m3ps = 0 : hrv_ret_air_m3ps = (floor_tz[:tz_floor_outdoor_air_m3ps] - floor_tz[:tz_floor_ret_air_m3ps]).abs
2256
+ if floor_systems.empty?
2257
+ floor_systems << {
2258
+ air_sys: air_sys,
2259
+ sys_hrv_flow_m3ps: hrv_ret_air_m3ps,
2260
+ num_tz: 1,
2261
+ tz_mult: floor_tz[:tz_mult],
2262
+ hrv_info: floor_tz[:hrv_info],
2263
+ floor_tz: [floor_tz]
2264
+ }
2265
+ else
2266
+ current_sys = floor_systems.select {|floor_sys| floor_sys[:air_sys] == air_sys}
2267
+ if current_sys.empty?
2268
+ floor_systems << {
2269
+ air_sys: air_sys,
2270
+ sys_hrv_flow_m3ps: hrv_ret_air_m3ps,
2271
+ num_tz: 1,
2272
+ tz_mult: floor_tz[:tz_mult],
2273
+ hrv_info: floor_tz[:hrv_info],
2274
+ floor_tz: [floor_tz]
2275
+ }
2276
+ else
2277
+ current_sys[0][:sys_hrv_flow_m3ps] += hrv_ret_air_m3ps
2278
+ current_sys[0][:num_tz] += 1
2279
+ current_sys[0][:tz_mult] += floor_tz[:tz_mult]
2280
+ current_sys[0][:floor_tz] << floor_tz
2281
+ end
2282
+ end
2283
+ end
2284
+ return floor_systems
2285
+ end
2286
+
2287
+ def add_tz_to_air_sys(air_system:, air_system_total:, air_system_totals:, floor_tz:)
2288
+ if air_system_totals.empty?
2289
+ air_system_totals << {
2290
+ air_system: air_system[:air_sys],
2291
+ hrv_air_m3ps: air_system_total[:hrv_air_m3ps],
2292
+ dist_to_roof_m: air_system_total[:dist_to_roof_m],
2293
+ num_systems: air_system_total[:num_systems],
2294
+ hrv_info: air_system[:hrv_info],
2295
+ floor_tz: [floor_tz]
2296
+ }
2297
+ else
2298
+ curr_air_sys = air_system_totals.select {|air_sys| air_sys[:air_system] == air_system[:air_sys]}
2299
+ if curr_air_sys.empty?
2300
+ air_system_totals << {
2301
+ air_system: air_system[:air_sys],
2302
+ hrv_air_m3ps: air_system_total[:hrv_air_m3ps],
2303
+ dist_to_roof_m: air_system_total[:dist_to_roof_m],
2304
+ num_systems: air_system_total[:num_systems],
2305
+ hrv_info: air_system[:hrv_info],
2306
+ floor_tz: [floor_tz]
2307
+ }
2308
+ else
2309
+ curr_air_sys[0][:hrv_air_m3ps] += air_system_total[:hrv_air_m3ps]
2310
+ curr_air_sys[0][:dist_to_roof_m] = [curr_air_sys[0][:dist_to_roof_m], air_system_total[:dist_to_roof_m]].max
2311
+ curr_air_sys[0][:num_systems] += air_system_total[:num_systems]
2312
+ curr_air_sys[0][:floor_tz] << floor_tz
2313
+ end
2314
+ end
2315
+ return air_system_totals
2316
+ end
2317
+
2318
+ def get_hrv_floor_trunk_cost(mech_table:, air_system:, floor_trunk_dist_m:)
2319
+ return 0 if air_system[:sys_hrv_flow_m3ps].round(2) == 0.0
2320
+ hrv_trunk_cost = 0
2321
+ duct_comp_search = []
2322
+ floor_trunk_dist = (OpenStudio.convert(floor_trunk_dist_m, 'm', 'ft').get)
2323
+ trunk_duct_sz = mech_table.select {|sz_range|
2324
+ air_system[:sys_hrv_flow_m3ps] > sz_range['max_flow_range_m3pers'][0] && air_system[:sys_hrv_flow_m3ps] <= sz_range['max_flow_range_m3pers'][1]
2325
+ }
2326
+ trunk_duct_sz << mech_table[mech_table.size-1] if trunk_duct_sz.empty?
2327
+ trunk_dia_in = (trunk_duct_sz[0]['duct_dia_inch'])
2328
+ duct_comp_search << {
2329
+ mat: 'Ductwork-S',
2330
+ unit: 'L.F.',
2331
+ size: trunk_dia_in,
2332
+ mult: floor_trunk_dist
2333
+ }
2334
+ trunk_area_sqrft = (trunk_dia_in.to_f/12)*Math::PI*floor_trunk_dist
2335
+ duct_comp_search << {
2336
+ mat: 'Ductinsulation',
2337
+ unit: 'ft2',
2338
+ size: 1.5,
2339
+ mult: trunk_area_sqrft
2340
+ }
2341
+ hrv_trunk_cost += get_comp_cost(cost_info: duct_comp_search)
2342
+ hrv_trunk_cost_rep = {
2343
+ duct_length_m: floor_trunk_dist_m.round(1),
2344
+ dia_in: trunk_dia_in.round(2),
2345
+ cost: hrv_trunk_cost.round(2)
2346
+ }
2347
+ return hrv_trunk_cost, hrv_trunk_cost_rep
2348
+ end
2349
+
2350
+ def short_dist_point_and_line(point:, line:)
2351
+ line_eq = get_line_eq(a: line[:start], b: line[:end])
2352
+ if line_eq[:int] == 1 and line_eq[:inf] == true
2353
+ dist = point[0] - line_eq[:slope]
2354
+ elsif line_eq[:int] == 0 and line_eq[:inf] == true
2355
+ dist = nil
2356
+ else
2357
+ # Turn equation of line as: y = slope*x + intercept
2358
+ # into: a*x + b*y + c = 0
2359
+ # a = slope, b = -1, c = intercept
2360
+ a = line_eq[:slope]
2361
+ b = -1
2362
+ c = line_eq[:int]
2363
+ # Use dot product to get shortest distance from point to line
2364
+ dist = (a*point[0] + b*point[1] + c) / Math.sqrt(a**2 + b**2)
2365
+ end
2366
+ return dist
2367
+ end
2368
+
2369
+ # This method consumes the following:
2370
+ # hrv_info: (hash) Information about the modeled HRV.
2371
+ # airloop: (OpenStudio Object) The OpenStudio air loop object.
2372
+ # vent_tags: (array of strings) Tags used to associate the costing output list with whichever component of the
2373
+ # building is being costed.
2374
+ # report_mult: (float) When recreating the cost of items from the costing output list this multiplier is used to
2375
+ # multiply the total of the localized material and labour costs.
2376
+ def hrv_cost(hrv_info:, airloop:, vent_tags: [], report_mult: 1.0)
2377
+ hrv_tags = vent_tags.clone
2378
+ hrv_tags << "ERV duct cost"
2379
+ hrv_cost_tot = 0
2380
+ number_zones = 0
2381
+ duct_comp_search = []
2382
+ # Calculate the number of thermal zones served by the ERV
2383
+ airloop.thermalZones.each do |tz|
2384
+ number_zones += tz.multiplier
2385
+ end
2386
+
2387
+ # Get additional ductwork costs
2388
+ duct_comp_search << {
2389
+ mat: 'Ductwork-Fitting',
2390
+ unit: 'each',
2391
+ size: 8,
2392
+ mult: number_zones
2393
+ }
2394
+ hrv_cost_tot += get_comp_cost(cost_info: duct_comp_search, vent_tags: hrv_tags, report_mult: report_mult)
2395
+ hrv_tags.pop
2396
+
2397
+ # Get the return air fan cost (if applicable)
2398
+ hrv_info[:return_cap_m3ps] >= hrv_info[:hrv_size_m3ps] ? hrv_return_flow_m3ps = 0.0 : hrv_return_flow_m3ps = hrv_info[:hrv_size_m3ps] - hrv_info[:return_cap_m3ps]
2399
+ hrv_tags << "ERV return air fan"
2400
+ unless hrv_return_flow_m3ps.round(2) == 0
2401
+ hrv_return_flow_cfm = (OpenStudio.convert(hrv_return_flow_m3ps, 'm^3/s', 'cfm').get)
2402
+ if hrv_return_flow_cfm < 800
2403
+ hrv_cost_tot += get_mech_costing(mech_name: 'FansDD-LP', size: hrv_return_flow_cfm, terminal: hrv_info[:hrv_data], use_mult: true, vent_tags: hrv_tags, report_mult: report_mult)
2404
+ else
2405
+ hrv_cost_tot += get_mech_costing(mech_name: 'FansBelt', size: hrv_return_flow_cfm, terminal: hrv_info[:hrv_data], use_mult: true, vent_tags: hrv_tags, report_mult: report_mult)
2406
+ end
2407
+ end
2408
+
2409
+
2410
+ hrv_tags.pop
2411
+ hrv_tags << "ERV with adjustment factor"
2412
+
2413
+ hrv_size_cfm = (OpenStudio.convert(hrv_info[:hrv_size_m3ps], 'm^3/s', 'cfm').get)
2414
+ # Turn the HRV information into something the 'get_vent_cost_data' method expects.
2415
+ hrv_requirements = {
2416
+ cat_search: 'ERV',
2417
+ mech_capacity_kw: hrv_size_cfm, # This key really should just be called mech_capacity since the units vary.
2418
+ supply_component: hrv_info[:hrv_data]
2419
+ }
2420
+ # Get the HRV costing information
2421
+ hrv_mult, hrv_cost_info = get_vent_cost_data(equipment_info: hrv_requirements)
2422
+ # Calculate the HRV cost adjustment factor
2423
+ hrv_cost_adj = hrv_size_cfm*hrv_mult/(hrv_cost_info['Size'].to_f)
2424
+ ind_hrv_cost = get_vent_mat_cost(mat_cost_info: hrv_cost_info, vent_tags: hrv_tags, report_mult: hrv_cost_adj)
2425
+
2426
+ ind_hrv_cost_rep = hrv_cost_tot + ind_hrv_cost
2427
+ hrv_cost_tot += ind_hrv_cost*hrv_cost_adj
2428
+ hrv_rep = {
2429
+ hrv_type: (hrv_info[:hrv_data].iddObjectType.valueName.to_s)[3..-1],
2430
+ hrv_name: hrv_info[:hrv_data].nameString,
2431
+ hrv_size_m3ps: hrv_info[:hrv_size_m3ps].round(3),
2432
+ hrv_return_fan_size_m3ps: hrv_return_flow_m3ps.round(3),
2433
+ hrv_cost: ind_hrv_cost_rep.round(2),
2434
+ revised_hrv_cost: hrv_cost_tot.round(2)
2435
+ }
2436
+
2437
+ return hrv_rep
2438
+ end
2439
+
2440
+ # This method collects air loop heating and cooling costing information into the al_eq_reporting_info hash. This hash
2441
+ # will be included in the ventilation costing report. It collects air loops by system type.
2442
+ def add_heat_cool_to_report(equipment_info:, heat_cool_cost:, al_eq_reporting_info:)
2443
+ # If there is no air loop heating or cooling equipment casting information add it to the hash.
2444
+ if al_eq_reporting_info.empty?
2445
+ al_eq_reporting_info << {
2446
+ eq_category: equipment_info[:obj_type][3..-1],
2447
+ heating_fuel: equipment_info[:heating_fuel],
2448
+ cooling_type: equipment_info[:cooling_type],
2449
+ total_modeled_capacity_kw: equipment_info[:mech_capacity_kw].round(3),
2450
+ cost: heat_cool_cost.round(2)
2451
+ }
2452
+ else
2453
+ # look for an air loop with the appropriate system type.
2454
+ ahu_heat_cool = al_eq_reporting_info.select {|aloop|
2455
+ aloop[:eq_category] == equipment_info[:obj_type][3..-1]
2456
+ }
2457
+ # If air loops with that system type are present add a new one.
2458
+ if ahu_heat_cool.empty?
2459
+ al_eq_reporting_info << {
2460
+ eq_category: equipment_info[:obj_type][3..-1],
2461
+ heating_fuel: equipment_info[:heating_fuel],
2462
+ cooling_type: equipment_info[:cooling_type],
2463
+ total_modeled_capacity_kw: equipment_info[:mech_capacity_kw].round(3),
2464
+ cost: heat_cool_cost.round(2)
2465
+ }
2466
+ else
2467
+ # If there is an air loop with the appropriate system type add the capacity and cost to the hash.
2468
+ ahu_heat_cool[0][:total_modeled_capacity_kw] += equipment_info[:mech_capacity_kw].round(3)
2469
+ ahu_heat_cool[0][:cost] += heat_cool_cost.round(2)
2470
+ end
2471
+ end
2472
+ end
2473
+
2474
+ # This method oversees the costing of heating and cooling equipment in an air loop. It takes in:
2475
+ # airloop_equipment: A hash containing all heating and cooling supply equipment in the air loop
2476
+ # The method retruns the airloop_equip_return_info hash which contains:
2477
+ # al_eq_reporting_info: A hash containing information that will be included in the ventilation costing report
2478
+ # heat_cool_cost: The total cost of heating and cooling equipment in the air loop
2479
+ def airloop_equipment_costing(airloop_equipment:, ahu_mult:, vent_tags: [])
2480
+ # Initialize return data
2481
+ ret_heat_cool_cost = 0
2482
+ al_eq_reporting_info = []
2483
+ ccashp_cost = 0
2484
+ vent_equip_tags = vent_tags.clone
2485
+ vent_equip_tags << "air loop equipment"
2486
+
2487
+ # Look for a heat pump. Heat pump air loop equipment costing is treated differently.
2488
+ heat_pumps = airloop_equipment.select{|airloop_eq| airloop_eq[:heating_fuel].to_s.include?('HP')}
2489
+ unless heat_pumps.empty?
2490
+ cool_eq = airloop_equipment.select{|airloop_eq| airloop_eq[:cooling_type].to_s.include?("DX")}
2491
+ unless cool_eq.empty?
2492
+ heat_pumps[0][:mech_capacity_kw] = cool_eq[0][:mech_capacity_kw].to_f if cool_eq[0][:mech_capacity_kw].to_f > heat_pumps[0][:mech_capacity_kw].to_f
2493
+ heat_pumps[0][:cooling_type] = heat_pumps[0][:heating_fuel]
2494
+ airloop_equipment.delete_if{|data| data[:cooling_type].to_s.include?("DX")}
2495
+ end
2496
+ if heat_pumps[0][:heating_fuel].to_s == "CCASHP"
2497
+ ccashp_cost = cost_ccashp_additional_components(ahu_mult: ahu_mult, heat_pump: heat_pumps[0], vent_tags: vent_equip_tags)
2498
+ end
2499
+ elec_eq = airloop_equipment.select{|airloop_eq| airloop_eq[:heating_fuel] == 'elec'}
2500
+ # If a backup electric heating coil is present look for a different item in the 'hvac_materials' costing sheet
2501
+ # than if the coil where part of an air loop without a heat pump.
2502
+ elec_eq.each do |el_eq|
2503
+ el_eq[:cat_search] = 'elecduct'
2504
+ end
2505
+ #airloop_equipment.select.with_index{|airloop_eq, index| airloop_eq[:cooling_type] == 'DX' || airloop_eq[:cooling_type] == 'CCASHP'}
2506
+ end
2507
+
2508
+ # Cost all of the heating and cooling equipment in the air loop
2509
+ airloop_equipment.each do |airloop_eq|
2510
+ # Costing of air loop equipment should be done on a per air handler basis. Thus, divide the total capacity of the
2511
+ # piece of air loop equipment by the number of air handlers required.
2512
+ total_modeled_capacity = airloop_eq[:mech_capacity_kw].to_f
2513
+ airloop_eq[:mech_capacity_kw] = total_modeled_capacity / ahu_mult
2514
+ # Get ventilation heating and cooling equipment costs.
2515
+ heat_cool_cost = cost_heat_cool_equip(equipment_info: airloop_eq, vent_tags: vent_equip_tags, report_mult: ahu_mult) * ahu_mult
2516
+ heat_cool_cost += ccashp_cost if airloop_eq[:heating_fuel].to_s == "CCASHP"
2517
+ # Add the equipment cost to the total air loop equipment cost
2518
+ ret_heat_cool_cost += heat_cool_cost
2519
+ # Only the total modeled capacity of the piece of air loop equipment should be reported to the user rather than
2520
+ # the capacity per air handler.
2521
+ airloop_eq[:mech_capacity_kw] = total_modeled_capacity
2522
+ # Add the air loop hetaing/cooling equipment information to the total air loop heating/cooling equipment report hash
2523
+ al_eq_reporting_info = add_heat_cool_to_report(equipment_info: airloop_eq, heat_cool_cost: heat_cool_cost, al_eq_reporting_info: al_eq_reporting_info)
2524
+ end
2525
+
2526
+ # Create the return hash and return it.
2527
+ airloop_equip_return_info = {
2528
+ al_eq_reporting_info: al_eq_reporting_info,
2529
+ heat_cool_cost: ret_heat_cool_cost
2530
+ }
2531
+ return airloop_equip_return_info
2532
+ end
2533
+
2534
+ # This method calculates the costs of CCASHP equipment beyond the coil cost and any backup heating costs. It takes in
2535
+ # ahu_mult: The number of air handlers required to meet the model air loop flow rate, cooling type, heating type and
2536
+ # system type.
2537
+ # heat_pumps: The heat pump hash for the ccashp which contains the OpenStudio heat pump object and the size of the
2538
+ # heat pump in kW.
2539
+ # The method uses a number of different costing methods to get equipment costs. The methods used depend on what best
2540
+ # suits the costing. For example evaporator costing is found by size and material so the get_vent_cost_data method
2541
+ # is most appropriate. Wiring has a material and size but the size should be an exact match so the get_comp_cost
2542
+ # method is used. Finally, a number of pieces of equipment with no size are costed. The esiest way to cost these
2543
+ # items was to refer to their 'materials_hvac' sheet 'material_id' column numbers and associated quantities and use
2544
+ # the vent_assembly_cost method.
2545
+ def cost_ccashp_additional_components(ahu_mult:, heat_pump:, vent_tags: [], report_mult: 1.0)
2546
+ ccashp_tags = vent_tags.clone
2547
+ # Initialize the ccashp additional equipment cost.
2548
+ ccashp_add_cost = 0
2549
+ # Set a variable to represent the capacity of each heat pump per air handler
2550
+ cap = heat_pump[:mech_capacity_kw].to_f/ahu_mult
2551
+ # Set a variable to represent the capacity in tons of cooling (for costing the refrigerent line).
2552
+ # cap_tonc = (OpenStudio.convert(cap.to_f, 'kW', 'kBtu/hr').get)/12.0 # No longer needed but keeping for future reference
2553
+
2554
+ # This variable holds the number of condensing units.
2555
+ cond_mult = 1.0
2556
+
2557
+ # An array of hashes containing the information required to cost the heat pump evaporator valve and condenser.
2558
+ ccashp_lrg_equips = []
2559
+ ccashp_lrg_equips << {
2560
+ supply_comp: heat_pump[:supply_comp],
2561
+ mech_capacity_kw: cap,
2562
+ cat_search: "EV_valve"
2563
+ }
2564
+ ccashp_lrg_equips << {
2565
+ supply_comp: heat_pump[:supply_comp],
2566
+ mech_capacity_kw: cap,
2567
+ cat_search: "ccashp_condensor"
2568
+ }
2569
+
2570
+ # Cost the heat pump evaporator valve and condenser.
2571
+ ccashp_lrg_equips.each do |ccashp_lrg_equip|
2572
+ equip_mult, cost_info = get_vent_cost_data(equipment_info: ccashp_lrg_equip)
2573
+ ccashp_add_cost += get_vent_mat_cost(mat_cost_info: cost_info, vent_tags: ccashp_tags, report_mult: (report_mult*equip_mult*ahu_mult)) * equip_mult * ahu_mult
2574
+ # cond_mult is supposed to be the number of condensors there are. It is set to be the multiplier if one condensor
2575
+ # is not enough. It should be set to the number of condesors because the condensors should be the last item in
2576
+ # this loop to be costed.
2577
+ cond_mult = equip_mult
2578
+ end
2579
+
2580
+ # Cost the wiring per heat pump condenser. Correcting to use 20 ft rather than 20 m.
2581
+ #ccashp_wiring_dist = (OpenStudio.convert(20, 'm', 'ft').get)/100.0
2582
+ ccashp_add_equip = [
2583
+ {
2584
+ mat: "Wiring",
2585
+ unit: "CLF",
2586
+ size: 10,
2587
+ mult: 0.2 * ahu_mult * cond_mult
2588
+ }
2589
+ ]
2590
+ # Get the Wiring costs.
2591
+ ccashp_add_cost += get_comp_cost(cost_info: ccashp_add_equip, vent_tags: ccashp_tags)
2592
+
2593
+ # Set an array containing the equipment 'material_id' references to search in the costing spreadsheet
2594
+ # 'materials_hvac' sheet.
2595
+ ids = [
2596
+ #1307, #Low Temperature Kit this belongs with the air handlers not the equipment
2597
+ 1295, # Remote Condensor Controller
2598
+ 1662, # Refrigerant tubing-large, 20' of 0.5" supply and 1-1/8" return
2599
+ 30, # 1.25" pipe insulation for refrigerant tubing
2600
+ 1415 # Safety Switch
2601
+ ]
2602
+
2603
+ # Set the quantities associated with the above ids. Note that ahu_mult is included when getting the cost.
2604
+ id_quants = [
2605
+ #1.0,
2606
+ cond_mult,
2607
+ cond_mult,
2608
+ cond_mult * 20 * 2, # 20' of supply and return pipe insulation for refrigerant tubing
2609
+ cond_mult
2610
+ ]
2611
+
2612
+ # Get the costs for equipment in the ids with id_quants quantities above.
2613
+ ccashp_add_cost += vent_assembly_cost(ids: ids, id_quants: id_quants, overall_mult: ahu_mult, vent_tags: ccashp_tags)
2614
+ return ccashp_add_cost
2615
+ end
2616
+ end