openstudio-standards 0.2.16 → 0.2.17.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (181) hide show
  1. checksums.yaml +4 -4
  2. data/data/standards/manage_OpenStudio_Standards.rb +31 -4
  3. data/lib/openstudio-standards/btap/geometry.rb +1 -1
  4. data/lib/openstudio-standards/hvac_sizing/Siz.HeatingCoolingFuels.rb +354 -2
  5. data/lib/openstudio-standards/hvac_sizing/Siz.ThermalZone.rb +79 -0
  6. data/lib/openstudio-standards/prototypes/common/buildings/Prototype.College.rb +1 -1
  7. data/lib/openstudio-standards/prototypes/common/buildings/Prototype.Laboratory.rb +1 -1
  8. data/lib/openstudio-standards/prototypes/common/do_not_edit_metaclasses.rb +3313 -0
  9. data/lib/openstudio-standards/prototypes/common/objects/Prototype.Fan.rb +12 -0
  10. data/lib/openstudio-standards/prototypes/common/objects/Prototype.Model.rb +3 -4
  11. data/lib/openstudio-standards/prototypes/common/objects/Prototype.SizingSystem.rb +1 -1
  12. data/lib/openstudio-standards/prototypes/common/objects/Prototype.hvac_systems.rb +167 -93
  13. data/lib/openstudio-standards/prototypes/common/objects/Prototype.utilities.rb +2 -4
  14. data/lib/openstudio-standards/prototypes/common/prototype_metaprogramming.rb +1 -0
  15. data/lib/openstudio-standards/refs/references.rb +3 -0
  16. data/lib/openstudio-standards/standards/Standards.AirLoopHVAC.rb +279 -6
  17. data/lib/openstudio-standards/standards/Standards.AirTerminalSingleDuctParallelPIUReheat.rb +50 -2
  18. data/lib/openstudio-standards/standards/Standards.ChillerElectricEIR.rb +4 -0
  19. data/lib/openstudio-standards/standards/Standards.CoilCoolingWaterToAirHeatPumpEquationFit.rb +0 -1
  20. data/lib/openstudio-standards/standards/Standards.Construction.rb +185 -3
  21. data/lib/openstudio-standards/standards/Standards.Fan.rb +14 -6
  22. data/lib/openstudio-standards/standards/Standards.HeatExchangerSensLat.rb +1 -0
  23. data/lib/openstudio-standards/standards/Standards.Model.rb +1751 -383
  24. data/lib/openstudio-standards/standards/Standards.PlanarSurface.rb +130 -9
  25. data/lib/openstudio-standards/standards/Standards.PlantLoop.rb +50 -3
  26. data/lib/openstudio-standards/standards/Standards.ScheduleCompact.rb +44 -0
  27. data/lib/openstudio-standards/standards/Standards.ScheduleConstant.rb +27 -0
  28. data/lib/openstudio-standards/standards/Standards.ScheduleRuleset.rb +543 -0
  29. data/lib/openstudio-standards/standards/Standards.Space.rb +665 -15
  30. data/lib/openstudio-standards/standards/Standards.SpaceType.rb +141 -4
  31. data/lib/openstudio-standards/standards/Standards.SubSurface.rb +2 -1
  32. data/lib/openstudio-standards/standards/Standards.Surface.rb +117 -0
  33. data/lib/openstudio-standards/standards/Standards.ThermalZone.rb +197 -49
  34. data/lib/openstudio-standards/standards/Standards.ZoneHVACComponent.rb +41 -0
  35. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2004/ashrae_90_1_2004.Model.rb +6 -8
  36. 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
  37. 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
  38. 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
  39. 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
  40. 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
  41. 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
  42. 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
  43. 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
  44. 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
  45. 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
  46. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2019/ashrae_90_1_2019.AirLoopHVAC.rb +5 -5
  47. 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
  48. 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
  49. data/lib/openstudio-standards/standards/ashrae_90_1/data/ashrae_90_1.constructions.json +2 -2
  50. data/lib/openstudio-standards/standards/ashrae_90_1/data/ashrae_90_1.fans.json +12 -0
  51. 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
  52. 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
  53. 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
  54. 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
  55. data/lib/openstudio-standards/standards/ashrae_90_1/nrel_zne_ready_2017/nrel_zne_ready_2017.AirLoopHVAC.rb +1 -0
  56. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.AirLoopHVAC.rb +792 -0
  57. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.AirTerminalSingleDuctParallelPIUReheat.rb +10 -0
  58. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.AirTerminalSingleDuctVAVReheat.rb +31 -0
  59. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.BoilerHotWater.rb +91 -0
  60. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.ChillerElectricEIR.rb +84 -0
  61. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.CoilCoolingDXSingleSpeed.rb +145 -0
  62. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.CoilCoolingDXTwoSpeed.rb +106 -0
  63. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.CoilDX.rb +71 -0
  64. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.CoilHeatingDXSingleSpeed.rb +194 -0
  65. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.CoilHeatingGas.rb +120 -0
  66. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.CoolingTower.rb +110 -0
  67. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.CoolingTowerVariableSpeed.rb +5 -0
  68. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.Fan.rb +73 -0
  69. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.FanConstantVolume.rb +5 -0
  70. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.FanOnOff.rb +5 -0
  71. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.FanVariableVolume.rb +24 -0
  72. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.FanZoneExhaust.rb +5 -0
  73. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.HeatExchangerSensLat.rb +55 -0
  74. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.Model.rb +3045 -0
  75. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.PlanarSurface.rb +187 -0
  76. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.PlantLoop.rb +450 -0
  77. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.Space.rb +106 -0
  78. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.SpaceType.rb +666 -0
  79. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.Surface.rb +54 -0
  80. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.ThermalZone.rb +168 -0
  81. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.ZoneHVACComponent.rb +132 -0
  82. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.rb +239 -0
  83. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm_2019/ashrae_90_1_prm_2019.Model.rb +176 -0
  84. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm_2019/ashrae_90_1_prm_2019.rb +25 -0
  85. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm_2019/data/ashrae_90_1_prm_2019.boilers.json +52 -0
  86. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm_2019/data/ashrae_90_1_prm_2019.chillers.json +112 -0
  87. 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
  88. 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
  89. 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
  90. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm_2019/data/ashrae_90_1_prm_2019.furnaces.json +43 -0
  91. 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
  92. 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
  93. 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
  94. 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
  95. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm_2019/data/ashrae_90_1_prm_2019.motors.json +264 -0
  96. 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
  97. 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
  98. 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
  99. 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
  100. 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
  101. 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
  102. 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
  103. 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
  104. 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
  105. 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
  106. 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
  107. data/lib/openstudio-standards/standards/ashrae_90_1_prm/data/ashrae_90_1_prm.climate_zone_sets.json +210 -0
  108. data/lib/openstudio-standards/standards/ashrae_90_1_prm/data/ashrae_90_1_prm.curves.json +18329 -0
  109. data/lib/openstudio-standards/standards/ashrae_90_1_prm/data/ashrae_90_1_prm.fans.json +340 -0
  110. data/lib/openstudio-standards/standards/ashrae_90_1_prm/data/ashrae_90_1_prm.materials.json +49924 -0
  111. data/lib/openstudio-standards/standards/ashrae_90_1_prm/docs/baseline_building_rotation_exception.md +44 -0
  112. data/lib/openstudio-standards/standards/ashrae_90_1_prm/docs/check_pump_power_and_control.md +71 -0
  113. data/lib/openstudio-standards/standards/ashrae_90_1_prm/docs/dcv.md +68 -0
  114. data/lib/openstudio-standards/standards/ashrae_90_1_prm/docs/dcv_implementation.png +0 -0
  115. data/lib/openstudio-standards/standards/ashrae_90_1_prm/docs/elevators.md +14 -0
  116. data/lib/openstudio-standards/standards/ashrae_90_1_prm/docs/exhaust_air_energy_recovery.md +36 -0
  117. data/lib/openstudio-standards/standards/ashrae_90_1_prm/docs/f_c_factors.md +19 -0
  118. data/lib/openstudio-standards/standards/ashrae_90_1_prm/docs/fan_power_credits.md +15 -0
  119. data/lib/openstudio-standards/standards/ashrae_90_1_prm/docs/preheat_coil.md +59 -0
  120. data/lib/openstudio-standards/standards/ashrae_90_1_prm/docs/pump_power_control.md +46 -0
  121. data/lib/openstudio-standards/standards/ashrae_90_1_prm/docs/return_air_type.md +31 -0
  122. data/lib/openstudio-standards/standards/ashrae_90_1_prm/docs/set_baseline_wwr.md +191 -0
  123. data/lib/openstudio-standards/standards/ashrae_90_1_prm/docs/set_hw_and_chw_supply_water_temp_reset_control.md +24 -0
  124. data/lib/openstudio-standards/standards/ashrae_90_1_prm/docs/set_num_boilers_chillers_towers.md +49 -0
  125. data/lib/openstudio-standards/standards/ashrae_90_1_prm/docs/set_plug_load_measures.md +80 -0
  126. data/lib/openstudio-standards/standards/ashrae_90_1_prm/docs/set_space_lpd.md +73 -0
  127. data/lib/openstudio-standards/standards/ashrae_90_1_prm/docs/unenclosed_and_unconditioned_spaces.md +11 -0
  128. data/lib/openstudio-standards/standards/ashrae_90_1_prm/docs/unmet_load_hours.md +20 -0
  129. data/lib/openstudio-standards/standards/ashrae_90_1_prm/docs/vav_parallel_piu_terminals_fan_control.md +23 -0
  130. data/lib/openstudio-standards/standards/ashrae_90_1_prm/docs/vav_terminals_min_flow_setpoint.md +21 -0
  131. data/lib/openstudio-standards/standards/ashrae_90_1_prm/userdata_csv/userdata_airloop_hvac.csv +1 -0
  132. data/lib/openstudio-standards/standards/ashrae_90_1_prm/userdata_csv/userdata_airloop_hvac_doas.csv +1 -0
  133. data/lib/openstudio-standards/standards/ashrae_90_1_prm/userdata_csv/userdata_building.csv +1 -0
  134. data/lib/openstudio-standards/standards/ashrae_90_1_prm/userdata_csv/userdata_design_specification_outdoor_air.csv +1 -0
  135. data/lib/openstudio-standards/standards/ashrae_90_1_prm/userdata_csv/userdata_electric_equipment.csv +1 -0
  136. data/lib/openstudio-standards/standards/ashrae_90_1_prm/userdata_csv/userdata_exterior_lights.csv +1 -0
  137. data/lib/openstudio-standards/standards/ashrae_90_1_prm/userdata_csv/userdata_gas_equipment.csv +1 -0
  138. data/lib/openstudio-standards/standards/ashrae_90_1_prm/userdata_csv/userdata_lights.csv +1 -0
  139. data/lib/openstudio-standards/standards/ashrae_90_1_prm/userdata_csv/userdata_space.csv +1 -0
  140. data/lib/openstudio-standards/standards/ashrae_90_1_prm/userdata_csv/userdata_spacetype.csv +1 -0
  141. data/lib/openstudio-standards/standards/ashrae_90_1_prm/userdata_csv/userdata_thermal_zone.csv +1 -0
  142. data/lib/openstudio-standards/standards/ashrae_90_1_prm/userdata_csv/userdata_wateruse_connections.csv +1 -0
  143. data/lib/openstudio-standards/standards/ashrae_90_1_prm/userdata_csv/userdata_wateruse_equipment.csv +1 -0
  144. data/lib/openstudio-standards/standards/ashrae_90_1_prm/userdata_csv/userdata_wateruse_equipment_definition.csv +1 -0
  145. data/lib/openstudio-standards/standards/ashrae_90_1_prm/userdata_csv/userdata_zone_hvac.csv +1 -0
  146. data/lib/openstudio-standards/standards/ashrae_90_1_prm/userdata_csv/userdata_zone_infiltration.csv +1 -0
  147. data/lib/openstudio-standards/standards/cbes/data/cbes.fans.json +12 -0
  148. data/lib/openstudio-standards/standards/deer/data/deer.fans.json +12 -0
  149. data/lib/openstudio-standards/standards/necb/ECMS/data/heat_pumps.json +1 -1
  150. data/lib/openstudio-standards/standards/necb/ECMS/data/heat_pumps_heating.json +1 -1
  151. data/lib/openstudio-standards/standards/necb/ECMS/data/unitary_acs.json +24 -11
  152. data/lib/openstudio-standards/standards/necb/ECMS/erv.rb +13 -15
  153. data/lib/openstudio-standards/standards/necb/NECB2011/data/province_map.json +17 -0
  154. data/lib/openstudio-standards/standards/necb/NECB2011/hvac_system_3_and_8_multi_speed.rb +1 -1
  155. data/lib/openstudio-standards/standards/necb/NECB2011/hvac_system_3_and_8_single_speed.rb +1 -1
  156. data/lib/openstudio-standards/standards/necb/NECB2011/hvac_system_4.rb +2 -2
  157. data/lib/openstudio-standards/standards/necb/NECB2011/hvac_system_6.rb +6 -5
  158. data/lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb +3 -2
  159. data/lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb +2 -3
  160. data/lib/openstudio-standards/standards/necb/NECB2020/data/chillers.json +2 -2
  161. data/lib/openstudio-standards/standards/necb/NECB2020/data/space_types.json +33 -924
  162. data/lib/openstudio-standards/standards/necb/NECB2020/data/unitary_acs.json +15 -15
  163. data/lib/openstudio-standards/standards/necb/common/btap_data.rb +135 -29
  164. data/lib/openstudio-standards/standards/necb/common/btap_datapoint.rb +16 -4
  165. data/lib/openstudio-standards/standards/necb/common/neb_end_use_prices.csv +40 -42
  166. data/lib/openstudio-standards/standards/necb/common/necb_reference_runs.csv +1 -1
  167. data/lib/openstudio-standards/standards/necb/common/space_type_upgrade_map.json +89 -89
  168. data/lib/openstudio-standards/utilities/array.rb +11 -0
  169. data/lib/openstudio-standards/utilities/logging.rb +48 -0
  170. data/lib/openstudio-standards/utilities/object_info.rb +20 -0
  171. data/lib/openstudio-standards/utilities/schedule_translator.rb +348 -0
  172. data/lib/openstudio-standards/utilities/sqlfile.rb +68 -0
  173. data/lib/openstudio-standards/version.rb +2 -2
  174. data/lib/openstudio-standards/weather/Weather.Model.rb +42 -55
  175. data/lib/openstudio-standards/weather/Weather.stat_file.rb +1 -1
  176. data/lib/openstudio-standards.rb +35 -1
  177. metadata +111 -6
  178. data/data/standards/OpenStudio_Standards-ashrae_90_1.xlsx +0 -0
  179. data/data/standards/OpenStudio_Standards-ashrae_90_1_28Jan2022.xlsx +0 -0
  180. data/data/standards/OpenStudio_Standards-ashrae_90_1_28_Jan2022_2.xlsx +0 -0
  181. data/data/standards/openstudio_standards_duplicates_log.csv +0 -143
@@ -14,229 +14,514 @@ class Standard
14
14
  # @!group Model
15
15
 
16
16
  # Creates a Performance Rating Method (aka Appendix G aka LEED) baseline building model
17
- # based on the inputs currently in the model.
17
+ # Method used for 90.1-2016 and onward
18
18
  #
19
19
  # @note Per 90.1, the Performance Rating Method "does NOT offer an alternative compliance path for minimum standard compliance."
20
- # This means you can't use this method for code compliance to get a permit.
21
- #
22
- # @param model [OpenStudio::Model::Model] OpenStudio model object
20
+ # This means you can't use this method for code compliance to get a permit.
21
+ # @param user_model [OpenStudio::model::Model] User specified OpenStudio model
23
22
  # @param building_type [String] the building type
24
- # @param climate_zone [String] ASHRAE climate zone, e.g. 'ASHRAE 169-2013-4A'
23
+ # @param climate_zone [String] the climate zone
24
+ # @param hvac_building_type [String] the building type for baseline HVAC system determination (90.1-2016 and onward)
25
+ # @param wwr_building_type [String] the building type for baseline WWR determination (90.1-2016 and onward)
26
+ # @param swh_building_type [String] the building type for baseline SWH determination (90.1-2016 and onward)
25
27
  # @param custom [String] the custom logic that will be applied during baseline creation. Valid choices are 'Xcel Energy CO EDA' or '90.1-2007 with addenda dn'.
26
28
  # If nothing is specified, no custom logic will be applied; the process will follow the template logic explicitly.
27
29
  # @param sizing_run_dir [String] the directory where the sizing runs will be performed
30
+ # @param run_all_orients [Boolean] indicate weather a baseline model should be created for all 4 orientations: same as user model, +90 deg, +180 deg, +270 deg
28
31
  # @param debug [Boolean] If true, will report out more detailed debugging output
29
32
  # @return [Bool] returns true if successful, false if not
33
+
34
+ # Method used for 90.1-2016 and onward
35
+ def model_create_prm_stable_baseline_building(model, building_type, climate_zone, hvac_building_type, wwr_building_type, swh_building_type, custom = nil, sizing_run_dir = Dir.pwd, run_all_orients = true, unmet_load_hours_check = true, debug = false)
36
+ model_create_prm_any_baseline_building(model, building_type, climate_zone, hvac_building_type, wwr_building_type, swh_building_type, true, custom, sizing_run_dir, run_all_orients, unmet_load_hours_check, debug)
37
+ end
38
+
39
+ # Creates a Performance Rating Method (aka Appendix G aka LEED) baseline building model
40
+ # Method used for 90.1-2013 and prior
41
+ # @param user_model [OpenStudio::model::Model] User specified OpenStudio model
42
+ # @param building_type [String] the building type
43
+ # @param climate_zone [String] the climate zone
44
+ # @param custom [String] the custom logic that will be applied during baseline creation. Valid choices are 'Xcel Energy CO EDA' or '90.1-2007 with addenda dn'.
45
+ # If nothing is specified, no custom logic will be applied; the process will follow the template logic explicitly.
46
+ # @param sizing_run_dir [String] the directory where the sizing runs will be performed
47
+ # @param debug [Boolean] If true, will report out more detailed debugging output
30
48
  def model_create_prm_baseline_building(model, building_type, climate_zone, custom = nil, sizing_run_dir = Dir.pwd, debug = false)
31
- model.getBuilding.setName("#{template}-#{building_type}-#{climate_zone} PRM baseline created: #{Time.new}")
49
+ model_create_prm_any_baseline_building(model, building_type, climate_zone, 'All others', 'All others', 'All others', false, custom, sizing_run_dir, false, false, debug)
50
+ end
32
51
 
33
- # Remove external shading devices
34
- OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', '*** Removing External Shading Devices ***')
35
- model_remove_external_shading_devices(model)
52
+ # Creates a Performance Rating Method (aka Appendix G aka LEED) baseline building model
53
+ # based on the inputs currently in the model.
54
+ #
55
+ # @note Per 90.1, the Performance Rating Method "does NOT offer an alternative compliance path for minimum standard compliance."
56
+ # This means you can't use this method for code compliance to get a permit.
57
+ # @param user_model [OpenStudio::model::Model] User specified OpenStudio model
58
+ # @param building_type [String] the building type
59
+ # @param climate_zone [String] the climate zone
60
+ # @param hvac_building_type [String] the building type for baseline HVAC system determination (90.1-2016 and onward)
61
+ # @param wwr_building_type [String] the building type for baseline WWR determination (90.1-2016 and onward)
62
+ # @param swh_building_type [String] the building type for baseline SWH determination (90.1-2016 and onward)
63
+ # @param model_deep_copy [Boolean] indicate if the baseline model is created based on a deep copy of the user specified model
64
+ # @param custom [String] the custom logic that will be applied during baseline creation. Valid choices are 'Xcel Energy CO EDA' or '90.1-2007 with addenda dn'.
65
+ # If nothing is specified, no custom logic will be applied; the process will follow the template logic explicitly.
66
+ # @param sizing_run_dir [String] the directory where the sizing runs will be performed
67
+ # @param run_all_orients [Boolean] indicate weather a baseline model should be created for all 4 orientations: same as user model, +90 deg, +180 deg, +270 deg
68
+ # @param debug [Boolean] If true, will report out more detailed debugging output
69
+ # @return [Bool] returns true if successful, false if not
70
+ def model_create_prm_any_baseline_building(user_model, building_type, climate_zone, hvac_building_type = 'All others', wwr_building_type = 'All others', swh_building_type = 'All others', model_deep_copy = false, custom = nil, sizing_run_dir = Dir.pwd, run_all_orients = false, unmet_load_hours_check = true, debug = false)
71
+ # Check proposed model unmet load hours
72
+ if unmet_load_hours_check
73
+ # Run proposed model; need annual simulation to get unmet load hours
74
+ if model_run_simulation_and_log_errors(user_model, run_dir = "#{sizing_run_dir}/PROP")
75
+ umlh = model_get_unmet_load_hours(user_model)
76
+ if umlh > 300
77
+ OpenStudio.logFree(OpenStudio::Error, 'prm.log', "Proposed model unmet load hours exceed 300. Baseline model(s) won't be created.")
78
+ raise "Proposed model unmet load hours exceed 300. Baseline model(s) won't be created."
79
+ end
80
+ end
81
+ end
36
82
 
