openstudio-standards 0.2.16.rc2 → 0.2.17.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (189) hide show
  1. checksums.yaml +4 -4
  2. data/data/geometry/ASHRAECollege.osm +117 -117
  3. data/data/standards/manage_OpenStudio_Standards.rb +31 -4
  4. data/data/standards/test_performance_expected_dd_results.csv +287 -287
  5. data/lib/openstudio-standards/btap/geometry.rb +1 -1
  6. data/lib/openstudio-standards/hvac_sizing/Siz.HeatingCoolingFuels.rb +354 -2
  7. data/lib/openstudio-standards/hvac_sizing/Siz.ThermalZone.rb +79 -0
  8. data/lib/openstudio-standards/prototypes/common/buildings/Prototype.College.rb +1 -1
  9. data/lib/openstudio-standards/prototypes/common/buildings/Prototype.Laboratory.rb +1 -1
  10. data/lib/openstudio-standards/prototypes/common/do_not_edit_metaclasses.rb +3376 -63
  11. data/lib/openstudio-standards/prototypes/common/objects/Prototype.Fan.rb +12 -0
  12. data/lib/openstudio-standards/prototypes/common/objects/Prototype.Model.rb +3 -4
  13. data/lib/openstudio-standards/prototypes/common/objects/Prototype.SizingSystem.rb +1 -1
  14. data/lib/openstudio-standards/prototypes/common/objects/Prototype.hvac_systems.rb +168 -93
  15. data/lib/openstudio-standards/prototypes/common/objects/Prototype.utilities.rb +2 -4
  16. data/lib/openstudio-standards/prototypes/common/prototype_metaprogramming.rb +2 -1
  17. data/lib/openstudio-standards/refs/references.rb +3 -0
  18. data/lib/openstudio-standards/standards/Standards.AirLoopHVAC.rb +279 -6
  19. data/lib/openstudio-standards/standards/Standards.AirTerminalSingleDuctParallelPIUReheat.rb +50 -2
  20. data/lib/openstudio-standards/standards/Standards.ChillerElectricEIR.rb +4 -0
  21. data/lib/openstudio-standards/standards/Standards.CoilCoolingWaterToAirHeatPumpEquationFit.rb +0 -1
  22. data/lib/openstudio-standards/standards/Standards.Construction.rb +185 -3
  23. data/lib/openstudio-standards/standards/Standards.Fan.rb +14 -6
  24. data/lib/openstudio-standards/standards/Standards.HeatExchangerSensLat.rb +1 -0
  25. data/lib/openstudio-standards/standards/Standards.Model.rb +1752 -384
  26. data/lib/openstudio-standards/standards/Standards.PlanarSurface.rb +130 -9
  27. data/lib/openstudio-standards/standards/Standards.PlantLoop.rb +50 -3
  28. data/lib/openstudio-standards/standards/Standards.ScheduleCompact.rb +44 -0
  29. data/lib/openstudio-standards/standards/Standards.ScheduleConstant.rb +27 -0
  30. data/lib/openstudio-standards/standards/Standards.ScheduleRuleset.rb +543 -0
  31. data/lib/openstudio-standards/standards/Standards.Space.rb +665 -15
  32. data/lib/openstudio-standards/standards/Standards.SpaceType.rb +141 -4
  33. data/lib/openstudio-standards/standards/Standards.SubSurface.rb +2 -1
  34. data/lib/openstudio-standards/standards/Standards.Surface.rb +117 -0
  35. data/lib/openstudio-standards/standards/Standards.ThermalZone.rb +197 -49
  36. data/lib/openstudio-standards/standards/Standards.ZoneHVACComponent.rb +41 -0
  37. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2004/ashrae_90_1_2004.Model.rb +6 -8
  38. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2004/comstock_ashrae_90_1_2004/data/ashrae_90_1.schedules.json +45 -45
  39. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2004/comstock_ashrae_90_1_2004/data/comstock_ashrae_90_1_2004.spc_typ.json +7 -7
  40. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2004/data/ashrae_90_1_2004.spc_typ.json +19 -19
  41. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2007/comstock_ashrae_90_1_2007/data/ashrae_90_1.schedules.json +45 -45
  42. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2007/comstock_ashrae_90_1_2007/data/comstock_ashrae_90_1_2007.spc_typ.json +7 -7
  43. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2007/data/ashrae_90_1_2007.spc_typ.json +5 -5
  44. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2010/comstock_ashrae_90_1_2010/data/ashrae_90_1.schedules.json +45 -45
  45. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2010/comstock_ashrae_90_1_2010/data/comstock_ashrae_90_1_2010.spc_typ.json +9 -9
  46. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2010/data/ashrae_90_1_2010.spc_typ.json +3 -3
  47. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2013/comstock_ashrae_90_1_2013/data/ashrae_90_1.schedules.json +45 -45
  48. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2013/comstock_ashrae_90_1_2013/data/comstock_ashrae_90_1_2013.spc_typ.json +4 -4
  49. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2013/data/ashrae_90_1_2013.spc_typ.json +6 -6
  50. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2016/comstock_ashrae_90_1_2016/data/ashrae_90_1.schedules.json +45 -45
  51. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2016/comstock_ashrae_90_1_2016/data/comstock_ashrae_90_1_2016.spc_typ.json +5 -5
  52. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2016/data/ashrae_90_1_2016.spc_typ.json +6 -6
  53. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2019/ashrae_90_1_2019.AirLoopHVAC.rb +5 -5
  54. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2019/comstock_ashrae_90_1_2019/data/ashrae_90_1.schedules.json +45 -45
  55. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2019/comstock_ashrae_90_1_2019/data/comstock_ashrae_90_1_2019.spc_typ.json +5 -5
  56. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2019/data/ashrae_90_1_2019.spc_typ.json +6 -6
  57. data/lib/openstudio-standards/standards/ashrae_90_1/data/ashrae_90_1.constructions.json +2 -2
  58. data/lib/openstudio-standards/standards/ashrae_90_1/data/ashrae_90_1.fans.json +12 -0
  59. data/lib/openstudio-standards/standards/ashrae_90_1/doe_ref_1980_2004/comstock_doe_ref_1980_2004/data/ashrae_90_1.schedules.json +45 -45
  60. data/lib/openstudio-standards/standards/ashrae_90_1/doe_ref_1980_2004/comstock_doe_ref_1980_2004/data/comstock_doe_ref_1980_2004.spc_typ.json +10 -10
  61. data/lib/openstudio-standards/standards/ashrae_90_1/doe_ref_pre_1980/comstock_doe_ref_pre_1980/data/ashrae_90_1.schedules.json +45 -45
  62. data/lib/openstudio-standards/standards/ashrae_90_1/doe_ref_pre_1980/comstock_doe_ref_pre_1980/data/comstock_doe_ref_pre_1980.spc_typ.json +10 -10
  63. data/lib/openstudio-standards/standards/ashrae_90_1/nrel_zne_ready_2017/nrel_zne_ready_2017.AirLoopHVAC.rb +1 -0
  64. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.AirLoopHVAC.rb +792 -0
  65. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.AirTerminalSingleDuctParallelPIUReheat.rb +10 -0
  66. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.AirTerminalSingleDuctVAVReheat.rb +31 -0
  67. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.BoilerHotWater.rb +91 -0
  68. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.ChillerElectricEIR.rb +84 -0
  69. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.CoilCoolingDXSingleSpeed.rb +145 -0
  70. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.CoilCoolingDXTwoSpeed.rb +106 -0
  71. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.CoilDX.rb +71 -0
  72. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.CoilHeatingDXSingleSpeed.rb +194 -0
  73. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.CoilHeatingGas.rb +120 -0
  74. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.CoolingTower.rb +110 -0
  75. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.CoolingTowerVariableSpeed.rb +5 -0
  76. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.Fan.rb +73 -0
  77. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.FanConstantVolume.rb +5 -0
  78. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.FanOnOff.rb +5 -0
  79. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.FanVariableVolume.rb +24 -0
  80. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.FanZoneExhaust.rb +5 -0
  81. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.HeatExchangerSensLat.rb +55 -0
  82. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.Model.rb +3045 -0
  83. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.PlanarSurface.rb +187 -0
  84. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.PlantLoop.rb +450 -0
  85. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.Space.rb +106 -0
  86. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.SpaceType.rb +666 -0
  87. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.Surface.rb +54 -0
  88. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.ThermalZone.rb +168 -0
  89. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.ZoneHVACComponent.rb +132 -0
  90. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.rb +239 -0
  91. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm_2019/ashrae_90_1_prm_2019.Model.rb +176 -0
  92. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm_2019/ashrae_90_1_prm_2019.rb +25 -0
  93. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm_2019/data/ashrae_90_1_prm_2019.boilers.json +52 -0
  94. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm_2019/data/ashrae_90_1_prm_2019.chillers.json +112 -0
  95. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm_2019/data/ashrae_90_1_prm_2019.climate_zone_sets.json +210 -0
  96. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm_2019/data/ashrae_90_1_prm_2019.construction_properties.json +10384 -0
  97. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm_2019/data/ashrae_90_1_prm_2019.construction_sets.json +133 -0
  98. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm_2019/data/ashrae_90_1_prm_2019.furnaces.json +43 -0
  99. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm_2019/data/ashrae_90_1_prm_2019.heat_pumps.json +119 -0
  100. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm_2019/data/ashrae_90_1_prm_2019.heat_pumps_heating.json +130 -0
  101. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm_2019/data/ashrae_90_1_prm_2019.heat_rejection.json +13 -0
  102. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm_2019/data/ashrae_90_1_prm_2019.lpd_space_type.json +568 -0
  103. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm_2019/data/ashrae_90_1_prm_2019.motors.json +264 -0
  104. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm_2019/data/ashrae_90_1_prm_2019.prm_baseline_hvac.json +439 -0
  105. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm_2019/data/ashrae_90_1_prm_2019.prm_constructions.json +685 -0
  106. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm_2019/data/ashrae_90_1_prm_2019.prm_economizers.json +213 -0
  107. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm_2019/data/ashrae_90_1_prm_2019.prm_ext_ltg.json +32 -0
  108. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm_2019/data/ashrae_90_1_prm_2019.prm_heat_type.json +136 -0
  109. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm_2019/data/ashrae_90_1_prm_2019.prm_hvac_bldg_type.json +32 -0
  110. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm_2019/data/ashrae_90_1_prm_2019.prm_interior_lighting.json +1837 -0
  111. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm_2019/data/ashrae_90_1_prm_2019.prm_swh_bldg_type.json +184 -0
  112. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm_2019/data/ashrae_90_1_prm_2019.prm_wwr_bldg_type.json +84 -0
  113. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm_2019/data/ashrae_90_1_prm_2019.unitary_acs.json +148 -0
  114. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm_2019/data/ashrae_90_1_prm_2019.water_heaters.json +157 -0
  115. data/lib/openstudio-standards/standards/ashrae_90_1_prm/data/ashrae_90_1_prm.climate_zone_sets.json +210 -0
  116. data/lib/openstudio-standards/standards/ashrae_90_1_prm/data/ashrae_90_1_prm.curves.json +18329 -0
  117. data/lib/openstudio-standards/standards/ashrae_90_1_prm/data/ashrae_90_1_prm.fans.json +340 -0
  118. data/lib/openstudio-standards/standards/ashrae_90_1_prm/data/ashrae_90_1_prm.materials.json +49924 -0
  119. data/lib/openstudio-standards/standards/ashrae_90_1_prm/docs/baseline_building_rotation_exception.md +44 -0
  120. data/lib/openstudio-standards/standards/ashrae_90_1_prm/docs/check_pump_power_and_control.md +71 -0
  121. data/lib/openstudio-standards/standards/ashrae_90_1_prm/docs/dcv.md +68 -0
  122. data/lib/openstudio-standards/standards/ashrae_90_1_prm/docs/dcv_implementation.png +0 -0
  123. data/lib/openstudio-standards/standards/ashrae_90_1_prm/docs/elevators.md +14 -0
  124. data/lib/openstudio-standards/standards/ashrae_90_1_prm/docs/exhaust_air_energy_recovery.md +36 -0
  125. data/lib/openstudio-standards/standards/ashrae_90_1_prm/docs/f_c_factors.md +19 -0
  126. data/lib/openstudio-standards/standards/ashrae_90_1_prm/docs/fan_power_credits.md +15 -0
  127. data/lib/openstudio-standards/standards/ashrae_90_1_prm/docs/preheat_coil.md +59 -0
  128. data/lib/openstudio-standards/standards/ashrae_90_1_prm/docs/pump_power_control.md +46 -0
  129. data/lib/openstudio-standards/standards/ashrae_90_1_prm/docs/return_air_type.md +31 -0
  130. data/lib/openstudio-standards/standards/ashrae_90_1_prm/docs/set_baseline_wwr.md +191 -0
  131. data/lib/openstudio-standards/standards/ashrae_90_1_prm/docs/set_hw_and_chw_supply_water_temp_reset_control.md +24 -0
  132. data/lib/openstudio-standards/standards/ashrae_90_1_prm/docs/set_num_boilers_chillers_towers.md +49 -0
  133. data/lib/openstudio-standards/standards/ashrae_90_1_prm/docs/set_plug_load_measures.md +80 -0
  134. data/lib/openstudio-standards/standards/ashrae_90_1_prm/docs/set_space_lpd.md +73 -0
  135. data/lib/openstudio-standards/standards/ashrae_90_1_prm/docs/unenclosed_and_unconditioned_spaces.md +11 -0
  136. data/lib/openstudio-standards/standards/ashrae_90_1_prm/docs/unmet_load_hours.md +20 -0
  137. data/lib/openstudio-standards/standards/ashrae_90_1_prm/docs/vav_parallel_piu_terminals_fan_control.md +23 -0
  138. data/lib/openstudio-standards/standards/ashrae_90_1_prm/docs/vav_terminals_min_flow_setpoint.md +21 -0
  139. data/lib/openstudio-standards/standards/ashrae_90_1_prm/userdata_csv/userdata_airloop_hvac.csv +1 -0
  140. data/lib/openstudio-standards/standards/ashrae_90_1_prm/userdata_csv/userdata_airloop_hvac_doas.csv +1 -0
  141. data/lib/openstudio-standards/standards/ashrae_90_1_prm/userdata_csv/userdata_building.csv +1 -0
  142. data/lib/openstudio-standards/standards/ashrae_90_1_prm/userdata_csv/userdata_design_specification_outdoor_air.csv +1 -0
  143. data/lib/openstudio-standards/standards/ashrae_90_1_prm/userdata_csv/userdata_electric_equipment.csv +1 -0
  144. data/lib/openstudio-standards/standards/ashrae_90_1_prm/userdata_csv/userdata_exterior_lights.csv +1 -0
  145. data/lib/openstudio-standards/standards/ashrae_90_1_prm/userdata_csv/userdata_gas_equipment.csv +1 -0
  146. data/lib/openstudio-standards/standards/ashrae_90_1_prm/userdata_csv/userdata_lights.csv +1 -0
  147. data/lib/openstudio-standards/standards/ashrae_90_1_prm/userdata_csv/userdata_space.csv +1 -0
  148. data/lib/openstudio-standards/standards/ashrae_90_1_prm/userdata_csv/userdata_spacetype.csv +1 -0
  149. data/lib/openstudio-standards/standards/ashrae_90_1_prm/userdata_csv/userdata_thermal_zone.csv +1 -0
  150. data/lib/openstudio-standards/standards/ashrae_90_1_prm/userdata_csv/userdata_wateruse_connections.csv +1 -0
  151. data/lib/openstudio-standards/standards/ashrae_90_1_prm/userdata_csv/userdata_wateruse_equipment.csv +1 -0
  152. data/lib/openstudio-standards/standards/ashrae_90_1_prm/userdata_csv/userdata_wateruse_equipment_definition.csv +1 -0
  153. data/lib/openstudio-standards/standards/ashrae_90_1_prm/userdata_csv/userdata_zone_hvac.csv +1 -0
  154. data/lib/openstudio-standards/standards/ashrae_90_1_prm/userdata_csv/userdata_zone_infiltration.csv +1 -0
  155. data/lib/openstudio-standards/standards/cbes/data/cbes.fans.json +12 -0
  156. data/lib/openstudio-standards/standards/deer/data/deer.fans.json +12 -0
  157. data/lib/openstudio-standards/standards/necb/ECMS/data/heat_pumps.json +1 -1
  158. data/lib/openstudio-standards/standards/necb/ECMS/data/heat_pumps_heating.json +1 -1
  159. data/lib/openstudio-standards/standards/necb/ECMS/data/unitary_acs.json +24 -11
  160. data/lib/openstudio-standards/standards/necb/ECMS/erv.rb +13 -15
  161. data/lib/openstudio-standards/standards/necb/NECB2011/data/province_map.json +17 -0
  162. data/lib/openstudio-standards/standards/necb/NECB2011/hvac_system_3_and_8_multi_speed.rb +1 -1
  163. data/lib/openstudio-standards/standards/necb/NECB2011/hvac_system_3_and_8_single_speed.rb +1 -1
  164. data/lib/openstudio-standards/standards/necb/NECB2011/hvac_system_4.rb +2 -2
  165. data/lib/openstudio-standards/standards/necb/NECB2011/hvac_system_6.rb +6 -5
  166. data/lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb +3 -2
  167. data/lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb +2 -3
  168. data/lib/openstudio-standards/standards/necb/NECB2020/data/chillers.json +2 -2
  169. data/lib/openstudio-standards/standards/necb/NECB2020/data/space_types.json +33 -924
  170. data/lib/openstudio-standards/standards/necb/NECB2020/data/unitary_acs.json +15 -15
  171. data/lib/openstudio-standards/standards/necb/common/btap_data.rb +135 -29
  172. data/lib/openstudio-standards/standards/necb/common/btap_datapoint.rb +16 -4
  173. data/lib/openstudio-standards/standards/necb/common/neb_end_use_prices.csv +40 -42
  174. data/lib/openstudio-standards/standards/necb/common/necb_reference_runs.csv +1 -1
  175. data/lib/openstudio-standards/standards/necb/common/space_type_upgrade_map.json +89 -89
  176. data/lib/openstudio-standards/utilities/array.rb +11 -0
  177. data/lib/openstudio-standards/utilities/logging.rb +48 -0
  178. data/lib/openstudio-standards/utilities/object_info.rb +20 -0
  179. data/lib/openstudio-standards/utilities/schedule_translator.rb +348 -0
  180. data/lib/openstudio-standards/utilities/sqlfile.rb +68 -0
  181. data/lib/openstudio-standards/version.rb +2 -2
  182. data/lib/openstudio-standards/weather/Weather.Model.rb +42 -55
  183. data/lib/openstudio-standards/weather/Weather.stat_file.rb +1 -1
  184. data/lib/openstudio-standards.rb +35 -1
  185. metadata +111 -6
  186. data/data/standards/OpenStudio_Standards-ashrae_90_1.xlsx +0 -0
  187. data/data/standards/OpenStudio_Standards-ashrae_90_1_28Jan2022.xlsx +0 -0
  188. data/data/standards/OpenStudio_Standards-ashrae_90_1_28_Jan2022_2.xlsx +0 -0
  189. data/data/standards/openstudio_standards_duplicates_log.csv +0 -143
