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