37
- # Reduce the WWR and SRR, if necessary
38
- OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', '*** Adjusting Window and Skylight Ratios ***')
39
- model_apply_prm_baseline_window_to_wall_ratio(model, climate_zone)
40
- model_apply_prm_baseline_skylight_to_roof_ratio(model)
83
+ # User data process
84
+ # bldg_type_hvac_zone_hash could be an empty hash if all zones in the models are unconditioned
85
+ bldg_type_hvac_zone_hash = {}
86
+ handle_user_input_data(user_model, climate_zone, hvac_building_type, wwr_building_type, swh_building_type, bldg_type_hvac_zone_hash)
87
+ # Define different orientation from original orientation
88
+ # for each individual baseline models
89
+ # Need to run proposed model sizing simulation if no sql data is available
90
+ degs_from_org = run_all_orientations(run_all_orients, user_model) ? [0, 90, 180, 270] : [0]
91
+
92
+ # Create baseline model for each orientation
93
+ degs_from_org.each do |degs|
94
+ # New baseline model:
95
+ # Starting point is the original proposed model
96
+ # Create a deep copy of the user model if requested
97
+ model = model_deep_copy ? BTAP::FileIO.deep_copy(user_model) : user_model
98
+ model.getBuilding.setName("#{template}-#{building_type}-#{climate_zone} PRM baseline created: #{Time.new}")
99
+
100
+ # Rotate building if requested,
101
+ # Site shading isn't rotated
102
+ model_rotate(model, degs) unless degs == 0
103
+ # Perform a sizing run of the proposed model.
104
+ #
105
+ # Among others, one of the goal is to get individual
106
+ # space load to determine each space's conditioning
107
+ # type: conditioned, unconditioned, semiheated.
108
+ if model_create_prm_baseline_building_requires_proposed_model_sizing_run(model)
109
+ # Set up some special reports to be used for baseline system selection later
110
+ # Zone return air flows
111
+ node_list = []
112
+ var_name = 'System Node Standard Density Volume Flow Rate'
113
+ frequency = 'hourly'
114
+ model.getThermalZones.each do |zone|
115
+ port_list = zone.returnPortList
116
+ port_list_objects = port_list.modelObjects
117
+ port_list_objects.each do |node|
118
+ node_name = node.nameString
119
+ node_list << node_name
120
+ output = OpenStudio::Model::OutputVariable.new(var_name, model)
121
+ output.setKeyValue(node_name)
122
+ output.setReportingFrequency(frequency)
123
+ end
124
+ end
41
125
 
42
- # Assign building stories to spaces in the building where stories are not yet assigned.
43
- model_assign_spaces_to_stories(model)
126
+ # air loop relief air flows
127
+ var_name = 'System Node Standard Density Volume Flow Rate'
128
+ frequency = 'hourly'
129
+ model.getAirLoopHVACs.sort.each do |air_loop_hvac|
130
+ relief_node = air_loop_hvac.reliefAirNode.get
131
+ output = OpenStudio::Model::OutputVariable.new(var_name, model)
132
+ output.setKeyValue(relief_node.nameString)
133
+ output.setReportingFrequency(frequency)
134
+ end
44
135
 
45
- # Modify the internal loads in each space type, keeping user-defined schedules.
46
- OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', '*** Changing Lighting Loads ***')
47
- model.getSpaceTypes.sort.each do |space_type|
48
- set_people = false
49
- set_lights = true
50
- set_electric_equipment = false
51
- set_gas_equipment = false
52
- set_ventilation = false
53
- set_infiltration = false
54
- space_type_apply_internal_loads(space_type, set_people, set_lights, set_electric_equipment, set_gas_equipment, set_ventilation, set_infiltration)
55
- end
136
+ # Run the sizing run
137
+ if model_run_sizing_run(model, "#{sizing_run_dir}/SR_PROP#{degs}") == false
138
+ return false
139
+ end
56
140
 
57
- # If any of the lights are missing schedules, assign an always-off schedule to those lights.
58
- # This is assumed to be the user's intent in the proposed model.
59
- model.getLightss.sort.each do |lights|
60
- if lights.schedule.empty?
61
- lights.setSchedule(model.alwaysOffDiscreteSchedule)
141
+ # Set baseline model space conditioning category based on proposed model
142
+ model.getSpaces.each do |space|
143
+ # Get conditioning category at the space level
144
+ space_conditioning_category = space_conditioning_category(space)
145
+
146
+ # Set space conditioning category
147
+ space.additionalProperties.setFeature('space_conditioning_category', space_conditioning_category)
148
+ end
149
+
150
+ # The following should be done after a sizing run of the proposed model
151
+ # because the proposed model zone design air flow is needed
152
+ model_identify_return_air_type(model)
153
+ end
154
+ # Remove external shading devices
155
+ OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', '*** Removing External Shading Devices ***')
156
+ model_remove_external_shading_devices(model)
157
+
158
+ # Reduce the WWR and SRR, if necessary
159
+ OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', '*** Adjusting Window and Skylight Ratios ***')
160
+ success, wwr_info = model_apply_prm_baseline_window_to_wall_ratio(model, climate_zone, wwr_building_type: wwr_building_type)
161
+ model_apply_prm_baseline_skylight_to_roof_ratio(model)
162
+
163
+ # Assign building stories to spaces in the building where stories are not yet assigned.
164
+ model_assign_spaces_to_stories(model)
165
+
166
+ # Modify the internal loads in each space type, keeping user-defined schedules.
167
+ OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', '*** Changing Lighting Loads ***')
168
+ model.getSpaceTypes.sort.each do |space_type|
169
+ set_people = false
170
+ set_lights = true
171
+ set_electric_equipment = false
172
+ set_gas_equipment = false
173
+ set_ventilation = false
174
+ set_infiltration = false
175
+ # For PRM, it only applies lights for now.
176
+ space_type_apply_internal_loads(space_type, set_people, set_lights, set_electric_equipment, set_gas_equipment, set_ventilation, set_infiltration)
177
+ end
178
+ # Modify the lighting schedule to handle lighting occupancy sensors
179
+ # Modify the upper limit value of fractional schedule to avoid the fatal error caused by schedule value higher than 1
180
+ space_type_light_sch_change(model)
181
+
182
+ model_apply_baseline_exterior_lighting(model)
183
+
184
+ # Modify the elevator motor peak power
185
+ model_add_prm_elevators(model)
186
+
187
+ # Calculate infiltration as per 90.1 PRM rules
188
+ model_baseline_apply_infiltration_standard(model, climate_zone)
189
+
190
+ # If any of the lights are missing schedules, assign an always-off schedule to those lights.
191
+ # This is assumed to be the user's intent in the proposed model.
192
+ model.getLightss.sort.each do |lights|
193
+ if lights.schedule.empty?
194
+ lights.setSchedule(model.alwaysOffDiscreteSchedule)
195
+ end
62
196
  end
63
- end
64
197
 
65
- OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', '*** Adding Daylighting Controls ***')
198
+ OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', '*** Adding Daylighting Controls ***')
66
199
 
67
- # Run a sizing run to calculate VLT for layer-by-layer windows.
68
- if model_create_prm_baseline_building_requires_vlt_sizing_run(model)
69
- if model_run_sizing_run(model, "#{sizing_run_dir}/SRVLT") == false
70
- return false
200
+ # Run a sizing run to calculate VLT for layer-by-layer windows.
201
+ if model_create_prm_baseline_building_requires_vlt_sizing_run(model)
202
+ if model_run_sizing_run(model, "#{sizing_run_dir}/SRVLT") == false
203
+ return false
204
+ end
71
205
  end
72
- end
73
206
 
74
- # Add daylighting controls to each space
75
- model.getSpaces.sort.each do |space|
76
- added = space_add_daylighting_controls(space, false, false)
77
- end
207
+ # Add or remove daylighting controls to each space
208
+ # Add daylighting controls for 90.1-2013 and prior
209
+ # Remove daylighting control for 90.1-PRM-2019 and onward
210
+ model.getSpaces.sort.each do |space|
211
+ space_set_baseline_daylighting_controls(space, false, false)
212
+ end
78
213
 
79
- OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', '*** Applying Baseline Constructions ***')
214
+ OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', '*** Applying Baseline Constructions ***')
80
215
 
81
- # Modify some of the construction types as necessary
82
- model_apply_prm_construction_types(model)
216
+ # Modify some of the construction types as necessary
217
+ model_apply_prm_construction_types(model)
83
218
 
84
- # Set the construction properties of all the surfaces in the model
85
- model_apply_standard_constructions(model, climate_zone)
219
+ # Get the groups of zones that define the baseline HVAC systems for later use.
220
+ # This must be done before removing the HVAC systems because it requires knowledge of proposed HVAC fuels.
221
+ OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', '*** Grouping Zones by Fuel Type and Occupancy Type ***')
222
+ zone_fan_scheds = nil
86
223
 
87
- # Get the groups of zones that define the baseline HVAC systems for later use.
88
- # This must be done before removing the HVAC systems because it requires knowledge of proposed HVAC fuels.
89
- OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', '*** Grouping Zones by Fuel Type and Occupancy Type ***')
90
- sys_groups = model_prm_baseline_system_groups(model, custom)
224
+ sys_groups = model_prm_baseline_system_groups(model, custom, bldg_type_hvac_zone_hash)
91
225
 
92
- # Remove all HVAC from model, excluding service water heating
93
- model_remove_prm_hvac(model)
226
+ # Also get hash of zoneName:boolean to record which zones have district heating, if any
227
+ district_heat_zones = model_get_district_heating_zones(model)
94
228
 
95
- # Remove all EMS objects from the model
96
- model_remove_prm_ems_objects(model)
229
+ # Store occupancy and fan operation schedules for each zone before deleting HVAC objects
230
+ zone_fan_scheds = get_fan_schedule_for_each_zone(model)
97
231
 
98
- # Modify the service water heating loops per the baseline rules
99
- OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', '*** Cleaning up Service Water Heating Loops ***')
100
- model_apply_baseline_swh_loops(model, building_type)
232
+ # Set the construction properties of all the surfaces in the model
233
+ model_apply_constructions(model, climate_zone, wwr_building_type, wwr_info)
101
234
 
102
- # Determine the baseline HVAC system type for each of the groups of zones and add that system type.
103
- OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', '*** Adding Baseline HVAC Systems ***')
104
- sys_groups.each do |sys_group|
105
- # Determine the primary baseline system type
106
- system_type = model_prm_baseline_system_type(model,
107
- climate_zone,
108
- sys_group['occ'],
109
- sys_group['fuel'],
110
- sys_group['area_ft2'],
111
- sys_group['stories'],
112
- custom)
113
-
114
- sys_group['zones'].sort.each_slice(5) do |zone_list|
115
- zone_names = []
116
- zone_list.each do |zone|
117
- zone_names << zone.name.get.to_s
235
+ # Update ground temperature profile (for F/C-factor construction objects)
236
+ model_update_ground_temperature_profile(model, climate_zone)
237
+
238
+ # Identify non-mechanically cooled systems if necessary
239
+ model_identify_non_mechanically_cooled_systems(model)
240
+
241
+ # Get supply, return, relief fan power for each air loop
242
+ if model_get_fan_power_breakdown
243
+ model.getAirLoopHVACs.sort.each do |air_loop|
244
+ supply_fan_w = air_loop_hvac_get_supply_fan_power(air_loop)
245
+ return_fan_w = air_loop_hvac_get_return_fan_power(air_loop)
246
+ relief_fan_w = air_loop_hvac_get_relief_fan_power(air_loop)
247
+
248
+ # Save fan power at the zone to determining
249
+ # baseline fan power
250
+ air_loop.thermalZones.sort.each do |zone|
251
+ zone.additionalProperties.setFeature('supply_fan_w', supply_fan_w.to_f)
252
+ zone.additionalProperties.setFeature('return_fan_w', return_fan_w.to_f)
253
+ zone.additionalProperties.setFeature('relief_fan_w', relief_fan_w.to_f)
254
+ end
118
255
  end
119
- OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "--- #{zone_names.join(', ')}")
120
256
  end
121
257
 
122
- # Add the system type for these zones
123
- model_add_prm_baseline_system(model,
124
- system_type[0],
125
- system_type[1],
126
- system_type[2],
127
- system_type[3],
128
- sys_group['zones'])
129
- end
258
+ # Compute and marke DCV related information before deleting proposed model HVAC systems
259
+ model_mark_zone_dcv_existence(model)
260
+ model_add_dcv_user_exception_properties(model)
261
+ model_add_dcv_requirement_properties(model)
262
+ model_add_apxg_dcv_properties(model)
263
+ model_raise_user_model_dcv_errors(model)
130
264
 
131
- # Set the zone sizing SAT for each zone in the model
132
- model.getThermalZones.each do |zone|
133
- thermal_zone_apply_prm_baseline_supply_temperatures(zone)
134
- end
265
+ # Remove all HVAC from model, excluding service water heating
266
+ model_remove_prm_hvac(model)
135
267
 
136
- # Set the system sizing properties based on the zone sizing information
137
- model.getAirLoopHVACs.each do |air_loop|
138
- air_loop_hvac_apply_prm_sizing_temperatures(air_loop)
139
- end
268
+ # Remove all EMS objects from the model
269
+ model_remove_prm_ems_objects(model)
140
270
 
141
- OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', '*** Applying Baseline HVAC System Controls ***')
271
+ # Modify the service water heating loops per the baseline rules
272
+ OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', '*** Cleaning up Service Water Heating Loops ***')
273
+ model_apply_baseline_swh_loops(model, building_type)
142
274
 
143
- # SAT reset, economizers
144
- model.getAirLoopHVACs.sort.each do |air_loop|
145
- air_loop_hvac_apply_prm_baseline_controls(air_loop, climate_zone)
146
- end
275
+ # Determine the baseline HVAC system type for each of the groups of zones and add that system type.
276
+ OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', '*** Adding Baseline HVAC Systems ***')
277
+ air_loop_name_array = []
278
+ sys_groups.each do |sys_group|
279
+ # Determine the primary baseline system type
280
+ system_type = model_prm_baseline_system_type(model, climate_zone, sys_group, custom, hvac_building_type, district_heat_zones)
147
281
 
148
- # Apply the minimum damper positions, assuming no DDC control of VAV terminals
149
- model.getAirLoopHVACs.sort.each do |air_loop|
150
- air_loop_hvac_apply_minimum_vav_damper_positions(air_loop, false)
151
- end
282
+ sys_group['zones'].sort.each_slice(5) do |zone_list|
283
+ zone_names = []
284
+ zone_list.each do |zone|
285
+ zone_names << zone.name.get.to_s
286
+ end
287
+ OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "--- #{zone_names.join(', ')}")
288
+ end
152
289
 
153
- # Apply the baseline system temperatures
154
- model.getPlantLoops.sort.each do |plant_loop|
155
- # Skip the SWH loops
156
- next if plant_loop_swh_loop?(plant_loop)
290
+ # Add system type reference to zone
291
+ sys_group['zones'].sort.each do |zone|
292
+ zone.additionalProperties.setFeature('baseline_system_type', system_type[0])
293
+ end
157
294
 
158
- plant_loop_apply_prm_baseline_temperatures(plant_loop)
159
- end
295
+ # Add the system type for these zones
296
+ model_add_prm_baseline_system(model,
297
+ system_type[0],
298
+ system_type[1],
299
+ system_type[2],
300
+ system_type[3],
301
+ sys_group['zones'],
302
+ zone_fan_scheds)
303
+
304
+ model.getAirLoopHVACs.each do |air_loop|
305
+ air_loop_name = air_loop.name.get
306
+ unless air_loop_name_array.include?(air_loop_name)
307
+ air_loop.additionalProperties.setFeature('zone_group_type', sys_group['zone_group_type'] || 'None')
308
+ air_loop.additionalProperties.setFeature('sys_group_occ', sys_group['occ'] || 'None')
309
+ air_loop_name_array << air_loop_name
310
+ end
160
311
 
161
- # Set the heating and cooling sizing parameters
162
- model_apply_prm_sizing_parameters(model)
312
+ # Determine return air type
313
+ plenum, return_air_type = model_determine_baseline_return_air_type(model, system_type[0], air_loop.thermalZones)
314
+ air_loop.thermalZones.sort.each do |zone|
315
+ # Set up return air plenum
316
+ zone.setReturnPlenum(model.getThermalZoneByName(plenum).get) if return_air_type == 'return_plenum'
317
+ end
318
+ end
319
+ end
163
320
 
164
- # Run sizing run with the HVAC equipment
165
- if model_run_sizing_run(model, "#{sizing_run_dir}/SR1") == false
166
- return false
167
- end
321
+ # Add system type reference to all air loops
322
+ model.getAirLoopHVACs.sort.each do |air_loop|
323
+ if air_loop.thermalZones[0].additionalProperties.hasFeature('baseline_system_type')
324
+ sys_type = air_loop.thermalZones[0].additionalProperties.getFeatureAsString('baseline_system_type').get
325
+ air_loop.additionalProperties.setFeature('baseline_system_type', sys_type)
326
+ else
327
+ OpenStudio.logFree(OpenStudio::Info, 'openstudio.model.Model', "Thermal zone #{air_loop.thermalZones[0].name} is not associated to a particular system type.")
328
+ end
329
+ end
168
330
 
169
- # If there are any multizone systems, reset damper positions to achieve a 60% ventilation effectiveness minimum for the system
170
- # following the ventilation rate procedure from 62.1
171
- model_apply_multizone_vav_outdoor_air_sizing(model)
331
+ # Set the zone sizing SAT for each zone in the model
332
+ OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', '*** Applying Baseline HVAC System Sizing Settings ***')
333
+ model.getThermalZones.each do |zone|
334
+ thermal_zone_apply_prm_baseline_supply_temperatures(zone)
335
+ end
172
336
 
173
- # Set the baseline fan power for all airloops
174
- model.getAirLoopHVACs.sort.each do |air_loop|
175
- air_loop_hvac_apply_prm_baseline_fan_power(air_loop)
176
- end
337
+ # Set the system sizing properties based on the zone sizing information
338
+ model.getAirLoopHVACs.each do |air_loop|
339
+ air_loop_hvac_apply_prm_sizing_temperatures(air_loop)
340
+ end
177
341
 
178
- # Set the baseline fan power for all zone HVAC
179
- model.getZoneHVACComponents.sort.each do |zone_hvac|
180
- zone_hvac_component_apply_prm_baseline_fan_power(zone_hvac)
181
- end
342
+ # Set internal load sizing run schedules
343
+ model_apply_prm_baseline_sizing_schedule(model)
182
344
 
183
- # Set the baseline number of boilers and chillers
184
- model.getPlantLoops.sort.each do |plant_loop|
185
- # Skip the SWH loops
186
- next if plant_loop_swh_loop?(plant_loop)
345
+ # Set the heating and cooling sizing parameters
346
+ model_apply_prm_sizing_parameters(model)
187
347
 
188
- plant_loop_apply_prm_number_of_boilers(plant_loop)
189
- plant_loop_apply_prm_number_of_chillers(plant_loop)
190
- end
348
+ OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', '*** Applying Baseline HVAC System Controls ***')
191
349
 