@@ -0,0 +1,3045 @@
1
+ class ASHRAE901PRM < Standard
2
+ # @!group Model
3
+
4
+ # Determines the area of the building above which point
5
+ # the non-dominant area type gets it's own HVAC system type.
6
+ # @return [Double] the minimum area (m^2)
7
+ def model_prm_baseline_system_group_minimum_area(model, custom)
8
+ exception_min_area_ft2 = 20_000
9
+ # Customization - Xcel EDA Program Manual 2014
10
+ # 3.2.1 Mechanical System Selection ii
11
+ if custom == 'Xcel Energy CO EDA'
12
+ exception_min_area_ft2 = 5000
13
+ OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "Customization; per Xcel EDA Program Manual 2014 3.2.1 Mechanical System Selection ii, minimum area for non-predominant conditions reduced to #{exception_min_area_ft2} ft2.")
14
+ end
15
+ exception_min_area_m2 = OpenStudio.convert(exception_min_area_ft2, 'ft^2', 'm^2').get
16
+ return exception_min_area_m2
17
+ end
18
+
19
+ # Determines which system number is used
20
+ # for the baseline system.
21
+ # @return [String] the system number: 1_or_2, 3_or_4,
22
+ # 5_or_6, 7_or_8, 9_or_10
23
+ def model_prm_baseline_system_number(model, climate_zone, area_type, fuel_type, area_ft2, num_stories, custom)
24
+ sys_num = nil
25
+
26
+ # Customization - Xcel EDA Program Manual 2014
27
+ # Table 3.2.2 Baseline HVAC System Types
28
+ if custom == 'Xcel Energy CO EDA'
29
+ OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', 'Custom; per Xcel EDA Program Manual 2014 Table 3.2.2 Baseline HVAC System Types, the 90.1-2010 lookup for HVAC system types shall be used.')
30
+
31
+ # Set the area limit
32
+ limit_ft2 = 25_000
33
+
34
+ case area_type
35
+ when 'residential'
36
+ sys_num = '1_or_2'
37
+ when 'nonresidential'
38
+ # nonresidential and 3 floors or less and <25,000 ft2
39
+ if num_stories <= 3 && area_ft2 < limit_ft2
40
+ sys_num = '3_or_4'
41
+ # nonresidential and 4 or 5 floors or 5 floors or less and 25,000 ft2 to 150,000 ft2
42
+ elsif ((num_stories == 4 || num_stories == 5) && area_ft2 < limit_ft2) || (num_stories <= 5 && (area_ft2 >= limit_ft2 && area_ft2 <= 150_000))
43
+ sys_num = '5_or_6'
44
+ # nonresidential and more than 5 floors or >150,000 ft2
45
+ elsif num_stories >= 5 || area_ft2 > 150_000
46
+ sys_num = '7_or_8'
47
+ end
48
+ when 'heatedonly'
49
+ sys_num = '9_or_10'
50
+ when 'retail'
51
+ # Should only be hit by Xcel EDA
52
+ sys_num = '3_or_4'
53
+ end
54
+
55
+ else
56
+
57
+ # Set the area limit
58
+ limit_ft2 = 25_000
59
+
60
+ case area_type
61
+ when 'residential'
62
+ sys_num = '1_or_2'
63
+ when 'nonresidential'
64
+ # nonresidential and 3 floors or less and <25,000 ft2
65
+ if num_stories <= 3 && area_ft2 < limit_ft2
66
+ sys_num = '3_or_4'
67
+ # nonresidential and 4 or 5 floors or 5 floors or less and 25,000 ft2 to 150,000 ft2
68
+ elsif ((num_stories == 4 || num_stories == 5) && area_ft2 < limit_ft2) || (num_stories <= 5 && (area_ft2 >= limit_ft2 && area_ft2 <= 150_000))
69
+ sys_num = '5_or_6'
70
+ # nonresidential and more than 5 floors or >150,000 ft2
71
+ elsif num_stories >= 5 || area_ft2 > 150_000
72
+ sys_num = '7_or_8'
73
+ end
74
+ when 'heatedonly'
75
+ sys_num = '9_or_10'
76
+ when 'retail'
77
+ sys_num = '3_or_4'
78
+ end
79
+
80
+ end
81
+
82
+ return sys_num
83
+ end
84
+
85
+ # Change the fuel type based on climate zone, depending on the standard.
86
+ # For 90.1-2013, fuel type is based on climate zone, not the proposed model.
87
+ # @return [String] the revised fuel type
88
+ def model_prm_baseline_system_change_fuel_type(model, fuel_type, climate_zone, custom = nil)
89
+ if custom == 'Xcel Energy CO EDA'
90
+ OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', 'Custom; per Xcel EDA Program Manual 2014 Table 3.2.2 Baseline HVAC System Types, the 90.1-2010 rules for heating fuel type (based on proposed model) rules apply.')
91
+ return fuel_type
92
+ end
93
+
94
+ # For 90.1-2013 the fuel type is determined based on climate zone.
95
+ # Don't change the fuel if it purchased heating or cooling.
96
+ if fuel_type == 'electric' || fuel_type == 'fossil'
97
+ case climate_zone
98
+ when 'ASHRAE 169-2006-1A',
99
+ 'ASHRAE 169-2006-2A',
100
+ 'ASHRAE 169-2006-3A',
101
+ 'ASHRAE 169-2013-1A',
102
+ 'ASHRAE 169-2013-2A',
103
+ 'ASHRAE 169-2013-3A'
104
+ fuel_type = 'electric'
105
+ else
106
+ fuel_type = 'fossil'
107
+ end
108
+ OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "Heating fuel is #{fuel_type} for 90.1-2013, climate zone #{climate_zone}. This is independent of the heating fuel type in the proposed building, per G3.1.1-3. This is different than previous versions of 90.1.")
109
+ end
110
+
111
+ return fuel_type
112
+ end
113
+
114
+ # Determines the fan type used by VAV_Reheat and VAV_PFP_Boxes systems.
115
+ # Variable speed fan for 90.1-2013
116
+ # @return [String] the fan type: TwoSpeed Fan, Variable Speed Fan
117
+ def model_baseline_system_vav_fan_type(model)
118
+ fan_type = 'Variable Speed Fan'
119
+ return fan_type
120
+ end
121
+
122
+ # This method creates customized infiltration objects for each
123
+ # space and removes the SpaceType-level infiltration objects.
124
+ #
125
+ # @return [Bool] true if successful, false if not
126
+ def model_baseline_apply_infiltration_standard(model, climate_zone)
127
+ # Model shouldn't use SpaceInfiltrationEffectiveLeakageArea
128
+ # Excerpt from the EnergyPlus Input/Output reference manual:
129
+ # "This model is based on work by Sherman and Grimsrud (1980)
130
+ # and is appropriate for smaller, residential-type buildings."
131
+ # Return an error if the model does use this object
132
+ ela = 0
133
+ model.getSpaceInfiltrationEffectiveLeakageAreas.sort.each do |eff_la|
134
+ ela += 1
135
+ end
136
+ if ela > 0
137
+ OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Model', 'The current model cannot include SpaceInfiltrationEffectiveLeakageArea. These objects cannot be used to model infiltration according to the 90.1-PRM rules.')
138
+ end
139
+
140
+ # Get the space building envelope area
141
+ # According to the 90.1 definition, building envelope include:
142
+ # - "the elements of a building that separate conditioned spaces from the exterior"
143
+ # - "the elements of a building that separate conditioned space from unconditioned
144
+ # space or that enclose semiheated spaces through which thermal energy may be
145
+ # transferred to or from the exterior, to or from unconditioned spaces or to or
146
+ # from conditioned spaces."
147
+ building_envelope_area_m2 = 0
148
+ model.getSpaces.sort.each do |space|
149
+ building_envelope_area_m2 += space_envelope_area(space, climate_zone)
150
+ end
151
+ if building_envelope_area_m2 == 0.0
152
+ OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', 'Calculated building envelope area is 0 m2, no infiltration will be added.')
153
+ return 0.0
154
+ end
155
+
156
+ # Calculate current model air leakage rate @ 75 Pa and report it
157
+ curr_tot_infil_m3_per_s_per_envelope_area = model_current_building_envelope_infiltration_at_75pa(model, building_envelope_area_m2)
158
+ OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "The proposed model I_75Pa is estimated to be #{curr_tot_infil_m3_per_s_per_envelope_area} m3/s per m2 of total building envelope.")
159
+
160
+ # Calculate building adjusted building envelope
161
+ # air infiltration following the 90.1 PRM rules
162
+ tot_infil_m3_per_s = model_adjusted_building_envelope_infiltration(model, building_envelope_area_m2)
163
+
164
+ # Find infiltration method used in the model, if any.
165
+ #
166
+ # If multiple methods are used, use per above grade wall
167
+ # area (i.e. exterior wall area), if air/changes per hour
168
+ # or exterior surface area is used, use Flow/ExteriorWallArea
169
+ infil_method = model_get_infiltration_method(model)
170
+ infil_method = 'Flow/ExteriorWallArea' if infil_method != 'Flow/Area' || infil_method != 'Flow/ExteriorWallArea'
171
+ infil_coefficients = model_get_infiltration_coefficients(model)
172
+
173
+ # Set the infiltration rate at each space
174
+ model.getSpaces.sort.each do |space|
175
+ space_apply_infiltration_rate(space, tot_infil_m3_per_s, infil_method, infil_coefficients)
176
+ end
177
+
178
+ # Remove infiltration rates set at the space type
179
+ model.getSpaceTypes.sort.each do |space_type|
180
+ space_type.spaceInfiltrationDesignFlowRates.each(&:remove)
181
+ end
182
+
183
+ return true
184
+ end
185
+
186
+ # This method retrieves the type of infiltration input
187
+ # used in the model. If input is inconsitent, returns
188
+ # Flow/Area
189
+ #
190
+ # @return [String] infiltration input type
191
+ def model_get_infiltration_method(model)
192
+ infil_method = nil
193
+ model.getSpaces.sort.each do |space|
194
+ # Infiltration at the space level
195
+ unless space.spaceInfiltrationDesignFlowRates.empty?
196
+ old_infil = space.spaceInfiltrationDesignFlowRates[0]
197
+ old_infil_method = old_infil.designFlowRateCalculationMethod.to_s
198
+ # Return flow per space floor area if method is inconsisten in proposed model
199
+ return 'Flow/Area' if infil_method != old_infil_method && !infil_method.nil?
200
+
201
+ infil_method = old_infil_method
202
+ end
203
+
204
+ # Infiltration at the space type level
205
+ if infil_method.nil? && space.spaceType.is_initialized
206
+ space_type = space.spaceType.get
207
+ unless space_type.spaceInfiltrationDesignFlowRates.empty?
208
+ old_infil = space_type.spaceInfiltrationDesignFlowRates[0]
209
+ old_infil_method = old_infil.designFlowRateCalculationMethod.to_s
210
+ # Return flow per space floor area if method is inconsisten in proposed model
211
+ return 'Flow/Area' if infil_method != old_infil_method && !infil_method.nil?
212
+
213
+ infil_method = old_infil_method
214
+ end
215
+ end
216
+ end
217
+
218
+ return infil_method
219
+ end
220
+
221
+ # This method retrieves the infiltration coefficients
222
+ # used in the model. If input is inconsitent, returns
223
+ # [0, 0, 0.224, 0] as per PRM user manual
224
+ #
225
+ # @return [String] infiltration input type
226
+ def model_get_infiltration_coefficients(model)
227
+ cst = nil
228
+ temp = nil
229
+ vel = nil
230
+ vel_2 = nil
231
+ infil_coeffs = [cst, temp, vel, vel_2]
232
+ model.getSpaces.sort.each do |space|
233
+ # Infiltration at the space level
234
+ unless space.spaceInfiltrationDesignFlowRates.empty?
235
+ old_infil = space.spaceInfiltrationDesignFlowRates[0]
236
+ cst = old_infil.constantTermCoefficient
237
+ temp = old_infil.temperatureTermCoefficient
238
+ vel = old_infil.velocityTermCoefficient
239
+ vel_2 = old_infil.velocitySquaredTermCoefficient
240
+ old_infil_coeffs = [cst, temp, vel, vel_2] if !(cst.nil? && temp.nil? && vel.nil? && vel_2.nil?)
241
+ # Return flow per space floor area if method is inconsisten in proposed model
242
+ return [0.0, 0.0, 0.224, 0.0] if infil_coeffs != old_infil_coeffs && !(infil_coeffs[0].nil? &&
243
+ infil_coeffs[1].nil? &&
244
+ infil_coeffs[2].nil? &&
245
+ infil_coeffs[3].nil?)
246
+
247
+ infil_coeffs = old_infil_coeffs
248
+ end
249
+
250
+ # Infiltration at the space type level
251
+ if infil_coeffs == [nil, nil, nil, nil] && space.spaceType.is_initialized
252
+ space_type = space.spaceType.get
253
+ unless space_type.spaceInfiltrationDesignFlowRates.empty?
254
+ old_infil = space_type.spaceInfiltrationDesignFlowRates[0]
255
+ cst = old_infil.constantTermCoefficient
256
+ temp = old_infil.temperatureTermCoefficient
257
+ vel = old_infil.velocityTermCoefficient
258
+ vel_2 = old_infil.velocitySquaredTermCoefficient
259
+ old_infil_coeffs = [cst, temp, vel, vel_2] if !(cst.nil? && temp.nil? && vel.nil? && vel_2.nil?)
260
+ # Return flow per space floor area if method is inconsisten in proposed model
261
+ return [0.0, 0.0, 0.224, 0.0] unless infil_coeffs != old_infil_coeffs && !(infil_coeffs[0].nil? &&
262
+ infil_coeffs[1].nil? &&
263
+ infil_coeffs[2].nil? &&
264
+ infil_coeffs[3].nil?)
265
+
266
+ infil_coeffs = old_infil_coeffs
267
+ end
268
+ end
269
+ end
270
+ return infil_coeffs
271
+ end
272
+
273
+ # This methods calculate the current model air leakage rate @ 75 Pa.
274
+ # It assumes that the model follows the PRM methods, see G3.1.1.4
275
+ # in 90.1-2019 for reference.
276
+ #
277
+ # @param [OpenStudio::Model::Model] OpenStudio Model object
278
+ # @param [Double] Building envelope area as per 90.1 in m^2
279
+ #
280
+ # @return [Float] building model air leakage rate
281
+ def model_current_building_envelope_infiltration_at_75pa(model, building_envelope_area_m2)
282
+ bldg_air_leakage_rate = 0
283
+ model.getSpaces.sort.each do |space|
284
+ # Infiltration at the space level
285
+ unless space.spaceInfiltrationDesignFlowRates.empty?
286
+ infil_obj = space.spaceInfiltrationDesignFlowRates[0]
287
+ unless infil_obj.designFlowRate.is_initialized
288
+ if infil_obj.flowperSpaceFloorArea.is_initialized
289
+ bldg_air_leakage_rate += infil_obj.flowperSpaceFloorArea.get * space.floorArea
290
+ elsif infil_obj.flowperExteriorSurfaceArea.is_initialized
291
+ bldg_air_leakage_rate += infil_obj.flowperExteriorSurfaceArea.get * space.exteriorArea
292
+ elsif infil_obj.flowperExteriorWallArea.is_initialized
293
+ bldg_air_leakage_rate += infil_obj.flowperExteriorWallArea.get * space.exteriorWallArea
294
+ elsif infil_obj.airChangesperHour.is_initialized
295
+ bldg_air_leakage_rate += infil_obj.airChangesperHour.get * space.volume / 3600
296
+ end
297
+ end
298
+ end
299
+
300
+ # Infiltration at the space type level
301
+ if space.spaceType.is_initialized
302
+ space_type = space.spaceType.get
303
+ unless space_type.spaceInfiltrationDesignFlowRates.empty?
304
+ infil_obj = space_type.spaceInfiltrationDesignFlowRates[0]
305
+ unless infil_obj.designFlowRate.is_initialized
306
+ if infil_obj.flowperSpaceFloorArea.is_initialized
307
+ bldg_air_leakage_rate += infil_obj.flowperSpaceFloorArea.get * space.floorArea
308
+ elsif infil_obj.flowperExteriorSurfaceArea.is_initialized
309
+ bldg_air_leakage_rate += infil_obj.flowperExteriorSurfaceArea.get * space.exteriorArea
310
+ elsif infil_obj.flowperExteriorWallArea.is_initialized
311
+ bldg_air_leakage_rate += infil_obj.flowperExteriorWallArea.get * space.exteriorWallArea
312
+ elsif infil_obj.airChangesperHour.is_initialized
313
+ bldg_air_leakage_rate += infil_obj.airChangesperHour.get * space.volume / 3600
314
+ end
315
+ end
316
+ end
317
+ end
318
+ end
319
+ # adjust_infiltration_to_prototype_building_conditions(1) corresponds
320
+ # to the 0.112 shown in G3.1.1.4
321
+ curr_tot_infil_m3_per_s_per_envelope_area = bldg_air_leakage_rate / adjust_infiltration_to_prototype_building_conditions(1) / building_envelope_area_m2
322
+ return curr_tot_infil_m3_per_s_per_envelope_area
323
+ end
324
+
325
+ # This method calculates the building envelope infiltration,
326
+ # this approach uses the 90.1 PRM rules
327
+ #
328
+ # @return [Float] building envelope infiltration
329
+ def model_adjusted_building_envelope_infiltration(model, building_envelope_area_m2)
330
+ # Determine the total building baseline infiltration rate in cfm per ft2 of the building envelope at 75 Pa
331
+ basic_infil_rate_cfm_per_ft2 = space_infiltration_rate_75_pa
332
+
333
+ # Do nothing if no infiltration
334
+ return 0.0 if basic_infil_rate_cfm_per_ft2.zero?
335
+
336
+ # Conversion factor
337
+ conv_fact = OpenStudio.convert(1, 'm^3/s', 'ft^3/min').to_f / OpenStudio.convert(1, 'm^2', 'ft^2').to_f
338
+
339
+ # Adjust the infiltration rate to the average pressure for the prototype buildings.
340
+ # adj_infil_rate_cfm_per_ft2 = 0.112 * basic_infil_rate_cfm_per_ft2
341
+ adj_infil_rate_cfm_per_ft2 = adjust_infiltration_to_prototype_building_conditions(basic_infil_rate_cfm_per_ft2)
342
+ adj_infil_rate_m3_per_s_per_m2 = adj_infil_rate_cfm_per_ft2 / conv_fact
343
+
344
+ # Calculate the total infiltration
345
+ tot_infil_m3_per_s = adj_infil_rate_m3_per_s_per_m2 * building_envelope_area_m2
346
+
347
+ return tot_infil_m3_per_s
348
+ end
349
+
350
+ # Apply the standard construction to each surface in the model, based on the construction type currently assigned.
351
+ #
352
+ # @return [Bool] true if successful, false if not
353
+ # @param model [OpenStudio::Model::Model] OpenStudio model object
354
+ # @param climate_zone [String] ASHRAE climate zone, e.g. 'ASHRAE 169-2013-4A'
355
+ # @return [Bool] returns true if successful, false if not
356
+ def model_apply_standard_constructions(model, climate_zone, wwr_building_type: nil, wwr_info: {})
357
+ types_to_modify = []
358
+
359
+ # Possible boundary conditions are
360
+ # Adiabatic
361
+ # Surface
362
+ # Outdoors
363
+ # Ground
364
+ # Foundation
365
+ # GroundFCfactorMethod
366
+ # OtherSideCoefficients
367
+ # OtherSideConditionsModel
368
+ # GroundSlabPreprocessorAverage
369
+ # GroundSlabPreprocessorCore
370
+ # GroundSlabPreprocessorPerimeter
371
+ # GroundBasementPreprocessorAverageWall
372
+ # GroundBasementPreprocessorAverageFloor
373
+ # GroundBasementPreprocessorUpperWall
374
+ # GroundBasementPreprocessorLowerWall
375
+
376
+ # Possible surface types are
377
+ # Floor
378
+ # Wall
379
+ # RoofCeiling
380
+ # FixedWindow
381
+ # OperableWindow
382
+ # Door
383
+ # GlassDoor
384
+ # OverheadDoor
385
+ # Skylight
386
+ # TubularDaylightDome
387
+ # TubularDaylightDiffuser
388
+
389
+ # Create an array of surface types
390
+ types_to_modify << ['Outdoors', 'Floor']
391
+ types_to_modify << ['Outdoors', 'Wall']
392
+ types_to_modify << ['Outdoors', 'RoofCeiling']
393
+ types_to_modify << ['Outdoors', 'FixedWindow']
394
+ types_to_modify << ['Outdoors', 'OperableWindow']
395
+ types_to_modify << ['Outdoors', 'Door']
396
+ types_to_modify << ['Outdoors', 'GlassDoor']
397
+ types_to_modify << ['Outdoors', 'OverheadDoor']
398
+ types_to_modify << ['Outdoors', 'Skylight']
399
+ types_to_modify << ['Surface', 'Floor']
400
+ types_to_modify << ['Surface', 'Wall']
401
+ types_to_modify << ['Surface', 'RoofCeiling']
402
+ types_to_modify << ['Surface', 'FixedWindow']
403
+ types_to_modify << ['Surface', 'OperableWindow']
404
+ types_to_modify << ['Surface', 'Door']
405
+ types_to_modify << ['Surface', 'GlassDoor']
406
+ types_to_modify << ['Surface', 'OverheadDoor']
407
+ types_to_modify << ['Ground', 'Floor']
408
+ types_to_modify << ['Ground', 'Wall']
409
+ types_to_modify << ['Foundation', 'Wall']
410
+ types_to_modify << ['GroundFCfactorMethod', 'Wall']
411
+ types_to_modify << ['OtherSideCoefficients', 'Wall']
412
+ types_to_modify << ['OtherSideConditionsModel', 'Wall']
413
+ types_to_modify << ['GroundBasementPreprocessorAverageWall', 'Wall']
414
+ types_to_modify << ['GroundBasementPreprocessorUpperWall', 'Wall']
415
+ types_to_modify << ['GroundBasementPreprocessorLowerWall', 'Wall']
416
+ types_to_modify << ['Foundation', 'Floor']
417
+ types_to_modify << ['GroundFCfactorMethod', 'Floor']
418
+ types_to_modify << ['OtherSideCoefficients', 'Floor']
419
+ types_to_modify << ['OtherSideConditionsModel', 'Floor']
420
+ types_to_modify << ['GroundSlabPreprocessorAverage', 'Floor']
421
+ types_to_modify << ['GroundSlabPreprocessorCore', 'Floor']
422
+ types_to_modify << ['GroundSlabPreprocessorPerimeter', 'Floor']
423
+
424
+ # Find just those surfaces
425
+ surfaces_to_modify = []
426
+ surface_category = {}
427
+ org_surface_boundary_conditions = {}
428
+ types_to_modify.each do |boundary_condition, surface_type|
429
+ # Surfaces
430
+ model.getSurfaces.sort.each do |surf|
431
+ next unless surf.outsideBoundaryCondition == boundary_condition
432
+ next unless surf.surfaceType == surface_type
433
+
434
+ # Check if surface is adjacent to an unenclosed or unconditioned space (e.g. attic or parking garage)
435
+ if surf.outsideBoundaryCondition == 'Surface'
436
+ adj_space = surf.adjacentSurface.get.space.get
437
+ adj_space_cond_type = space_conditioning_category(adj_space)
438
+ if adj_space_cond_type == 'Unconditioned'
439
+ # Get adjacent surface
440
+ adjacent_surf = surf.adjacentSurface.get
441
+
442
+ # Store original boundary condition type
443
+ org_surface_boundary_conditions[surf.name.to_s] = adjacent_surf
444
+
445
+ # Identify this surface as exterior
446
+ surface_category[surf] = 'ExteriorSurface'
447
+
448
+ # Temporary change the surface's boundary condition to 'Outdoors' so it can be assigned a baseline construction
449
+ surf.setOutsideBoundaryCondition('Outdoors')
450
+ adjacent_surf.setOutsideBoundaryCondition('Outdoors')
451
+ end
452
+ end
453
+
454
+ if boundary_condition == 'Outdoors'
455
+ surface_category[surf] = 'ExteriorSurface'
456
+ elsif ['Ground', 'Foundation', 'GroundFCfactorMethod', 'OtherSideCoefficients', 'OtherSideConditionsModel', 'GroundSlabPreprocessorAverage', 'GroundSlabPreprocessorCore', 'GroundSlabPreprocessorPerimeter', 'GroundBasementPreprocessorAverageWall', 'GroundBasementPreprocessorAverageFloor', 'GroundBasementPreprocessorUpperWall', 'GroundBasementPreprocessorLowerWall'].include?(boundary_condition)
457
+ surface_category[surf] = 'GroundSurface'
458
+ else
459
+ surface_category[surf] = 'NA'
460
+ end
461
+ surfaces_to_modify << surf
462
+ end
463
+
464
+ # SubSurfaces
465
+ model.getSubSurfaces.sort.each do |surf|
466
+ next unless surf.outsideBoundaryCondition == boundary_condition
467
+ next unless surf.subSurfaceType == surface_type
468
+
469
+ surface_category[surf] = 'ExteriorSubSurface'
470
+ surfaces_to_modify << surf
471
+ end
472
+ end
473
+
474
+ # Modify these surfaces
475
+ prev_created_consts = {}
476
+ surfaces_to_modify.sort.each do |surf|
477
+ # Get space conditioning
478
+ space = surf.space.get
479
+ space_cond_type = space_conditioning_category(space)
480
+
481
+ # Do not modify constructions for unconditioned spaces
482
+ prev_created_consts = planar_surface_apply_standard_construction(surf, climate_zone, prev_created_consts, wwr_building_type, wwr_info, surface_category[surf]) unless space_cond_type == 'Unconditioned'
483
+
484
+ # Reset boundary conditions to original if they were temporary modified
485
+ if org_surface_boundary_conditions.include?(surf.name.to_s)
486
+ surf.setAdjacentSurface(org_surface_boundary_conditions[surf.name.to_s])
487
+ end
488
+ end
489
+
490
+ # List the unique array of constructions
491
+ if prev_created_consts.size.zero?
492
+ OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', 'None of the constructions in your proposed model have both Intended Surface Type and Standards Construction Type')
493
+ else
494
+ prev_created_consts.each do |surf_type, construction|
495
+ OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "For #{surf_type.join(' ')}, applied #{construction.name}.")
496
+ end
497
+ end
498
+
499
+ return true
500
+ end
501
+
502
+ # Go through the default construction sets and hard-assigned constructions.
503
+ # Clone the existing constructions and set their intended surface type and standards construction type per the PRM.
504
+ # For some standards, this will involve making modifications. For others, it will not.
505
+ #
506
+ # 90.1-2007, 90.1-2010, 90.1-2013
507
+ # @param model [OpenStudio::Model::Model] OpenStudio model object
508
+ # @return [Bool] returns true if successful, false if not
509
+ def model_apply_prm_construction_types(model)
510
+ types_to_modify = []
511
+
512
+ # Possible boundary conditions are
513
+ # Adiabatic
514
+ # Surface
515
+ # Outdoors
516
+ # Ground
517
+ # Foundation
518
+ # GroundFCfactorMethod
519
+ # OtherSideCoefficients
520
+ # OtherSideConditionsModel
521
+ # GroundSlabPreprocessorAverage
522
+ # GroundSlabPreprocessorCore
523
+ # GroundSlabPreprocessorPerimeter
524
+ # GroundBasementPreprocessorAverageWall
525
+ # GroundBasementPreprocessorAverageFloor
526
+ # GroundBasementPreprocessorUpperWall
527
+ # GroundBasementPreprocessorLowerWall
528
+
529
+ # Possible surface types are
530
+ # AtticFloor
531
+ # AtticWall
532
+ # AtticRoof
533
+ # DemisingFloor
534
+ # DemisingWall
535
+ # DemisingRoof
536
+ # ExteriorFloor
537
+ # ExteriorWall
538
+ # ExteriorRoof
539
+ # ExteriorWindow
540
+ # ExteriorDoor
541
+ # GlassDoor
542
+ # GroundContactFloor
543
+ # GroundContactWall
544
+ # GroundContactRoof
545
+ # InteriorFloor
546
+ # InteriorWall
547
+ # InteriorCeiling
548
+ # InteriorPartition
549
+ # InteriorWindow
550
+ # InteriorDoor
551
+ # OverheadDoor
552
+ # Skylight
553
+ # TubularDaylightDome
554
+ # TubularDaylightDiffuser
555
+
556
+ # Possible standards construction types
557
+ # Mass
558
+ # SteelFramed
559
+ # WoodFramed
560
+ # IEAD
561
+ # View
562
+ # Daylight
563
+ # Swinging
564
+ # NonSwinging
565
+ # Heated
566
+ # Unheated
567
+ # RollUp
568
+ # Sliding
569
+ # Metal
570
+ # Nonmetal framing (all)
571
+ # Metal framing (curtainwall/storefront)
572
+ # Metal framing (entrance door)
573
+ # Metal framing (all other)
574
+ # Metal Building
575
+ # Attic and Other
576
+ # Glass with Curb
577
+ # Plastic with Curb
578
+ # Without Curb
579
+
580
+ # Create an array of types
581
+ types_to_modify << ['Outdoors', 'ExteriorWall', 'SteelFramed']
582
+ types_to_modify << ['Outdoors', 'ExteriorRoof', 'IEAD']
583
+ types_to_modify << ['Outdoors', 'ExteriorFloor', 'SteelFramed']
584
+ types_to_modify << ['Ground', 'GroundContactFloor', 'Unheated']
585
+ types_to_modify << ['Ground', 'GroundContactWall', 'Mass']
586
+
587
+ # Foundation
588
+ types_to_modify << ['Foundation', 'GroundContactFloor', 'Unheated']
589
+ types_to_modify << ['Foundation', 'GroundContactWall', 'Mass']
590
+
591
+ # F/C-Factor methods
592
+ types_to_modify << ['GroundFCfactorMethod', 'GroundContactFloor', 'Unheated']
593
+ types_to_modify << ['GroundFCfactorMethod', 'GroundContactWall', 'Mass']
594
+
595
+ # Other side coefficients
596
+ types_to_modify << ['OtherSideCoefficients', 'GroundContactFloor', 'Unheated']
597
+ types_to_modify << ['OtherSideConditionsModel', 'GroundContactFloor', 'Unheated']
598
+ types_to_modify << ['OtherSideCoefficients', 'GroundContactWall', 'Mass']
599
+ types_to_modify << ['OtherSideConditionsModel', 'GroundContactWall', 'Mass']
600
+
601
+ # Slab preprocessor
602
+ types_to_modify << ['GroundSlabPreprocessorAverage', 'GroundContactFloor', 'Unheated']
603
+ types_to_modify << ['GroundSlabPreprocessorCore', 'GroundContactFloor', 'Unheated']
604
+ types_to_modify << ['GroundSlabPreprocessorPerimeter', 'GroundContactFloor', 'Unheated']
605
+
606
+ # Basement preprocessor
607
+ types_to_modify << ['GroundBasementPreprocessorAverageWall', 'GroundContactWall', 'Mass']
608
+ types_to_modify << ['GroundBasementPreprocessorAverageFloor', 'GroundContactFloor', 'Unheated']
609
+ types_to_modify << ['GroundBasementPreprocessorUpperWall', 'GroundContactWall', 'Mass']
610
+ types_to_modify << ['GroundBasementPreprocessorLowerWall', 'GroundContactWall', 'Mass']
611
+
612
+ # Modify all constructions of each type
613
+ types_to_modify.each do |boundary_cond, surf_type, const_type|
614
+ constructions = model_find_constructions(model, boundary_cond, surf_type)
615
+
616
+ constructions.sort.each do |const|
617
+ standards_info = const.standardsInformation
618
+ standards_info.setIntendedSurfaceType(surf_type)
619
+ standards_info.setStandardsConstructionType(const_type)
620
+ end
621
+ end
622
+
623
+ return true
624
+ end
625
+
626
+ # Reduces the SRR to the values specified by the PRM. SRR reduction will be done by shrinking vertices toward the centroid.
627
+ #
628
+ # @param model [OpenStudio::model::Model] OpenStudio model object
629
+ def model_apply_prm_baseline_skylight_to_roof_ratio(model)
630
+ # Loop through all spaces in the model, and
631
+ # per the 90.1-2019 PRM User Manual, only
632
+ # account for exterior roofs for enclosed
633
+ # spaces. Include space multipliers.
634
+ roof_m2 = 0.001 # Avoids divide by zero errors later
635
+ sky_m2 = 0
636
+ total_roof_m2 = 0.001
637
+ total_subsurface_m2 = 0
638
+ model.getSpaces.sort.each do |space|
639
+ next if space_conditioning_category(space) == 'Unconditioned'
640
+
641
+ # Loop through all surfaces in this space
642
+ roof_area_m2 = 0
643
+ sky_area_m2 = 0
644
+ space.surfaces.sort.each do |surface|
645
+ # Skip non-outdoor surfaces
646
+ next unless surface.outsideBoundaryCondition == 'Outdoors'
647
+ # Skip non-walls
648
+ next unless surface.surfaceType == 'RoofCeiling'
649
+
650
+ # This roof's gross area (including skylight area)
651
+ roof_area_m2 += surface.grossArea * space.multiplier
652
+ # Subsurfaces in this surface
653
+ surface.subSurfaces.sort.each do |ss|
654
+ next unless ss.subSurfaceType == 'Skylight'
655
+
656
+ sky_area_m2 += ss.netArea * space.multiplier
657
+ end
658
+ end
659
+
660
+ total_roof_m2 += roof_area_m2
661
+ total_subsurface_m2 += sky_area_m2
662
+ end
663
+
664
+ # Calculate the SRR of each category
665
+ srr = ((total_subsurface_m2 / total_roof_m2) * 100.0).round(1)
666
+ OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "The skylight to roof ratios (SRRs) is: : #{srr.round}%.")
667
+
668
+ # SRR limit
669
+ srr_lim = model_prm_skylight_to_roof_ratio_limit(model)
670
+
671
+ # Check against SRR limit
672
+ red = srr > srr_lim
673
+
674
+ # Stop here unless skylights need reducing
675
+ return true unless red
676
+
677
+ OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "Reducing the size of all skylights equally down to the limit of #{srr_lim.round}%.")
678
+
679
+ # Determine the factors by which to reduce the skylight area
680
+ mult = srr_lim / srr
681
+
682
+ # Reduce the skylight area if any of the categories necessary
683
+ model.getSpaces.sort.each do |space|
684
+ next if space_conditioning_category(space) == 'Unconditioned'
685
+
686
+ # Loop through all surfaces in this space
687
+ space.surfaces.sort.each do |surface|
688
+ # Skip non-outdoor surfaces
689
+ next unless surface.outsideBoundaryCondition == 'Outdoors'
690
+ # Skip non-walls
691
+ next unless surface.surfaceType == 'RoofCeiling'
692
+
693
+ # Subsurfaces in this surface
694
+ surface.subSurfaces.sort.each do |ss|
695
+ next unless ss.subSurfaceType == 'Skylight'
696
+
697
+ # Reduce the size of the skylight
698
+ red = 1.0 - mult
699
+ sub_surface_reduce_area_by_percent_by_shrinking_toward_centroid(ss, red)
700
+ end
701
+ end
702
+ end
703
+
704
+ return true
705
+ end
706
+
707
+ # Apply baseline values to exterior lights objects
708
+ # Characterization of objects must be done via user data
709
+ #
710
+ # @param model [OpenStudio::model::Model] OpenStudio model object
711
+ def model_apply_baseline_exterior_lighting(model)
712
+ user_ext_lights = @standards_data.key?('userdata_exterior_lights') ? @standards_data['userdata_exterior_lights'] : nil
713
+ return false if user_ext_lights.nil?
714
+
715
+ non_tradeable_cats = ['nontradeable_general', 'building_facades_area', 'building_facades_perim', 'automated_teller_machines_per_location', 'automated_teller_machines_per_machine', 'entries_and_gates', 'loading_areas_for_emergency_vehicles', 'drive_through_windows_and_doors', 'parking_near_24_hour_entrances', 'roadway_parking']
716
+ search_criteria = {
717
+ 'template' => template
718
+ }
719
+
720
+ ext_ltg_baseline_values = standards_lookup_table_first(table_name: 'prm_exterior_lighting', search_criteria: search_criteria)
721
+
722
+ user_ext_lights.each do |user_data|
723
+ lights_name = user_data['name']
724
+
725
+ # model.getExteriorLightss.each do |exterior_lights|
726
+
727
+ if model.getExteriorLightsByName(lights_name).is_initialized
728
+ ext_lights_obj = model.getExteriorLightsByName(lights_name).get
729
+ else
730
+ # Report invalid name in user data
731
+ OpenStudio.logFree(OpenStudio::Warn, 'prm.log', "ExteriorLights object named #{lights_name} from user data file not found in model")
732
+ next
733
+ end
734
+
735
+ # Make sure none of the categories are nontradeable and not a mix of tradeable and nontradeable
736
+ num_trade = 0
737
+ num_notrade = 0
738
+ ext_ltg_cats = {}
739
+ num_cats = user_data['num_ext_lights_subcats'].to_i
740
+ (1..num_cats).each do |icat|
741
+ cat_key = format('end_use_subcategory_%02d', icat)
742
+ subcat = user_data[cat_key]
743
+ if non_tradeable_cats.include?(subcat)
744
+ num_notrade += 1
745
+ else
746
+ num_trade += 1
747
+ meas_val_key = format('end_use_measurement_value_%02d', icat)
748
+ meas_val = user_data[meas_val_key]
749
+ ext_ltg_cats[subcat] = meas_val.to_f
750
+ end
751
+ end
752
+
753
+ # Skip this if all lights are non-tradeable
754
+ next if num_trade == 0
755
+
756
+ # Error if mix of tradeable and nontradeable
757
+ if (num_trade > 0) && (num_notrade > 0)
758
+ OpenStudio.logFree(OpenStudio::Warn, 'prm.log', "ExteriorLights object named #{lights_name} from user data file has mix of tradeable and non-tradeable lighting types. All will be treated as non-tradeable.")
759
+ next
760
+ end
761
+
762
+ ext_ltg_pwr = 0
763
+ ext_ltg_cats.each do |cat_key, meas_val|
764
+ # Get baseline power for this type of exterior lighting
765
+ baseline_value = ext_ltg_baseline_values[cat_key].to_f
766
+ ext_ltg_pwr += baseline_value * meas_val
767
+ end
768
+
769
+ # Update existing exterior lights object: control, schedule, power
770
+ ext_lights_obj.setControlOption('AstronomicalClock')
771
+ ext_lights_obj.setSchedule(model.alwaysOnDiscreteSchedule)
772
+ ext_lights_obj.setMultiplier(1)
773
+ ext_lights_def = ext_lights_obj.exteriorLightsDefinition
774
+ ext_lights_def.setDesignLevel(ext_ltg_pwr)
775
+ end
776
+ end
777
+
778
+ # Function to add baseline elevators based on user data
779
+ # @param model [OpenStudio::Model::Model] OpenStudio model object
780
+ def model_add_prm_elevators(model)
781
+ # Load elevator data from userdata csv files
782
+ user_elevators = @standards_data.key?('userdata_electric_equipment') ? @standards_data['userdata_electric_equipment'] : nil
783
+ user_elevators.each do |user_elevator|
784
+ num_lifts = user_elevator['elevator_number_of_lifts'].to_i
785
+ next if num_lifts == 0
786
+
787
+ equip_name = user_elevator['name']
788
+ number_of_levels = user_elevator['elevator_number_of_stories'].to_i
789
+
790
+ elevator_weight_of_car = user_elevator['elevator_weight_of_car'].to_f
791
+ elevator_rated_load = user_elevator['elevator_rated_load'].to_f
792
+ elevator_speed_of_car = user_elevator['elevator_speed_of_car'].to_f
793
+ if number_of_levels < 5
794
+ # From Table G3.9.2 performance rating method baseline elevator motor
795
+ elevator_mech_eff = 0.58
796
+ elevator_counter_weight_of_car = 0.0
797
+ search_criteria = {
798
+ 'template' => template,
799
+ 'type' => 'Hydraulic'
800
+ }
801
+ else
802
+ # From Table G3.9.2 performance rating method baseline elevator motor
803
+ elevator_mech_eff = 0.64
804
+ # Determine the elevator counterweight
805
+ if user_elevator['elevator_counter_weight_of_car'].nil?
806
+ # When the proposed design counterweight is not specified
807
+ # it is determined as per Table G3.9.2
808
+ elevator_counter_weight_of_car = elevator_weight_of_car + 0.4 * elevator_rated_load
809
+ else
810
+ elevator_counter_weight_of_car = user_elevator['elevator_counter_weight_of_car'].to_f
811
+ end
812
+ search_criteria = {
813
+ 'template' => template,
814
+ 'type' => 'Any'
815
+ }
816
+ end
817
+
818
+ elevator_motor_bhp = (elevator_weight_of_car + elevator_rated_load - elevator_counter_weight_of_car) * elevator_speed_of_car / (33000 * elevator_mech_eff)
819
+
820
+ # Lookup the minimum motor efficiency
821
+ elevator_motor_eff = standards_data['motors']
822
+ motor_properties = model_find_object(elevator_motor_eff, search_criteria, nil, nil, nil, nil, elevator_motor_bhp)
823
+ if motor_properties.nil?
824
+ OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.elevator', "For #{equip_name}, could not find motor properties using search criteria: #{search_criteria}, motor_bhp = #{motor_bhp} hp.")
825
+ return false
826
+ end
827
+
828
+ nominal_hp = motor_properties['maximum_capacity'].to_f.round(1)
829
+ # Round to nearest whole HP for niceness
830
+ if nominal_hp >= 2
831
+ nominal_hp = nominal_hp.round
832
+ end
833
+
834
+ # Get the efficiency based on the nominal horsepower
835
+ # Add 0.01 hp to avoid search errors.
836
+ motor_properties = model_find_object(elevator_motor_eff, search_criteria, nil, nil, nil, nil, nominal_hp + 0.01)
837
+ if motor_properties.nil?
838
+ OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.model', "For #{equip_name}, could not find nominal motor properties using search criteria: #{search_criteria}, motor_hp = #{nominal_hp} hp.")
839
+ return false
840
+ end
841
+ motor_eff = motor_properties['nominal_full_load_efficiency'].to_f
842
+ elevator_power = num_lifts * elevator_motor_bhp * 746 / motor_eff
843
+
844
+ # Set elevator power to either regular electric equipment object or
845
+ # exterior fuel equipment
846
+ if model.getElectricEquipmentByName(equip_name).is_initialized
847
+ model.getElectricEquipmentByName(equip_name).get.electricEquipmentDefinition.setDesignLevel(elevator_power)
848
+ elevator_space = model.getElectricEquipmentByName(equip_name).get.space.get
849
+ end
850
+ if model.getExteriorFuelEquipmentByName(equip_name).is_initialized
851
+ model.getExteriorFuelEquipmentByName(equip_name).exteriorFuelEquipmentDefinition.setDesignLevel(elevator_power)
852
+ elevator_space = model.getElectricEquipmentByName(equip_name).get.space.get
853
+ end
854
+
855
+ # Add ventilation and lighting process loads if modeled in the proposed model
856
+ misc_elevator_process_loads = 0.0
857
+ misc_elevator_process_loads += user_elevator['elevator_ventilation_cfm'].to_f * 0.33
858
+ misc_elevator_process_loads += user_elevator['elevator_area_ft2'].to_f * 3.14
859
+ if misc_elevator_process_loads > 0
860
+ misc_elevator_process_loads_def = OpenStudio::Model::ElectricEquipmentDefinition.new(model)
861
+ misc_elevator_process_loads_def.setName("#{equip_name} - Misc Process Loads - Def")
862
+ misc_elevator_process_loads_def.setDesignLevel(misc_elevator_process_loads)
863
+ misc_elevator_process_loads = OpenStudio::Model::ElectricEquipment.new(misc_elevator_process_loads_def)
864
+ misc_elevator_process_loads.setName("#{equip_name} - Misc Process Loads")
865
+ misc_elevator_process_loads.setEndUseSubcategory('Elevators')
866
+ misc_elevator_process_loads.setSchedule(model.alwaysOnDiscreteSchedule)
867
+ misc_elevator_process_loads.setSpace(elevator_space)
868
+ end
869
+ end
870
+ end
871
+
872
+ # Add design day schedule objects for space loads, for PRM 2019 baseline models
873
+ # @author Xuechen (Jerry) Lei, PNNL
874
+ # @param model [OpenStudio::model::Model] OpenStudio model object
875
+ #
876
+ def model_apply_prm_baseline_sizing_schedule(model)
877
+ space_loads = model.getSpaceLoads
878
+ loads = []
879
+ space_loads.sort.each do |space_load|
880
+ load_type = space_load.iddObjectType.valueName.sub('OS_', '').strip.sub('_', '')
881
+ casting_method_name = "to_#{load_type}"
882
+ if space_load.respond_to?(casting_method_name)
883
+ casted_load = space_load.public_send(casting_method_name).get
884
+ loads << casted_load
885
+ else
886
+ p 'Need Debug, casting method not found @JXL'
887
+ end
888
+ end
889
+
890
+ load_schedule_name_hash = {
891
+ 'People' => 'numberofPeopleSchedule',
892
+ 'Lights' => 'schedule',
893
+ 'ElectricEquipment' => 'schedule',
894
+ 'GasEquipment' => 'schedule',
895
+ 'SpaceInfiltration_DesignFlowRate' => 'schedule'
896
+ }
897
+
898
+ loads.each do |load|
899
+ load_type = load.iddObjectType.valueName.sub('OS_', '').strip
900
+ load_schedule_name = load_schedule_name_hash[load_type]
901
+ next unless !load_schedule_name.nil?
902
+
903
+ # check if the load is in a dwelling space
904
+ if load.spaceType.is_initialized
905
+ space_type = load.spaceType.get
906
+ elsif load.space.is_initialized && load.space.get.spaceType.is_initialized
907
+ space_type = load.space.get.spaceType.get
908
+ else
909
+ space_type = nil
910
+ puts "No hosting space/spacetype found for load: #{load.name}"
911
+ end
912
+ if !space_type.nil? && /apartment/i =~ space_type.standardsSpaceType.to_s
913
+ load_in_dwelling = true
914
+ else
915
+ load_in_dwelling = false
916
+ end
917
+
918
+ load_schedule = load.public_send(load_schedule_name).get
919
+ schedule_type = load_schedule.iddObjectType.valueName.sub('OS_', '').strip.sub('_', '')
920
+ load_schedule = load_schedule.public_send("to_#{schedule_type}").get
921
+
922
+ case schedule_type
923
+ when 'ScheduleRuleset'
924
+ load_schmax = get_8760_values_from_schedule(model, load_schedule).max
925
+ load_schmin = get_8760_values_from_schedule(model, load_schedule).min
926
+ load_schmode = get_weekday_values_from_8760(model,
927
+ Array(get_8760_values_from_schedule(model, load_schedule)),
928
+ value_includes_holiday = true).mode[0]
929
+
930
+ # AppendixG-2019 G3.1.2.2.1
931
+ if load_type == 'SpaceInfiltration_DesignFlowRate'
932
+ summer_value = load_schmax
933
+ winter_value = load_schmax
934
+ else
935
+ summer_value = load_schmax
936
+ winter_value = load_schmin
937
+ end
938
+
939
+ # AppendixG-2019 Exception to G3.1.2.2.1
940
+ if load_in_dwelling
941
+ summer_value = load_schmode
942
+ end
943
+
944
+ # set cooling design day schedule
945
+ summer_dd_schedule = OpenStudio::Model::ScheduleDay.new(model)
946
+ summer_dd_schedule.setName("#{load.name} Summer Design Day")
947
+ summer_dd_schedule.addValue(OpenStudio::Time.new(1.0), summer_value)
948
+ load_schedule.setSummerDesignDaySchedule(summer_dd_schedule)
949
+
950
+ # set heating design day schedule
951
+ winter_dd_schedule = OpenStudio::Model::ScheduleDay.new(model)
952
+ winter_dd_schedule.setName("#{load.name} Winter Design Day")
953
+ winter_dd_schedule.addValue(OpenStudio::Time.new(1.0), winter_value)
954
+ load_schedule.setWinterDesignDaySchedule(winter_dd_schedule)
955
+
956
+ when 'ScheduleConstant'
957
+ OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "Space load #{load.name} has schedule type of ScheduleConstant. Nothing to be done for ScheduleConstant")
958
+ next
959
+ end
960
+ end
961
+ end
962
+
963
+ # Applies the multi-zone VAV outdoor air sizing requirements to all applicable air loops in the model.
964
+ # @note This is not applicable to the stable baseline; hence no action in this method
965
+ #
966
+ # @param model [OpenStudio::Model::Model] OpenStudio model object
967
+ # @return [Bool] returns true if successful, false if not
968
+ def model_apply_multizone_vav_outdoor_air_sizing(model)
969
+ return true
970
+ end
971
+
972
+ # Identifies non mechanically cooled ("nmc") systems, if applicable
973
+ #
974
+ # TODO: Zone-level evaporative cooler is not currently supported by
975
+ # by OpenStudio, will need to be added to the method when
976
+ # supported.
977
+ #
978
+ # @param model [OpenStudio::model::Model] OpenStudio model object
979
+ # @return zone_nmc_sys_type [Hash] Zone to nmc system type mapping
980
+ def model_identify_non_mechanically_cooled_systems(model)
981
+ # Iterate through zones to find out if they are served by nmc systems
982
+ model.getThermalZones.sort.each do |zone|
983
+ # Check if airloop has economizer and either:
984
+ # - No cooling coil and/or,
985
+ # - An evaporative cooling coil
986
+ air_loop = zone.airLoopHVAC
987
+
988
+ unless air_loop.empty?
989
+ # Iterate through all the airloops assigned to a zone
990
+ zone.airLoopHVACs.each do |airloop|
991
+ air_loop = air_loop.get
992
+ if (!air_loop_hvac_include_cooling_coil?(air_loop) &&
993
+ air_loop_hvac_include_evaporative_cooler?(air_loop)) ||
994
+ (!air_loop_hvac_include_cooling_coil?(air_loop) &&
995
+ air_loop_hvac_include_economizer?(air_loop))
996
+ air_loop.additionalProperties.setFeature('non_mechanically_cooled', true)
997
+ air_loop.thermalZones.each do |thermal_zone|
998
+ thermal_zone.additionalProperties.setFeature('non_mechanically_cooled', true)
999
+ end
1000
+ end
1001
+ end
1002
+ end
1003
+ end
1004
+ end
1005
+
1006
+ # Specify supply air temperature setpoint for unit heaters based on 90.1 Appendix G G3.1.2.8.2
1007
+ #
1008
+ # @param thermal_zone [OpenStudio::Model::ThermalZone] OpenStudio ThermalZone Object
1009
+ #
1010
+ # @return [Double] for zone with unit heaters, return design supply temperature; otherwise, return nil
1011
+ def thermal_zone_prm_unitheater_design_supply_temperature(thermal_zone)
1012
+ thermal_zone.equipment.each do |eqt|
1013
+ if eqt.to_ZoneHVACUnitHeater.is_initialized
1014
+ return OpenStudio.convert(105, 'F', 'C').get
1015
+ end
1016
+ end
1017
+ return nil
1018
+ end
1019
+
1020
+ # Specify supply to room delta for laboratory spaces based on 90.1 Appendix G Exception to G3.1.2.8.1
1021
+ #
1022
+ # @param thermal_zone [OpenStudio::Model::ThermalZone] OpenStudio ThermalZone Object
1023
+ #
1024
+ # @return [Double] for zone with laboratory space, return 17; otherwise, return nil
1025
+ def thermal_zone_prm_lab_delta_t(thermal_zone)
1026
+ # For labs, add 17 delta-T; otherwise, add 20 delta-T
1027
+ thermal_zone.spaces.each do |space|
1028
+ space_std_type = space.spaceType.get.standardsSpaceType.get
1029
+ if space_std_type == 'laboratory'
1030
+ return 17
1031
+ end
1032
+ end
1033
+ return nil
1034
+ end
1035
+
1036
+ # Indicate if fan power breakdown (supply, return, and relief)
1037
+ # are needed
1038
+ #
1039
+ # @return [Boolean] true if necessary, false otherwise
1040
+ def model_get_fan_power_breakdown
1041
+ return true
1042
+ end
1043
+
1044
+ # Applies the HVAC parts of the template to all objects in the model using the the template specified in the model.
1045
+ #
1046
+ # @param model [OpenStudio::Model::Model] OpenStudio model object
1047
+ # @param apply_controls [Bool] toggle whether to apply air loop and plant loop controls
1048
+ # @param sql_db_vars_map [Hash] hash map
1049
+ # @return [Bool] returns true if successful, false if not
1050
+ def model_apply_hvac_efficiency_standard(model, climate_zone, apply_controls: true, sql_db_vars_map: nil)
1051
+ sql_db_vars_map = {} if sql_db_vars_map.nil?
1052
+
1053
+ OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "Started applying HVAC efficiency standards for #{template} template.")
1054
+
1055
+ # Air Loop Controls
1056
+ if apply_controls.nil? || apply_controls == true
1057
+ model.getAirLoopHVACs.sort.each { |obj| air_loop_hvac_apply_standard_controls(obj, climate_zone) }
1058
+ end
1059
+
1060
+ # Plant Loop Controls
1061
+ if apply_controls.nil? || apply_controls == true
1062
+ model.getPlantLoops.sort.each { |obj| plant_loop_apply_standard_controls(obj, climate_zone) }
1063
+ end
1064
+
1065
+ # Zone HVAC Controls
1066
+ model.getZoneHVACComponents.sort.each { |obj| zone_hvac_component_apply_standard_controls(obj) }
1067
+
1068
+ # TODO: The fan and pump efficiency will be done by another task.
1069
+ # Fans
1070
+ # model.getFanVariableVolumes.sort.each { |obj| fan_apply_standard_minimum_motor_efficiency(obj, fan_brake_horsepower(obj)) }
1071
+ # model.getFanConstantVolumes.sort.each { |obj| fan_apply_standard_minimum_motor_efficiency(obj, fan_brake_horsepower(obj)) }
1072
+ # model.getFanOnOffs.sort.each { |obj| fan_apply_standard_minimum_motor_efficiency(obj, fan_brake_horsepower(obj)) }
1073
+ # model.getFanZoneExhausts.sort.each { |obj| fan_apply_standard_minimum_motor_efficiency(obj, fan_brake_horsepower(obj)) }
1074
+
1075
+ # Pumps
1076
+ # model.getPumpConstantSpeeds.sort.each { |obj| pump_apply_standard_minimum_motor_efficiency(obj) }
1077
+ # model.getPumpVariableSpeeds.sort.each { |obj| pump_apply_standard_minimum_motor_efficiency(obj) }
1078
+ # model.getHeaderedPumpsConstantSpeeds.sort.each { |obj| pump_apply_standard_minimum_motor_efficiency(obj) }
1079
+ # model.getHeaderedPumpsVariableSpeeds.sort.each { |obj| pump_apply_standard_minimum_motor_efficiency(obj) }
1080
+
1081
+ # Zone level systems/components
1082
+ model.getThermalZones.each do |zone|
1083
+ if zone.additionalProperties.getFeatureAsString('baseline_system_type').is_initialized
1084
+ sys_type = zone.additionalProperties.getFeatureAsString('baseline_system_type').get
1085
+ end
1086
+ zone.equipment.each do |zone_equipment|
1087
+ if zone_equipment.to_ZoneHVACPackagedTerminalAirConditioner.is_initialized
1088
+ ptac = zone_equipment.to_ZoneHVACPackagedTerminalAirConditioner.get
1089
+ cooling_coil = ptac.coolingCoil
1090
+ sql_db_vars_map = set_coil_cooling_efficiency_and_curves(cooling_coil, sql_db_vars_map, sys_type)
1091
+ elsif zone_equipment.to_ZoneHVACPackagedTerminalHeatPump.is_initialized
1092
+ pthp = zone_equipment.to_ZoneHVACPackagedTerminalHeatPump.get
1093
+ cooling_coil = pthp.coolingCoil
1094
+ heating_coil = pthp.heatingCoil
1095
+ sql_db_vars_map = set_coil_cooling_efficiency_and_curves(cooling_coil, sql_db_vars_map, sys_type)
1096
+ sql_db_vars_map = set_coil_heating_efficiency_and_curves(heating_coil, sql_db_vars_map, sys_type)
1097
+ elsif zone_equipment.to_ZoneHVACUnitHeater.is_initialized
1098
+ unit_heater = zone_equipment.to_ZoneHVACUnitHeater.get
1099
+ heating_coil = unit_heater.heatingCoil
1100
+ sql_db_vars_map = set_coil_heating_efficiency_and_curves(heating_coil, sql_db_vars_map, sys_type)
1101
+ end
1102
+ end
1103
+ end
1104
+
1105
+ # Airloop HVAC level components
1106
+ model.getAirLoopHVACs.sort.each do |air_loop|
1107
+ sys_type = air_loop.additionalProperties.getFeatureAsString('baseline_system_type').get
1108
+ air_loop.components.each do |icomponent|
1109
+ if icomponent.to_AirLoopHVACUnitarySystem.is_initialized
1110
+ unitary_system = icomponent.to_AirLoopHVACUnitarySystem.get
1111
+ if unitary_system.coolingCoil.is_initialized
1112
+ cooling_coil = unitary_system.coolingCoil.get
1113
+ sql_db_vars_map = set_coil_cooling_efficiency_and_curves(cooling_coil, sql_db_vars_map, sys_type)
1114
+ end
1115
+ if unitary_system.heatingCoil.is_initialized
1116
+ heating_coil = unitary_system.heatingCoil.get
1117
+ sql_db_vars_map = set_coil_heating_efficiency_and_curves(heating_coil, sql_db_vars_map, sys_type)
1118
+ end
1119
+ elsif icomponent.to_CoilCoolingDXSingleSpeed.is_initialized
1120
+ cooling_coil = icomponent.to_CoilCoolingDXSingleSpeed.get
1121
+ sql_db_vars_map = coil_cooling_dx_single_speed_apply_efficiency_and_curves(cooling_coil, sql_db_vars_map, sys_type)
1122
+ elsif icomponent.to_CoilCoolingDXTwoSpeed.is_initialized
1123
+ cooling_coil = icomponent.to_CoilCoolingDXTwoSpeed.get
1124
+ sql_db_vars_map = coil_cooling_dx_two_speed_apply_efficiency_and_curves(cooling_coil, sql_db_vars_map, sys_type)
1125
+ elsif icomponent.to_CoilHeatingDXSingleSpeed.is_initialized
1126
+ heating_coil = icomponent.to_CoilHeatingDXSingleSpeed.get
1127
+ sql_db_vars_map = coil_heating_dx_single_speed_apply_efficiency_and_curves(heating_coil, sql_db_vars_map, sys_type)
1128
+ elsif icomponent.to_CoilHeatingGas.is_initialized
1129
+ heating_coil = icomponent.to_CoilHeatingGas.get
1130
+ sql_db_vars_map = coil_heating_gas_apply_efficiency_and_curves(heating_coil, sql_db_vars_map, sys_type)
1131
+ end
1132
+ end
1133
+ end
1134
+
1135
+ # Chillers
1136
+ model.getChillerElectricEIRs.sort.each { |obj| chiller_electric_eir_apply_efficiency_and_curves(obj) }
1137
+
1138
+ # Boilers
1139
+ model.getBoilerHotWaters.sort.each { |obj| boiler_hot_water_apply_efficiency_and_curves(obj) }
1140
+
1141
+ # Cooling Towers
1142
+ model.getCoolingTowerVariableSpeeds.sort.each { |obj| cooling_tower_variable_speed_apply_efficiency_and_curves(obj) }
1143
+
1144
+ OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "Finished applying HVAC efficiency standards for #{template} template.")
1145
+ return true
1146
+ end
1147
+
1148
+ def set_coil_cooling_efficiency_and_curves(cooling_coil, sql_db_vars_map, sys_type)
1149
+ if cooling_coil.to_CoilCoolingDXSingleSpeed.is_initialized
1150
+ # single speed coil
1151
+ sql_db_vars_map = coil_cooling_dx_single_speed_apply_efficiency_and_curves(cooling_coil.to_CoilCoolingDXSingleSpeed.get, sql_db_vars_map, sys_type)
1152
+ elsif cooling_coil.to_CoilCoolingDXTwoSpeed.is_initialized
1153
+ # two speed coil
1154
+ sql_db_vars_map = coil_cooling_dx_two_speed_apply_efficiency_and_curves(cooling_coil.to_CoilCoolingDXTwoSpeed.get, sql_db_vars_map, sys_type)
1155
+ else
1156
+ OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "#{cooling_coil.name} is not single speed or two speed DX cooling coil. Nothing to be done for efficiency")
1157
+ end
1158
+
1159
+ return sql_db_vars_map
1160
+ end
1161
+
1162
+ def set_coil_heating_efficiency_and_curves(heating_coil, sql_db_vars_map, sys_type)
1163
+ if heating_coil.to_CoilHeatingDXSingleSpeed.is_initialized
1164
+ # single speed coil
1165
+ sql_db_vars_map = coil_heating_dx_single_speed_apply_efficiency_and_curves(heating_coil.to_CoilHeatingDXSingleSpeed.get, sql_db_vars_map, sys_type)
1166
+ elsif heating_coil.to_CoilHeatingGas.is_initialized
1167
+ # single speed coil
1168
+ sql_db_vars_map = coil_heating_gas_apply_efficiency_and_curves(heating_coil.to_CoilHeatingGas.get, sql_db_vars_map, sys_type)
1169
+ else
1170
+ OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "#{heating_coil.name} is not single speed DX heating coil. Nothing to be done for efficiency")
1171
+ end
1172
+
1173
+ return sql_db_vars_map
1174
+ end
1175
+
1176
+ # Template method for adding a setpoint manager for a coil control logic to a heating coil.
1177
+ # ASHRAE 90.1-2019 Appendix G.
1178
+ #
1179
+ # @param model [OpenStudio::Model::Model] Openstudio model
1180
+ # @param thermalZones Array([OpenStudio::Model::ThermalZone]) thermal zone array
1181
+ # @param coil Heating Coils
1182
+ # @return [Boolean] true
1183
+ def model_set_central_preheat_coil_spm(model, thermal_zones, coil)
1184
+ # search for the highest zone setpoint temperature
1185
+ max_heat_setpoint = 0.0
1186
+ coil_name = coil.name.get.to_s
1187
+ thermal_zones.each do |zone|
1188
+ tstat = zone.thermostatSetpointDualSetpoint
1189
+ if tstat.is_initialized
1190
+ tstat = tstat.get
1191
+ setpoint_sch = tstat.heatingSetpointTemperatureSchedule
1192
+ setpoint_min_max = search_min_max_value_from_design_day_schedule(setpoint_sch, 'heating')
1193
+ setpoint_c = setpoint_min_max['max']
1194
+ if setpoint_c > max_heat_setpoint
1195
+ max_heat_setpoint = setpoint_c
1196
+ end
1197
+ end
1198
+ end
1199
+ # in this situation, we hard set the temperature to be 22 F
1200
+ # (ASHRAE 90.1 Room heating stepoint temperature is 72 F)
1201
+ max_heat_setpoint = 22.2 if max_heat_setpoint == 0.0
1202
+
1203
+ max_heat_setpoint_f = OpenStudio.convert(max_heat_setpoint, 'C', 'F').get
1204
+ preheat_setpoint_f = max_heat_setpoint_f - 20
1205
+ preheat_setpoint_c = OpenStudio.convert(preheat_setpoint_f, 'F', 'C').get
1206
+
1207
+ # create a new constant schedule and this method will add schedule limit type
1208
+ preheat_coil_sch = model_add_constant_schedule_ruleset(model,
1209
+ preheat_setpoint_c,
1210
+ name = "#{coil_name} Setpoint Temp - #{preheat_setpoint_f.round}F")
1211
+ preheat_coil_manager = OpenStudio::Model::SetpointManagerScheduled.new(model, preheat_coil_sch)
1212
+ preheat_coil_manager.setName("#{coil_name} Preheat Coil Setpoint Manager")
1213
+
1214
+ if coil.to_CoilHeatingWater.is_initialized
1215
+ preheat_coil_manager.addToNode(coil.airOutletModelObject.get.to_Node.get)
1216
+ elsif coil.to_CoilHeatingElectric.is_initialized
1217
+ preheat_coil_manager.addToNode(coil.outletModelObject.get.to_Node.get)
1218
+ elsif coil.to_CoilHeatingGas.is_initialized
1219
+ OpenStudio.logFree(OpenStudio::Warn, 'openstudio.models.CoilHeatingGas', 'Preheat coils in baseline system shall only be electric or hydronic. Current coil type: Natural Gas')
1220
+ preheat_coil_manager.addToNode(coil.airOutletModelObject.get.to_Node.get)
1221
+ end
1222
+
1223
+ return true
1224
+ end
1225
+
1226
+ # Add zone additional property "zone DCV implemented in user model":
1227
+ # - 'true' if zone OA flow requirement is specified as per person & airloop supporting this zone has DCV enabled
1228
+ # - 'false' otherwise
1229
+ #
1230
+ # @author Xuechen (Jerry) Lei, PNNL
1231
+ # @param model [OpenStudio::Model::Model] Openstudio model
1232
+ def model_mark_zone_dcv_existence(model)
1233
+ model.getAirLoopHVACs.each do |air_loop_hvac|
1234
+ next unless air_loop_hvac.airLoopHVACOutdoorAirSystem.is_initialized
1235
+
1236
+ oa_system = air_loop_hvac.airLoopHVACOutdoorAirSystem.get
1237
+ controller_oa = oa_system.getControllerOutdoorAir
1238
+ controller_mv = controller_oa.controllerMechanicalVentilation
1239
+ next unless controller_mv.demandControlledVentilation == true
1240
+
1241
+ air_loop_hvac.thermalZones.each do |thermal_zone|
1242
+ zone_dcv = false
1243
+ thermal_zone.spaces.each do |space|
1244
+ dsn_oa = space.designSpecificationOutdoorAir
1245
+ next if dsn_oa.empty?
1246
+
1247
+ dsn_oa = dsn_oa.get
1248
+ next if dsn_oa.outdoorAirMethod == 'Maximum'
1249
+
1250
+ if dsn_oa.outdoorAirFlowperPerson > 0
1251
+ # only in this case the thermal zone is considered to be implemented with DCV
1252
+ zone_dcv = true
1253
+ end
1254
+ end
1255
+
1256
+ if zone_dcv == true
1257
+ thermal_zone.additionalProperties.setFeature('zone DCV implemented in user model', true)
1258
+ end
1259
+ end
1260
+ end
1261
+
1262
+ # mark unmarked zones
1263
+ model.getThermalZones.each do |zone|
1264
+ next if zone.additionalProperties.hasFeature('zone DCV implemented in user model')
1265
+
1266
+ zone.additionalProperties.setFeature('zone DCV implemented in user model', false)
1267
+ end
1268
+
1269
+ return true
1270
+ end
1271
+
1272
+ # read user data and add to zone additional properties
1273
+ # "airloop user specified DCV exception"
1274
+ # "one user specified DCV exception"
1275
+ #
1276
+ # @author Xuechen (Jerry) Lei, PNNL
1277
+ # @param model [OpenStudio::Model::Model] Openstudio model
1278
+ def model_add_dcv_user_exception_properties(model)
1279
+ model.getAirLoopHVACs.each do |air_loop_hvac|
1280
+ dcv_airloop_user_exception = false
1281
+ if standards_data.key?('userdata_airloop_hvac')
1282
+ standards_data['userdata_airloop_hvac'].each do |row|
1283
+ next unless row['name'].to_s.downcase.strip == air_loop_hvac.name.to_s.downcase.strip
1284
+
1285
+ if row['dcv_exception_airloop'].to_s.upcase.strip == 'TRUE'
1286
+ dcv_airloop_user_exception = true
1287
+ break
1288
+ end
1289
+ end
1290
+ end
1291
+ air_loop_hvac.thermalZones.each do |thermal_zone|
1292
+ if dcv_airloop_user_exception
1293
+ thermal_zone.additionalProperties.setFeature('airloop user specified DCV exception', true)
1294
+ end
1295
+ end
1296
+ end
1297
+
1298
+ # zone level exception tagging is put outside of airloop because it directly reads from user data and
1299
+ # a zone not under an airloop in user model may be in an airloop in baseline
1300
+ model.getThermalZones.each do |thermal_zone|
1301
+ dcv_zone_user_exception = false
1302
+ if standards_data.key?('userdata_thermal_zone')
1303
+ standards_data['userdata_thermal_zone'].each do |row|
1304
+ next unless row['name'].to_s.downcase.strip == thermal_zone.name.to_s.downcase.strip
1305
+
1306
+ if row['dcv_exception_thermal_zone'].to_s.upcase.strip == 'TRUE'
1307
+ dcv_zone_user_exception = true
1308
+ break
1309
+ end
1310
+ end
1311
+ end
1312
+ if dcv_zone_user_exception
1313
+ thermal_zone.additionalProperties.setFeature('zone user specified DCV exception', true)
1314
+ end
1315
+ end
1316
+
1317
+ # mark unmarked zones
1318
+ model.getThermalZones.each do |zone|
1319
+ next if zone.additionalProperties.hasFeature('airloop user specified DCV exception')
1320
+
1321
+ zone.additionalProperties.setFeature('airloop user specified DCV exception', false)
1322
+ end
1323
+
1324
+ model.getThermalZones.each do |zone|
1325
+ next if zone.additionalProperties.hasFeature('zone user specified DCV exception')
1326
+
1327
+ zone.additionalProperties.setFeature('zone user specified DCV exception', false)
1328
+ end
1329
+ end
1330
+
1331
+ # add zone additional property "airloop dcv required by 901"
1332
+ # - "true" if the airloop supporting this zone is required by 90.1 (non-exception requirement + user provided exception flag) to have DCV regarding user model
1333
+ # - "false" otherwise
1334
+ # add zone additional property "zone dcv required by 901"
1335
+ # - "true" if the zone is required by 90.1(non-exception requirement + user provided exception flag) to have DCV regarding user model
1336
+ # - 'flase' otherwise
1337
+ #
1338
+ # @author Xuechen (Jerry) Lei, PNNL
1339
+ # @param model [OpenStudio::Model::Model] Openstudio model
1340
+ def model_add_dcv_requirement_properties(model)
1341
+ model.getAirLoopHVACs.each do |air_loop_hvac|
1342
+ if user_model_air_loop_hvac_demand_control_ventilation_required?(air_loop_hvac)
1343
+ air_loop_hvac.thermalZones.each do |thermal_zone|
1344
+ thermal_zone.additionalProperties.setFeature('airloop dcv required by 901', true)
1345
+
1346
+ # the zone level dcv requirement can only be true if it is in an airloop that is required to have DCV
1347
+ if user_model_zone_demand_control_ventilation_required?(thermal_zone)
1348
+ thermal_zone.additionalProperties.setFeature('zone dcv required by 901', true)
1349
+ end
1350
+ end
1351
+ end
1352
+ end
1353
+
1354
+ # mark unmarked zones
1355
+ model.getThermalZones.each do |zone|
1356
+ next if zone.additionalProperties.hasFeature('airloop dcv required by 901')
1357
+
1358
+ zone.additionalProperties.setFeature('airloop dcv required by 901', false)
1359
+ end
1360
+
1361
+ model.getThermalZones.each do |zone|
1362
+ next if zone.additionalProperties.hasFeature('zone dcv required by 901')
1363
+
1364
+ zone.additionalProperties.setFeature('zone dcv required by 901', false)
1365
+ end
1366
+ end
1367
+
1368
+ # based on previously added flag, raise error if DCV is required but not implemented in zones, in which case
1369
+ # baseline generation will be terminated; raise warning if DCV is not required but implemented, and continue baseline
1370
+ # generation
1371
+ #
1372
+ # @author Xuechen (Jerry) Lei, PNNL
1373
+ # @param model [OpenStudio::Model::Model] Openstudio model
1374
+ def model_raise_user_model_dcv_errors(model)
1375
+ # TODO: JXL add log msgs to PRM logger
1376
+ model.getThermalZones.each do |thermal_zone|
1377
+ if thermal_zone.additionalProperties.getFeatureAsBoolean('zone DCV implemented in user model').get &&
1378
+ (!thermal_zone.additionalProperties.getFeatureAsBoolean('zone dcv required by 901').get ||
1379
+ !thermal_zone.additionalProperties.getFeatureAsBoolean('airloop dcv required by 901').get)
1380
+ OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "For thermal zone #{thermal_zone.name}, ASHRAE 90.1 2019 6.4.3.8 does NOT require this zone to have demand control ventilation, but it was implemented in the user model, Appendix G baseline generation will continue!")
1381
+ if thermal_zone.additionalProperties.hasFeature('apxg no need to have DCV')
1382
+ if !thermal_zone.additionalProperties.getFeatureAsBoolean('apxg no need to have DCV').get
1383
+ OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "Moreover, for thermal zone #{thermal_zone.name}, Appendix G baseline model will have DCV based on ASHRAE 90.1 2019 G3.1.2.5")
1384
+ end
1385
+ end
1386
+ end
1387
+ if thermal_zone.additionalProperties.getFeatureAsBoolean('zone dcv required by 901').get &&
1388
+ thermal_zone.additionalProperties.getFeatureAsBoolean('airloop dcv required by 901').get &&
1389
+ !thermal_zone.additionalProperties.getFeatureAsBoolean('zone DCV implemented in user model').get
1390
+ OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Model', "For thermal zone #{thermal_zone.name}, ASHRAE 90.1 2019 6.4.3.8 requires this zone to have demand control ventilation, but it was not implemented in the user model, Appendix G baseline generation should be terminated!")
1391
+ end
1392
+ end
1393
+ end
1394
+
1395
+ # Check if zones in the baseline model (to be created) should have DCV based on 90.1 2019 G3.1.2.5. Zone additional
1396
+ # property 'apxg no need to have DCV' added
1397
+ #
1398
+ # @author Xuechen (Jerry) Lei, PNNL
1399
+ # @param model [OpenStudio::Model::Model] Openstudio model
1400
+ def model_add_apxg_dcv_properties(model)
1401
+ model.getAirLoopHVACs.each do |air_loop_hvac|
1402
+ if air_loop_hvac.airLoopHVACOutdoorAirSystem.is_initialized
1403
+ oa_flow_m3_per_s = get_airloop_hvac_design_oa_from_sql(air_loop_hvac)
1404
+ else
1405
+ OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "For #{air_loop_hvac.name}, DCV not applicable because it has no OA intake.")
1406
+ return false
1407
+ end
1408
+ oa_flow_cfm = OpenStudio.convert(oa_flow_m3_per_s, 'm^3/s', 'cfm').get
1409
+ if oa_flow_cfm <= 3000
1410
+ air_loop_hvac.thermalZones.each do |thermal_zone|
1411
+ thermal_zone.additionalProperties.setFeature('apxg no need to have DCV', true)
1412
+ end
1413
+ else # oa_flow_cfg > 3000, check zone people density
1414
+ air_loop_hvac.thermalZones.each do |thermal_zone|
1415
+ area_served_m2 = 0
1416
+ num_people = 0
1417
+ thermal_zone.spaces.each do |space|
1418
+ area_served_m2 += space.floorArea
1419
+ num_people += space.numberOfPeople
1420
+ end
1421
+ area_served_ft2 = OpenStudio.convert(area_served_m2, 'm^2', 'ft^2').get
1422
+ occ_per_1000_ft2 = num_people / area_served_ft2 * 1000
1423
+ if occ_per_1000_ft2 <= 100
1424
+ thermal_zone.additionalProperties.setFeature('apxg no need to have DCV', true)
1425
+ else
1426
+ thermal_zone.additionalProperties.setFeature('apxg no need to have DCV', false)
1427
+ end
1428
+ end
1429
+ end
1430
+ end
1431
+ # if a zone does not have this additional property, it means it was not served by airloop.
1432
+ end
1433
+
1434
+ # Set DCV in baseline HVAC system if required
1435
+ #
1436
+ # @author Xuechen (Jerry) Lei, PNNL
1437
+ # @param model [OpenStudio::Model::Model] Openstudio model
1438
+ def model_set_baseline_demand_control_ventilation(model, climate_zone)
1439
+ model.getAirLoopHVACs.each do |air_loop_hvac|
1440
+ if baseline_air_loop_hvac_demand_control_ventilation_required?(air_loop_hvac)
1441
+ air_loop_hvac_enable_demand_control_ventilation(air_loop_hvac, climate_zone)
1442
+ air_loop_hvac.thermalZones.sort.each do |zone|
1443
+ unless baseline_thermal_zone_demand_control_ventilation_required?(zone)
1444
+ thermal_zone_convert_oa_req_to_per_area(zone)
1445
+ end
1446
+ end
1447
+ end
1448
+ end
1449
+ end
1450
+
1451
+ # A template method that handles the loading of user input data from multiple sources
1452
+ # include data source from:
1453
+ # 1. user data csv files
1454
+ # 2. data from measure and OpenStudio interface
1455
+ # @param [Openstudio:model:Model] model
1456
+ # @param [String] climate_zone
1457
+ # @param [String] default_hvac_building_type
1458
+ # @param [String] default_wwr_building_type
1459
+ # @param [String] default_swh_building_type
1460
+ # @param [Hash] bldg_type_hvac_zone_hash A hash maps building type for hvac to a list of thermal zones
1461
+ # @return True
1462
+ def handle_user_input_data(model, climate_zone, default_hvac_building_type, default_wwr_building_type, default_swh_building_type, bldg_type_hvac_zone_hash)
1463
+ # load the multiple building area types from user data
1464
+ handle_multi_building_area_types(model, climate_zone, default_hvac_building_type, default_wwr_building_type, default_swh_building_type, bldg_type_hvac_zone_hash)
1465
+ # load user data from proposed model
1466
+ handle_airloop_user_input_data(model)
1467
+ # load air loop DOAS user data from the proposed model
1468
+ handle_airloop_doas_user_input_data(model)
1469
+ # load zone HVAC user data from proposed model
1470
+ handle_zone_hvac_user_input_data(model)
1471
+ # load thermal zone user data from proposed model
1472
+ handle_thermal_zone_user_input_data(model)
1473
+ end
1474
+
1475
+ # A function to load airloop data from userdata csv files
1476
+ # @param [OpenStudio::Model::Model] OpenStudio model object
1477
+ def handle_airloop_user_input_data(model)
1478
+ # ============================Process airloop info ============================================
1479
+ user_airloops = @standards_data.key?('userdata_airloop_hvac') ? @standards_data['userdata_airloop_hvac'] : nil
1480
+ model.getAirLoopHVACs.each do |air_loop|
1481
+ air_loop_name = air_loop.name.get
1482
+ if user_airloops && user_airloops.length > 1
1483
+ user_airloops.each do |user_airloop|
1484
+ if air_loop_name == user_airloop['name']
1485
+ # gas phase air cleaning is system base - add proposed hvac system name to zones
1486
+ if user_airloop.key?('economizer_exception_for_gas_phase_air_cleaning') && !user_airloop['economizer_exception_for_gas_phase_air_cleaning'].nil?
1487
+ if user_airloop['economizer_exception_for_gas_phase_air_cleaning'].downcase == 'yes'
1488
+ air_loop.thermalZones.each do |thermal_zone|
1489
+ thermal_zone.additionalProperties.setFeature('economizer_exception_for_gas_phase_air_cleaning', air_loop_name)
1490
+ end
1491
+ end
1492
+ end
1493
+ # Open refrigerated cases is zone based - add yes or no to zones
1494
+ if user_airloop.key?('economizer_exception_for_open_refrigerated_cases') && !user_airloop['economizer_exception_for_open_refrigerated_cases'].nil?
1495
+ if user_airloop['economizer_exception_for_open_refrigerated_cases'].downcase == 'yes'
1496
+ air_loop.thermalZones.each do |thermal_zone|
1497
+ thermal_zone.additionalProperties.setFeature('economizer_exception_for_open_refrigerated_cases', 'yes')
1498
+ end
1499
+ end
1500
+ end
1501
+ # Fan power credits, exhaust air energy recovery
1502
+ user_airloop.keys.each do |info_key|
1503
+ # Fan power credits
1504
+ if info_key.include?('fan_power_credit')
1505
+ if !user_airloop[info_key].to_s.empty?
1506
+ if info_key.include?('has_')
1507
+ if user_airloop[info_key].downcase == 'yes'
1508
+ air_loop.thermalZones.each do |thermal_zone|
1509
+ if thermal_zone.additionalProperties.hasFeature(info_key)
1510
+ current_value = thermal_zone.additionalProperties.getFeatureAsDouble(info_key).to_f
1511
+ thermal_zone.additionalProperties.setFeature(info_key, current_value + 1.0)
1512
+ else
1513
+ thermal_zone.additionalProperties.setFeature(info_key, 1.0)
1514
+ end
1515
+ end
1516
+ end
1517
+ else
1518
+ air_loop.thermalZones.each do |thermal_zones|
1519
+ if thermal_zone.additionalProperties.hasFeature(info_key)
1520
+ current_value = thermal_zone.additionalProperties.getFeatureAsDouble(info_key).to_f
1521
+ thermal_zone.additionalProperties.setFeature(info_key, current_value + user_airloop[info_key])
1522
+ else
1523
+ thermal_zone.additionalProperties.setFeature(info_key, user_airloop[info_key])
1524
+ end
1525
+ end
1526
+ end
1527
+ end
1528
+ end
1529
+ # Exhaust air energy recovery
1530
+ if info_key.include?('exhaust_energy_recovery_exception') && !user_airloop[info_key].to_s.empty?
1531
+ if user_airloop[info_key].downcase == 'yes'
1532
+ air_loop.thermalZones.each do |thermal_zone|
1533
+ thermal_zone.additionalProperties.setFeature(info_key, 'yes')
1534
+ end
1535
+ end
1536
+ end
1537
+ end
1538
+ end
1539
+ end
1540
+ end
1541
+ end
1542
+ end
1543
+
1544
+ # A function to load airloop DOAS data from userdata csv files
1545
+ # @param [OpenStudio::Model::Model] OpenStudio model object
1546
+ def handle_airloop_doas_user_input_data(model)
1547
+ # Get user data
1548
+ user_airloop_doass = @standards_data.key?('userdata_airloop_hvac_doas') ? @standards_data['userdata_airloop_hvac_doas'] : nil
1549
+
1550
+ # Parse user data
1551
+ if user_airloop_doass && user_airloop_doass.length >= 1
1552
+ user_airloop_doass.each do |user_airloop_doas|
1553
+ # Get AirLoopHVACDedicatedOutdoorAirSystem
1554
+ air_loop_doas = model.getAirLoopHVACDedicatedOutdoorAirSystemByName(user_airloop_doas['name'])
1555
+ if !air_loop_doas.is_initialized
1556
+ OpenStudio.logFree(OpenStudio::Warn, 'openstudio.ashrae_90_1_prm.Model', "The AirLoopHVACDedicatedOutdoorAirSystem named #{user_airloop_doass['name']} mentioned in the userdata_airloop_hvac_doas was not found in the model, user specified data associated with it will be ignored.")
1557
+ next
1558
+ else
1559
+ air_loop_doas = air_loop_doas.get
1560
+ end
1561
+
1562
+ # Parse fan power credits data
1563
+ user_airloop_doas.keys.each do |info_key|
1564
+ if info_key.include?('fan_power_credit')
1565
+ if !user_airloop_doas[info_key].to_s.empty?
1566
+ # Case 1: Yes/no
1567
+ if info_key.include?('has_')
1568
+ if user_airloop_doas[info_key].downcase == 'yes'
1569
+ air_loop_doas.airLoops.each do |air_loop|
1570
+ air_loop.thermalZones.each do |thermal_zone|
1571
+ if thermal_zone.additionalProperties.hasFeature(info_key)
1572
+ current_value = thermal_zone.additionalProperties.getFeatureAsDouble(info_key).to_f
1573
+ thermal_zone.additionalProperties.setFeature(info_key, current_value + 1.0)
1574
+ else
1575
+ thermal_zone.additionalProperties.setFeature(info_key, 1.0)
1576
+ end
1577
+ end
1578
+ end
1579
+ end
1580
+ else
1581
+ # Case 2: user provided value
1582
+ air_loop_doas.airLoops.each do |air_loop|
1583
+ air_loop.thermalZones.each do |thermal_zones|
1584
+ if thermal_zone.additionalProperties.hasFeature(info_key)
1585
+ current_value = thermal_zone.additionalProperties.getFeatureAsDouble(info_key).to_f
1586
+ thermal_zone.additionalProperties.setFeature(info_key, current_value + user_airloop_doas[info_key])
1587
+ else
1588
+ thermal_zone.additionalProperties.setFeature(info_key, user_airloop_doas[info_key])
1589
+ end
1590
+ end
1591
+ end
1592
+ end
1593
+ end
1594
+ end
1595
+ end
1596
+ end
1597
+ end
1598
+ end
1599
+
1600
+ # A function to load thermal zone data from userdata csv files
1601
+ # @param [OpenStudio::Model::Model] OpenStudio model object
1602
+ def handle_thermal_zone_user_input_data(model)
1603
+ model.getThermalZones.each do |thermal_zone|
1604
+ nightcycle_exception = false
1605
+ if standards_data.key?('userdata_thermal_zone')
1606
+ standards_data['userdata_thermal_zone'].each do |row|
1607
+ next unless row['name'].to_s.downcase.strip == thermal_zone.name.to_s.downcase.strip
1608
+
1609
+ if row['has_health_safety_night_cycle_exception'].to_s.upcase.strip == 'TRUE'
1610
+ nightcycle_exception = true
1611
+ break
1612
+ end
1613
+ end
1614
+ end
1615
+ if nightcycle_exception
1616
+ thermal_zone.additionalProperties.setFeature('has_health_safety_night_cycle_exception', true)
1617
+ end
1618
+ end
1619
+
1620
+ # mark unmarked zones
1621
+ model.getThermalZones.each do |zone|
1622
+ next if zone.additionalProperties.hasFeature('has_health_safety_night_cycle_exception')
1623
+
1624
+ zone.additionalProperties.setFeature('has_health_safety_night_cycle_exception', false)
1625
+ end
1626
+ end
1627
+
1628
+ # Analyze HVAC, window-to-wall ratio and SWH building (area) types from user data inputs in the @standard_data library
1629
+ # This function returns True, but the values are stored in the multi-building_data argument.
1630
+ # The hierarchy for process the building types
1631
+ # 1. Highest: PRM rules - if rules applied against user inputs, the function will use the calculated value to reset the building type
1632
+ # 2. Second: User defined building type in the csv file.
1633
+ # 3. Third: User defined userdata_building.csv file. If an object (e.g. space, thermalzone) are not defined in their correspondent userdata csv file, use the building csv file
1634
+ # 4. Fourth: Dropdown list in the measure GUI. If none presented, use the data from the dropdown list.
1635
+ # NOTE! This function will add building types to OpenStudio objects as an additional features for hierarchy 1-3
1636
+ # The object additional feature is empty when the function determined it uses fourth hierarchy.
1637
+ #
1638
+ # @param [OpenStudio::Model::Model] model
1639
+ # @param [String] climate_zone
1640
+ # @param [String] default_hvac_building_type (Fourth Hierarchy hvac building type)
1641
+ # @param [String] default_wwr_building_type (Fourth Hierarchy wwr building type)
1642
+ # @param [String] default_swh_building_type (Fourth Hierarchy swh building type)
1643
+ # @param [Hash] bldg_type_zone_hash An empty hash that maps building type for hvac to a list of thermal zones
1644
+ # @return True
1645
+ def handle_multi_building_area_types(model, climate_zone, default_hvac_building_type, default_wwr_building_type, default_swh_building_type, bldg_type_hvac_zone_hash)
1646
+ # Construct the user_building hashmap
1647
+ user_buildings = @standards_data.key?('userdata_building') ? @standards_data['userdata_building'] : nil
1648
+
1649
+ # Build up a hvac_building_type : thermal zone hash map
1650
+ # =============================HVAC user data process===========================================
1651
+ user_thermal_zones = @standards_data.key?('userdata_thermal_zone') ? @standards_data['userdata_thermal_zone'] : nil
1652
+ # First construct hvac building type -> thermal Zone hash and hvac building type -> floor area
1653
+ bldg_type_zone_hash = {}
1654
+ bldg_type_zone_area_hash = {}
1655
+ model.getThermalZones.each do |thermal_zone|
1656
+ # get climate zone to check the conditioning category
1657
+ thermal_zone_condition_category = thermal_zone_conditioning_category(thermal_zone, climate_zone)
1658
+ if thermal_zone_condition_category == 'Semiheated' || thermal_zone_condition_category == 'Unconditioned'
1659
+ next
1660
+ end
1661
+
1662
+ # Check for Second hierarchy
1663
+ hvac_building_type = nil
1664
+ if user_thermal_zones && user_thermal_zones.length >= 1
1665
+ user_thermal_zone_index = user_thermal_zones.index { |user_thermal_zone| user_thermal_zone['name'] == thermal_zone.name.get }
1666
+ # make sure the thermal zone has assigned a building_type_for_hvac
1667
+ unless user_thermal_zone_index.nil? || user_thermal_zones[user_thermal_zone_index]['building_type_for_hvac'].nil?
1668
+ # Only thermal zone in the user data and have building_type_for_hvac data will be assigned.
1669
+ hvac_building_type = user_thermal_zones[user_thermal_zone_index]['building_type_for_hvac']
1670
+ end
1671
+ end
1672
+ # Second hierarchy does not apply, check Third hierarchy
1673
+ if hvac_building_type.nil? && user_buildings && user_buildings.length >= 1
1674
+ building_name = thermal_zone.model.building.get.name.get
1675
+ user_building_index = user_buildings.index { |user_building| user_building['name'] == building_name }
1676
+ unless user_building_index.nil? || user_buildings[user_building_index]['building_type_for_hvac'].nil?
1677
+ # Only thermal zone in the buildings user data and have building_type_for_hvac data will be assigned.
1678
+ hvac_building_type = user_buildings[user_building_index]['building_type_for_hvac']
1679
+ end
1680
+ end
1681
+ # Third hierarchy does not apply, apply Fourth hierarchy
1682
+ if hvac_building_type.nil?
1683
+ hvac_building_type = default_hvac_building_type
1684
+ end
1685
+ # Add data to the hash map
1686
+ unless bldg_type_zone_hash.key?(hvac_building_type)
1687
+ bldg_type_zone_hash[hvac_building_type] = []
1688
+ end
1689
+ unless bldg_type_zone_area_hash.key?(hvac_building_type)
1690
+ bldg_type_zone_area_hash[hvac_building_type] = 0.0
1691
+ end
1692
+ # calculate floor area for the thermal zone
1693
+ part_of_floor_area = false
1694
+ thermal_zone.spaces.sort.each do |space|
1695
+ next unless space.partofTotalFloorArea
1696
+
1697
+ # a space in thermal zone is part of floor area.
1698
+ part_of_floor_area = true
1699
+ bldg_type_zone_area_hash[hvac_building_type] += space.floorArea * space.multiplier
1700
+ end
1701
+ if part_of_floor_area
1702
+ # Only add the thermal_zone if it is part of the floor area
1703
+ bldg_type_zone_hash[hvac_building_type].append(thermal_zone)
1704
+ end
1705
+ end
1706
+
1707
+ if bldg_type_zone_hash.empty?
1708
+ # Build hash with all zones assigned to default hvac building type
1709
+ zone_array = []
1710
+ model.getThermalZones.each do |thermal_zone|
1711
+ zone_array.append(thermal_zone)
1712
+ thermal_zone.additionalProperties.setFeature('building_type_for_hvac', default_hvac_building_type)
1713
+ end
1714
+ bldg_type_hvac_zone_hash[default_hvac_building_type] = zone_array
1715
+ else
1716
+ # Calculate the total floor area.
1717
+ # If the max tie, this algorithm will pick the first encountered hvac building type as the maximum.
1718
+ total_floor_area = 0.0
1719
+ hvac_bldg_type_with_max_floor = nil
1720
+ hvac_bldg_type_max_floor_area = 0.0
1721
+ bldg_type_zone_area_hash.each do |key, value|
1722
+ if value > hvac_bldg_type_max_floor_area
1723
+ hvac_bldg_type_with_max_floor = key
1724
+ hvac_bldg_type_max_floor_area = value
1725
+ end
1726
+ total_floor_area += value
1727
+ end
1728
+
1729
+ # Reset the thermal zones by going through the hierarchy 1 logics
1730
+ bldg_type_hvac_zone_hash.clear
1731
+ # Add the thermal zones for the maximum floor (primary system)
1732
+ bldg_type_hvac_zone_hash[hvac_bldg_type_with_max_floor] = bldg_type_zone_hash[hvac_bldg_type_with_max_floor]
1733
+ bldg_type_zone_hash.each do |bldg_type, bldg_type_zone|
1734
+ # loop the rest bldg_types
1735
+ unless bldg_type.eql? hvac_bldg_type_with_max_floor
1736
+ if OpenStudio.convert(total_floor_area, 'm^2', 'ft^2').get <= 40000
1737
+ # Building is smaller than 40k sqft, it could only have one hvac_building_type, reset all the thermal zones.
1738
+ bldg_type_hvac_zone_hash[hvac_bldg_type_with_max_floor].push(*bldg_type_zone)
1739
+ OpenStudio.logFree(OpenStudio::Warn, 'openstudio.model.Model', "The building floor area is less than 40,000 square foot. Thermal zones under hvac building type #{bldg_type} is reset to #{hvac_bldg_type_with_max_floor}")
1740
+ else
1741
+ if OpenStudio.convert(bldg_type_zone_area_hash[bldg_type], 'm^2', 'ft^2').get < 20000
1742
+ # in this case, all thermal zones shall be categorized as the primary hvac_building_type
1743
+ bldg_type_hvac_zone_hash[hvac_bldg_type_with_max_floor].push(*bldg_type_zone)
1744
+ OpenStudio.logFree(OpenStudio::Warn, 'openstudio.model.Model', "The floor area in hvac building type #{bldg_type} is less than 20,000 square foot. Thermal zones under this hvac building type is reset to #{hvac_bldg_type_with_max_floor}")
1745
+ else
1746
+ bldg_type_hvac_zone_hash[bldg_type] = bldg_type_zone
1747
+ end
1748
+ end
1749
+ end
1750
+ end
1751
+
1752
+ # Write in hvac building type thermal zones by thermal zone
1753
+ bldg_type_hvac_zone_hash.each do |h1_bldg_type, bldg_type_zone_array|
1754
+ bldg_type_zone_array.each do |thermal_zone|
1755
+ thermal_zone.additionalProperties.setFeature('building_type_for_hvac', h1_bldg_type)
1756
+ end
1757
+ end
1758
+ end
1759
+
1760
+ # =============================SPACE user data process===========================================
1761
+ user_spaces = @standards_data.key?('userdata_space') ? @standards_data['userdata_space'] : nil
1762
+ model.getSpaces.each do |space|
1763
+ type_for_wwr = nil
1764
+ # Check for 2nd level hierarchy
1765
+ if user_spaces && user_spaces.length >= 1
1766
+ user_spaces.each do |user_space|
1767
+ unless user_space['building_type_for_wwr'].nil?
1768
+ if space.name.get == user_space['name']
1769
+ type_for_wwr = user_space['building_type_for_wwr']
1770
+ end
1771
+ end
1772
+ end
1773
+ end
1774
+
1775
+ if type_for_wwr.nil?
1776
+ # 2nd Hierarchy does not apply, check for 3rd level hierarchy
1777
+ building_name = space.model.building.get.name.get
1778
+ if user_buildings && user_buildings.length >= 1
1779
+ user_buildings.each do |user_building|
1780
+ unless user_building['building_type_for_wwr'].nil?
1781
+ if user_building['name'] == building_name
1782
+ type_for_wwr = user_building['building_type_for_wwr']
1783
+ end
1784
+ end
1785
+ end
1786
+ end
1787
+ end
1788
+
1789
+ if type_for_wwr.nil?
1790
+ # 3rd level hierarchy does not apply, Apply 4th level hierarchy
1791
+ type_for_wwr = default_wwr_building_type
1792
+ end
1793
+ # add wwr type to space:
1794
+ space.additionalProperties.setFeature('building_type_for_wwr', type_for_wwr)
1795
+ end
1796
+ # =============================SWH user data process===========================================
1797
+ user_wateruse_equipments = @standards_data.key?('userdata_wateruse_equipment') ? @standards_data['userdata_wateruse_equipment'] : nil
1798
+ model.getWaterUseEquipments.each do |wateruse_equipment|
1799
+ type_for_swh = nil
1800
+ # Check for 2nd hierarchy
1801
+ if user_wateruse_equipments && user_wateruse_equipments.length >= 1
1802
+ user_wateruse_equipments.each do |user_wateruse_equipment|
1803
+ unless user_wateruse_equipment['building_type_for_swh'].nil?
1804
+ if wateruse_equipment.name.get == user_wateruse_equipment['name']
1805
+ type_for_swh = user_wateruse_equipment['building_type_for_swh']
1806
+ end
1807
+ end
1808
+ end
1809
+ end
1810
+
1811
+ if type_for_swh.nil?
1812
+ # 2nd hierarchy does not apply, check for 3rd hierarchy
1813
+ # get space building type
1814
+ building_name = wateruse_equipment.model.building.get.name.get
1815
+ if user_buildings && user_buildings.length >= 1
1816
+ user_buildings.each do |user_building|
1817
+ unless user_building['building_type_for_swh'].nil?
1818
+ if user_building['name'] == building_name
1819
+ type_for_swh = user_building['building_type_for_swh']
1820
+ end
1821
+ end
1822
+ end
1823
+ end
1824
+ end
1825
+
1826
+ if type_for_swh.nil?
1827
+ # 3rd hierarchy does not apply, apply 4th hierarchy
1828
+ type_for_swh = default_swh_building_type
1829
+ end
1830
+ # add swh type to wateruse equipment:
1831
+ wateruse_equipment.additionalProperties.setFeature('building_type_for_swh', type_for_swh)
1832
+ end
1833
+ return true
1834
+ end
1835
+
1836
+ # Modify the existing service water heating loops to match the baseline required heating type.
1837
+ #
1838
+ # @param model [OpenStudio::Model::Model] the model
1839
+ # @param building_type [String] the building type
1840
+ # @return [Bool] returns true if successful, false if not
1841
+ def model_apply_baseline_swh_loops(model, building_type)
1842
+ model.getPlantLoops.sort.each do |plant_loop|
1843
+ # Skip non service water heating loops
1844
+ next unless plant_loop_swh_loop?(plant_loop)
1845
+
1846
+ # Rename the loop to avoid accidentally hooking up the HVAC systems to this loop later.
1847
+ plant_loop.setName('Service Water Heating Loop')
1848
+
1849
+ htg_fuels, combination_system, storage_capacity, total_heating_capacity = plant_loop_swh_system_type(plant_loop)
1850
+
1851
+ # htg_fuels.size == 0 shoudln't happen
1852
+
1853
+ electric = true
1854
+
1855
+ if htg_fuels.include?('NaturalGas') ||
1856
+ htg_fuels.include?('PropaneGas') ||
1857
+ htg_fuels.include?('FuelOilNo1') ||
1858
+ htg_fuels.include?('FuelOilNo2') ||
1859
+ htg_fuels.include?('Coal') ||
1860
+ htg_fuels.include?('Diesel') ||
1861
+ htg_fuels.include?('Gasoline')
1862
+ electric = false
1863
+ end
1864
+
1865
+ # Per Table G3.1 11.e, if the baseline system was a combination of heating and service water heating,
1866
+ # delete all heating equipment and recreate a WaterHeater:Mixed.
1867
+
1868
+ if combination_system
1869
+ a = plant_loop.supplyComponents
1870
+ b = plant_loop.demandComponents
1871
+ plantloopComponents = a += b
1872
+ plantloopComponents.each do |component|
1873
+ # Get the object type
1874
+ obj_type = component.iddObjectType.valueName.to_s
1875
+ next if ['OS_Node', 'OS_Pump_ConstantSpeed', 'OS_Pump_VariableSpeed', 'OS_Connector_Splitter', 'OS_Connector_Mixer', 'OS_Pipe_Adiabatic'].include?(obj_type)
1876
+
1877
+ component.remove
1878
+ end
1879
+
1880
+ water_heater = OpenStudio::Model::WaterHeaterMixed.new(model)
1881
+ water_heater.setName('Baseline Water Heater')
1882
+ water_heater.setHeaterMaximumCapacity(total_heating_capacity)
1883
+ water_heater.setTankVolume(storage_capacity)
1884
+ plant_loop.addSupplyBranchForComponent(water_heater)
1885
+
1886
+ if electric
1887
+ # G3.1.11.b: If electric, WaterHeater:Mixed with electric resistance
1888
+ water_heater.setHeaterFuelType('Electricity')
1889
+ water_heater.setHeaterThermalEfficiency(1.0)
1890
+ else
1891
+ # @todo for now, just get the first fuel that isn't Electricity
1892
+ # A better way would be to count the capacities associated
1893
+ # with each fuel type and use the preponderant one
1894
+ fuels = htg_fuels - ['Electricity']
1895
+ fossil_fuel_type = fuels[0]
1896
+ water_heater.setHeaterFuelType(fossil_fuel_type)
1897
+ water_heater.setHeaterThermalEfficiency(0.8)
1898
+ end
1899
+ # If it's not a combination heating and service water heating system
1900
+ # just change the fuel type of all water heaters on the system
1901
+ # to electric resistance if it's electric
1902
+ else
1903
+ # Per Table G3.1 11.i, piping losses was deleted
1904
+
1905
+ a = plant_loop.supplyComponents
1906
+ b = plant_loop.demandComponents
1907
+ plantloopComponents = a += b
1908
+ plantloopComponents.each do |component|
1909
+ # Get the object type
1910
+ obj_type = component.iddObjectType.valueName.to_s
1911
+ next if !['OS_Pipe_Indoor', 'OS_Pipe_Outdoor'].include?(obj_type)
1912
+
1913
+ pipe = component.to_PipeIndoor.get
1914
+ node = pipe.to_StraightComponent.get.outletModelObject.get.to_Node.get
1915
+
1916
+ node_name = node.name.get
1917
+ pipe_name = pipe.name.get
1918
+
1919
+ # Add Pipe_Adiabatic
1920
+ newpipe = OpenStudio::Model::PipeAdiabatic.new(model)
1921
+ newpipe.setName(pipe_name)
1922
+ newpipe.addToNode(node)
1923
+ component.remove
1924
+ end
1925
+
1926
+ if electric
1927
+ plant_loop.supplyComponents.each do |component|
1928
+ next unless component.to_WaterHeaterMixed.is_initialized
1929
+
1930
+ water_heater = component.to_WaterHeaterMixed.get
1931
+ # G3.1.11.b: If electric, WaterHeater:Mixed with electric resistance
1932
+ water_heater.setHeaterFuelType('Electricity')
1933
+ water_heater.setHeaterThermalEfficiency(1.0)
1934
+ end
1935
+ end
1936
+ end
1937
+ end
1938
+
1939
+ # Set the water heater fuel types if it's 90.1-2013
1940
+ model.getWaterHeaterMixeds.sort.each do |water_heater|
1941
+ water_heater_mixed_apply_prm_baseline_fuel_type(water_heater, building_type)
1942
+ end
1943
+
1944
+ return true
1945
+ end
1946
+
1947
+ # Check whether the baseline model generation needs to run all four orientations
1948
+ # The default shall be true
1949
+ #
1950
+ # @param [Boolean] run_all_orients: user inputs to indicate whether it is required to run all orientations
1951
+ # @param [OpenStudio::Model::Model] Openstudio model
1952
+ def run_all_orientations(run_all_orients, user_model)
1953
+ # Step 0, assign the default value
1954
+ run_orients_flag = run_all_orients
1955
+ # Step 1 check orientation variations - priority 2
1956
+ fenestration_area_hash = get_model_fenestration_area_by_orientation(user_model)
1957
+ fenestration_area_hash.each do |orientation, fenestration_area|
1958
+ fenestration_area_hash.each do |other_orientation, other_fenestration_area|
1959
+ next unless orientation != other_orientation
1960
+
1961
+ variance = (other_fenestration_area - fenestration_area) / fenestration_area
1962
+ if variance.abs > 0.05
1963
+ # if greater then 0.05
1964
+ run_orients_flag = true
1965
+ end
1966
+ end
1967
+ end
1968
+ # Step 2 read user data - priority 1 - user data will override the priority 2
1969
+ user_buildings = @standards_data.key?('userdata_building') ? @standards_data['userdata_building'] : nil
1970
+ if user_buildings
1971
+ building_name = user_model.building.get.name.get
1972
+ user_building_index = user_buildings.index { |user_building| building_name.include? user_building['name'] }
1973
+ unless user_building_index.nil? || user_buildings[user_building_index]['is_exempt_from_rotations'].nil?
1974
+ # user data exempt the rotation, No indicates true for running orients.
1975
+ run_orients_flag = user_buildings[user_building_index]['is_exempt_from_rotations'].casecmp('No') == 0
1976
+ end
1977
+ end
1978
+ return run_orients_flag
1979
+ end
1980
+
1981
+ def get_model_fenestration_area_by_orientation(user_model)
1982
+ # First index is wall, second index is window
1983
+ fenestration_area_hash = {
1984
+ 'N' => 0.0,
1985
+ 'S' => 0.0,
1986
+ 'E' => 0.0,
1987
+ 'W' => 0.0
1988
+ }
1989
+ user_model.getSpaces.each do |space|
1990
+ space_cond_type = space_conditioning_category(space)
1991
+ next if space_cond_type == 'Unconditioned'
1992
+
1993
+ # Get zone multiplier
1994
+ multiplier = space.thermalZone.get.multiplier
1995
+ space.surfaces.each do |surface|
1996
+ next if surface.surfaceType != 'Wall'
1997
+ next if surface.outsideBoundaryCondition != 'Outdoors'
1998
+
1999
+ orientation = surface_cardinal_direction(surface)
2000
+ surface.subSurfaces.each do |subsurface|
2001
+ subsurface_type = subsurface.subSurfaceType.to_s.downcase
2002
+ # Do not count doors
2003
+ next unless (subsurface_type.include? 'window') || (subsurface_type.include? 'glass')
2004
+
2005
+ fenestration_area_hash[orientation] += subsurface.grossArea * subsurface.multiplier * multiplier
2006
+ end
2007
+ end
2008
+ end
2009
+ return fenestration_area_hash
2010
+ end
2011
+
2012
+ # Apply the standard construction to each surface in the model, based on the construction type currently assigned.
2013
+ #
2014
+ # @return [Bool] true if successful, false if not
2015
+ # @param model [OpenStudio::Model::Model] OpenStudio model object
2016
+ # @param climate_zone [String] ASHRAE climate zone, e.g. 'ASHRAE 169-2013-4A'
2017
+ # @return [Bool] returns true if successful, false if not
2018
+ def model_apply_constructions(model, climate_zone, wwr_building_type, wwr_info)
2019
+ model_apply_standard_constructions(model, climate_zone, wwr_building_type: wwr_building_type, wwr_info: wwr_info)
2020
+
2021
+ return true
2022
+ end
2023
+
2024
+ # Update ground temperature profile based on the weather file specified in the model
2025
+ #
2026
+ # @param model [OpenStudio::Model::Model] OpenStudio model object
2027
+ # @param climate_zone [String] ASHRAE climate zone, e.g. 'ASHRAE 169-2013-4A'
2028
+ # @return [Bool] returns true if successful, false if not
2029
+ def model_update_ground_temperature_profile(model, climate_zone)
2030
+ # Check if the ground temperature profile is needed
2031
+ surfaces_with_fc_factor_boundary = false
2032
+ model.getSurfaces.each do |surface|
2033
+ if surface.outsideBoundaryCondition.to_s == 'GroundFCfactorMethod'
2034
+ surfaces_with_fc_factor_boundary = true
2035
+ break
2036
+ end
2037
+ end
2038
+
2039
+ return false unless surfaces_with_fc_factor_boundary
2040
+
2041
+ # Remove existing FCFactor temperature profile
2042
+ model.getSiteGroundTemperatureFCfactorMethod.remove
2043
+
2044
+ # Get path to weather file specified in the model
2045
+ weather_file_path = model.getWeatherFile.path.get.to_s
2046
+
2047
+ # Look for stat file corresponding to the weather file
2048
+ stat_file_path = weather_file_path.sub('.epw', '.stat').to_s
2049
+ if !File.exist? stat_file_path
2050
+ # When the stat file corresponding with the weather file in the model is missing,
2051
+ # use the weather file that represent the climate zone
2052
+ climate_zone_weather_file_map = model_get_climate_zone_weather_file_map
2053
+ weather_file = climate_zone_weather_file_map[climate_zone]
2054
+ stat_file_path = model_get_weather_file(weather_file).sub('.epw', '.stat').to_s
2055
+ end
2056
+
2057
+ ground_temp = OpenStudio::Model::SiteGroundTemperatureFCfactorMethod.new(model)
2058
+ ground_temperatures = model_get_monthly_ground_temps_from_stat_file(stat_file_path)
2059
+ unless ground_temperatures.empty?
2060
+ # set the site ground temperature building surface
2061
+ ground_temp.setAllMonthlyTemperatures(ground_temperatures)
2062
+ end
2063
+
2064
+ return true
2065
+ end
2066
+
2067
+ # Generate baseline log to a specific file directory
2068
+ # @param file_directory [String] file directory
2069
+ def generate_baseline_log(file_directory)
2070
+ log_messages_to_file_prm("#{file_directory}/prm.log", false)
2071
+ end
2072
+
2073
+ # Retrieve zone HVAC user specified compliance inputs from CSV file
2074
+ #
2075
+ # @param [OpenStudio::Model::Model] OpenStudio model object
2076
+ def handle_zone_hvac_user_input_data(model)
2077
+ user_zone_hvac = @standards_data.key?('userdata_zone_hvac') ? @standards_data['userdata_zone_hvac'] : nil
2078
+ return unless user_zone_hvac && !user_zone_hvac.empty?
2079
+
2080
+ zone_hvac_equipment = model.getZoneHVACComponents
2081
+ if zone_hvac_equipment.empty?
2082
+ OpenStudio.logFree(OpenStudio::Error, 'openstudio.ashrae_90_1_prm.model', 'No zone HVAC equipment is present in the proposed model, user provided information cannot be used to generate the baseline building model.')
2083
+ return
2084
+ end
2085
+
2086
+ user_zone_hvac.each do |zone_hvac_eqp_info|
2087
+ user_defined_zone_hvac_obj_name = zone_hvac_eqp_info['name']
2088
+ user_defined_zone_hvac_obj_type_name = zone_hvac_eqp_info['zone_hvac_object_type_name']
2089
+
2090
+ # Check that the object type name do exist
2091
+ begin
2092
+ user_defined_zone_hvac_obj_type_name_idd = user_defined_zone_hvac_obj_type_name.to_IddObjectType
2093
+ rescue StandardError => e
2094
+ OpenStudio.logFree(OpenStudio::Error, 'openstudio.ashrae_90_1_prm.model', "#{user_defined_zone_hvac_obj_type_name}, provided in the user zone HVAC user data, is not a valid OpenStudio model object.")
2095
+ end
2096
+
2097
+ # Retrieve zone HVAC object(s) by name
2098
+ zone_hvac_eqp = model.getZoneHVACComponentsByName(user_defined_zone_hvac_obj_name, false)
2099
+
2100
+ # If multiple object have the same name
2101
+ if zone_hvac_eqp.empty?
2102
+ OpenStudio.logFree(OpenStudio::Error, 'openstudio.ashrae_90_1_prm.model', "The #{user_defined_zone_hvac_obj_type_name} object named #{user_defined_zone_hvac_obj_name} provided in the user zone HVAC user data could not be found in the model.")
2103
+ elsif zone_hvac_eqp.length == 1
2104
+ zone_hvac_eqp = zone_hvac_eqp[0]
2105
+ zone_hvac_eqp_idd = zone_hvac_eqp.iddObjectType.to_s
2106
+ if zone_hvac_eqp_idd != user_defined_zone_hvac_obj_type_name
2107
+ OpenStudio.logFree(OpenStudio::Error, 'openstudio.ashrae_90_1_prm.model', "The object type name provided in the zone HVAC user data (#{user_defined_zone_hvac_obj_type_name}) does not match with the one in the model: #{zone_hvac_eqp_idd}.")
2108
+ end
2109
+ else
2110
+ zone_hvac_eqp.each do |eqp|
2111
+ zone_hvac_eqp_idd = eqp.iddObjectType
2112
+ if zone_hvac_eqp_idd == user_defined_zone_hvac_obj_type_name
2113
+ zone_hvac_eqp = eqp
2114
+ break
2115
+ end
2116
+ end
2117
+ OpenStudio.logFree(OpenStudio::Error, 'openstudio.ashrae_90_1_prm.model', "A #{user_defined_zone_hvac_obj_type_name} object named #{user_defined_zone_hvac_obj_name} (as specified in the user zone HVAC data) could not be found in the model.")
2118
+ end
2119
+
2120
+ if zone_hvac_eqp.thermalZone.is_initialized
2121
+ thermal_zone = zone_hvac_eqp.thermalZone.get
2122
+
2123
+ zone_hvac_eqp_info.keys.each do |info_key|
2124
+ if info_key.include?('fan_power_credit')
2125
+ if !zone_hvac_eqp_info[info_key].to_s.empty?
2126
+ if info_key.include?('has_')
2127
+ if thermal_zone.additionalProperties.hasFeature(info_key)
2128
+ current_value = thermal_zone.additionalProperties.getFeatureAsDouble(info_key).to_f
2129
+ thermal_zone.additionalProperties.setFeature(info_key, current_value + 1.0)
2130
+ else
2131
+ thermal_zone.additionalProperties.setFeature(info_key, 1.0)
2132
+ end
2133
+ else
2134
+ if thermal_zone.additionalProperties.hasFeature(info_key)
2135
+ current_value = thermal_zone.additionalProperties.getFeatureAsDouble(info_key).to_f
2136
+ thermal_zone.additionalProperties.setFeature(info_key, current_value + zone_hvac_eqp_info[info_key])
2137
+ else
2138
+ thermal_zone.additionalProperties.setFeature(info_key, zone_hvac_eqp_info[info_key])
2139
+ end
2140
+ end
2141
+ end
2142
+ end
2143
+ end
2144
+ end
2145
+ end
2146
+ end
2147
+
2148
+ # This function checks whether it is required to adjust the window to wall ratio based on the model WWR and wwr limit.
2149
+ # @param wwr_limit [Float] return wwr_limit
2150
+ # @param wwr_list [Array] list of wwr of zone conditioning category in a building area type category - residential, nonresidential and semiheated
2151
+ # @return require_adjustment [Boolean] True, require adjustment, false not require adjustment.
2152
+ def model_does_require_wwr_adjustment?(wwr_limit, wwr_list)
2153
+ # 90.1 PRM routine requires
2154
+ return true
2155
+ end
2156
+
2157
+ # For 2019, it is required to adjusted wwr based on building categories for all other types
2158
+ #
2159
+ # @param bat [String] building category
2160
+ # @param wwr_list [Array] list of zone conditioning category-based WWR - residential, nonresidential and semiheated
2161
+ # @return wwr_limit [Float] return adjusted wwr_limit
2162
+ def model_get_bat_wwr_target(bat, wwr_list)
2163
+ wwr_limit = 40.0
2164
+ # Lookup WWR target from stable baseline table
2165
+ wwr_lib = standards_data['prm_wwr_bldg_type']
2166
+ search_criteria = {
2167
+ 'template' => template,
2168
+ 'wwr_building_type' => bat
2169
+ }
2170
+ wwr_limit_bat = model_find_object(wwr_lib, search_criteria)
2171
+ # If building type isn't found, assume that it's
2172
+ # the same as 'All Others'
2173
+ if wwr_limit_bat.nil? || bat.casecmp?('all others')
2174
+ wwr = wwr_list.max
2175
+ # All others type
2176
+ # use the min of 40% and the max wwr in the ZCC-wwr list.
2177
+ wwr_limit = [wwr_limit, wwr].min
2178
+ else
2179
+ # Matched type: use WWR from database.
2180
+ wwr_limit = wwr_limit_bat['wwr'] * 100.0
2181
+ end
2182
+ return wwr_limit
2183
+ end
2184
+
2185
+ # Calculate the window to wall ratio reduction factor
2186
+ #
2187
+ # @param multiplier [Float] multiplier of the wwr
2188
+ # @param surface_wwr [Float] the surface window to wall ratio
2189
+ # @param surface_dr [Float] the surface door to wall ratio
2190
+ # @param wwr_building_type[String] building type for wwr
2191
+ # @param wwr_target [Float] target window to wall ratio
2192
+ # @param total_wall_m2 [Float] total wall area of the category in m2.
2193
+ # @param total_wall_with_fene_m2 [Float] total wall area of the category with fenestrations in m2.
2194
+ # @param total_fene_m2 [Float] total fenestration area
2195
+ # @return [Float] reduction factor
2196
+ def model_get_wwr_reduction_ratio(multiplier,
2197
+ surface_wwr: 0.0,
2198
+ surface_dr: 0.0,
2199
+ wwr_building_type: 'All others',
2200
+ wwr_target: nil,
2201
+ total_wall_m2: 0.0, # prevent 0.0 division
2202
+ total_wall_with_fene_m2: 0.0,
2203
+ total_fene_m2: 0.0,
2204
+ total_plenum_wall_m2: 0.0)
2205
+
2206
+ if multiplier < 1.0
2207
+ # Case when reduction is required
2208
+ reduction_ratio = 1.0 - multiplier
2209
+ else
2210
+ # Case when increase is required - takes the door area into consideration.
2211
+ # The target is to increase each surface to maximum 90% WWR deduct the total door area.
2212
+ total_dr = 0.0
2213
+ exist_max_wwr = 0.0
2214
+ if total_wall_m2 > 0 then exist_max_wwr = total_wall_with_fene_m2 * 0.9 / total_wall_m2 end
2215
+ if exist_max_wwr < wwr_target
2216
+ # In this case, it is required to add vertical fenestration to other surfaces
2217
+ if surface_wwr == 0.0
2218
+ # delta_fenestration_surface_area / delta_wall_surface_area + 1.0 = increase_ratio for a surface with no windows.
2219
+ # ASSUMPTION!! assume adding windows to surface with no windows will never be window_m2 + door_m2 > surface_m2.
2220
+ reduction_ratio = (wwr_target - exist_max_wwr) * total_wall_m2 / (total_wall_m2 - total_wall_with_fene_m2 - total_plenum_wall_m2) + 1.0
2221
+ else
2222
+ # surface has fenestration - expand it to 90% WWR or surface area minus door area, whichever is smaller.
2223
+ if (1.0 - surface_dr) < 0.9
2224
+ # A negative reduction ratio as a flat to main function that this reduction ratio is adjusted by doors
2225
+ # and it is needed to adjust the WWR of the no fenestration surfaces to meet the lost
2226
+ reduction_ratio = (surface_dr - 1.0) / surface_wwr
2227
+ else
2228
+ reduction_ratio = 0.9 / surface_wwr
2229
+ end
2230
+ end
2231
+ else
2232
+ # multiplier will be negative number thus resulting in > 1 reduction_ratio
2233
+ if surface_wwr == 0.0
2234
+ # 1.0 means remain the original form
2235
+ reduction_ratio = 1.0
2236
+ else
2237
+ reduction_ratio = multiplier
2238
+ end
2239
+ end
2240
+ end
2241
+ return reduction_ratio
2242
+ end
2243
+
2244
+ # Readjusted the WWR for surfaces previously has no windows to meet the
2245
+ # overall WWR requirement.
2246
+ # This function shall only be called if the maximum WWR value for surfaces with fenestration is lower than 90% due to
2247
+ # accommodating the total door surface areas
2248
+ #
2249
+ # @param residual_ratio: [Float] the ratio of residual surfaces among the total wall surface area with no fenestrations
2250
+ # @param space [OpenStudio::Model:Space] a space
2251
+ # @param model [OpenStudio::Model::Model] openstudio model
2252
+ # @return [Bool] return true if successful, false if not
2253
+ def model_readjust_surface_wwr(residual_ratio, space, model)
2254
+ # In this loop, we will focus on the surfaces with newly added a fenestration.
2255
+ space.surfaces.sort.each do |surface|
2256
+ next unless surface.additionalProperties.hasFeature('added_wwr')
2257
+
2258
+ added_wwr = surface.additionalProperties.getFeatureAsDouble('added_wwr').to_f
2259
+ # The full calculation of adjustment is:
2260
+ # ((residual_ratio * surface_area + added_wwr * surface_area) / surface_area ) / added_wwr
2261
+ adjustment_ratio = residual_ratio / added_wwr + 1.0
2262
+ surface_adjust_fenestration_in_a_surface(surface, adjustment_ratio, model)
2263
+ end
2264
+ end
2265
+
2266
+ # Assign spaces to system groups based on building area type
2267
+ # Get zone groups separately for each hvac building type
2268
+ # @param custom [String] identifier for custom programs, not used here, but included for backwards compatibility
2269
+ # @param bldg_type_hvac_zone_hash [Hash of bldg_type:list of zone objects] association of zones to each hvac building type
2270
+ # @return [Array<Hash>] an array of hashes of area information,
2271
+ # with keys area_ft2, type, fuel, and zones (an array of zones)
2272
+ def model_prm_baseline_system_groups(model, custom, bldg_type_hvac_zone_hash)
2273
+ bldg_groups = []
2274
+
2275
+ bldg_type_hvac_zone_hash.keys.each do |hvac_building_type, zones_in_building_type|
2276
+ # Get all groups for this hvac building type
2277
+ new_groups = get_baseline_system_groups_for_one_building_type(model, hvac_building_type, zones_in_building_type)
2278
+
2279
+ # Add the groups for this hvac building type to the full list
2280
+ new_groups.each do |group|
2281
+ bldg_groups << group
2282
+ end
2283
+ end
2284
+
2285
+ return bldg_groups
2286
+ end
2287
+
2288
+ # Assign spaces to system groups for one hvac building type
2289
+ # One group contains all zones associated with one HVAC type
2290
+ # Separate groups are made for laboratories, computer rooms, district cooled zones, heated-only zones, or hybrids of these
2291
+ # Groups may include zones from multiple floors; separating by floor is handled later
2292
+ # For stable baseline, heating type is based on climate, not proposed heating type
2293
+ # Isolate zones that have heating-only or district (purchased) heat or chilled water
2294
+ # @param bldg_type_hvac_zone_hash [Hash of bldg_type:list of zone objects] association of zones to each hvac building type
2295
+ # @return [Array<Hash>] an array of hashes of area information,
2296
+ # with keys area_ft2, type, fuel, and zones (an array of zones)
2297
+ def get_baseline_system_groups_for_one_building_type(model, hvac_building_type, zones_in_building_type)
2298
+ # Build zones hash of [zone, zone area, occupancy type, building type, fuel]
2299
+ zones = model_zones_with_occ_and_fuel_type(model, 'custom')
2300
+
2301
+ # Ensure that there is at least one conditioned zone
2302
+ if zones.size.zero?
2303
+ OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', 'The building does not appear to have any conditioned zones. Make sure zones have thermostat with appropriate heating and cooling setpoint schedules.')
2304
+ return []
2305
+ end
2306
+
2307
+ # Consider special rules for computer rooms
2308
+ # need load of all
2309
+
2310
+ # Get cooling load of all computer rooms to establish system types
2311
+ comp_room_loads = {}
2312
+ bldg_comp_room_load = 0
2313
+ zones.each do |zn|
2314
+ zone_load = 0.0
2315
+ has_computer_room = false
2316
+ # First check if any space in zone has a computer room
2317
+ zn['zone'].spaces.each do |space|
2318
+ if space.spaceType.get.standardsSpaceType.get == 'computer room'
2319
+ has_computer_room = true
2320
+ break
2321
+ end
2322
+ end
2323
+ if has_computer_room
2324
+ # Collect load for entire zone
2325
+ zone_load_w = zn['zone'].coolingDesignLoad.to_f
2326
+ zone_load_w *= zn['zone'].floorArea * zn['zone'].multiplier
2327
+ zone_load = OpenStudio.convert(zone_load_w, 'W', 'Btu/hr').get
2328
+ end
2329
+ comp_room_loads[zn['zone'].name.get] = zone_load
2330
+ bldg_comp_room_load += zone_load
2331
+ end
2332
+
2333
+ # Lab zones are grouped separately if total lab exhaust in building > 15000 cfm
2334
+ # Make list of zone objects that contain laboratory spaces
2335
+ lab_zones = []
2336
+ has_lab_spaces = {}
2337
+ model.getThermalZones.sort.each do |zone|
2338
+ # Check if this zone includes laboratory space
2339
+ zone.spaces.each do |space|
2340
+ spacetype = space.spaceType.get.standardsSpaceType.get
2341
+ has_lab_spaces[zone.name.get] = false
2342
+ if space.spaceType.get.standardsSpaceType.get == 'laboratory'
2343
+ lab_zones << zone
2344
+ has_lab_spaces[zone.name.get] = true
2345
+ break
2346
+ end
2347
+ end
2348
+ end
2349
+
2350
+ lab_exhaust_si = 0
2351
+ lab_relief_si = 0
2352
+ if !lab_zones.empty?
2353
+ # Build a hash of return_node:zone_name
2354
+ node_list = {}
2355
+ zone_return_flow_si = Hash.new(0)
2356
+ var_name = 'System Node Standard Density Volume Flow Rate'
2357
+ frequency = 'hourly'
2358
+ model.getThermalZones.each do |zone|
2359
+ port_list = zone.returnPortList
2360
+ port_list_objects = port_list.modelObjects
2361
+ port_list_objects.each do |node|
2362
+ node_name = node.nameString
2363
+ node_list[node_name] = zone.name.get
2364
+ end
2365
+ zone_return_flow_si[zone.name.get] = 0
2366
+ end
2367
+
2368
+ # Get return air flow for each zone (even non-lab zones are needed)
2369
+ # Take from hourly reports created during sizing run
2370
+ node_list.each do |node_name, zone_name|
2371
+ sql = model.sqlFile
2372
+ if sql.is_initialized
2373
+ sql = sql.get
2374
+ query = "SELECT ReportDataDictionaryIndex FROM ReportDataDictionary WHERE KeyValue = '#{node_name}' COLLATE NOCASE"
2375
+ val = sql.execAndReturnFirstDouble(query)
2376
+ query = "SELECT MAX(Value) FROM ReportData WHERE ReportDataDictionaryIndex = '#{val.get}'"
2377
+ val = sql.execAndReturnFirstDouble(query)
2378
+ if val.is_initialized
2379
+ result = OpenStudio::OptionalDouble.new(val.get)
2380
+ end
2381
+ zone_return_flow_si[zone_name] += result.to_f
2382
+ end
2383
+ end
2384
+
2385
+ # Calc ratio of Air Loop relief to sum of zone return for each air loop
2386
+ # and store in zone hash
2387
+
2388
+ # For each air loop, get relief air flow and calculate lab exhaust from the central air handler
2389
+ # Take from hourly reports created during sizing run
2390
+ zone_relief_flow_si = {}
2391
+ model.getAirLoopHVACs.sort.each do |air_loop_hvac|
2392
+ # First get relief air flow from sizing run sql file
2393
+ relief_node = air_loop_hvac.reliefAirNode.get
2394
+ node_name = relief_node.nameString
2395
+ relief_flow_si = 0
2396
+ relief_fraction = 0
2397
+ sql = model.sqlFile
2398
+ if sql.is_initialized
2399
+ sql = sql.get
2400
+ query = "SELECT ReportDataDictionaryIndex FROM ReportDataDictionary WHERE KeyValue = '#{node_name}' COLLATE NOCASE"
2401
+ val = sql.execAndReturnFirstDouble(query)
2402
+ query = "SELECT MAX(Value) FROM ReportData WHERE ReportDataDictionaryIndex = '#{val.get}'"
2403
+ val = sql.execAndReturnFirstDouble(query)
2404
+ if val.is_initialized
2405
+ result = OpenStudio::OptionalDouble.new(val.get)
2406
+ end
2407
+ relief_flow_si = result.to_f
2408
+ end
2409
+
2410
+ # Get total flow of zones on this air loop
2411
+ total_zone_return_si = 0
2412
+ air_loop_hvac.thermalZones.each do |zone|
2413
+ total_zone_return_si += zone_return_flow_si[zone.name.get]
2414
+ end
2415
+
2416
+ relief_fraction = relief_flow_si / total_zone_return_si unless total_zone_return_si == 0
2417
+
2418
+ # For each zone calc total effective exhaust
2419
+ air_loop_hvac.thermalZones.each do |zone|
2420
+ zone_relief_flow_si[zone.name.get] = relief_fraction * zone_return_flow_si[zone.name.get]
2421
+ end
2422
+ end
2423
+
2424
+ # Now check for exhaust driven by zone exhaust fans
2425
+ lab_zones.each do |zone|
2426
+ zone.equipment.each do |zone_equipment|
2427
+ # Get tally of exhaust fan flow
2428
+ if zone_equipment.to_FanZoneExhaust.is_initialized
2429
+ zone_exh_fan = zone_equipment.to_FanZoneExhaust.get
2430
+ # Check if any spaces in this zone are laboratory
2431
+ lab_exhaust_si += zone_exh_fan.maximumFlowRate.get
2432
+ end
2433
+ end
2434
+
2435
+ # Also account for outdoor air exhausted from this zone via return/relief
2436
+ lab_relief_si += zone_relief_flow_si[zone.name.get]
2437
+ end
2438
+ end
2439
+
2440
+ lab_exhaust_si += lab_relief_si
2441
+ lab_exhaust_cfm = OpenStudio.convert(lab_exhaust_si, 'm^3/s', 'cfm').get
2442
+
2443
+ # Isolate computer rooms onto separate groups
2444
+ # Computer rooms may need to be split to two groups, depending on load
2445
+ # Isolate heated-only and destrict cooling zones onto separate groups
2446
+ # District heating does not require separate group
2447
+ final_groups = []
2448
+ # Initialize arrays of zone objects by category
2449
+ heated_only_zones = []
2450
+ heated_cooled_zones = []
2451
+ district_cooled_zones = []
2452
+ comp_room_svav_zones = []
2453
+ comp_room_psz_zones = []
2454
+ dist_comp_room_svav_zones = []
2455
+ dist_comp_room_psz_zones = []
2456
+ lab_zones = []
2457
+
2458
+ total_area_ft2 = 0
2459
+ zones.each do |zn|
2460
+ if thermal_zone_heated?(zn['zone']) && !thermal_zone_cooled?(zn['zone'])
2461
+ # this will occur when there is no cooling tstat, or when min cooling setpoint is above 91 F
2462
+ heated_only_zones << zn['zone']
2463
+ elsif comp_room_loads[zn['zone'].name.get] > 0
2464
+ # This is a computer room zone
2465
+ if bldg_comp_room_load > 3_000_000 || comp_room_loads[zn['zone'].name.get] > 600_000
2466
+ # System 11
2467
+ if zn['fuel'].include?('DistrictCooling')
2468
+ dist_comp_room_svav_zones << zn['zone']
2469
+ else
2470
+ comp_room_svav_zones << zn['zone']
2471
+ end
2472
+ else
2473
+ # PSZ
2474
+ if zn['fuel'].include?('DistrictCooling')
2475
+ dist_comp_room_psz_zones << zn['zone']
2476
+ else
2477
+ comp_room_psz_zones << zn['zone']
2478
+ end
2479
+ end
2480
+
2481
+ elsif has_lab_spaces[zn['zone'].name.get] && lab_exhaust_cfm > 15_000
2482
+ lab_zones << zn['zone']
2483
+ elsif zn['fuel'].include?('DistrictCooling')
2484
+ district_cooled_zones << zn['zone']
2485
+ else
2486
+ heated_cooled_zones << zn['zone']
2487
+ end
2488
+ # Collect total floor area of all zones for this building area type
2489
+ area_m2 = zn['zone'].floorArea * zn['zone'].multiplier
2490
+ total_area_ft2 += OpenStudio.convert(area_m2, 'm^2', 'ft^2').get
2491
+ end
2492
+
2493
+ # Build final_groups array
2494
+ unless heated_only_zones.empty?
2495
+ htd_only_group = {}
2496
+ htd_only_group['occ'] = 'heated-only storage'
2497
+ htd_only_group['fuel'] = 'any'
2498
+ htd_only_group['zone_group_type'] = 'heated_only_zones'
2499
+ area_m2 = 0
2500
+ heated_only_zones.each do |zone|
2501
+ area_m2 += zone.floorArea * zone.multiplier
2502
+ end
2503
+ area_ft2 = OpenStudio.convert(area_m2, 'm^2', 'ft^2').get
2504
+ htd_only_group['group_area_ft2'] = area_ft2
2505
+ htd_only_group['building_area_type_ft2'] = total_area_ft2
2506
+ htd_only_group['zones'] = heated_only_zones
2507
+ final_groups << htd_only_group
2508
+ end
2509
+ unless district_cooled_zones.empty?
2510
+ district_cooled_group = {}
2511
+ district_cooled_group['occ'] = hvac_building_type
2512
+ district_cooled_group['fuel'] = 'districtcooling'
2513
+ district_cooled_group['zone_group_type'] = 'district_cooled_zones'
2514
+ area_m2 = 0
2515
+ district_cooled_zones.each do |zone|
2516
+ area_m2 += zone.floorArea * zone.multiplier
2517
+ end
2518
+ area_ft2 = OpenStudio.convert(area_m2, 'm^2', 'ft^2').get
2519
+ district_cooled_group['group_area_ft2'] = area_ft2
2520
+ district_cooled_group['building_area_type_ft2'] = total_area_ft2
2521
+ district_cooled_group['zones'] = district_cooled_zones
2522
+ # store info if any zone has district, fuel, or electric heating
2523
+ district_cooled_group['fuel'] = get_group_heat_types(model, district_cooled_zones)
2524
+ final_groups << district_cooled_group
2525
+ end
2526
+ unless heated_cooled_zones.empty?
2527
+ heated_cooled_group = {}
2528
+ heated_cooled_group['occ'] = hvac_building_type
2529
+ heated_cooled_group['fuel'] = 'any'
2530
+ heated_cooled_group['zone_group_type'] = 'heated_cooled_zones'
2531
+ area_m2 = 0
2532
+ heated_cooled_zones.each do |zone|
2533
+ area_m2 += zone.floorArea * zone.multiplier
2534
+ end
2535
+ area_ft2 = OpenStudio.convert(area_m2, 'm^2', 'ft^2').get
2536
+ heated_cooled_group['group_area_ft2'] = area_ft2
2537
+ heated_cooled_group['building_area_type_ft2'] = total_area_ft2
2538
+ heated_cooled_group['zones'] = heated_cooled_zones
2539
+ # store info if any zone has district, fuel, or electric heating
2540
+ heated_cooled_group['fuel'] = get_group_heat_types(model, heated_cooled_zones)
2541
+ final_groups << heated_cooled_group
2542
+ end
2543
+ unless lab_zones.empty?
2544
+ lab_group = {}
2545
+ lab_group['occ'] = hvac_building_type
2546
+ lab_group['fuel'] = 'any'
2547
+ lab_group['zone_group_type'] = 'lab_zones'
2548
+ area_m2 = 0
2549
+ lab_zones.each do |zone|
2550
+ area_m2 += zone.floorArea * zone.multiplier
2551
+ end
2552
+ area_ft2 = OpenStudio.convert(area_m2, 'm^2', 'ft^2').get
2553
+ lab_group['group_area_ft2'] = area_ft2
2554
+ lab_group['building_area_type_ft2'] = total_area_ft2
2555
+ lab_group['zones'] = lab_zones
2556
+ # store info if any zone has district, fuel, or electric heating
2557
+ lab_group['fuel'] = get_group_heat_types(model, lab_zones)
2558
+ final_groups << lab_group
2559
+ end
2560
+ unless comp_room_svav_zones.empty?
2561
+ comp_room_svav_group = {}
2562
+ comp_room_svav_group['occ'] = 'computer room szvav'
2563
+ comp_room_svav_group['fuel'] = 'any'
2564
+ comp_room_svav_group['zone_group_type'] = 'computer_zones'
2565
+ area_m2 = 0
2566
+ comp_room_svav_zones.each do |zone|
2567
+ area_m2 += zone.floorArea * zone.multiplier
2568
+ end
2569
+ area_ft2 = OpenStudio.convert(area_m2, 'm^2', 'ft^2').get
2570
+ comp_room_svav_group['group_area_ft2'] = area_ft2
2571
+ comp_room_svav_group['building_area_type_ft2'] = total_area_ft2
2572
+ comp_room_svav_group['zones'] = comp_room_svav_zones
2573
+ # store info if any zone has district, fuel, or electric heating
2574
+ comp_room_svav_group['fuel'] = get_group_heat_types(model, comp_room_svav_zones)
2575
+ final_groups << comp_room_svav_group
2576
+ end
2577
+ unless comp_room_psz_zones.empty?
2578
+ comp_room_psz_group = {}
2579
+ comp_room_psz_group['occ'] = 'computer room psz'
2580
+ comp_room_psz_group['fuel'] = 'any'
2581
+ comp_room_psz_group['zone_group_type'] = 'computer_zones'
2582
+ area_m2 = 0
2583
+ comp_room_psz_zones.each do |zone|
2584
+ area_m2 += zone.floorArea * zone.multiplier
2585
+ end
2586
+ area_ft2 = OpenStudio.convert(area_m2, 'm^2', 'ft^2').get
2587
+ comp_room_psz_group['group_area_ft2'] = area_ft2
2588
+ comp_room_psz_group['building_area_type_ft2'] = total_area_ft2
2589
+ comp_room_psz_group['zones'] = comp_room_psz_zones
2590
+ # store info if any zone has district, fuel, or electric heating
2591
+ comp_room_psz_group['fuel'] = get_group_heat_types(model, comp_room_psz_zones)
2592
+ final_groups << comp_room_psz_group
2593
+ end
2594
+ unless dist_comp_room_svav_zones.empty?
2595
+ dist_comp_room_svav_group = {}
2596
+ dist_comp_room_svav_group['occ'] = hvac_building_type
2597
+ dist_comp_room_svav_group['fuel'] = 'districtcooling'
2598
+ dist_comp_room_svav_group['zone_group_type'] = 'computer_zones'
2599
+ area_m2 = 0
2600
+ dist_comp_room_svav_zones.each do |zone|
2601
+ area_m2 += zone.floorArea * zone.multiplier
2602
+ end
2603
+ area_ft2 = OpenStudio.convert(area_m2, 'm^2', 'ft^2').get
2604
+ dist_comp_room_svav_group['group_area_ft2'] = area_ft2
2605
+ dist_comp_room_svav_group['building_area_type_ft2'] = total_area_ft2
2606
+ dist_comp_room_svav_group['zones'] = dist_comp_room_svav_zones
2607
+ # store info if any zone has district, fuel, or electric heating
2608
+ dist_comp_room_svav_group['fuel'] = get_group_heat_types(model, dist_comp_room_svav_zones)
2609
+ final_groups << dist_comp_room_svav_group
2610
+ end
2611
+ unless dist_comp_room_psz_zones.empty?
2612
+ dist_comp_room_psz_group = {}
2613
+ dist_comp_room_psz_group['occ'] = hvac_building_type
2614
+ dist_comp_room_psz_group['fuel'] = 'districtcooling'
2615
+ dist_comp_room_psz_group['zone_group_type'] = 'computer_zones'
2616
+ area_m2 = 0
2617
+ dist_comp_room_psz_zones.each do |zone|
2618
+ end
2619
+ area_ft2 = OpenStudio.convert(area_m2, 'm^2', 'ft^2').get
2620
+ dist_comp_room_psz_group['group_area_ft2'] = area_ft2
2621
+ dist_comp_room_psz_group['building_area_type_ft2'] = total_area_ft2
2622
+ dist_comp_room_psz_group['zones'] = dist_comp_room_psz_zones
2623
+ # store info if any zone has district, fuel, or electric heating
2624
+ dist_comp_room_psz_group['fuel'] = get_group_heat_types(model, dist_comp_room_psz_zones)
2625
+ final_groups << dist_comp_room_psz_group
2626
+ end
2627
+
2628
+ ngrps = final_groups.count
2629
+ # Determine the number of stories spanned by each group and report out info.
2630
+ final_groups.each do |group|
2631
+ # Determine the number of stories this group spans
2632
+ num_stories = model_num_stories_spanned(model, group['zones'])
2633
+ group['stories'] = num_stories
2634
+ # Report out the final grouping
2635
+ OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "Final system type group: occ = #{group['occ']}, fuel = #{group['fuel']}, area = #{group['group_area_ft2'].round} ft2, num stories = #{group['stories']}, zones:")
2636
+ group['zones'].sort.each_slice(5) do |zone_list|
2637
+ zone_names = []
2638
+ zone_list.each do |zone|
2639
+ zone_names << zone.name.get.to_s
2640
+ end
2641
+ OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "--- #{zone_names.join(', ')}")
2642
+ end
2643
+ end
2644
+
2645
+ return final_groups
2646
+ end
2647
+
2648
+ # Alternate method for 2016 and later stable baseline
2649
+ # Limits for each building area type are taken from data table
2650
+ # Heating fuel is based on climate zone, unless district heat is in proposed
2651
+ #
2652
+ # @note Select system type from data table base on key parameters
2653
+ # @param climate_zone [string] id code for the climate
2654
+ # @param sys_group [hash] Hash defining a group of zones that have the same Appendix G system type
2655
+ # @param custom [string] included here for backwards compatibility (not used here)
2656
+ # @param hvac_building_type [String] Chosen by user via measure interface or user data files
2657
+ # @param district_heat_zones [hash] of zone name => true for has district heat, false for has not
2658
+ # @return [String] The system type. Possibilities are PTHP, PTAC, PSZ_AC, PSZ_HP, PVAV_Reheat, PVAV_PFP_Boxes,
2659
+ # VAV_Reheat, VAV_PFP_Boxes, Gas_Furnace, Electric_Furnace
2660
+ def model_prm_baseline_system_type(model, climate_zone, sys_group, custom, hvac_building_type, district_heat_zones)
2661
+ area_type = sys_group['occ']
2662
+ fuel_type = sys_group['fuel']
2663
+ area_ft2 = sys_group['building_area_type_ft2']
2664
+ num_stories = sys_group['stories']
2665
+ zones = sys_group['zones']
2666
+
2667
+ # [type, central_heating_fuel, zone_heating_fuel, cooling_fuel]
2668
+ system_type = [nil, nil, nil, nil]
2669
+
2670
+ # Find matching record from prm baseline hvac table
2671
+ # First filter by number of stories
2672
+ iStoryGroup = 0
2673
+ props = {}
2674
+ 0.upto(9) do |i|
2675
+ iStoryGroup += 1
2676
+ props = model_find_object(standards_data['prm_baseline_hvac'],
2677
+ 'template' => template,
2678
+ 'hvac_building_type' => area_type,
2679
+ 'flrs_range_group' => iStoryGroup,
2680
+ 'area_range_group' => 1)
2681
+
2682
+ if !props
2683
+ OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Model', "Could not find baseline HVAC type for: #{template}-#{area_type}.")
2684
+ end
2685
+ if num_stories <= props['bldg_flrs_max']
2686
+ # Story Group Is found
2687
+ break
2688
+ end
2689
+ end
2690
+
2691
+ # Next filter by floor area
2692
+ iAreaGroup = 0
2693
+ baseine_is_found = false
2694
+ loop do
2695
+ iAreaGroup += 1
2696
+ props = model_find_object(standards_data['prm_baseline_hvac'],
2697
+ 'template' => template,
2698
+ 'hvac_building_type' => area_type,
2699
+ 'flrs_range_group' => iStoryGroup,
2700
+ 'area_range_group' => iAreaGroup)
2701
+
2702
+ if !props
2703
+ OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Model', "Could not find baseline HVAC type for: #{template}-#{area_type}.")
2704
+ end
2705
+ below_max = false
2706
+ above_min = false
2707
+ # check if actual building floor area is within range for this area group
2708
+ if props['max_area_qual'] == 'LT'
2709
+ if area_ft2 < props['bldg_area_max']
2710
+ below_max = true
2711
+ end
2712
+ elsif props['max_area_qual'] == 'LE'
2713
+ if area_ft2 <= props['bldg_area_max']
2714
+ below_max = true
2715
+ end
2716
+ end
2717
+ if props['min_area_qual'] == 'GT'
2718
+ if area_ft2 > props['bldg_area_min']
2719
+ above_min = true
2720
+ end
2721
+ elsif props['min_area_qual'] == 'GE'
2722
+ if area_ft2 >= props['bldg_area_min']
2723
+ above_min = true
2724
+ end
2725
+ end
2726
+ if (above_min == true) && (below_max == true)
2727
+ baseline_is_found = true
2728
+ break
2729
+ end
2730
+ if iAreaGroup > 9
2731
+ OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Model', "Could not find baseline HVAC type for: #{template}-#{area_type}.")
2732
+ break
2733
+ end
2734
+ end
2735
+
2736
+ heat_type = find_prm_heat_type(hvac_building_type, climate_zone)
2737
+
2738
+ # hash to relate apx G systype categories to sys types for model
2739
+ sys_hash = {}
2740
+ if heat_type == 'fuel'
2741
+ sys_hash['PTAC'] = 'PTAC'
2742
+ sys_hash['PSZ'] = 'PSZ_AC'
2743
+ sys_hash['SZ-CV'] = 'SZ_CV'
2744
+ sys_hash['Heating and ventilation'] = 'Gas_Furnace'
2745
+ sys_hash['PSZ-AC'] = 'PSZ_AC'
2746
+ sys_hash['Packaged VAV'] = 'PVAV_Reheat'
2747
+ sys_hash['VAV'] = 'VAV_Reheat'
2748
+ sys_hash['Unconditioned'] = 'None'
2749
+ sys_hash['SZ-VAV'] = 'SZ_VAV'
2750
+ else
2751
+ sys_hash['PTAC'] = 'PTHP'
2752
+ sys_hash['PSZ'] = 'PSZ_HP'
2753
+ sys_hash['SZ-CV'] = 'SZ_CV'
2754
+ sys_hash['Heating and ventilation'] = 'Electric_Furnace'
2755
+ sys_hash['PSZ-AC'] = 'PSZ_HP'
2756
+ sys_hash['Packaged VAV'] = 'PVAV_PFP_Boxes'
2757
+ sys_hash['VAV'] = 'VAV_PFP_Boxes'
2758
+ sys_hash['Unconditioned'] = 'None'
2759
+ sys_hash['SZ-VAV'] = 'SZ_VAV'
2760
+ end
2761
+
2762
+ model_sys_type = sys_hash[props['system_type']]
2763
+
2764
+ if /districtheating/i =~ fuel_type
2765
+ central_heat = 'DistrictHeating'
2766
+ elsif heat_type =~ /fuel/i
2767
+ central_heat = 'NaturalGas'
2768
+ else
2769
+ central_heat = 'Electricity'
2770
+ end
2771
+ if /districtheating/i =~ fuel_type && /elec/i !~ fuel_type && /fuel/i !~ fuel_type
2772
+ # if no zone has fuel or elect, set default to district for zones
2773
+ zone_heat = 'DistrictHeating'
2774
+ elsif heat_type =~ /fuel/i
2775
+ zone_heat = 'NaturalGas'
2776
+ else
2777
+ zone_heat = 'Electricity'
2778
+ end
2779
+ if /districtcooling/i =~ fuel_type
2780
+ cool_type = 'DistrictCooling'
2781
+ elsif props['system_type'] =~ /Heating and ventilation/i || props['system_type'] =~ /unconditioned/i
2782
+ cool_type = nil
2783
+ end
2784
+
2785
+ system_type = [model_sys_type, central_heat, zone_heat, cool_type]
2786
+ return system_type
2787
+ end
2788
+
2789
+ # For a multizone system, create the fan schedule based on zone occupancy/fan schedules
2790
+ # @author Doug Maddox, PNNL
2791
+ # @param model
2792
+ # @param zone_fan_scheds [Hash] of hash of zoneName:8760FanSchedPerZone
2793
+ # @param pri_zones [Array<String>] names of zones served by the multizone system
2794
+ # @param system_name [String] name of air loop
2795
+ def model_create_multizone_fan_schedule(model, zone_op_hrs, pri_zones, system_name)
2796
+ # Create fan schedule for multizone system
2797
+ fan_8760 = []
2798
+ # If any zone is on for an hour, then the system fan must be on for that hour
2799
+ pri_zones.each do |zone|
2800
+ zone_name = zone.name.get.to_s
2801
+ if fan_8760.empty?
2802
+ fan_8760 = zone_op_hrs[zone_name]
2803
+ else
2804
+ (0..fan_8760.size - 1).each do |ihr|
2805
+ if zone_op_hrs[zone_name][ihr] > 0
2806
+ fan_8760[ihr] = 1
2807
+ end
2808
+ end
2809
+ end
2810
+ end
2811
+
2812
+ # Convert 8760 array to schedule ruleset
2813
+ fan_sch_limits = model.getScheduleTypeLimitsByName('fan schedule limits for prm')
2814
+ if fan_sch_limits.empty?
2815
+ fan_sch_limits = OpenStudio::Model::ScheduleTypeLimits.new(model)
2816
+ fan_sch_limits.setName('fan schedule limits for prm')
2817
+ fan_sch_limits.setNumericType('DISCRETE')
2818
+ fan_sch_limits.setUnitType('Dimensionless')
2819
+ fan_sch_limits.setLowerLimitValue(0)
2820
+ fan_sch_limits.setUpperLimitValue(1)
2821
+ else
2822
+ fan_sch_limits = fan_sch_limits.get
2823
+ end
2824
+ sch_name = system_name + ' ' + 'fan schedule'
2825
+ make_ruleset_sched_from_8760(model, fan_8760, sch_name, fan_sch_limits)
2826
+
2827
+ air_loop = model.getAirLoopHVACByName(system_name).get
2828
+ air_loop.additionalProperties.setFeature('fan_sched_name', sch_name)
2829
+ end
2830
+
2831
+ # For a multizone system, identify any zones to isolate to separate PSZ systems
2832
+ # isolated zones are on the 'secondary' list
2833
+ # This version of the method applies to standard years 2016 and later (stable baseline)
2834
+ # @author Doug Maddox, PNNL
2835
+ # @param model
2836
+ # @param zones [Array<Object>]
2837
+ # @param zone_fan_scheds [Hash] hash of zoneName:8760FanSchedPerZone
2838
+ # @return [Hash] A hash of two arrays of ThermalZones,
2839
+ # where the keys are 'primary' and 'secondary'
2840
+ def model_differentiate_primary_secondary_thermal_zones(model, zones, zone_fan_scheds)
2841
+ pri_zones = []
2842
+ sec_zones = []
2843
+ pri_zone_names = []
2844
+ sec_zone_names = []
2845
+ zone_op_hrs = {} # hash of zoneName: 8760 array of operating hours
2846
+
2847
+ # If there is only one zone, then set that as primary
2848
+ if zones.size == 1
2849
+ zones.each do |zone|
2850
+ pri_zones << zone
2851
+ pri_zone_names << zone.name.get.to_s
2852
+ zone_name = zone.name.get.to_s
2853
+ if zone_fan_scheds.key?(zone_name)
2854
+ zone_fan_sched = zone_fan_scheds[zone_name]
2855
+ else
2856
+ zone_fan_sched = nil
2857
+ end
2858
+ zone_op_hrs[zone.name.get.to_s] = thermal_zone_get_annual_operating_hours(model, zone, zone_fan_sched)
2859
+ end
2860
+ # Report out the primary vs. secondary zones
2861
+ unless sec_zone_names.empty?
2862
+ OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "Secondary system zones = #{sec_zone_names.join(', ')}.")
2863
+ end
2864
+
2865
+ return { 'primary' => pri_zones, 'secondary' => sec_zones, 'zone_op_hrs' => zone_op_hrs }
2866
+ end
2867
+
2868
+ zone_eflh = {} # hash of zoneName: eflh for zone
2869
+ zone_max_load = {} # hash of zoneName: coincident max internal load
2870
+ load_limit = 10 # differ by 10 Btu/hr-sf or more
2871
+ eflh_limit = 40 # differ by more than 40 EFLH/week from average of other zones
2872
+ zone_area = {} # hash of zoneName:area
2873
+
2874
+ # Get coincident peak internal load for each zone
2875
+ zones.each do |zone|
2876
+ zone_name = zone.name.get.to_s
2877
+ if zone_fan_scheds.key?(zone_name)
2878
+ zone_fan_sched = zone_fan_scheds[zone_name]
2879
+ else
2880
+ zone_fan_sched = nil
2881
+ end
2882
+ zone_op_hrs[zone_name] = thermal_zone_get_annual_operating_hours(model, zone, zone_fan_sched)
2883
+ zone_eflh[zone_name] = thermal_zone_occupancy_eflh(zone, zone_op_hrs[zone_name])
2884
+ zone_max_load_w = thermal_zone_peak_internal_load(model, zone)
2885
+ zone_max_load_w_m2 = zone_max_load_w / zone.floorArea
2886
+ zone_max_load[zone_name] = OpenStudio.convert(zone_max_load_w_m2, 'W/m^2', 'Btu/hr*ft^2').get
2887
+ zone_area[zone_name] = zone.floorArea
2888
+ end
2889
+
2890
+ # Eliminate all zones for which both max load and EFLH exceed limits
2891
+ zones.each do |zone|
2892
+ zone_name = zone.name.get.to_s
2893
+ max_load = zone_max_load[zone_name]
2894
+ avg_max_load = get_wtd_avg_of_other_zones(zone_max_load, zone_area, zone_name)
2895
+ max_load_diff = (max_load - avg_max_load).abs
2896
+ avg_eflh = get_avg_of_other_zones(zone_eflh, zone_name)
2897
+ eflh_diff = (avg_eflh - zone_eflh[zone_name]).abs
2898
+
2899
+ if max_load_diff >= load_limit && eflh_diff > eflh_limit
2900
+ # Add zone to secondary list, and remove from hashes
2901
+ OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "Zone moved to PSZ due to load AND eflh: #{zone_name}; load limit = #{load_limit}, eflh_limit = #{eflh_limit}")
2902
+ OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "load diff = #{max_load_diff}, this zone load = #{max_load}, avg zone load = #{avg_max_load}")
2903
+ OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "eflh diff = #{eflh_diff}, this zone load = #{zone_eflh[zone_name]}, avg zone eflh = #{avg_eflh}")
2904
+
2905
+ sec_zones << zone
2906
+ sec_zone_names << zone_name
2907
+ zone_eflh.delete(zone_name)
2908
+ zone_max_load.delete(zone_name)
2909
+ end
2910
+ end
2911
+
2912
+ # Eliminate worst zone where EFLH exceeds limit
2913
+ # Repeat until all zones are within limit
2914
+ num_zones = zone_eflh.size
2915
+ avg_eflh_save = 0
2916
+ max_zone_name = ''
2917
+ max_eflh_diff = 0
2918
+ max_zone = nil
2919
+ (1..num_zones).each do |izone|
2920
+ # This loop is to iterate to eliminate one zone at a time
2921
+ max_eflh_diff = 0
2922
+ zones.each do |zone|
2923
+ # This loop finds the worst remaining zone to eliminate if above threshold
2924
+ zone_name = zone.name.get.to_s
2925
+ next if !zone_eflh.key?(zone_name)
2926
+
2927
+ avg_eflh = get_avg_of_other_zones(zone_eflh, zone_name)
2928
+ eflh_diff = (avg_eflh - zone_eflh[zone_name]).abs
2929
+ if eflh_diff > max_eflh_diff
2930
+ max_eflh_diff = eflh_diff
2931
+ max_zone_name = zone_name
2932
+ max_zone = zone
2933
+ avg_eflh_save = avg_eflh
2934
+ end
2935
+ end
2936
+ if max_eflh_diff > eflh_limit
2937
+ # Move the max Zone to the secondary list
2938
+ OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "Zone moved to PSZ due to eflh: #{max_zone_name}; limit = #{eflh_limit}")
2939
+ OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "eflh diff = #{max_eflh_diff}, this zone load = #{zone_eflh[max_zone_name]}, avg zone eflh = #{avg_eflh_save}")
2940
+ sec_zones << max_zone
2941
+ sec_zone_names << max_zone_name
2942
+ zone_eflh.delete(max_zone_name)
2943
+ zone_max_load.delete(max_zone_name)
2944
+ else
2945
+ # All zones are now within the limit, exit the iteration
2946
+ break
2947
+ end
2948
+ end
2949
+
2950
+ # Eliminate worst zone where max load exceeds limit and repeat until all pass
2951
+ num_zones = zone_eflh.size
2952
+ highest_max_load_diff = -1
2953
+ highest_zone = nil
2954
+ highest_zone_name = ''
2955
+ highest_max_load = 0
2956
+ avg_max_load_save = 0
2957
+
2958
+ (1..num_zones).each do |izone|
2959
+ # This loop is to iterate to eliminate one zone at a time
2960
+ highest_max_load_diff = 0
2961
+ zones.each do |zone|
2962
+ # This loop finds the worst remaining zone to eliminate if above threshold
2963
+ zone_name = zone.name.get.to_s
2964
+ next if !zone_max_load.key?(zone_name)
2965
+
2966
+ max_load = zone_max_load[zone_name]
2967
+ avg_max_load = get_wtd_avg_of_other_zones(zone_max_load, zone_area, zone_name)
2968
+ max_load_diff = (max_load - avg_max_load).abs
2969
+ if max_load_diff >= highest_max_load_diff
2970
+ highest_max_load_diff = max_load_diff
2971
+ highest_zone_name = zone_name
2972
+ highest_zone = zone
2973
+ highest_max_load = max_load
2974
+ avg_max_load_save = avg_max_load
2975
+ end
2976
+ end
2977
+ if highest_max_load_diff > load_limit
2978
+ # Move the max Zone to the secondary list
2979
+ OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "Zone moved to PSZ due to load: #{highest_zone_name}; load limit = #{load_limit}")
2980
+ OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "load diff = #{highest_max_load_diff}, this zone load = #{highest_max_load}, avg zone load = #{avg_max_load_save}")
2981
+ sec_zones << highest_zone
2982
+ sec_zone_names << highest_zone_name
2983
+ zone_eflh.delete(highest_zone_name)
2984
+ zone_max_load.delete(highest_zone_name)
2985
+ else
2986
+ # All zones are now within the limit, exit the iteration
2987
+ break
2988
+ end
2989
+ end
2990
+
2991
+ # Place remaining zones in multizone system list
2992
+ zone_eflh.each_key do |key|
2993
+ zones.each do |zone|
2994
+ if key == zone.name.get.to_s
2995
+ pri_zones << zone
2996
+ pri_zone_names << key
2997
+ end
2998
+ end
2999
+ end
3000
+
3001
+ # Report out the primary vs. secondary zones
3002
+ unless pri_zone_names.empty?
3003
+ OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "Primary system zones = #{pri_zone_names.join(', ')}.")
3004
+ end
3005
+ unless sec_zone_names.empty?
3006
+ OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "Secondary system zones = #{sec_zone_names.join(', ')}.")
3007
+ end
3008
+
3009
+ return { 'primary' => pri_zones, 'secondary' => sec_zones, 'zone_op_hrs' => zone_op_hrs }
3010
+ end
3011
+
3012
+ # This method is a catch-all run at the end of create-baseline to make final adjustements to HVAC capacities
3013
+ # to account for recent model changes
3014
+ # @author Doug Maddox, PNNL
3015
+ # @param model
3016
+ # @return [Bool] true if successful, false if not
3017
+ def model_refine_size_dependent_values(model, sizing_run_dir)
3018
+ # Final sizing run before refining size-dependent values
3019
+ if model_run_sizing_run(model, "#{sizing_run_dir}/SR3") == false
3020
+ return false
3021
+ end
3022
+
3023
+ model.getAirLoopHVACs.sort.each do |air_loop_hvac|
3024
+ # Reset secondary design secondary flow rate based on updated primary flow
3025
+ air_loop_hvac.demandComponents.each do |dc|
3026
+ next if dc.to_AirTerminalSingleDuctParallelPIUReheat.empty?
3027
+
3028
+ pfp_term = dc.to_AirTerminalSingleDuctParallelPIUReheat.get
3029
+ sec_flow_frac = 0.5
3030
+
3031
+ # Get the maximum flow rate through the terminal
3032
+ max_primary_air_flow_rate = nil
3033
+ if pfp_term.autosizedMaximumPrimaryAirFlowRate.is_initialized
3034
+ max_primary_air_flow_rate = pfp_term.autosizedMaximumPrimaryAirFlowRate.get
3035
+ elsif pfp_term.maximumPrimaryAirFlowRate.is_initialized
3036
+ max_primary_air_flow_rate = pfp_term.maximumPrimaryAirFlowRate.get
3037
+ end
3038
+
3039
+ max_sec_flow_rate_m3_per_s = max_primary_air_flow_rate * sec_flow_frac
3040
+ pfp_term.setMaximumSecondaryAirFlowRate(max_sec_flow_rate_m3_per_s)
3041
+ end
3042
+ end
3043
+ return true
3044
+ end
3045
+ end