192
- # Set the baseline number of cooling towers
193
- # Must be done after all chillers are added
194
- model.getPlantLoops.sort.each do |plant_loop|
195
- # Skip the SWH loops
196
- next if plant_loop_swh_loop?(plant_loop)
350
+ # SAT reset, economizers
351
+ model.getAirLoopHVACs.sort.each do |air_loop|
352
+ air_loop_hvac_apply_prm_baseline_controls(air_loop, climate_zone)
353
+ end
197
354
 
198
- plant_loop_apply_prm_number_of_cooling_towers(plant_loop)
199
- end
355
+ # Apply the baseline system water loop temperature reset control
356
+ model.getPlantLoops.sort.each do |plant_loop|
357
+ # Skip the SWH loops
358
+ next if plant_loop_swh_loop?(plant_loop)
200
359
 
201
- # Run sizing run with the new chillers, boilers, and cooling towers to determine capacities
202
- if model_run_sizing_run(model, "#{sizing_run_dir}/SR2") == false
203
- return false
204
- end
360
+ plant_loop_apply_prm_baseline_temperatures(plant_loop)
361
+ end
205
362
 
206
- # Set the pumping control strategy and power
207
- # Must be done after sizing components
208
- model.getPlantLoops.sort.each do |plant_loop|
209
- # Skip the SWH loops
210
- next if plant_loop_swh_loop?(plant_loop)
363
+ # Run sizing run with the HVAC equipment
364
+ if model_run_sizing_run(model, "#{sizing_run_dir}/SR1") == false
365
+ return false
366
+ end
211
367
 
212
- plant_loop_apply_prm_baseline_pump_power(plant_loop)
213
- plant_loop_apply_prm_baseline_pumping_type(plant_loop)
214
- end
368
+ # Apply the minimum damper positions, assuming no DDC control of VAV terminals
369
+ model.getAirLoopHVACs.sort.each do |air_loop|
370
+ air_loop_hvac_apply_minimum_vav_damper_positions(air_loop, false)
371
+ end
372
+
373
+ # If there are any multi-zone systems, reset damper positions to achieve a 60% ventilation effectiveness minimum for the system
374
+ # following the ventilation rate procedure from 62.1
375
+ model_apply_multizone_vav_outdoor_air_sizing(model)
376
+
377
+ # Set the baseline fan power for all air loops
378
+ model.getAirLoopHVACs.sort.each do |air_loop|
379
+ air_loop_hvac_apply_prm_baseline_fan_power(air_loop)
380
+ end
381
+
382
+ # Set the baseline fan power for all zone HVAC
383
+ model.getZoneHVACComponents.sort.each do |zone_hvac|
384
+ zone_hvac_component_apply_prm_baseline_fan_power(zone_hvac)
385
+ end
386
+
387
+ # Set the baseline number of boilers and chillers
388
+ model.getPlantLoops.sort.each do |plant_loop|
389
+ # Skip the SWH loops
390
+ next if plant_loop_swh_loop?(plant_loop)
391
+
392
+ plant_loop_apply_prm_number_of_boilers(plant_loop)
393
+ plant_loop_apply_prm_number_of_chillers(plant_loop, sizing_run_dir)
394
+ end
395
+
396
+ # Set the baseline number of cooling towers
397
+ # Must be done after all chillers are added
398
+ model.getPlantLoops.sort.each do |plant_loop|
399
+ # Skip the SWH loops
400
+ next if plant_loop_swh_loop?(plant_loop)
401
+
402
+ plant_loop_apply_prm_number_of_cooling_towers(plant_loop)
403
+ end
404
+
405
+ # Run sizing run with the new chillers, boilers, and cooling towers to determine capacities
406
+ if model_run_sizing_run(model, "#{sizing_run_dir}/SR2") == false
407
+ return false
408
+ end
409
+
410
+ # Set the pumping control strategy and power
411
+ # Must be done after sizing components
412
+ model.getPlantLoops.sort.each do |plant_loop|
413
+ # Skip the SWH loops
414
+ next if plant_loop_swh_loop?(plant_loop)
415
+
416
+ plant_loop_apply_prm_baseline_pump_power(plant_loop)
417
+ plant_loop_apply_prm_baseline_pumping_type(plant_loop)
418
+ end
419
+
420
+ OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', '*** Applying Prescriptive HVAC Controls and Equipment Efficiencies ***')
215
421
 
216
- OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', '*** Applying Prescriptive HVAC Controls and Equipment Efficiencies ***')
422
+ # Apply the HVAC efficiency standard
423
+ model_apply_hvac_efficiency_standard(model, climate_zone)
217
424
 
218
- # Apply the HVAC efficiency standard
219
- model_apply_hvac_efficiency_standard(model, climate_zone)
425
+ # Set baseline DCV system
426
+ model_set_baseline_demand_control_ventilation(model, climate_zone)
220
427
 
221
- # Fix EMS references.
222
- # Temporary workaround for OS issue #2598
223
- model_temp_fix_ems_references(model)
428
+ # Final sizing run and adjustements to values that need refinement
429
+ model_refine_size_dependent_values(model, sizing_run_dir)
224
430
 
225
- # Delete all the unused resource objects
226
- model_remove_unused_resource_objects(model)
431
+ # Fix EMS references.
432
+ # Temporary workaround for OS issue #2598
433
+ model_temp_fix_ems_references(model)
227
434
 
228
- # @todo turn off self shading
229
- # Set Solar Distribution to MinimalShadowing... problem is when you also have detached shading such as surrounding buildings etc
230
- # It won't be taken into account, while it should: only self shading from the building itself should be turned off but to my knowledge there isn't a way to do this in E+
435
+ # Delete all the unused resource objects
436
+ model_remove_unused_resource_objects(model)
231
437
 
232
- model_status = 'final'
233
- model.save(OpenStudio::Path.new("#{sizing_run_dir}/#{model_status}.osm"), true)
438
+ # Add reporting tolerances
439
+ model_add_reporting_tolerances(model)
234
440
 
235
- # Translate to IDF and save for debugging
236
- forward_translator = OpenStudio::EnergyPlus::ForwardTranslator.new
237
- idf = forward_translator.translateModel(model)
238
- idf_path = OpenStudio::Path.new("#{sizing_run_dir}/#{model_status}.idf")
239
- idf.save(idf_path, true)
441
+ # @todo: turn off self shading
442
+ # Set Solar Distribution to MinimalShadowing... problem is when you also have detached shading such as surrounding buildings etc
443
+ # It won't be taken into account, while it should: only self shading from the building itself should be turned off but to my knowledge there isn't a way to do this in E+
444
+
445
+ model_status = degs > 0 ? "final_#{degs}" : 'final'
446
+ model.save(OpenStudio::Path.new("#{sizing_run_dir}/#{model_status}.osm"), true)
447
+
448
+ # Translate to IDF and save for debugging
449
+ forward_translator = OpenStudio::EnergyPlus::ForwardTranslator.new
450
+ idf = forward_translator.translateModel(model)
451
+ idf_path = OpenStudio::Path.new("#{sizing_run_dir}/#{model_status}.idf")
452
+ idf.save(idf_path, true)
453
+
454
+ # Check unmet load hours
455
+ if unmet_load_hours_check
456
+ nb_adjustments = 0
457
+ loop do
458
+ model_run_simulation_and_log_errors(model, "#{sizing_run_dir}/final#{degs}") == false
459
+ # If UMLH are greater than the threshold allowed by Appendix G,
460
+ # increase zone air flow and load as per the recommendation in
461
+ # the PRM-RM; Note that the PRM-RM only suggest to increase
462
+ # air zone air flow, but the zone sizing factor in EnergyPlus
463
+ # increase both air flow and load.
464
+ if model_get_unmet_load_hours(model) > 300
465
+ # Limit the number of zone sizing factor adjustment to 8
466
+ unless nb_adjustments < 8
467
+ OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Model', "After 8 rounds of zone sizing factor adjustments the unmet load hours for the baseline model (#{degs} degree of rotation) still exceed 300 hours. Please open an issue on GitHub (https://github.com/NREL/openstudio-standards/issues) and share your user model with the developers.")
468
+ break
469
+ end
470
+ model.getThermalZones.each do |thermal_zone|
471
+ # Cooling adjustments
472
+ clg_umlh = thermal_zone_get_unmet_load_hours(thermal_zone, 'Cooling')
473
+ if clg_umlh > 50
474
+ # Get zone cooling sizing factor
475
+ if thermal_zone.sizingZone.zoneCoolingSizingFactor.is_initialized
476
+ sizing_factor = thermal_zone.sizingZone.zoneCoolingSizingFactor.get
477
+ else
478
+ sizing_factor = 1.0
479
+ end
480
+
481
+ # Make adjustment to zone cooling sizing factor
482
+ # Do not adjust factors greater or equal to 2
483
+ if sizing_factor < 2.0
484
+ if clg_umlh > 150
485
+ sizing_factor *= 1.1
486
+ elsif clg_umlh > 50
487
+ sizing_factor *= 1.05
488
+ end
489
+ thermal_zone.sizingZone.setZoneCoolingSizingFactor(sizing_factor)
490
+ end
491
+ end
492
+
493
+ # Heating adjustments
494
+ htg_umlh = thermal_zone_get_unmet_load_hours(thermal_zone, 'Heating')
495
+ if htg_umlh > 50
496
+ # Get zone cooling sizing factor
497
+ if thermal_zone.sizingZone.zoneHeatingSizingFactor.is_initialized
498
+ sizing_factor = thermal_zone.sizingZone.zoneHeatingSizingFactor.get
499
+ else
500
+ sizing_factor = 1.0
501
+ end
502
+
503
+ # Make adjustment to zone heating sizing factor
504
+ # Do not adjust factors greater or equal to 2
505
+ if sizing_factor < 2.0
506
+ if htg_umlh > 150
507
+ sizing_factor *= 1.1
508
+ elsif htg_umlh > 50
509
+ sizing_factor *= 1.05
510
+ end
511
+ thermal_zone.sizingZone.setZoneHeatingSizingFactor(sizing_factor)
512
+ end
513
+ end
514
+ end
515
+ else
516
+ break
517
+ end
518
+ end
519
+ end
520
+ end
521
+
522
+ if debug
523
+ generate_baseline_log(sizing_run_dir)
524
+ end
240
525
 
241
526
  return true
242
527
  end
@@ -251,6 +536,17 @@ class Standard
251
536
  return false # Not required for most templates
252
537
  end
253
538
 
539
+ # Determine if there is a need for a proposed model sizing run.
540
+ # A typical application of such sizing run is to determine space
541
+ # conditioning type.
542
+ #
543
+ # @param model [OpenStudio::Model::Model] OpenStudio model object
544
+ #
545
+ # @return [Boolean] Returns true if a sizing run is required
546
+ def model_create_prm_baseline_building_requires_proposed_model_sizing_run(model)
547
+ return false
548
+ end
549
+
254
550
  # Determine the residential and nonresidential floor areas based on the space type properties for each space.
255
551
  # For spaces with no space type, assume nonresidential.
256
552
  #
@@ -302,13 +598,23 @@ class Standard
302
598
  return num_stories
303
599
  end
304
600
 
601
+ # Add design day schedule objects for space loads,
602
+ # not used for 2013 and earlier
603
+ # @author Xuechen (Jerry) Lei, PNNL
604
+ # @param model [OpenStudio::model::Model] OpenStudio model object
605
+ #
606
+ def model_apply_prm_baseline_sizing_schedule(model)
607
+ return true
608
+ end
609
+
305
610
  # Categorize zones by occupancy type and fuel type, where the types depend on the standard.
306
611
  #
307
612
  # @param model [OpenStudio::Model::Model] OpenStudio model object
308
613
  # @param custom [String] custom fuel type
614
+ # @param applicable_zones [list of zone objects]
309
615
  # @return [Array<Hash>] an array of hashes, one for each zone,
310
616
  # with the keys 'zone', 'type' (occ type), 'fuel', and 'area'
311
- def model_zones_with_occ_and_fuel_type(model, custom)
617
+ def model_zones_with_occ_and_fuel_type(model, custom, applicable_zones = nil)
312
618
  zones = []
313
619
 
314
620
  model.getThermalZones.sort.each do |zone|
@@ -318,6 +624,14 @@ class Standard
318
624
  next
319
625
  end
320
626
 
627
+ if !applicable_zones.nil?
628
+ # This is only used for the stable baseline (2016 and later)
629
+ if !applicable_zones.include?(zone)
630
+ # This zone is not part of the current hvac_building_type
631
+ next
632
+ end
633
+ end
634
+
321
635
  # Skip unconditioned zones
322
636
  heated = thermal_zone_heated?(zone)
323
637
  cooled = thermal_zone_cooled?(zone)
@@ -341,7 +655,9 @@ class Standard
341
655
  zn_hash['bldg_type'] = thermal_zone_building_type(zone)
342
656
 
343
657
  # Fuel type
344
- zn_hash['fuel'] = thermal_zone_fossil_or_electric_type(zone, custom)
658
+ # for 2013 and prior, baseline fuel = proposed fuel
659
+ # for 2016 and later, use fuel to identify zones with district energy
660
+ zn_hash['fuel'] = thermal_zone_get_zone_fuels_for_occ_and_fuel_type(zone)
345
661
 
346
662
  zones << zn_hash
347
663
  end
@@ -355,7 +671,7 @@ class Standard
355
671
  # @param custom [String] custom fuel type
356
672
  # @return [Array<Hash>] an array of hashes of area information,
357
673
  # with keys area_ft2, type, fuel, and zones (an array of zones)
358
- def model_prm_baseline_system_groups(model, custom)
674
+ def model_prm_baseline_system_groups(model, custom, bldg_type_hvac_zone_hash = nil)
359
675
  # Define the minimum area for the
360
676
  # exception that allows a different
361
677
  # system type in part of the building.
@@ -611,6 +927,209 @@ class Standard
611
927
  return final_groups
612
928
  end
613
929
 
930
+ # Before deleting proposed HVAC components, determine for each zone if it has district heating
931
+ # @return [Hash] of boolean with zone name as key
932
+ def model_get_district_heating_zones(model)
933
+ has_district_hash = {}
934
+ model.getThermalZones.sort.each do |zone|
935
+ has_district_hash['building'] = false
936
+ htg_fuels = zone.heating_fuels
937
+ if htg_fuels.include?('DistrictHeating')
938
+ has_district_hash[zone.name] = true
939
+ has_district_hash['building'] = true
940
+ else
941
+ has_district_hash[zone.name] = false
942
+ end
943
+ end
944
+ return has_district_hash
945
+ end
946
+
947
+ # Get list of heat types across a list of zones
948
+ # @param zones [array of objects] array of zone objects
949
+ # @return [string] concatenated string showing different fuel types in a group of zones
950
+ def get_group_heat_types(model, zones)
951
+ heat_list = ''
952
+ has_district_heat = false
953
+ has_fuel_heat = false
954
+ has_elec_heat = false
955
+ zones.each do |zone|
956
+ if zone.heating_fuels.include?('DistrictHeating')
957
+ has_district_heat = true
958
+ end
959
+ other_heat = thermal_zone_fossil_or_electric_type(zone, '')
960
+ if other_heat == 'fossil'
961
+ has_fuel_heat = true
962
+ elsif other_heat == 'electric'
963
+ has_elec_heat = true
964
+ end
965
+ end
966
+ if has_district_heat
967
+ heat_list = 'districtheating'
968
+ end
969
+ if has_fuel_heat
970
+ heat_list += '_fuel'
971
+ end
972
+ if has_elec_heat
973
+ heat_list += '_electric'
974
+ end
975
+ return heat_list
976
+ end
977
+
978
+ # Store fan operation schedule for each zone before deleting HVAC objects
979
+ # @author Doug Maddox, PNNL
980
+ # @param model [object]
981
+ # @return [hash] of zoneName:fan_schedule_8760
982
+ def get_fan_schedule_for_each_zone(model)
983
+ fan_sch_names = {}
984
+
985
+ # Start with air loops
986
+ model.getAirLoopHVACs.sort.each do |air_loop_hvac|
987
+ fan_schedule_8760 = []
988
+ # Check for availability managers
989
+ # Assume only AvailabilityManagerScheduled will control fan schedule
990
+ # TODO: also check AvailabilityManagerScheduledOn
991
+ avail_mgrs = air_loop_hvac.availabilityManagers
992
+ # if avail_mgrs.is_initialized
993
+ if !avail_mgrs.nil?
994
+ avail_mgrs.each do |avail_mgr|
995
+ # avail_mgr = avail_mgr.get
996
+ # Check each type of AvailabilityManager
997
+ # If the current one matches, get the fan schedule
998
+ if avail_mgr.to_AvailabilityManagerScheduled.is_initialized
999
+ avail_mgr = avail_mgr.to_AvailabilityManagerScheduled.get
1000
+ fan_schedule = avail_mgr.schedule
1001
+ # fan_sch_translator = ScheduleTranslator.new(model, fan_schedule)
1002
+ # fan_sch_ruleset = fan_sch_translator.translate
1003
+ fan_schedule_8760 = get_8760_values_from_schedule(model, fan_schedule)
1004
+ end
1005
+ end
1006
+ end
1007
+ if fan_schedule_8760.empty?
1008
+ # If there are no availability managers, then use the schedule in the supply fan object
1009
+ # Note: testing showed that the fan object schedule is not used by OpenStudio
1010
+ # Instead, get the fan schedule from the air_loop_hvac object
1011
+ # fan_object = nil
1012
+ # fan_object = get_fan_object_for_airloop(model, air_loop_hvac)
1013
+ fan_object = 'nothing'
1014
+ if !fan_object.nil?
1015
+ # fan_schedule = fan_object.availabilitySchedule
1016
+ fan_schedule = air_loop_hvac.availabilitySchedule
1017
+ else
1018
+ OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "Failed to retreive fan object for AirLoop #{air_loop_hvac.name}")
1019
+ end
1020
+ fan_schedule_8760 = get_8760_values_from_schedule(model, fan_schedule)
1021
+ end
1022
+
1023
+ # Assign this schedule to each zone on this air loop
1024
+ air_loop_hvac.thermalZones.each do |zone|
1025
+ fan_sch_names[zone.name.get] = fan_schedule_8760
1026
+ end
1027
+ end
1028
+
1029
+ # Handle Zone equipment
1030
+ model.getThermalZones.sort.each do |zone|
1031
+ if !fan_sch_names.key?(zone.name.get)
1032
+ # This zone was not assigned a schedule via air loop
1033
+ # Check for zone equipment fans
1034
+ zone.equipment.each do |zone_equipment|
1035
+ next if zone_equipment.to_FanZoneExhaust.is_initialized
1036
+
1037
+ # get fan schedule
1038
+ fan_object = zone_hvac_get_fan_object(zone_equipment)
1039
+ if !fan_object.nil?
1040
+ fan_schedule = fan_object.availabilitySchedule
1041
+ fan_schedule_8760 = get_8760_values_from_schedule(model, fan_schedule)
1042
+ fan_sch_names[zone.name.get] = fan_schedule_8760
1043
+ break
1044
+ end
1045
+ end
1046
+ end
1047
+ end
1048
+
1049
+ return fan_sch_names
1050
+ end
1051
+
1052
+ # Get the supply fan object for an air loop
1053
+ # @author Doug Maddox, PNNL
1054
+ # @param model [object]
1055
+ # @param air_loop [object]
1056
+ # @return [object] supply fan of zone equipment component
1057
+ def get_fan_object_for_airloop(model, air_loop)
1058
+ if !air_loop.supplyFan.empty?
1059
+ fan_component = air_loop.supplyFan.get
1060
+ else
1061
+ # Check if system has unitary wrapper
1062
+ air_loop.supplyComponents.each do |component|
1063
+ # Get the object type, getting the internal coil
1064
+ # type if inside a unitary system.
1065
+ obj_type = component.iddObjectType.valueName.to_s
1066
+ fan_component = nil
1067
+ case obj_type
1068
+ when 'OS_AirLoopHVAC_UnitaryHeatCool_VAVChangeoverBypass'
1069
+ component = component.to_AirLoopHVACUnitaryHeatCoolVAVChangeoverBypass.get
1070
+ fan_component = component.supplyFan.get
1071
+ when 'OS_AirLoopHVAC_UnitaryHeatPump_AirToAir'
1072
+ component = component.to_AirLoopHVACUnitaryHeatPumpAirToAir.get
1073
+ fan_component = component.supplyFan.get
1074
+ when 'OS_AirLoopHVAC_UnitaryHeatPump_AirToAir_MultiSpeed'
1075
+ component = component.to_AirLoopHVACUnitaryHeatPumpAirToAirMultiSpeed.get
1076
+ fan_component = component.supplyFan.get
1077
+ when 'OS_AirLoopHVAC_UnitarySystem'
1078
+ component = component.to_AirLoopHVACUnitarySystem.get
1079
+ fan_component = component.supplyFan.get
1080
+ end
1081
+
1082
+ if !fan_component.nil?
1083
+ break
1084
+ end
1085
+ end
1086
+ end
1087
+
1088
+ # Get the fan object for this fan
1089
+ fan_obj_type = fan_component.iddObjectType.valueName.to_s
1090
+ case fan_obj_type
1091
+ when 'OS_Fan_OnOff'
1092
+ fan_obj = fan_component.to_FanOnOff.get
1093
+ when 'OS_Fan_ConstantVolume'
1094
+ fan_obj = fan_component.to_FanConstantVolume.get
1095
+ when 'OS_Fan_SystemModel'
1096
+ fan_obj = fan_component.to_FanSystemModel.get
1097
+ when 'OS_Fan_VariableVolume'
1098
+ fan_obj = fan_component.to_FanVariableVolume.get
1099
+ end
1100
+ return fan_obj
1101
+ end
1102
+
1103
+ # Convert from schedule object to array of hourly values for entire year
1104
+ # Array will include extra 24 values for leap year
1105
+ # Array will also include extra 24 values at end for holiday day type
1106
+ # @author: Doug Maddox, PNNL
1107
+ # @TODO: consider moving this to Standards.Schedule.rb
1108
+ # @param: model [Object]
1109
+ # @param: fan_schedule [Object]
1110
+ # @return: [Array<String>] annual hourly values from schedule
1111
+ def get_8760_values_from_schedule(model, fan_schedule)
1112
+ sch_object_type = fan_schedule.iddObjectType.valueName.to_s
1113
+ fan_8760 = nil
1114
+ case sch_object_type
1115
+ when 'OS_Schedule_Ruleset'
1116
+ fan_8760 = get_8760_values_from_schedule_ruleset(model, fan_schedule)
1117
+ when 'OS_Schedule_Constant'
1118
+ fan_schedule_constant = fan_schedule.to_ScheduleConstant.get
1119
+ fan_8760 = get_8760_values_from_schedule_constant(model, fan_schedule_constant)
1120
+ when 'OS_Schedule_Compact'
1121
+ # First convert to ScheduleRuleset
1122
+ sch_translator = ScheduleTranslator.new(model, fan_schedule)
1123
+ fan_schedule_ruleset = sch_translator.convert_schedule_compact_to_schedule_ruleset
1124
+ fan_8760 = get_8760_values_from_schedule_ruleset(model, fan_schedule_ruleset)
1125
+ when 'OS_Schedule_Year'
1126
+ # TODO: add function for ScheduleYear
1127
+ # fan_8760 = get_8760_values_from_schedule_year(model, fan_schedule)
1128
+ OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', 'Automated baseline measure does not support use of Schedule Year')
1129
+ end
1130
+ return fan_8760
1131
+ end
1132
+
614
1133
  # Determines the area of the building above which point
615
1134
  # the non-dominant area type gets it's own HVAC system type.
616
1135
  #
@@ -629,16 +1148,17 @@ class Standard
629
1148
  #
630
1149
  # @param model [OpenStudio::Model::Model] OpenStudio model object
631
1150
  # @param climate_zone [String] ASHRAE climate zone, e.g. 'ASHRAE 169-2013-4A'
632
- # @param area_type [String] Valid choices are residential, nonresidential, and heatedonly
633
- # @param fuel_type [String] Valid choices are electric, fossil, fossilandelectric,
634
- # purchasedheat, purchasedcooling, purchasedheatandcooling
635
- # @param area_ft2 [Double] Area in ft^2
636
- # @param num_stories [Integer] Number of stories
1151
+ # @param sys_group [hash] Hash defining a group of zones that have the same Appendix G system type
637
1152
  # @param custom [String] custom fuel type
638
1153
  # @return [String] The system type. Possibilities are PTHP, PTAC, PSZ_AC, PSZ_HP, PVAV_Reheat, PVAV_PFP_Boxes,
639
1154
  # VAV_Reheat, VAV_PFP_Boxes, Gas_Furnace, Electric_Furnace
640
1155
  # @todo add 90.1-2013 systems 11-13
641
- def model_prm_baseline_system_type(model, climate_zone, area_type, fuel_type, area_ft2, num_stories, custom)
1156
+ def model_prm_baseline_system_type(model, climate_zone, sys_group, custom, hvac_building_type = nil, district_heat_zones = nil)
1157
+ area_type = sys_group['occ']
1158
+ fuel_type = sys_group['fuel']
1159
+ area_ft2 = sys_group['area_ft2']
1160
+ num_stories = sys_group['stories']
1161
+
642
1162
  # [type, central_heating_fuel, zone_heating_fuel, cooling_fuel]
643
1163
  system_type = [nil, nil, nil, nil]
644
1164
 
@@ -752,6 +1272,72 @@ class Standard
752
1272
  return fuel_type # Don't change fuel type for most templates
753
1273
  end
754
1274
 
1275
+ # Determine whether heating type is fuel or electric
1276
+ # @param hvac_building_type [String] Key for lookup of baseline system type
1277
+ # @param climate_zone [String] full name of climate zone
1278
+ # @return [String] fuel or electric
1279
+ def find_prm_heat_type(hvac_building_type, climate_zone)
1280
+ climate_code = get_climate_zone_code(climate_zone)
1281
+ heat_type_props = model_find_object(standards_data['prm_heat_type'],
1282
+ 'template' => template,
1283
+ 'hvac_building_type' => hvac_building_type,
1284
+ 'climate_zone' => climate_code)
1285
+ if !heat_type_props
1286
+ # try again with wild card for climate
1287
+ heat_type_props = model_find_object(standards_data['prm_heat_type'],
1288
+ 'template' => template,
1289
+ 'hvac_building_type' => hvac_building_type,
1290
+ 'climate_zone' => 'any')
1291
+ end
1292
+ if !heat_type_props
1293
+ # try again with wild card for building type
1294
+ heat_type_props = model_find_object(standards_data['prm_heat_type'],
1295
+ 'template' => template,
1296
+ 'hvac_building_type' => 'all others',
1297
+ 'climate_zone' => climate_code)
1298
+ end
1299
+ if !heat_type_props
1300
+ OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Model', "Could not find baseline heat type for: #{template}-#{hvac_building_type}-#{climate_zone}.")
1301
+ else
1302
+ return heat_type_props['heat_type']
1303
+ end
1304
+ end
1305
+
1306
+ # Get ASHRAE ID code for climate zone
1307
+ # @param climate_zone [String] full name of climate zone
1308
+ # @return [String] ASHRAE ID code for climate zone
1309
+ def get_climate_zone_code(climate_zone)
1310
+ cz_codes = []
1311
+ cz_codes << '0A'
1312
+ cz_codes << '0B'
1313
+ cz_codes << '1A'
1314
+ cz_codes << '1B'
1315
+ cz_codes << '2A'
1316
+ cz_codes << '2B'
1317
+ cz_codes << '3A'
1318
+ cz_codes << '3B'
1319
+ cz_codes << '3C'
1320
+ cz_codes << '4A'
1321
+ cz_codes << '4B'
1322
+ cz_codes << '4C'
1323
+ cz_codes << '5A'
1324
+ cz_codes << '5B'
1325
+ cz_codes << '5C'
1326
+ cz_codes << '6A'
1327
+ cz_codes << '6B'
1328
+ cz_codes << '7A'
1329
+ cz_codes << '7B'
1330
+ cz_codes << '8A'
1331
+ cz_codes << '8B'
1332
+
1333
+ cz_codes.each do |cz|
1334
+ pattern = Regexp.new(cz, true)
1335
+ if pattern =~ climate_zone
1336
+ return cz.to_s
1337
+ end
1338
+ end
1339
+ end
1340
+
755
1341
  # Add the specified baseline system type to the specified zones based on the specified template.
756
1342
  # For some multi-zone system types, the standards require identifying zones whose loads or schedules
757
1343
  # are outliers and putting these systems on separate single-zone systems. This method does that.
@@ -766,7 +1352,7 @@ class Standard
766
1352
  # @param zones [Array<OpenStudio::Model::ThermalZone>] an array of zones
767
1353
  # @return [Bool] returns true if successful, false if not
768
1354
  # @todo Add 90.1-2013 systems 11-13
769
- def model_add_prm_baseline_system(model, system_type, main_heat_fuel, zone_heat_fuel, cool_fuel, zones)
1355
+ def model_add_prm_baseline_system(model, system_type, main_heat_fuel, zone_heat_fuel, cool_fuel, zones, zone_fan_scheds)
770
1356
  case system_type
771
1357
  when 'PTAC' # System 1
772
1358
  unless zones.empty?
@@ -884,9 +1470,10 @@ class Standard
884
1470
  # and add the suplemental system type to the secondary zones.
885
1471
  story_zone_lists.each do |story_group|
886
1472
  # Differentiate primary and secondary zones
887
- pri_sec_zone_lists = model_differentiate_primary_secondary_thermal_zones(model, story_group)
1473
+ pri_sec_zone_lists = model_differentiate_primary_secondary_thermal_zones(model, story_group, zone_fan_scheds)
888
1474
  pri_zones = pri_sec_zone_lists['primary']
889
1475
  sec_zones = pri_sec_zone_lists['secondary']
1476
+ zone_op_hrs = pri_sec_zone_lists['zone_op_hrs']
890
1477
 
891
1478
  # Add a PVAV with Reheat for the primary zones
892
1479
  stories = []
@@ -905,11 +1492,12 @@ class Standard
905
1492
  hot_water_loop: hot_water_loop,
906
1493
  chilled_water_loop: chilled_water_loop,
907
1494
  electric_reheat: electric_reheat)
1495
+ model_create_multizone_fan_schedule(model, zone_op_hrs, pri_zones, system_name)
908
1496
  end
909
1497
 
910
1498
  # Add a PSZ_AC for each secondary zone
911
1499
  unless sec_zones.empty?
912
- model_add_prm_baseline_system(model, 'PSZ_AC', main_heat_fuel, zone_heat_fuel, cool_fuel, sec_zones)
1500
+ model_add_prm_baseline_system(model, 'PSZ_AC', main_heat_fuel, zone_heat_fuel, cool_fuel, sec_zones, zone_fan_scheds)
913
1501
  end
914
1502
  end
915
1503
 
@@ -935,9 +1523,10 @@ class Standard
935
1523
  # and add the suplemental system type to the secondary zones.
936
1524
  story_zone_lists.each do |story_group|
937
1525
  # Differentiate primary and secondary zones
938
- pri_sec_zone_lists = model_differentiate_primary_secondary_thermal_zones(model, story_group)
1526
+ pri_sec_zone_lists = model_differentiate_primary_secondary_thermal_zones(model, story_group, zone_fan_scheds)
939
1527
  pri_zones = pri_sec_zone_lists['primary']
940
1528
  sec_zones = pri_sec_zone_lists['secondary']
1529
+ zone_op_hrs = pri_sec_zone_lists['zone_op_hrs']
941
1530
 
942
1531
  # Add an VAV for the primary zones
943
1532
  stories = []
@@ -955,10 +1544,11 @@ class Standard
955
1544
  fan_efficiency: 0.62,
956
1545
  fan_motor_efficiency: 0.9,
957
1546
  fan_pressure_rise: 4.0)
1547
+ model_create_multizone_fan_schedule(model, zone_op_hrs, pri_zones, system_name)
958
1548
  end
959
1549
  # Add a PSZ_HP for each secondary zone
960
1550
  unless sec_zones.empty?
961
- model_add_prm_baseline_system(model, 'PSZ_HP', main_heat_fuel, zone_heat_fuel, cool_fuel, sec_zones)
1551
+ model_add_prm_baseline_system(model, 'PSZ_HP', main_heat_fuel, zone_heat_fuel, cool_fuel, sec_zones, zone_fan_scheds)
962
1552
  end
963
1553
  end
964
1554
 
@@ -1013,9 +1603,10 @@ class Standard
1013
1603
  # next if zones.empty?
1014
1604
 
1015
1605
  # Differentiate primary and secondary zones
1016
- pri_sec_zone_lists = model_differentiate_primary_secondary_thermal_zones(model, story_group)
1606
+ pri_sec_zone_lists = model_differentiate_primary_secondary_thermal_zones(model, story_group, zone_fan_scheds)
1017
1607
  pri_zones = pri_sec_zone_lists['primary']
1018
1608
  sec_zones = pri_sec_zone_lists['secondary']
1609
+ zone_op_hrs = pri_sec_zone_lists['zone_op_hrs']
1019
1610
 
1020
1611
  # Add a VAV for the primary zones
1021
1612
  stories = []
@@ -1028,6 +1619,10 @@ class Standard
1028
1619
  # If and only if there are primary zones to attach to the loop
1029
1620
  # counter example: floor with only one elevator machine room that get classified as sec_zones
1030
1621
  unless pri_zones.empty?
1622
+ # if the loop configuration is primary / secondary loop
1623
+ if chilled_water_loop.additionalProperties.hasFeature('secondary_loop_name')
1624
+ chilled_water_loop = model.getPlantLoopByName(chilled_water_loop.additionalProperties.getFeatureAsString('secondary_loop_name').get).get
1625
+ end
1031
1626
  model_add_vav_reheat(model,
1032
1627
  pri_zones,
1033
1628
  system_name: system_name,
@@ -1037,11 +1632,12 @@ class Standard
1037
1632
  fan_efficiency: 0.62,
1038
1633
  fan_motor_efficiency: 0.9,
1039
1634
  fan_pressure_rise: 4.0)
1635
+ model_create_multizone_fan_schedule(model, zone_op_hrs, pri_zones, system_name)
1040
1636
  end
1041
1637
 
1042
1638
  # Add a PSZ_AC for each secondary zone
1043
1639
  unless sec_zones.empty?
1044
- model_add_prm_baseline_system(model, 'PSZ_AC', main_heat_fuel, zone_heat_fuel, cool_fuel, sec_zones)
1640
+ model_add_prm_baseline_system(model, 'PSZ_AC', main_heat_fuel, zone_heat_fuel, cool_fuel, sec_zones, zone_fan_scheds)
1045
1641
  end
1046
1642
  end
1047
1643
 
@@ -1080,9 +1676,10 @@ class Standard
1080
1676
  # and add the suplemental system type to the secondary zones.
1081
1677
  story_zone_lists.each do |story_group|
1082
1678
  # Differentiate primary and secondary zones
1083
- pri_sec_zone_lists = model_differentiate_primary_secondary_thermal_zones(model, story_group)
1679
+ pri_sec_zone_lists = model_differentiate_primary_secondary_thermal_zones(model, story_group, zone_fan_scheds)
1084
1680
  pri_zones = pri_sec_zone_lists['primary']
1085
1681
  sec_zones = pri_sec_zone_lists['secondary']
1682
+ zone_op_hrs = pri_sec_zone_lists['zone_op_hrs']
1086
1683
 
1087
1684
  # Add an VAV for the primary zones
1088
1685
  stories = []
@@ -1093,6 +1690,9 @@ class Standard
1093
1690
  system_name = "#{story_name} VAV_PFP_Boxes (Sys8)"
1094
1691
  # If and only if there are primary zones to attach to the loop
1095
1692
  unless pri_zones.empty?
1693
+ if chilled_water_loop.additionalProperties.hasFeature('secondary_loop_name')
1694
+ chilled_water_loop = model.getPlantLoopByName(chilled_water_loop.additionalProperties.getFeatureAsString('secondary_loop_name').get).get
1695
+ end
1096
1696
  model_add_vav_pfp_boxes(model,
1097
1697
  pri_zones,
1098
1698
  system_name: system_name,
@@ -1100,10 +1700,12 @@ class Standard
1100
1700
  fan_efficiency: 0.62,
1101
1701
  fan_motor_efficiency: 0.9,
1102
1702
  fan_pressure_rise: 4.0)
1703
+
1704
+ model_create_multizone_fan_schedule(model, zone_op_hrs, pri_zones, system_name)
1103
1705
  end
1104
1706
  # Add a PSZ_HP for each secondary zone
1105
1707
  unless sec_zones.empty?
1106
- model_add_prm_baseline_system(model, 'PSZ_HP', main_heat_fuel, zone_heat_fuel, cool_fuel, sec_zones)
1708
+ model_add_prm_baseline_system(model, 'PSZ_HP', main_heat_fuel, zone_heat_fuel, cool_fuel, sec_zones, zone_fan_scheds)
1107
1709
  end
1108
1710
  end
1109
1711
 
@@ -1137,6 +1739,83 @@ class Standard
1137
1739
  heating_type: main_heat_fuel)
1138
1740
  end
1139
1741
 
1742
+ when 'SZ_CV' # System 12 (gas or district heat) or System 13 (electric resistance heat)
1743
+ unless zones.empty?
1744
+ hot_water_loop = nil
1745
+ if zone_heat_fuel == 'DistrictHeating' || zone_heat_fuel == 'NaturalGas'
1746
+ heating_type = 'Water'
1747
+ hot_water_loop = if model.getPlantLoopByName('Hot Water Loop').is_initialized
1748
+ model.getPlantLoopByName('Hot Water Loop').get
1749
+ else
1750
+ model_add_hw_loop(model, main_heat_fuel)
1751
+ end
1752
+ else
1753
+ # If no hot water loop is defined, heat will default to electric resistance
1754
+ heating_type = 'Electric'
1755
+ end
1756
+ cooling_type = 'Water'
1757
+ chilled_water_loop = if model.getPlantLoopByName('Chilled Water Loop').is_initialized
1758
+ model.getPlantLoopByName('Chilled Water Loop').get
1759
+ else
1760
+ model_add_chw_loop(model,
1761
+ cooling_fuel: cool_fuel,
1762
+ chw_pumping_type: 'const_pri')
1763
+ end
1764
+
1765
+ model_add_four_pipe_fan_coil(model,
1766
+ zones,
1767
+ chilled_water_loop,
1768
+ hot_water_loop: hot_water_loop,
1769
+ ventilation: true,
1770
+ capacity_control_method: 'ConstantVolume')
1771
+ end
1772
+ when 'SZ_VAV' # System 11, chilled water, heating type varies by climate zone
1773
+ unless zones.empty?
1774
+ # htg type
1775
+ climate_zone = model_standards_climate_zone(model)
1776
+ case climate_zone
1777
+ when 'ASHRAE 169-2006-0A',
1778
+ 'ASHRAE 169-2006-0B',
1779
+ 'ASHRAE 169-2006-1A',
1780
+ 'ASHRAE 169-2006-1B',
1781
+ 'ASHRAE 169-2006-2A',
1782
+ 'ASHRAE 169-2006-2B',
1783
+ 'ASHRAE 169-2013-0A',
1784
+ 'ASHRAE 169-2013-0B',
1785
+ 'ASHRAE 169-2013-1A',
1786
+ 'ASHRAE 169-2013-1B',
1787
+ 'ASHRAE 169-2013-2A',
1788
+ 'ASHRAE 169-2013-2B'
1789
+ heating_type = 'Electric'
1790
+ hot_water_loop = nil
1791
+ else
1792
+ hot_water_loop = if model.getPlantLoopByName('Hot Water Loop').is_initialized
1793
+ model.getPlantLoopByName('Hot Water Loop').get
1794
+ else
1795
+ hot_water_loop = model_add_hw_loop(model, main_heat_fuel)
1796
+ end
1797
+ heating_type = 'Water'
1798
+ end
1799
+
1800
+ # clg type
1801
+ chilled_water_loop = if model.getPlantLoopByName('Chilled Water Loop').is_initialized
1802
+ model.getPlantLoopByName('Chilled Water Loop').get
1803
+ else
1804
+ chilled_water_loop = model_add_chw_loop(model, chw_pumping_type: 'const_pri')
1805
+ end
1806
+
1807
+ model_add_psz_vav(model,
1808
+ zones,
1809
+ heating_type: heating_type,
1810
+ cooling_type: 'WaterCooled',
1811
+ supplemental_heating_type: nil,
1812
+ hvac_op_sch: nil,
1813
+ fan_type: 'PSZ_VAV_System_Fan',
1814
+ oa_damper_sch: nil,
1815
+ hot_water_loop: hot_water_loop,
1816
+ chilled_water_loop: chilled_water_loop,
1817
+ minimum_volume_setpoint: 0.5)
1818
+ end
1140
1819
  else
1141
1820
  OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Model', "System type #{system_type} is not a valid choice, nothing will be added to the model.")
1142
1821
  return false
@@ -1179,13 +1858,7 @@ class Standard
1179
1858
  # the groups of zones and add that system type.
1180
1859
  sys_groups.each do |sys_group|
1181
1860
  # Determine the primary baseline system type
1182
- pri_system_type = model_prm_baseline_system_type(model,
1183
- climate_zone,
1184
- sys_group['occ'],
1185
- sys_group['fuel'],
1186
- sys_group['area_ft2'],
1187
- sys_group['stories'],
1188
- custom)[0]
1861
+ pri_system_type = model_prm_baseline_system_type(model, climate_zone, sys_group, custom)[0]
1189
1862
 
1190
1863
  # Record the zone-by-zone system type assignments
1191
1864
  case pri_system_type
@@ -1214,7 +1887,7 @@ class Standard
1214
1887
  # and add the suplemental system type to the secondary zones.
1215
1888
  story_zone_lists.each do |zones|
1216
1889
  # Differentiate primary and secondary zones
1217
- pri_sec_zone_lists = model_differentiate_primary_secondary_thermal_zones(model, zones)
1890
+ pri_sec_zone_lists = model_differentiate_primary_secondary_thermal_zones(model, story_group, zone_fan_scheds)
1218
1891
  # Record the primary zone system types
1219
1892
  pri_sec_zone_lists['primary'].each do |zone|
1220
1893
  zone_to_sys_type[zone] = pri_system_type
@@ -1318,8 +1991,8 @@ class Standard
1318
1991
  # @param model [OpenStudio::Model::Model] OpenStudio model object
1319
1992
  # @param zones [Array<OpenStudio::Model::ThermalZone>] an array of zones
1320
1993
  # @return [Hash] A hash of two arrays of ThermalZones,
1321
- # where the keys are 'primary' and 'secondary'
1322
- def model_differentiate_primary_secondary_thermal_zones(model, zones)
1994
+ # where the keys are 'primary' and 'secondary'
1995
+ def model_differentiate_primary_secondary_thermal_zones(model, zones, zone_fan_scheds = nil)
1323
1996
  OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', 'Determining which zones are served by the primary vs. secondary HVAC system.')
1324
1997
 
1325
1998
  # Determine the operational hours (proxy is annual
@@ -1431,7 +2104,58 @@ class Standard
1431
2104
  OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "Secondary system zones = #{sec_zone_names.join(', ')}.")
1432
2105
  end
1433
2106
 
1434
- return { 'primary' => pri_zones, 'secondary' => sec_zones }
2107
+ zone_op_hrs = []
2108
+ return { 'primary' => pri_zones, 'secondary' => sec_zones, 'zone_op_hrs' => zone_op_hrs }
2109
+ end
2110
+
2111
+ # For a multizone system, get straight average of hash values excluding the reference zone
2112
+ # @author Doug Maddox, PNNL
2113
+ # @param value_hash [Hash<String>] of zoneName:Value
2114
+ # @param ref_zone [String] name of reference zone
2115
+ def get_avg_of_other_zones(value_hash, ref_zone)
2116
+ num_others = value_hash.size - 1
2117
+ value_sum = 0
2118
+ value_hash.each do |key, val|
2119
+ value_sum += val unless key == ref_zone
2120
+ end
2121
+ if num_others == 0
2122
+ value_avg = value_hash[ref_zone]
2123
+ else
2124
+ value_avg = value_sum / num_others
2125
+ end
2126
+ return value_avg
2127
+ end
2128
+
2129
+ # For a multizone system, get area weighted average of hash values excluding the reference zone
2130
+ # @author Doug Maddox, PNNL
2131
+ # @param value_hash [Hash<String>] of zoneName:Value
2132
+ # @param area_hash [Hash<String>] of zoneName:Area
2133
+ # @param ref_zone [String] name of reference zone
2134
+ def get_wtd_avg_of_other_zones(value_hash, area_hash, ref_zone)
2135
+ num_others = value_hash.size - 1
2136
+ value_sum = 0
2137
+ area_sum = 0
2138
+ value_hash.each do |key, val|
2139
+ value_sum += val * area_hash[key] unless key == ref_zone
2140
+ area_sum += area_hash[key] unless key == ref_zone
2141
+ end
2142
+ if num_others == 0
2143
+ value_avg = value_hash[ref_zone]
2144
+ else
2145
+ value_avg = value_sum / area_sum
2146
+ end
2147
+ return value_avg
2148
+ end
2149
+
2150
+ # For a multizone system, create the fan schedule based on zone occupancy/fan schedules
2151
+ # @author Doug Maddox, PNNL
2152
+ # @param model
2153
+ # @param zone_fan_scheds [Hash] of hash of zoneName:8760FanSchedPerZone
2154
+ # @param pri_zones [Array<String>] names of zones served by the multizone system
2155
+ # @param system_name [String] name of air loop
2156
+ def model_create_multizone_fan_schedule(model, zone_op_hrs, pri_zones, system_name)
2157
+ # Not applicable if not stable baseline
2158
+ return
1435
2159
  end
1436
2160
 
1437
2161
  # Group an array of zones into multiple arrays, one for each story in the building.
@@ -1638,6 +2362,12 @@ class Standard
1638
2362
  return true
1639
2363
  end
1640
2364
 
2365
+ # For backward compatibility, infiltration standard not used for 2013 and earlier
2366
+ # @return [Bool] true if successful, false if not
2367
+ def model_baseline_apply_infiltration_standard(model, climate_zone)
2368
+ return true
2369
+ end
2370
+
1641
2371
  # Apply the air leakage requirements to the model, as described in PNNL section 5.2.1.6.
1642
2372
  # This method creates customized infiltration objects for each space
1643
2373
  # and removes the SpaceType-level infiltration objects.
@@ -1679,7 +2409,7 @@ class Standard
1679
2409
  # OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Model', "Cannot find data for schedule: #{schedule_name}, will not be created.")
1680
2410
  # return false
1681
2411
  # end
1682
- def model_find_objects(hash_of_objects, search_criteria, capacity = nil, date = nil, area = nil, num_floors = nil)
2412
+ def model_find_objects(hash_of_objects, search_criteria, capacity = nil, date = nil, area = nil, num_floors = nil, fan_motor_bhp = nil)
1683
2413
  matching_objects = []
1684
2414
  if hash_of_objects.is_a?(Hash) && hash_of_objects.key?('table')
1685
2415
  hash_of_objects = hash_of_objects['table']
@@ -1733,6 +2463,30 @@ class Standard
1733
2463
  end
1734
2464
  end
1735
2465
 
2466
+ # If fan_motor_bhp was specified, narrow down the matching objects
2467
+ unless fan_motor_bhp.nil?
2468
+ # Skip objects that don't have fields for minimum_capacity and maximum_capacity
2469
+ matching_objects = matching_objects.reject { |object| !object.key?('minimum_capacity') || !object.key?('maximum_capacity') }
2470
+
2471
+ # Skip objects that don't have values specified for minimum_capacity and maximum_capacity
2472
+ matching_objects = matching_objects.reject { |object| object['minimum_capacity'].nil? || object['maximum_capacity'].nil? }
2473
+
2474
+ # Skip objects whose the minimum capacity is below or maximum capacity above the specified fan_motor_bhp
2475
+ matching_capacity_objects = matching_objects.reject { |object| fan_motor_bhp.to_f <= object['minimum_capacity'].to_f || fan_motor_bhp.to_f > object['maximum_capacity'].to_f }
2476
+
2477
+ # Filter based on motor type
2478
+ matching_capacity_objects = matching_capacity_objects.select { |object| object['type'].downcase == search_criteria['type'].downcase } if search_criteria.keys.include?('type')
2479
+
2480
+ # If no object was found, round the fan_motor_bhp down in case the number fell between the limits in the json file.
2481
+ if matching_capacity_objects.size.zero?
2482
+ fan_motor_bhp *= 0.99
2483
+ # Skip objects whose minimum capacity is below or maximum capacity above the specified fan_motor_bhp
2484
+ matching_objects = matching_objects.reject { |object| fan_motor_bhp.to_f <= object['minimum_capacity'].to_f || fan_motor_bhp.to_f > object['maximum_capacity'].to_f }
2485
+ else
2486
+ matching_objects = matching_capacity_objects
2487
+ end
2488
+ end
2489
+
1736
2490
  # If date was specified, narrow down the matching objects
1737
2491
  unless date.nil?
1738
2492
  # Skip objects that don't have fields for start_date and end_date
@@ -1798,8 +2552,8 @@ class Standard
1798
2552
  # 'type' => 'Enclosed',
1799
2553
  # }
1800
2554
  # motor_properties = self.model.find_object(motors, search_criteria, capacity: 2.5)
1801
- def model_find_object(hash_of_objects, search_criteria, capacity = nil, date = nil, area = nil, num_floors = nil)
1802
- matching_objects = model_find_objects(hash_of_objects, search_criteria, capacity, date, area, num_floors)
2555
+ def model_find_object(hash_of_objects, search_criteria, capacity = nil, date = nil, area = nil, num_floors = nil, fan_motor_bhp = nil)
2556
+ matching_objects = model_find_objects(hash_of_objects, search_criteria, capacity, date, area, num_floors, fan_motor_bhp)
1803
2557
 
1804
2558
  # Check the number of matching objects found
1805
2559
  if matching_objects.size.zero?
@@ -2344,7 +3098,7 @@ class Standard
2344
3098
  # @param construction_props [Hash] hash of construction properties
2345
3099
  # @return [OpenStudio::Model::Construction] construction object
2346
3100
  # @todo make return an OptionalConstruction
2347
- def model_add_construction(model, construction_name, construction_props = nil)
3101
+ def model_add_construction(model, construction_name, construction_props = nil, surface = nil)
2348
3102
  # First check model and return construction if it already exists
2349
3103
  model.getConstructions.sort.each do |construction|
2350
3104
  if construction.name.get.to_s == construction_name
@@ -2356,14 +3110,34 @@ class Standard
2356
3110
  OpenStudio.logFree(OpenStudio::Debug, 'openstudio.standards.Model', "Adding construction: #{construction_name}")
2357
3111
 
2358
3112
  # Get the object data
2359
- data = model_find_object(standards_data['constructions'], 'name' => construction_name)
3113
+ if standards_data.keys.include?('prm_constructions')
3114
+ data = model_find_object(standards_data['prm_constructions'], 'name' => construction_name)
3115
+ else
3116
+ data = model_find_object(standards_data['constructions'], 'name' => construction_name)
3117
+ end
3118
+
2360
3119
  unless data
2361
3120
  OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "Cannot find data for construction: #{construction_name}, will not be created.")
2362
3121
  return OpenStudio::Model::OptionalConstruction.new
2363
3122
  end
2364
3123
 
2365
3124
  # Make a new construction and set the standards details
2366
- construction = OpenStudio::Model::Construction.new(model)
3125
+ if data['intended_surface_type'] == 'GroundContactFloor' && !surface.nil?
3126
+ construction = OpenStudio::Model::FFactorGroundFloorConstruction.new(model)
3127
+ elsif data['intended_surface_type'] == 'GroundContactWall' && !surface.nil?
3128
+ construction = OpenStudio::Model::CFactorUndergroundWallConstruction.new(model)
3129
+ else
3130
+ construction = OpenStudio::Model::Construction.new(model)
3131
+ # Add the material layers to the construction
3132
+ layers = OpenStudio::Model::MaterialVector.new
3133
+ data['materials'].each do |material_name|
3134
+ material = model_add_material(model, material_name)
3135
+ if material
3136
+ layers << material
3137
+ end
3138
+ end
3139
+ construction.setLayers(layers)
3140
+ end
2367
3141
  construction.setName(construction_name)
2368
3142
  standards_info = construction.standardsInformation
2369
3143
 
@@ -2377,16 +3151,6 @@ class Standard
2377
3151
 
2378
3152
  # @todo could put construction rendering color in the spreadsheet
2379
3153
 
2380
- # Add the material layers to the construction
2381
- layers = OpenStudio::Model::MaterialVector.new
2382
- data['materials'].each do |material_name|
2383
- material = model_add_material(model, material_name)
2384
- if material
2385
- layers << material
2386
- end
2387
- end
2388
- construction.setLayers(layers)
2389
-
2390
3154
  # Modify the R value of the insulation to hit the specified U-value, C-Factor, or F-Factor.
2391
3155
  # Doesn't currently operate on glazing constructions
2392
3156
  if construction_props
@@ -2403,11 +3167,19 @@ class Standard
2403
3167
  if target_u_value_ip
2404
3168
 
2405
3169
  # Handle Opaque and Fenestration Constructions differently
2406
- if construction.isFenestration && construction_simple_glazing?(construction)
2407
- # Set the U-Value and SHGC
2408
- construction_set_glazing_u_value(construction, target_u_value_ip.to_f, data['intended_surface_type'], u_includes_int_film, u_includes_ext_film)
2409
- construction_set_glazing_shgc(construction, target_shgc.to_f)
2410
- else # if !data['intended_surface_type'] == 'ExteriorWindow' && !data['intended_surface_type'] == 'Skylight'
3170
+ # if construction.isFenestration && construction_simple_glazing?(construction)
3171
+ if construction.isFenestration
3172
+ if construction_simple_glazing?(construction)
3173
+ # Set the U-Value and SHGC
3174
+ construction_set_glazing_u_value(construction, target_u_value_ip.to_f, data['intended_surface_type'], u_includes_int_film, u_includes_ext_film)
3175
+ construction_set_glazing_shgc(construction, target_shgc.to_f)
3176
+ else # if !data['intended_surface_type'] == 'ExteriorWindow' && !data['intended_surface_type'] == 'Skylight'
3177
+ # Set the U-Value
3178
+ construction_set_u_value(construction, target_u_value_ip.to_f, data['insulation_layer'], data['intended_surface_type'], u_includes_int_film, u_includes_ext_film)
3179
+ # else
3180
+ # OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "Not modifying U-value for #{data['intended_surface_type']} u_val #{target_u_value_ip} f_fac #{target_f_factor_ip} c_fac #{target_c_factor_ip}")
3181
+ end
3182
+ else
2411
3183
  # Set the U-Value
2412
3184
  construction_set_u_value(construction, target_u_value_ip.to_f, data['insulation_layer'], data['intended_surface_type'], u_includes_int_film, u_includes_ext_film)
2413
3185
  # else
@@ -2415,19 +3187,27 @@ class Standard
2415
3187
  end
2416
3188
 
2417
3189
  elsif target_f_factor_ip && data['intended_surface_type'] == 'GroundContactFloor'
2418
-
2419
- # Set the F-Factor (only applies to slabs on grade)
2420
- # @todo figure out what the prototype buildings did about ground heat transfer
2421
- # construction_set_slab_f_factor(construction, target_f_factor_ip.to_f, data['insulation_layer'])
2422
- construction_set_u_value(construction, 0.0, data['insulation_layer'], data['intended_surface_type'], u_includes_int_film, u_includes_ext_film)
2423
-
2424
- elsif target_c_factor_ip && data['intended_surface_type'] == 'GroundContactWall'
2425
-
2426
- # Set the C-Factor (only applies to underground walls)
2427
- # @todo figure out what the prototype buildings did about ground heat transfer
2428
- # construction_set_underground_wall_c_factor(construction, target_c_factor_ip.to_f, data['insulation_layer'])
2429
- construction_set_u_value(construction, 0.0, data['insulation_layer'], data['intended_surface_type'], u_includes_int_film, u_includes_ext_film)
2430
-
3190
+ # F-factor objects are unique to each surface, so a surface needs to be passed
3191
+ # If not surface is passed, use the older approach to model ground contact floors
3192
+ if surface.nil?
3193
+ # Set the F-Factor (only applies to slabs on grade)
3194
+ # @todo figure out what the prototype buildings did about ground heat transfer
3195
+ # construction_set_slab_f_factor(construction, target_f_factor_ip.to_f, data['insulation_layer'])
3196
+ construction_set_u_value(construction, 0.0, data['insulation_layer'], data['intended_surface_type'], u_includes_int_film, u_includes_ext_film)
3197
+ else
3198
+ construction_set_surface_slab_f_factor(construction, target_f_factor_ip, surface)
3199
+ end
3200
+ elsif target_c_factor_ip && (data['intended_surface_type'] == 'GroundContactWall' || data['intended_surface_type'] == 'GroundContactRoof')
3201
+ # C-factor objects are unique to each surface, so a surface needs to be passed
3202
+ # If not surface is passed, use the older approach to model ground contact walls
3203
+ if surface.nil?
3204
+ # Set the C-Factor (only applies to underground walls)
3205
+ # @todo figure out what the prototype buildings did about ground heat transfer
3206
+ # construction_set_underground_wall_c_factor(construction, target_c_factor_ip.to_f, data['insulation_layer'])
3207
+ construction_set_u_value(construction, 0.0, data['insulation_layer'], data['intended_surface_type'], u_includes_int_film, u_includes_ext_film)
3208
+ else
3209
+ construction_set_surface_underground_wall_c_factor(construction, target_c_factor_ip, surface)
3210
+ end
2431
3211
  end
2432
3212
 
2433
3213
  # If the construction is fenestration,
@@ -2518,23 +3298,32 @@ class Standard
2518
3298
  # @param intended_surface_type [String] intended surface type
2519
3299
  # @param standards_construction_type [String] standards construction type
2520
3300
  # @param building_category [String] building category
3301
+ # @param wwr_building_type [String] building type used to determine WWR for the PRM baseline model
3302
+ # @param wwr_info [Hash] @Todo - check what this is used for
3303
+ # @param surface [OpenStudio::Model::Surface] OpenStudio surface object, only used for surface specific construction, e.g F/C-factor constructions
2521
3304
  # @return [OpenStudio::Model::Construction] construction object
2522
- def model_find_and_add_construction(model, climate_zone_set, intended_surface_type, standards_construction_type, building_category)
3305
+ def model_find_and_add_construction(model, climate_zone_set, intended_surface_type, standards_construction_type, building_category, wwr_building_type: nil, wwr_info: {}, surface: nil)
2523
3306
  # Get the construction properties,
2524
3307
  # which specifies properties by construction category by climate zone set.
2525
3308
  # AKA the info in Tables 5.5-1-5.5-8
2526
3309
 
2527
- wwr = model_get_percent_of_surface_range(model, intended_surface_type)
2528
-
2529
3310
  search_criteria = { 'template' => template,
2530
3311
  'climate_zone_set' => climate_zone_set,
2531
3312
  'intended_surface_type' => intended_surface_type,
2532
3313
  'standards_construction_type' => standards_construction_type,
2533
3314
  'building_category' => building_category }
2534
3315
 
2535
- if !wwr['minimum_percent_of_surface'].nil? && !wwr['maximum_percent_of_surface'].nil?
2536
- search_criteria['minimum_percent_of_surface'] = wwr['minimum_percent_of_surface']
2537
- search_criteria['maximum_percent_of_surface'] = wwr['maximum_percent_of_surface']
3316
+ # Check if WWR criteria is needed for the construction search
3317
+ wwr_parameter = { 'intended_surface_type' => intended_surface_type }
3318
+ if wwr_building_type
3319
+ wwr_parameter['wwr_building_type'] = wwr_building_type
3320
+ wwr_parameter['wwr_info'] = wwr_info
3321
+ end
3322
+ wwr_range = model_get_percent_of_surface_range(model, wwr_parameter)
3323
+
3324
+ if !wwr_range['minimum_percent_of_surface'].nil? && !wwr_range['maximum_percent_of_surface'].nil?
3325
+ search_criteria['minimum_percent_of_surface'] = wwr_range['minimum_percent_of_surface']
3326
+ search_criteria['maximum_percent_of_surface'] = wwr_range['maximum_percent_of_surface']
2538
3327
  end
2539
3328
 
2540
3329
  # First search
@@ -2555,8 +3344,6 @@ class Standard
2555
3344
  almost_adiabatic = OpenStudio::Model::MasslessOpaqueMaterial.new(model, 'Smooth', 500)
2556
3345
  construction.insertLayer(0, almost_adiabatic)
2557
3346
  return construction
2558
- # else
2559
- # OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "Construction properties for: #{template}-#{climate_zone_set}-#{intended_surface_type}-#{standards_construction_type}-#{building_category} = #{props}.")
2560
3347
  end
2561
3348
 
2562
3349
  # Make sure that a construction is specified
@@ -2569,7 +3356,7 @@ class Standard
2569
3356
  end
2570
3357
 
2571
3358
  # Add the construction, modifying properties as necessary
2572
- construction = model_add_construction(model, props['construction'], props)
3359
+ construction = model_add_construction(model, props['construction'], props, surface)
2573
3360
 
2574
3361
  return construction
2575
3362
  end
@@ -2971,11 +3758,12 @@ class Standard
2971
3758
  # @param climate_zone [String] ASHRAE climate zone, e.g. 'ASHRAE 169-2013-4A'
2972
3759
  # @param building_type [String] the building type
2973
3760
  # @param run_type [String] design day is dd-only, otherwise annual run
3761
+ # @param lkp_template [String] The standards template, e.g.'90.1-2013'
2974
3762
  # @return [Hash] a hash of results for each fuel, where the keys are in the form 'End Use|Fuel Type',
2975
3763
  # e.g. Heating|Electricity, Exterior Equipment|Water. All end use/fuel type combos are present,
2976
3764
  # with values of 0.0 if none of this end use/fuel type combo was used by the simulation.
2977
3765
  # Returns nil if the legacy results couldn't be found.
2978
- def model_legacy_results_by_end_use_and_fuel_type(model, climate_zone, building_type, run_type)
3766
+ def model_legacy_results_by_end_use_and_fuel_type(model, climate_zone, building_type, run_type, lkp_template: nil)
2979
3767
  # Load the legacy idf results CSV file into a ruby hash
2980
3768
  top_dir = File.expand_path('../../..', File.dirname(__FILE__))
2981
3769
  standards_data_dir = "#{top_dir}/data/standards"
@@ -2999,10 +3787,14 @@ class Standard
2999
3787
  legacy_idf_csv = CSV.new(temp, headers: true, converters: :all)
3000
3788
  legacy_idf_results = legacy_idf_csv.to_a.map(&:to_hash)
3001
3789
 
3790
+ if lkp_template.nil?
3791
+ lkp_template = template
3792
+ end
3793
+
3002
3794
  # Get the results for this building
3003
3795
  search_criteria = {
3004
3796
  'Building Type' => building_type,
3005
- 'Template' => template,
3797
+ 'Template' => lkp_template,
3006
3798
  'Climate Zone' => climate_zone
3007
3799
  }
3008
3800
  energy_values = model_find_object(legacy_idf_results, search_criteria)
@@ -3019,9 +3811,10 @@ class Standard
3019
3811
  # @param model [OpenStudio::Model::Model] OpenStudio model object
3020
3812
  # @param climate_zone [String] ASHRAE climate zone, e.g. 'ASHRAE 169-2013-4A'
3021
3813
  # @param building_type [String] the building type
3814
+ # @param lkp_template [String] The standards template, e.g.'90.1-2013'
3022
3815
  # @return [Hash] Returns a hash with data presented in various bins.
3023
3816
  # Returns nil if no search results
3024
- def model_process_results_for_datapoint(model, climate_zone, building_type)
3817
+ def model_process_results_for_datapoint(model, climate_zone, building_type, lkp_template: nil)
3025
3818
  # Hash to store the legacy results by fuel and by end use
3026
3819
  legacy_results_hash = {}
3027
3820
  legacy_results_hash['total_legacy_energy_val'] = 0
@@ -3030,7 +3823,7 @@ class Standard
3030
3823
  legacy_results_hash['total_energy_by_end_use'] = {}
3031
3824
 
3032
3825
  # Get the legacy simulation results
3033
- legacy_values = model_legacy_results_by_end_use_and_fuel_type(model, climate_zone, building_type, 'annual')
3826
+ legacy_values = model_legacy_results_by_end_use_and_fuel_type(model, climate_zone, building_type, 'annual', lkp_template: lkp_template)
3034
3827
  if legacy_values.nil?
3035
3828
  OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Model', "Could not find legacy idf results for #{search_criteria}")
3036
3829
  return legacy_results_hash
@@ -3159,8 +3952,8 @@ class Standard
3159
3952
  #
3160
3953
  # @param model [OpenStudio::Model::Model] OpenStudio model object
3161
3954
  # @param remap_office [bool] re-map small office or leave it alone
3162
- # @return [hash] key for climate zone and building type, both values are strings
3163
- def model_get_building_climate_zone_and_building_type(model, remap_office = true)
3955
+ # @return [hash] key for climate zone, building type, and standards template. All values are strings.
3956
+ def model_get_building_properties(model, remap_office = true)
3164
3957
  # get climate zone from model
3165
3958
  climate_zone = model_standards_climate_zone(model)
3166
3959
 
@@ -3176,9 +3969,15 @@ class Standard
3176
3969
  building_type = model_remap_office(model, open_studio_area)
3177
3970
  end
3178
3971
 
3972
+ # get standards template
3973
+ if model.getBuilding.standardsTemplate.is_initialized
3974
+ standards_template = model.getBuilding.standardsTemplate.get
3975
+ end
3976
+
3179
3977
  results = {}
3180
3978
  results['climate_zone'] = climate_zone
3181
3979
  results['building_type'] = building_type
3980
+ results['standards_template'] = standards_template
3182
3981
 
3183
3982
  return results
3184
3983
  end
@@ -3210,12 +4009,13 @@ class Standard
3210
4009
  # @param model [OpenStudio::Model::Model] OpenStudio model object
3211
4010
  # @return [Double] EUI (MJ/m^2) for target template for given OSM. Returns nil if can't calculate EUI
3212
4011
  def model_find_target_eui(model)
3213
- building_data = model_get_building_climate_zone_and_building_type(model)
4012
+ building_data = model_get_building_properties(model)
3214
4013
  climate_zone = building_data['climate_zone']
3215
4014
  building_type = building_data['building_type']
4015
+ building_template = building_data['standards_template']
3216
4016
 
3217
4017
  # look up results
3218
- target_consumption = model_process_results_for_datapoint(model, climate_zone, building_type)
4018
+ target_consumption = model_process_results_for_datapoint(model, climate_zone, building_type, lkp_template: building_template)
3219
4019
 
3220
4020
  # lookup target floor area for prototype buildings
3221
4021
  target_floor_area = model_find_prototype_floor_area(model, building_type)
@@ -3243,12 +4043,13 @@ class Standard
3243
4043
  # @param model [OpenStudio::Model::Model] OpenStudio model object
3244
4044
  # @return [Hash] EUI (MJ/m^2) This will return a hash of end uses. key is end use, value is eui
3245
4045
  def model_find_target_eui_by_end_use(model)
3246
- building_data = model_get_building_climate_zone_and_building_type(model)
4046
+ building_data = model_get_building_properties(model)
3247
4047
  climate_zone = building_data['climate_zone']
3248
4048
  building_type = building_data['building_type']
4049
+ building_template = building_data['standards_template']
3249
4050
 
3250
4051
  # look up results
3251
- target_consumption = model_process_results_for_datapoint(model, climate_zone, building_type)
4052
+ target_consumption = model_process_results_for_datapoint(model, climate_zone, building_type, lkp_template: building_template)
3252
4053
 
3253
4054
  # lookup target floor area for prototype buildings
3254
4055
  target_floor_area = model_find_prototype_floor_area(model, building_type)
@@ -3520,7 +4321,7 @@ class Standard
3520
4321
  # @param model [OpenStudio::Model::Model] OpenStudio model object
3521
4322
  # @param climate_zone [String] ASHRAE climate zone, e.g. 'ASHRAE 169-2013-4A'
3522
4323
  # @return [Bool] returns true if successful, false if not
3523
- def model_apply_standard_constructions(model, climate_zone)
4324
+ def model_apply_standard_constructions(model, climate_zone, wwr_building_type: nil, wwr_info: {})
3524
4325
  types_to_modify = []
3525
4326
 
3526
4327
  # Possible boundary conditions are
@@ -3557,12 +4358,20 @@ class Standard
3557
4358
 
3558
4359
  # Find just those surfaces
3559
4360
  surfaces_to_modify = []
4361
+ surface_category = {}
3560
4362
  types_to_modify.each do |boundary_condition, surface_type|
3561
4363
  # Surfaces
3562
4364
  model.getSurfaces.sort.each do |surf|
3563
4365
  next unless surf.outsideBoundaryCondition == boundary_condition
3564
4366
  next unless surf.surfaceType == surface_type
3565
4367
 
4368
+ if boundary_condition == 'Outdoors'
4369
+ surface_category[surf] = 'ExteriorSurface'
4370
+ elsif boundary_condition == 'Ground'
4371
+ surface_category[surf] = 'GroundSurface'
4372
+ else
4373
+ surface_category[surf] = 'NA'
4374
+ end
3566
4375
  surfaces_to_modify << surf
3567
4376
  end
3568
4377
 
@@ -3571,6 +4380,7 @@ class Standard
3571
4380
  next unless surf.outsideBoundaryCondition == boundary_condition
3572
4381
  next unless surf.subSurfaceType == surface_type
3573
4382
 
4383
+ surface_category[surf] = 'ExteriorSubSurface'
3574
4384
  surfaces_to_modify << surf
3575
4385
  end
3576
4386
  end
@@ -3578,7 +4388,7 @@ class Standard
3578
4388
  # Modify these surfaces
3579
4389
  prev_created_consts = {}
3580
4390
  surfaces_to_modify.sort.each do |surf|
3581
- prev_created_consts = planar_surface_apply_standard_construction(surf, climate_zone, prev_created_consts)
4391
+ prev_created_consts = planar_surface_apply_standard_construction(surf, climate_zone, prev_created_consts, wwr_building_type, wwr_info, surface_category[surf])
3582
4392
  end
3583
4393
 
3584
4394
  # List the unique array of constructions
@@ -3603,7 +4413,7 @@ class Standard
3603
4413
  # @return [Hash] hash of construction properties
3604
4414
  def model_get_construction_properties(model, intended_surface_type, standards_construction_type, building_category, climate_zone = nil)
3605
4415
  # get climate_zone_set
3606
- climate_zone = model_get_building_climate_zone_and_building_type(model)['climate_zone'] if climate_zone.nil?
4416
+ climate_zone = model_get_building_properties(model)['climate_zone'] if climate_zone.nil?
3607
4417
  climate_zone_set = model_find_climate_zone_set(model, climate_zone)
3608
4418
 
3609
4419
  # populate search hash
@@ -3658,25 +4468,75 @@ class Standard
3658
4468
  # Currently just using existing window geometry, and shrinking as necessary if WWR is above limit.
3659
4469
  # @todo support semiheated spaces as a separate WWR category
3660
4470
  # @todo add window frame area to calculation of WWR
3661
- def model_apply_prm_baseline_window_to_wall_ratio(model, climate_zone)
3662
- # Loop through all spaces in the model, and
3663
- # per the PNNL PRM Reference Manual, find the areas
3664
- # of each space conditioning category (res, nonres, semi-heated)
3665
- # separately. Include space multipliers.
3666
- nr_wall_m2 = 0.001 # Avoids divide by zero errors later
3667
- nr_wind_m2 = 0
3668
- res_wall_m2 = 0.001
3669
- res_wind_m2 = 0
3670
- sh_wall_m2 = 0.001
3671
- sh_wind_m2 = 0
3672
- total_wall_m2 = 0.001
3673
- total_subsurface_m2 = 0.0
4471
+ def model_apply_prm_baseline_window_to_wall_ratio(model, climate_zone, wwr_building_type: nil)
4472
+ # Define a Hash that will contain wall and window area for all
4473
+ # building area types included in the model
4474
+ # bat = building area type
4475
+ bat_win_wall_info = {}
4476
+
4477
+ # Store the baseline wwr, only used for 90.1-PRM-2019,
4478
+ # it is necessary for looking up baseline fenestration
4479
+ # U-factor and SHGC requirements
4480
+ base_wwr = {}
4481
+
3674
4482
  # Store the space conditioning category for later use
3675
4483
  space_cats = {}
4484
+
3676
4485
  model.getSpaces.sort.each do |space|
4486
+ # Get standards space type and
4487
+ # catch spaces without space types
4488
+ #
4489
+ # Currently, priority is given to the wwr_building_type,
4490
+ # meaning that only one building area type is used. The
4491
+ # method can however handle models with multiple building
4492
+ # area type, if they are specified through each space's
4493
+ # space type standards building type.
4494
+ if space.hasAdditionalProperties && space.additionalProperties.hasFeature('building_type_for_wwr')
4495
+ std_spc_type = space.additionalProperties.getFeatureAsString('building_type_for_wwr').get
4496
+ else
4497
+ std_spc_type = 'no_space_type'
4498
+ if !wwr_building_type.nil?
4499
+ std_spc_type = wwr_building_type
4500
+ elsif space.spaceType.is_initialized
4501
+ std_spc_type = space.spaceType.get.standardsBuildingType.to_s
4502
+ end
4503
+ # insert space wwr type as additional properties for later search
4504
+ space.additionalProperties.setFeature('building_type_for_wwr', std_spc_type)
4505
+ end
4506
+
4507
+ # Initialize intermediate variables if space type hasn't
4508
+ # been encountered yet
4509
+ if !bat_win_wall_info.key?(std_spc_type)
4510
+ bat_win_wall_info[std_spc_type] = {}
4511
+ bat = bat_win_wall_info[std_spc_type]
4512
+
4513
+ # Loop through all spaces in the model, and
4514
+ # per the PNNL PRM Reference Manual, find the areas
4515
+ # of each space conditioning category (res, nonres, semi-heated)
4516
+ # separately. Include space multipliers.
4517
+ bat.store('nr_wall_m2', 0.001) # Avoids divide by zero errors later
4518
+ bat.store('nr_fene_only_wall_m2', 0.001)
4519
+ bat.store('nr_plenum_wall_m2', 0.001)
4520
+ bat.store('nr_wind_m2', 0)
4521
+ bat.store('res_wall_m2', 0.001)
4522
+ bat.store('res_fene_only_wall_m2', 0.001)
4523
+ bat.store('res_wind_m2', 0)
4524
+ bat.store('res_plenum_wall_m2', 0.001)
4525
+ bat.store('sh_wall_m2', 0.001)
4526
+ bat.store('sh_fene_only_wall_m2', 0.001)
4527
+ bat.store('sh_wind_m2', 0)
4528
+ bat.store('sh_plenum_wall_m2', 0.001)
4529
+ bat.store('total_wall_m2', 0.001)
4530
+ bat.store('total_plenum_m2', 0.001)
4531
+ else
4532
+ bat = bat_win_wall_info[std_spc_type]
4533
+ end
4534
+
3677
4535
  # Loop through all surfaces in this space
3678
4536
  wall_area_m2 = 0
3679
4537
  wind_area_m2 = 0
4538
+ # save wall area from walls that have fenestrations (subsurfaces)
4539
+ wall_only_area_m2 = 0
3680
4540
  space.surfaces.sort.each do |surface|
3681
4541
  # Skip non-outdoor surfaces
3682
4542
  next unless surface.outsideBoundaryCondition == 'Outdoors'
@@ -3685,142 +4545,202 @@ class Standard
3685
4545
 
3686
4546
  # This wall's gross area (including window area)
3687
4547
  wall_area_m2 += surface.grossArea * space.multiplier
3688
- # Subsurfaces in this surface
3689
- surface.subSurfaces.sort.each do |ss|
3690
- next unless ss.subSurfaceType == 'FixedWindow' || ss.subSurfaceType == 'OperableWindow'
4548
+ unless surface.subSurfaces.empty?
4549
+ # Subsurfaces in this surface
4550
+ surface.subSurfaces.sort.each do |ss|
4551
+ next unless ss.subSurfaceType == 'FixedWindow' || ss.subSurfaceType == 'OperableWindow' || ss.subSurfaceType == 'GlassDoor'
3691
4552
 
3692
- wind_area_m2 += ss.netArea * space.multiplier
4553
+ # Only add wall surfaces when the wall actually have windows
4554
+ wind_area_m2 += ss.netArea * space.multiplier
4555
+ end
4556
+ end
4557
+ if wind_area_m2 > 0.0
4558
+ wall_only_area_m2 += surface.grossArea * space.multiplier
3693
4559
  end
3694
4560
  end
3695
4561
 
3696
4562
  # Determine the space category
3697
- # @todo This should really use the heating/cooling loads from the proposed building.
3698
- # However, in an attempt to avoid another sizing run just for this purpose,
3699
- # conditioned status is based on heating/cooling setpoints.
3700
- # If heated-only, will be assumed Semiheated.
3701
- # The full-bore method is on the next line in case needed.
3702
- # cat = thermal_zone_conditioning_category(space, template, climate_zone)
3703
- cooled = space_cooled?(space)
3704
- heated = space_heated?(space)
3705
- cat = 'Unconditioned'
3706
- # Unconditioned
3707
- if !heated && !cooled
3708
- cat = 'Unconditioned'
3709
- # Heated-Only
3710
- elsif heated && !cooled
3711
- cat = 'Semiheated'
3712
- # Heated and Cooled
4563
+ if model_create_prm_baseline_building_requires_proposed_model_sizing_run(model)
4564
+ # For PRM 90.1-2019 and onward, determine space category
4565
+ # based on sizing run results
4566
+ cat = space_conditioning_category(space)
3713
4567
  else
3714
- res = space_residential?(space)
3715
- cat = if res
3716
- 'ResConditioned'
3717
- else
3718
- 'NonResConditioned'
3719
- end
4568
+ # TODO: This should really use the heating/cooling loads from the proposed building.
4569
+ # However, in an attempt to avoid another sizing run just for this purpose,
4570
+ # conditioned status is based on heating/cooling setpoints.
4571
+ # If heated-only, will be assumed Semiheated.
4572
+ # The full-bore method is on the next line in case needed.
4573
+ # cat = thermal_zone_conditioning_category(space, template, climate_zone)
4574
+ cooled = space_cooled?(space)
4575
+ heated = space_heated?(space)
4576
+ cat = 'Unconditioned'
4577
+ # Unconditioned
4578
+ if !heated && !cooled
4579
+ cat = 'Unconditioned'
4580
+ # Heated-Only
4581
+ elsif heated && !cooled
4582
+ cat = 'Semiheated'
4583
+ # Heated and Cooled
4584
+ else
4585
+ res = space_residential?(space)
4586
+ cat = if res
4587
+ 'ResConditioned'
4588
+ else
4589
+ 'NonResConditioned'
4590
+ end
4591
+ end
3720
4592
  end
3721
4593
  space_cats[space] = cat
3722
4594
 
3723
- # Add to the correct category
3724
- case cat
3725
- when 'Unconditioned'
3726
- next # Skip unconditioned spaces
3727
- when 'NonResConditioned'
3728
- nr_wall_m2 += wall_area_m2
3729
- nr_wind_m2 += wind_area_m2
3730
- when 'ResConditioned'
3731
- res_wall_m2 += wall_area_m2
3732
- res_wind_m2 += wind_area_m2
3733
- when 'Semiheated'
3734
- sh_wall_m2 += wall_area_m2
3735
- sh_wind_m2 += wind_area_m2
3736
- end
3737
- end
3738
-
3739
- # Calculate the WWR of each category
3740
- wwr_nr = ((nr_wind_m2 / nr_wall_m2) * 100.0).round(1)
3741
- wwr_res = ((res_wind_m2 / res_wall_m2) * 100).round(1)
3742
- wwr_sh = ((sh_wind_m2 / sh_wall_m2) * 100).round(1)
3743
-
3744
- # Convert to IP and report
3745
- nr_wind_ft2 = OpenStudio.convert(nr_wind_m2, 'm^2', 'ft^2').get
3746
- nr_wall_ft2 = OpenStudio.convert(nr_wall_m2, 'm^2', 'ft^2').get
3747
-
3748
- res_wind_ft2 = OpenStudio.convert(res_wind_m2, 'm^2', 'ft^2').get
3749
- res_wall_ft2 = OpenStudio.convert(res_wall_m2, 'm^2', 'ft^2').get
3750
-
3751
- sh_wind_ft2 = OpenStudio.convert(sh_wind_m2, 'm^2', 'ft^2').get
3752
- sh_wall_ft2 = OpenStudio.convert(sh_wall_m2, 'm^2', 'ft^2').get
3753
-
3754
- OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "WWR NonRes = #{wwr_nr.round}%; window = #{nr_wind_ft2.round} ft2, wall = #{nr_wall_ft2.round} ft2.")
3755
- OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "WWR Res = #{wwr_res.round}%; window = #{res_wind_ft2.round} ft2, wall = #{res_wall_ft2.round} ft2.")
3756
- OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "WWR Semiheated = #{wwr_sh.round}%; window = #{sh_wind_ft2.round} ft2, wall = #{sh_wall_ft2.round} ft2.")
3757
-
3758
- # WWR limit
3759
- wwr_lim = 40.0
3760
-
3761
- # Check against WWR limit
3762
- red_nr = wwr_nr > wwr_lim
3763
- red_res = wwr_res > wwr_lim
3764
- red_sh = wwr_sh > wwr_lim
3765
-
3766
- # Stop here unless windows need reducing
3767
- return true unless red_nr || red_res || red_sh
3768
-
3769
- # Determine the factors by which to reduce the window area
3770
- mult_nr_red = wwr_lim / wwr_nr
3771
- mult_res_red = wwr_lim / wwr_res
3772
- mult_sh_red = wwr_lim / wwr_sh
3773
-
3774
- # Reduce the window area if any of the categories necessary
3775
- model.getSpaces.sort.each do |space|
3776
- # Determine the space category
3777
- # from the previously stored values
3778
- cat = space_cats[space]
3779
-
3780
- # Get the correct multiplier
4595
+ # Add to the correct category is_space_plenum?
3781
4596
  case cat
3782
4597
  when 'Unconditioned'
3783
4598
  next # Skip unconditioned spaces
3784
4599
  when 'NonResConditioned'
3785
- next unless red_nr
3786
-
3787
- mult = mult_nr_red
4600
+ space_is_plenum(space) ? bat['nr_plenum_wall_m2'] += wall_area_m2 : bat['nr_plenum_wall_m2'] += 0.0
4601
+ bat['nr_wall_m2'] += wall_area_m2
4602
+ bat['nr_fene_only_wall_m2'] += wall_only_area_m2
4603
+ bat['nr_wind_m2'] += wind_area_m2
3788
4604
  when 'ResConditioned'
3789
- next unless red_res
3790
-
3791
- mult = mult_res_red
4605
+ space_is_plenum(space) ? bat['res_plenum_wall_m2'] += wall_area_m2 : bat['res_plenum_wall_m2'] += 0.0
4606
+ bat['res_wall_m2'] += wall_area_m2
4607
+ bat['res_fene_only_wall_m2'] += wall_only_area_m2
4608
+ bat['res_wind_m2'] += wind_area_m2
3792
4609
  when 'Semiheated'
3793
- next unless red_sh
3794
-
3795
- mult = mult_sh_red
3796
- end
3797
-
3798
- # Loop through all surfaces in this space
3799
- space.surfaces.sort.each do |surface|
3800
- # Skip non-outdoor surfaces
3801
- next unless surface.outsideBoundaryCondition == 'Outdoors'
3802
- # Skip non-walls
3803
- next unless surface.surfaceType.casecmp('wall').zero?
4610
+ space_is_plenum(space) ? bat['sh_plenum_wall_m2'] += wall_area_m2 : bat['sh_plenum_wall_m2'] += 0.0
4611
+ bat['sh_wall_m2'] += wall_area_m2
4612
+ bat['sh_fene_only_wall_m2'] += wall_only_area_m2
4613
+ bat['sh_wind_m2'] += wind_area_m2
4614
+ end
4615
+ end
4616
+
4617
+ # Retrieve WWR info for all Building Area Types included in the model
4618
+ # and perform adjustements if
4619
+ # bat = building area type
4620
+ bat_win_wall_info.each do |bat, vals|
4621
+ # Calculate the WWR of each category
4622
+ vals.store('wwr_nr', ((vals['nr_wind_m2'] / vals['nr_wall_m2']) * 100.0).round(1))
4623
+ vals.store('wwr_res', ((vals['res_wind_m2'] / vals['res_wall_m2']) * 100).round(1))
4624
+ vals.store('wwr_sh', ((vals['sh_wind_m2'] / vals['sh_wall_m2']) * 100).round(1))
4625
+
4626
+ # Convert to IP and report
4627
+ vals.store('nr_wind_ft2', OpenStudio.convert(vals['nr_wind_m2'], 'm^2', 'ft^2').get)
4628
+ vals.store('nr_wall_ft2', OpenStudio.convert(vals['nr_wall_m2'], 'm^2', 'ft^2').get)
4629
+
4630
+ vals.store('res_wind_ft2', OpenStudio.convert(vals['res_wind_m2'], 'm^2', 'ft^2').get)
4631
+ vals.store('res_wall_ft2', OpenStudio.convert(vals['res_wall_m2'], 'm^2', 'ft^2').get)
4632
+
4633
+ vals.store('sh_wind_ft2', OpenStudio.convert(vals['sh_wind_m2'], 'm^2', 'ft^2').get)
4634
+ vals.store('sh_wall_ft2', OpenStudio.convert(vals['sh_wall_m2'], 'm^2', 'ft^2').get)
4635
+
4636
+ OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "WWR NonRes = #{vals['wwr_nr'].round}%; window = #{vals['nr_wind_ft2'].round} ft2, wall = #{vals['nr_wall_ft2'].round} ft2.")
4637
+ OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "WWR Res = #{vals['wwr_res'].round}%; window = #{vals['res_wind_ft2'].round} ft2, wall = #{vals['res_wall_ft2'].round} ft2.")
4638
+ OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "WWR Semiheated = #{vals['wwr_sh'].round}%; window = #{vals['sh_wind_ft2'].round} ft2, wall = #{vals['sh_wall_ft2'].round} ft2.")
4639
+
4640
+ # WWR limit or target
4641
+ wwr_lim = model_get_bat_wwr_target(bat, [vals['wwr_nr'], vals['wwr_res'], vals['wwr_sh']])
4642
+
4643
+ # Check against WWR limit
4644
+ vals['red_nr'] = vals['wwr_nr'] > wwr_lim
4645
+ vals['red_res'] = vals['wwr_res'] > wwr_lim
4646
+ vals['red_sh'] = vals['wwr_sh'] > wwr_lim
4647
+
4648
+ # Stop here unless windows need reducing or increasing
4649
+ return true, base_wwr unless model_does_require_wwr_adjustment?(wwr_lim, [vals['wwr_nr'], vals['wwr_res'], vals['wwr_sh']])
4650
+
4651
+ # Determine the factors by which to reduce the window area
4652
+ vals['mult_nr_red'] = wwr_lim / vals['wwr_nr']
4653
+ vals['mult_res_red'] = wwr_lim / vals['wwr_res']
4654
+ vals['mult_sh_red'] = wwr_lim / vals['wwr_sh']
4655
+
4656
+ # Report baseline WWR
4657
+ vals['wwr_nr'] *= vals['mult_nr_red']
4658
+ vals['wwr_res'] *= vals['mult_res_red']
4659
+ vals['wwr_sh'] *= vals['mult_sh_red']
4660
+ wwrs = [vals['wwr_nr'], vals['wwr_res'], vals['wwr_sh']]
4661
+ wwrs = wwrs.reject! &:nan?
4662
+ base_wwr[bat] = wwrs.max
4663
+
4664
+ # Reduce the window area if any of the categories necessary
4665
+ model.getSpaces.sort.each do |space|
4666
+ # Catch spaces without space types
4667
+ std_spc_type = space.additionalProperties.getFeatureAsString('building_type_for_wwr').get
4668
+ # skip process the space unless the space wwr type matched.
4669
+ next unless bat == std_spc_type
4670
+ # supply and return plenum is now conditioned space but should be excluded from window adjustment
4671
+ next if space_is_plenum(space)
4672
+
4673
+ # Determine the space category
4674
+ # from the previously stored values
4675
+ cat = space_cats[space]
4676
+
4677
+ # Get the correct multiplier
4678
+ case cat
4679
+ when 'Unconditioned'
4680
+ next # Skip unconditioned spaces
4681
+ when 'NonResConditioned'
4682
+ mult = vals['mult_nr_red']
4683
+ total_wall_area = vals['nr_wall_m2']
4684
+ total_wall_with_fene_area = vals['nr_fene_only_wall_m2']
4685
+ total_plenum_wall_area = vals['nr_plenum_wall_m2']
4686
+ total_fene_area = vals['nr_wind_m2']
4687
+ when 'ResConditioned'
4688
+ mult = vals['mult_res_red']
4689
+ total_wall_area = vals['res_wall_m2']
4690
+ total_wall_with_fene_area = vals['res_fene_only_wall_m2']
4691
+ total_plenum_wall_area = vals['res_plenum_wall_m2']
4692
+ total_fene_area = vals['res_wind_m2']
4693
+ when 'Semiheated'
4694
+ mult = vals['mult_sh_red']
4695
+ total_wall_area = vals['sh_wall_m2']
4696
+ total_wall_with_fene_area = vals['sh_fene_only_wall_m2']
4697
+ total_plenum_wall_area = vals['sh_plenum_wall_m2']
4698
+ total_fene_area = vals['sh_wind_m2']
4699
+ end
3804
4700
 
3805
- # Subsurfaces in this surface
3806
- surface.subSurfaces.sort.each do |ss|
3807
- next unless ss.subSurfaceType == 'FixedWindow' || ss.subSurfaceType == 'OperableWindow'
4701
+ # used for counting how many window area is left for doors
4702
+ residual_fene = 0.0
4703
+ # Loop through all surfaces in this space
4704
+ space.surfaces.sort.each do |surface|
4705
+ # Skip non-outdoor surfaces
4706
+ next unless surface.outsideBoundaryCondition == 'Outdoors'
4707
+ # Skip non-walls
4708
+ next unless surface.surfaceType.casecmp('wall').zero?
3808
4709
 
3809
4710
  # Reduce the size of the window
3810
4711
  # If a vertical rectangle, raise sill height to avoid
3811
4712
  # impacting daylighting areas, otherwise
3812
4713
  # reduce toward centroid.
3813
- red = 1.0 - mult
3814
- if sub_surface_vertical_rectangle?(ss)
3815
- sub_surface_reduce_area_by_percent_by_raising_sill(ss, red)
3816
- else
3817
- sub_surface_reduce_area_by_percent_by_shrinking_toward_centroid(ss, red)
4714
+ #
4715
+ # daylighting control isn't modeled
4716
+ surface_wwr = surface_get_wwr_of_a_surface(surface)
4717
+ red = model_get_wwr_reduction_ratio(mult,
4718
+ surface_wwr: surface_wwr,
4719
+ surface_dr: surface_get_door_ratio_of_a_surface(surface),
4720
+ wwr_building_type: bat,
4721
+ wwr_target: wwr_lim / 100, # divide by 100 to revise it to decimals
4722
+ total_wall_m2: total_wall_area,
4723
+ total_wall_with_fene_m2: total_wall_with_fene_area,
4724
+ total_fene_m2: total_fene_area,
4725
+ total_plenum_wall_m2: total_plenum_wall_area)
4726
+
4727
+ if red < 0.0
4728
+ # surface with fenestration to its maximum but adjusted by door areas when need to add windows in surfaces no fenestration
4729
+ # turn negative to positive to get the correct adjustment factor.
4730
+ red = -red
4731
+ residual_fene += (0.9 - red * surface_wwr) * surface.grossArea
3818
4732
  end
4733
+ surface_adjust_fenestration_in_a_surface(surface, red, model)
4734
+ end
4735
+
4736
+ if residual_fene > 0.0
4737
+ residual_ratio = residual_fene / (total_wall_area - total_wall_with_fene_area)
4738
+ model_readjust_surface_wwr(residual_ratio, space, model)
3819
4739
  end
3820
4740
  end
3821
4741
  end
3822
4742
 
3823
- return true
4743
+ return true, base_wwr
3824
4744
  end
3825
4745
 
3826
4746
  # Reduces the SRR to the values specified by the PRM. SRR reduction will be done by shrinking vertices toward the centroid.
@@ -3968,6 +4888,21 @@ class Standard
3968
4888
  return srr_lim
3969
4889
  end
3970
4890
 
4891
+ # Apply baseline values to exterior lights objects
4892
+ # Only implemented for stable baseline
4893
+ #
4894
+ # @param model [OpenStudio::model::Model] OpenStudio model object
4895
+ def model_apply_baseline_exterior_lighting(model)
4896
+ return false
4897
+ end
4898
+
4899
+ # Function to add baseline elevators based on user data
4900
+ # Only applicable to stable baseline
4901
+ # @param model [OpenStudio::Model::Model] OpenStudio model object
4902
+ def model_add_prm_elevators(model)
4903
+ return false
4904
+ end
4905
+
3971
4906
  # Remove all HVAC that will be replaced during the performance rating method baseline generation.
3972
4907
  # This does not include plant loops that serve WaterUse:Equipment or Fan:ZoneExhaust
3973
4908
  #
@@ -3983,9 +4918,28 @@ class Standard
3983
4918
  end
3984
4919
 
3985
4920
  # Air loops
3986
- model.getAirLoopHVACs.each(&:remove)
3987
- if model.version > OpenStudio::VersionString.new('3.1.0')
3988
- model.getAirLoopHVACDedicatedOutdoorAirSystems.each(&:remove)
4921
+ model.getAirLoopHVACs.each do |air_loop|
4922
+ # Don't remove airloops representing non-mechanically cooled systems
4923
+ if !air_loop.additionalProperties.hasFeature('non_mechanically_cooled')
4924
+ air_loop.remove
4925
+ else
4926
+ # Remove heating coil on
4927
+ air_loop.supplyComponents.each do |supply_comp|
4928
+ # Remove standalone heating coils
4929
+ if supply_comp.iddObjectType.valueName.to_s.include?('OS_Coil_Heating')
4930
+ supply_comp.remove
4931
+ # Remove heating coils wrapped in a unitary system
4932
+ elsif supply_comp.iddObjectType.valueName.to_s.include?('OS_AirLoopHVAC_UnitarySystem')
4933
+ unitary_system = supply_comp.to_AirLoopHVACUnitarySystem.get
4934
+ htg_coil = unitary_system.heatingCoil
4935
+ if htg_coil.is_initialized
4936
+ htg_coil = htg_coil.get
4937
+ unitary_system.resetCoolingCoil
4938
+ htg_coil.remove
4939
+ end
4940
+ end
4941
+ end
4942
+ end
3989
4943
  end
3990
4944
 
3991
4945
  # Zone equipment
@@ -3993,13 +4947,16 @@ class Standard
3993
4947
  zone.equipment.each do |zone_equipment|
3994
4948
  next if zone_equipment.to_FanZoneExhaust.is_initialized
3995
4949
 
3996
- zone_equipment.remove
4950
+ zone_equipment.remove unless zone.additionalProperties.hasFeature('non_mechanically_cooled')
3997
4951
  end
3998
4952
  end
3999
4953
 
4000
4954
  # Outdoor VRF units (not in zone, not in loops)
4001
4955
  model.getAirConditionerVariableRefrigerantFlows.each(&:remove)
4002
4956
 
4957
+ # Air loop dedicated outdoor air systems
4958
+ model.getAirLoopHVACDedicatedOutdoorAirSystems.each(&:remove)
4959
+
4003
4960
  return true
4004
4961
  end
4005
4962
 
@@ -4098,7 +5055,7 @@ class Standard
4098
5055
  # @todo for types not in table use standards area normalized swh values
4099
5056
 
4100
5057
  # get building type
4101
- building_data = model_get_building_climate_zone_and_building_type(model)
5058
+ building_data = model_get_building_properties(model)
4102
5059
  building_type = building_data['building_type']
4103
5060
 
4104
5061
  result = []
@@ -4403,6 +5360,7 @@ class Standard
4403
5360
  # update count of ground wall areas
4404
5361
  next if surface.surfaceType != 'Wall'
4405
5362
  next if surface.outsideBoundaryCondition != 'Ground'
5363
+
4406
5364
  # @todo make more flexible for slab/basement model.modeling
4407
5365
 
4408
5366
  story_ground_wall_area += surface.grossArea
@@ -4770,7 +5728,7 @@ class Standard
4770
5728
  # @param model [OpenStudio::Model::Model] the model
4771
5729
  # @return [String] the ventilation method, either Sum or Maximum
4772
5730
  def model_ventilation_method(model)
4773
- building_data = model_get_building_climate_zone_and_building_type(model)
5731
+ building_data = model_get_building_properties(model)
4774
5732
  building_type = building_data['building_type']
4775
5733
  if building_type != 'Laboratory' # Laboratory has multiple criteria on ventilation, pick the greatest
4776
5734
  ventilation_method = 'Sum'
@@ -5168,6 +6126,40 @@ class Standard
5168
6126
 
5169
6127
  private
5170
6128
 
6129
+ # This function checks whether it is required to adjust the window to wall ratio based on the model WWR and wwr limit.
6130
+ # @param wwr_limit [Float] return wwr_limit
6131
+ # @param wwr_list [Array] list of wwr of zone conditioning category in a building area type category - residential, nonresidential and semiheated
6132
+ # @return require_adjustment [Boolean] True, require adjustment, false not require adjustment.
6133
+ def model_does_require_wwr_adjustment?(wwr_limit, wwr_list)
6134
+ require_adjustment = false
6135
+ wwr_list.each do |wwr|
6136
+ require_adjustment = true unless wwr > wwr_limit
6137
+ end
6138
+ return require_adjustment
6139
+ end
6140
+
6141
+ # The function is used for codes that requires to adjusted wwr based on building categories for all other types
6142
+ #
6143
+ # @param bat [String] building area type category
6144
+ # @param wwr_list [Array] list of wwr of zone conditioning category in a building area type category - residential, nonresidential and semiheated
6145
+ # @return wwr_limit [Float] return adjusted wwr_limit
6146
+ def model_get_bat_wwr_target(bat, wwr_list)
6147
+ return 40.0
6148
+ end
6149
+
6150
+ # Readjusted the WWR for surfaces previously has no windows to meet the
6151
+ # overall WWR requirement.
6152
+ # This function shall only be called if the maximum WWR value for surfaces with fenestration is lower than 90% due to
6153
+ # accommodating the total door surface areas
6154
+ #
6155
+ # @param residual_ratio: [Float] the ratio of residual surfaces among the total wall surface area with no fenestrations
6156
+ # @param space [OpenStudio::Model:Space] a space
6157
+ # @param model [OpenStudio::Model::Model] openstudio model
6158
+ # @return [Bool] return true if successful, false if not
6159
+ def model_readjust_surface_wwr(residual_ratio, space, model)
6160
+ return true
6161
+ end
6162
+
5171
6163
  # Helper method to fill in hourly values
5172
6164
  #
5173
6165
  # @param model [OpenStudio::Model::Model] the model
@@ -5300,6 +6292,28 @@ class Standard
5300
6292
  return true
5301
6293
  end
5302
6294
 
6295
+ # This method is a catch-all run at the end of create-baseline to make final adjustements to HVAC capacities
6296
+ # to account for recent model changes
6297
+ # @author Doug Maddox, PNNL
6298
+ # @param model
6299
+ def model_refine_size_dependent_values(model, sizing_run_dir)
6300
+ return true
6301
+ end
6302
+
6303
+ # This method rotates the building model from its original position
6304
+ #
6305
+ # @param model [OpenStudio::Model::Model] OpenStudio Model object
6306
+ # @param degs [Integer] Degress of rotation from original position
6307
+ #
6308
+ # @return [OpenStudio::Model::Model] OpenStudio Model object
6309
+ def model_rotate(model, degs)
6310
+ building = model.getBuilding
6311
+ org_north_axis = building.northAxis
6312
+ building.setNorthAxis(org_north_axis + degs)
6313
+ OpenStudio.logFree(OpenStudio::Info, 'openstudio.model.Model', "The model was rotated of #{degs} degrees from its original position.")
6314
+ return model
6315
+ end
6316
+
5303
6317
  # Loads a geometry osm as a starting point.
5304
6318
  #
5305
6319
  # @param osm_model_path [String] path to the .osm file, relative to the /data folder
@@ -5805,12 +6819,74 @@ class Standard
5805
6819
  return parametric_inputs
5806
6820
  end
5807
6821
 
6822
+ # Retrieves the lowest story in a model
6823
+ #
6824
+ # @param model [OpenStudio::model::Model] OpenStudio model object
6825
+ # @return [OpenStudio::model::BuildingStory] Lowest story included in the model
6826
+ def find_lowest_story(model)
6827
+ min_z_story = 1E+10
6828
+ lowest_story = nil
6829
+ model.getSpaces.sort.each do |space|
6830
+ story = space.buildingStory.get
6831
+ lowest_story = story if lowest_story.nil?
6832
+ space_min_z = building_story_minimum_z_value(story)
6833
+ if space_min_z < min_z_story
6834
+ min_z_story = space_min_z
6835
+ lowest_story = story
6836
+ end
6837
+ end
6838
+ return lowest_story
6839
+ end
6840
+
6841
+ # Utility function that returns the min and max value in a design day schedule.
6842
+ #
6843
+ # TODO: move this to Standards.Schedule.rb
6844
+ # @param schedule [OpenStudio::Model::Schedule] can be ScheduleCompact, ScheduleRuleset, ScheduleConstant
6845
+ # @param type [String] 'heating' for winter design day, 'cooling' for summer design day
6846
+ # @return [Hash] Hash has two keys, min and max. if failed, return 999.9 for min and max.
6847
+ def search_min_max_value_from_design_day_schedule(schedule, type = 'winter')
6848
+ if schedule.is_initialized
6849
+ schedule = schedule.get
6850
+ if schedule.to_ScheduleRuleset.is_initialized
6851
+ schedule = schedule.to_ScheduleRuleset.get
6852
+ setpoint_min_max = schedule_ruleset_design_day_min_max_value(schedule, type)
6853
+ elsif schedule.to_ScheduleConstant.is_initialized
6854
+ schedule = schedule.to_ScheduleConstant.get
6855
+ # for constant schedule, there is only one value, so the annual should be equal to design condition.
6856
+ setpoint_min_max = schedule_constant_annual_min_max_value(schedule)
6857
+ elsif schedule.to_ScheduleCompact.is_initialized
6858
+ schedule = schedule.to_ScheduleCompact.get
6859
+ setpoint_min_max = schedule_compact_design_day_min_max_value(schedule, type)
6860
+ end
6861
+ return setpoint_min_max
6862
+ end
6863
+ OpenStudio.logFree(OpenStudio::Error, 'openstudio::standards::Schedule', 'Schedule is not exist, or wrong type of schedule (not Ruleset, Compact or Constant), or cannot found the design day schedules. Return 999.9 for min and max')
6864
+ return { 'min' => 999.9, 'max' => 999.9 }
6865
+ end
6866
+
6867
+ # Identifies non mechanically cooled ("nmc") systems, if applicable
6868
+ #
6869
+ # @param model [OpenStudio::model::Model] OpenStudio model object
6870
+ # @return zone_nmc_sys_type [Hash] Zone to nmc system type mapping
6871
+ def model_identify_non_mechanically_cooled_systems(model)
6872
+ return true
6873
+ end
6874
+
6875
+ # Indicate if fan power breakdown (supply, return, and relief)
6876
+ # are needed
6877
+ #
6878
+ # @return [Boolean] true if necessary, false otherwise
6879
+ def model_get_fan_power_breakdown
6880
+ return false
6881
+ end
6882
+
5808
6883
  # Determine the surface range of a baseline model.
5809
6884
  # The method calculates the window to wall ratio (assuming all spaces are conditioned)
5810
6885
  # and select the range based on the calculated window to wall ratio
5811
6886
  # @param model [OpenStudio::Model::Model] OpenStudio model object
5812
- # @param intended_surface_type [String] surface type
5813
- def model_get_percent_of_surface_range(model, intended_surface_type)
6887
+ # @param wwr_parameter [Hash] parameters to choose min and max percent of surfaces,
6888
+ # could be different set in different standard
6889
+ def model_get_percent_of_surface_range(model, wwr_parameter = {})
5814
6890
  return { 'minimum_percent_of_surface' => nil, 'maximum_percent_of_surface' => nil }
5815
6891
  end
5816
6892
 
@@ -5821,4 +6897,296 @@ class Standard
5821
6897
  def air_loop_hvac_supply_air_temperature_reset_type(air_loop_hvac)
5822
6898
  return 'warmest_zone'
5823
6899
  end
6900
+
6901
+ # Calculate the window to wall ratio reduction factor
6902
+ #
6903
+ # @param multiplier [Float] multiplier of the wwr
6904
+ # @param surface_wwr [Float] the surface window to wall ratio
6905
+ # @param surface_dr [Float] the surface door to wall ratio
6906
+ # @param wwr_building_type[String] building type for wwr
6907
+ # @param wwr_target [Float] target window to wall ratio
6908
+ # @param total_wall_m2 [Float] total wall area of the category in m2.
6909
+ # @param total_wall_with_fene_m2 [Float] total wall area of the category with fenestrations in m2.
6910
+ # @param total_fene_m2 [Float] total fenestration area
6911
+ # @param total_plenum_wall_m2 [Float] total sqaure meter of a plenum
6912
+ # @return [Float] reduction factor
6913
+ def model_get_wwr_reduction_ratio(multiplier,
6914
+ surface_wwr: 0.0,
6915
+ surface_dr: 0.0,
6916
+ wwr_building_type: 'All others',
6917
+ wwr_target: 0.0,
6918
+ total_wall_m2: 0.0,
6919
+ total_wall_with_fene_m2: 0.0,
6920
+ total_fene_m2: 0.0,
6921
+ total_plenum_wall_m2: 0.0)
6922
+ return 1.0 - multiplier
6923
+ end
6924
+
6925
+ # A template method that handles the loading of user input data from multiple sources
6926
+ # include data source from:
6927
+ # 1. user data csv files
6928
+ # 2. data from measure and OpenStudio interface
6929
+ # @param [Openstudio:model:Model] model
6930
+ # @param [String] climate_zone
6931
+ # @param [String] default_hvac_building_type
6932
+ # @param [String] default_wwr_building_type
6933
+ # @param [String] default_swh_building_type
6934
+ # @param [Hash] bldg_type_hvac_zone_hash A hash maps building type for hvac to a list of thermal zones
6935
+ # @return True
6936
+ 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)
6937
+ return true
6938
+ end
6939
+
6940
+ # Template method for adding a setpoint manager for a coil control logic to a heating coil.
6941
+ # ASHRAE 90.1-2019 Appendix G.
6942
+ #
6943
+ # @param model [OpenStudio::Model::Model] Openstudio model
6944
+ # @param thermalZones Array([OpenStudio::Model::ThermalZone]) thermal zone array
6945
+ # @param coil Heating Coils
6946
+ # @return [Boolean] true
6947
+ def model_set_central_preheat_coil_spm(model, thermal_zones, coil)
6948
+ return true
6949
+ end
6950
+
6951
+ # Template method for adding zone additional property "zone DCV implemented in user model"
6952
+ #
6953
+ # @author Xuechen (Jerry) Lei, PNNL
6954
+ # @param model [OpenStudio::Model::Model] Openstudio model
6955
+ def model_mark_zone_dcv_existence(model)
6956
+ return true
6957
+ end
6958
+
6959
+ # Check whether the baseline model generation needs to run all four orientations
6960
+ # The default shall be true
6961
+ #
6962
+ # @param [Boolean] run_all_orients: user inputs to indicate whether it is required to run all orientations
6963
+ # @param [OpenStudio::Model::Model] Openstudio model
6964
+ def run_all_orientations(run_all_orients, user_model)
6965
+ return run_all_orients
6966
+ end
6967
+
6968
+ # Template method for reading user data and adding to zone additional properties
6969
+ #
6970
+ # @author Xuechen (Jerry) Lei, PNNL
6971
+ # @param model [OpenStudio::Model::Model] Openstudio model
6972
+ def model_add_dcv_user_exception_properties(model)
6973
+ return true
6974
+ end
6975
+
6976
+ # Template method for raising user model DCV warning and errors
6977
+ #
6978
+ # @author Xuechen (Jerry) Lei, PNNL
6979
+ # @param model [OpenStudio::Model::Model] Openstudio model
6980
+ def model_raise_user_model_dcv_errors(model)
6981
+ return true
6982
+ end
6983
+
6984
+ # Template method for adding zone additional property "airloop dcv required by 901" and "zone dcv required by 901"
6985
+ #
6986
+ # @author Xuechen (Jerry) Lei, PNNL
6987
+ # @param model [OpenStudio::Model::Model] Openstudio model
6988
+ def model_add_dcv_requirement_properties(model)
6989
+ return true
6990
+ end
6991
+
6992
+ # Template method for checking if zones in the baseline model should have DCV based on 90.1 2019 G3.1.2.5.
6993
+ # Zone additional property 'apxg no need to have DCV' added
6994
+ #
6995
+ # @author Xuechen (Jerry) Lei, PNNL
6996
+ # @param model [OpenStudio::Model::Model] Openstudio model
6997
+ def model_add_apxg_dcv_properties(model)
6998
+ return true
6999
+ end
7000
+
7001
+ # Template method for setting DCV in baseline HVAC system if required
7002
+ #
7003
+ # @author Xuechen (Jerry) Lei, PNNL
7004
+ # @param model [OpenStudio::Model::Model] Openstudio model
7005
+ def model_set_baseline_demand_control_ventilation(model, climate_zone)
7006
+ return true
7007
+ end
7008
+
7009
+ # Identify the return air type associated with each thermal zone
7010
+ #
7011
+ # @param model [OpenStudio::Model::Model] Openstudio model object
7012
+ def model_identify_return_air_type(model)
7013
+ # air-loop based system
7014
+ model.getThermalZones.each do |zone|
7015
+ # Conditioning category won't include indirectly conditioned thermal zones
7016
+ cond_cat = thermal_zone_conditioning_category(zone, model_standards_climate_zone(model))
7017
+
7018
+ # Initialize the return air type
7019
+ return_air_type = nil
7020
+
7021
+ # The thermal zone is conditioned by zonal system
7022
+ if (cond_cat != 'Unconditioned') && zone.airLoopHVACs.empty?
7023
+ return_air_type = 'ducted_return_or_direct_to_unit'
7024
+ end
7025
+
7026
+ # Assume that the primary heating and cooling (PHC) system
7027
+ # is last in the heating and cooling order (ignore DOAS)
7028
+ #
7029
+ # Get the heating and cooling PHC components
7030
+ heating_equipment = zone.equipmentInHeatingOrder[-1]
7031
+ cooling_equipment = zone.equipmentInCoolingOrder[-1]
7032
+ if heating_equipment.nil? && cooling_equipment.nil?
7033
+ next
7034
+ end
7035
+
7036
+ unless heating_equipment.nil?
7037
+ if heating_equipment.to_ZoneHVACComponent.is_initialized
7038
+ heating_equipment_type = 'ZoneHVACComponent'
7039
+ elsif heating_equipment.to_StraightComponent.is_initialized
7040
+ heating_equipment_type = 'StraightComponent'
7041
+ end
7042
+ end
7043
+ unless cooling_equipment.nil?
7044
+ if cooling_equipment.to_ZoneHVACComponent.is_initialized
7045
+ cooling_equipment_type = 'ZoneHVACComponent'
7046
+ elsif cooling_equipment.to_StraightComponent.is_initialized
7047
+ cooling_equipment_type = 'StraightComponent'
7048
+ end
7049
+ end
7050
+
7051
+ # Determine return configuration
7052
+ if (heating_equipment_type == 'ZoneHVACComponent') && (cooling_equipment_type == 'ZoneHVACComponent')
7053
+ return_air_type = 'ducted_return_or_direct_to_unit'
7054
+ else
7055
+ # Check heating air loop first
7056
+ unless heating_equipment.nil?
7057
+ if heating_equipment.to_StraightComponent.is_initialized
7058
+ air_loop = heating_equipment.to_StraightComponent.get.airLoopHVAC.get
7059
+ return_plenum = air_loop_hvac_return_air_plenum(air_loop)
7060
+ return_air_type = return_plenum.nil? ? 'ducted_return_or_direct_to_unit' : 'return_plenum'
7061
+ return_plenum = return_plenum.nil? ? nil : return_plenum.name.to_s
7062
+ end
7063
+ end
7064
+
7065
+ # Check cooling air loop second; Assume that return air plenum is the dominant case
7066
+ unless cooling_equipment.nil?
7067
+ if (return_air_type != 'return_plenum') && cooling_equipment.to_StraightComponent.is_initialized
7068
+ air_loop = cooling_equipment.to_StraightComponent.get.airLoopHVAC.get
7069
+ return_plenum = air_loop_hvac_return_air_plenum(air_loop)
7070
+ return_air_type = return_plenum.nil? ? 'ducted_return_or_direct_to_unit' : 'return_plenum'
7071
+ return_plenum = return_plenum.nil? ? nil : return_plenum.name.to_s
7072
+ end
7073
+ end
7074
+ end
7075
+
7076
+ # Catch all
7077
+ if return_air_type.nil?
7078
+ return_air_type = 'ducted_return_or_direct_to_unit'
7079
+ end
7080
+
7081
+ zone.additionalProperties.setFeature('return_air_type', return_air_type)
7082
+ zone.additionalProperties.setFeature('plenum', return_plenum) unless return_plenum.nil?
7083
+ zone.additionalProperties.setFeature('proposed_model_zone_design_air_flow', zone.designAirFlowRate.to_f)
7084
+ end
7085
+ end
7086
+
7087
+ # Determine the baseline return air type associated with each zone
7088
+ #
7089
+ # @param model [OpenStudio::Model::model] OpenStudio model object
7090
+ # @param baseline_system_type [String] Baseline system type name
7091
+ # @param zones [Array] List of zone associated with a system
7092
+ # @return [Array] Array of length 2, the first item is the name
7093
+ # of the plenum zone and the second the return air type
7094
+ def model_determine_baseline_return_air_type(model, baseline_system_type, zones)
7095
+ return ['', 'ducted_return_or_direct_to_unit'] unless ['PSZ_AC', 'PSZ_HP', 'PVAV_Reheat', 'PVAV_PFP_Boxes', 'VAV_Reheat', 'VAV_PFP_Boxes', 'SZ_VAV', 'SZ_CV'].include?(baseline_system_type)
7096
+
7097
+ zone_return_air_type = {}
7098
+ zones.each do |zone|
7099
+ if zone.additionalProperties.hasFeature('proposed_model_zone_design_air_flow')
7100
+ zone_design_air_flow = zone.additionalProperties.getFeatureAsDouble('proposed_model_zone_design_air_flow').get
7101
+
7102
+ if zone.additionalProperties.hasFeature('return_air_type')
7103
+ return_air_type = zone.additionalProperties.getFeatureAsString('return_air_type').get
7104
+
7105
+ if zone_return_air_type.keys.include?(return_air_type)
7106
+ zone_return_air_type[return_air_type] += zone_design_air_flow
7107
+ else
7108
+ zone_return_air_type[return_air_type] = zone_design_air_flow
7109
+ end
7110
+
7111
+ if zone.additionalProperties.hasFeature('plenum')
7112
+ plenum = zone.additionalProperties.getFeatureAsString('plenum').get
7113
+
7114
+ if zone_return_air_type.keys.include?('plenum')
7115
+ if zone_return_air_type['plenum'].keys.include?(plenum)
7116
+ zone_return_air_type['plenum'][plenum] += zone_design_air_flow
7117
+ end
7118
+ else
7119
+ zone_return_air_type['plenum'] = { plenum => zone_design_air_flow }
7120
+ end
7121
+ end
7122
+ end
7123
+ end
7124
+ end
7125
+
7126
+ # Find dominant zone return air type and plenum zone
7127
+ # if the return air type is return air plenum
7128
+ return_air_types = zone_return_air_type.keys - ['plenum']
7129
+ return_air_types_score = 0
7130
+ return_air_type = nil
7131
+ plenum_score = 0
7132
+ plenum = nil
7133
+ return_air_types.each do |return_type|
7134
+ if zone_return_air_type[return_type] > return_air_types_score
7135
+ return_air_type = return_type
7136
+ return_air_types_score = zone_return_air_type[return_type]
7137
+ end
7138
+ if return_air_type == 'return_plenum'
7139
+ zone_return_air_type['plenum'].keys.each do |p|
7140
+ if zone_return_air_type['plenum'][p] > plenum_score
7141
+ plenum = p
7142
+ plenum_score = zone_return_air_type['plenum'][p]
7143
+ end
7144
+ end
7145
+ end
7146
+ end
7147
+
7148
+ return plenum, return_air_type
7149
+ end
7150
+
7151
+ # Add reporting tolerances. Default values are based on the suggestions from the PRM-RM.
7152
+ #
7153
+ # @param model [OpenStudio::Model::Model] OpenStudio Model
7154
+ # @param heating_tolerance_deg_f [Float] Tolerance for time heating setpoint not met in degree F
7155
+ # @param cooling_tolerance_deg_f [Float] Tolerance for time cooling setpoint not met in degree F
7156
+ def model_add_reporting_tolerances(model, heating_tolerance_deg_f: 1.0, cooling_tolerance_deg_f: 1.0)
7157
+ reporting_tolerances = model.getOutputControlReportingTolerances
7158
+ heating_tolerance_deg_c = OpenStudio.convert(heating_tolerance_deg_f, 'R', 'K').get
7159
+ cooling_tolerance_deg_c = OpenStudio.convert(cooling_tolerance_deg_f, 'R', 'K').get
7160
+ reporting_tolerances.setToleranceforTimeHeatingSetpointNotMet(heating_tolerance_deg_c)
7161
+ reporting_tolerances.setToleranceforTimeCoolingSetpointNotMet(cooling_tolerance_deg_c)
7162
+
7163
+ return true
7164
+ end
7165
+
7166
+ # Apply the standard construction to each surface in the model, based on the construction type currently assigned.
7167
+ #
7168
+ # @return [Bool] true if successful, false if not
7169
+ # @param model [OpenStudio::Model::Model] OpenStudio model object
7170
+ # @param climate_zone [String] ASHRAE climate zone, e.g. 'ASHRAE 169-2013-4A'
7171
+ # @return [Bool] returns true if successful, false if not
7172
+ def model_apply_constructions(model, climate_zone, wwr_building_type, wwr_info)
7173
+ model_apply_standard_constructions(model, climate_zone, wwr_building_type: nil, wwr_info: {})
7174
+
7175
+ return true
7176
+ end
7177
+
7178
+ # Generate baseline log to a specific file directory
7179
+ # @param file_directory [String] file directory
7180
+ def generate_baseline_log(file_directory)
7181
+ return true
7182
+ end
7183
+
7184
+ # Update ground temperature profile based on the weather file specified in the model
7185
+ #
7186
+ # @param model [OpenStudio::Model::Model] OpenStudio model object
7187
+ # @param climate_zone [String] ASHRAE climate zone, e.g. 'ASHRAE 169-2013-4A'
7188
+ # @return [Bool] returns true if successful, false if not
7189
+ def model_update_ground_temperature_profile(model, climate_zone)
7190
+ return true
7191
+ end
5824
7192
  end