From 926c3dedc72f6dbe196788b2b0450c5d54f2ab8b Mon Sep 17 00:00:00 2001 From: Scott Horowitz Date: Mon, 9 Dec 2024 14:54:52 -0700 Subject: [PATCH 1/3] Added ruby documentation to waterheater.rb. --- BuildResidentialHPXML/README.md | 2 +- BuildResidentialHPXML/measure.rb | 2 +- BuildResidentialHPXML/measure.xml | 10 +- BuildResidentialScheduleFile/measure.xml | 6 +- .../resources/schedules.rb | 2 +- HPXMLtoOpenStudio/measure.xml | 20 +- HPXMLtoOpenStudio/resources/airflow.rb | 6 +- HPXMLtoOpenStudio/resources/defaults.rb | 77 +- HPXMLtoOpenStudio/resources/geometry.rb | 3 +- .../resources/hotwater_appliances.rb | 78 +- HPXMLtoOpenStudio/resources/hpxml.rb | 15 +- HPXMLtoOpenStudio/resources/hvac.rb | 16 +- HPXMLtoOpenStudio/resources/hvac_sizing.rb | 4 +- HPXMLtoOpenStudio/resources/waterheater.rb | 789 +++++++++--------- 14 files changed, 538 insertions(+), 492 deletions(-) diff --git a/BuildResidentialHPXML/README.md b/BuildResidentialHPXML/README.md index e502a47cfc..0c76275dc2 100644 --- a/BuildResidentialHPXML/README.md +++ b/BuildResidentialHPXML/README.md @@ -554,7 +554,7 @@ The number of bathrooms in the unit. If not provided, the OS-HPXML default (see **Geometry: Unit Number of Occupants** -The number of occupants in the unit. If not provided, an *asset* calculation is performed assuming standard occupancy, in which various end use defaults (e.g., plug loads, appliances, and hot water usage) are calculated based on Number of Bedrooms and Conditioned Floor Area per ANSI/RESNET/ICC 301-2019. If provided, an *operational* calculation is instead performed in which the end use defaults are adjusted using the relationship between Number of Bedrooms and Number of Occupants from RECS 2015. +The number of occupants in the unit. If not provided, an *asset* calculation is performed assuming standard occupancy, in which various end use defaults (e.g., plug loads, appliances, and hot water usage) are calculated based on Number of Bedrooms and Conditioned Floor Area per ANSI/RESNET/ICC 301. If provided, an *operational* calculation is instead performed in which the end use defaults to reflect real-world data (where possible). - **Name:** ``geometry_unit_num_occupants`` - **Type:** ``Double`` diff --git a/BuildResidentialHPXML/measure.rb b/BuildResidentialHPXML/measure.rb index 8434564b4d..b45579f0c6 100644 --- a/BuildResidentialHPXML/measure.rb +++ b/BuildResidentialHPXML/measure.rb @@ -343,7 +343,7 @@ def arguments(model) # rubocop:disable Lint/UnusedMethodArgument arg = OpenStudio::Measure::OSArgument::makeDoubleArgument('geometry_unit_num_occupants', false) arg.setDisplayName('Geometry: Unit Number of Occupants') arg.setUnits('#') - arg.setDescription('The number of occupants in the unit. If not provided, an *asset* calculation is performed assuming standard occupancy, in which various end use defaults (e.g., plug loads, appliances, and hot water usage) are calculated based on Number of Bedrooms and Conditioned Floor Area per ANSI/RESNET/ICC 301-2019. If provided, an *operational* calculation is instead performed in which the end use defaults are adjusted using the relationship between Number of Bedrooms and Number of Occupants from RECS 2015.') + arg.setDescription('The number of occupants in the unit. If not provided, an *asset* calculation is performed assuming standard occupancy, in which various end use defaults (e.g., plug loads, appliances, and hot water usage) are calculated based on Number of Bedrooms and Conditioned Floor Area per ANSI/RESNET/ICC 301. If provided, an *operational* calculation is instead performed in which the end use defaults to reflect real-world data (where possible).') args << arg arg = OpenStudio::Measure::OSArgument::makeIntegerArgument('geometry_building_num_units', false) diff --git a/BuildResidentialHPXML/measure.xml b/BuildResidentialHPXML/measure.xml index 94a6e96597..154e205c1e 100644 --- a/BuildResidentialHPXML/measure.xml +++ b/BuildResidentialHPXML/measure.xml @@ -3,8 +3,8 @@ 3.1 build_residential_hpxml a13a8983-2b01-4930-8af2-42030b6e4233 - c768997a-a50d-422b-8942-6c3249d0c01e - 2024-11-27T21:21:13Z + 32bd49ca-09dc-40ec-b676-f01d76e98566 + 2024-12-09T21:40:28Z 2C38F48B BuildResidentialHPXML HPXML Builder @@ -878,7 +878,7 @@ geometry_unit_num_occupants Geometry: Unit Number of Occupants - The number of occupants in the unit. If not provided, an *asset* calculation is performed assuming standard occupancy, in which various end use defaults (e.g., plug loads, appliances, and hot water usage) are calculated based on Number of Bedrooms and Conditioned Floor Area per ANSI/RESNET/ICC 301-2019. If provided, an *operational* calculation is instead performed in which the end use defaults are adjusted using the relationship between Number of Bedrooms and Number of Occupants from RECS 2015. + The number of occupants in the unit. If not provided, an *asset* calculation is performed assuming standard occupancy, in which various end use defaults (e.g., plug loads, appliances, and hot water usage) are calculated based on Number of Bedrooms and Conditioned Floor Area per ANSI/RESNET/ICC 301. If provided, an *operational* calculation is instead performed in which the end use defaults to reflect real-world data (where possible). Double # false @@ -7527,7 +7527,7 @@ README.md md readme - B595D2F2 + ECAEDA1E README.md.erb @@ -7544,7 +7544,7 @@ measure.rb rb script - 5F60558F + 612549F4 constants.rb diff --git a/BuildResidentialScheduleFile/measure.xml b/BuildResidentialScheduleFile/measure.xml index 92ee872119..5ac98b35e0 100644 --- a/BuildResidentialScheduleFile/measure.xml +++ b/BuildResidentialScheduleFile/measure.xml @@ -3,8 +3,8 @@ 3.1 build_residential_schedule_file f770b2db-1a9f-4e99-99a7-7f3161a594b1 - fac68af6-8045-433e-8eca-df53c8274e61 - 2024-09-29T23:07:27Z + 12b68002-26b0-4e2d-bb89-f337226f0ac3 + 2024-12-09T21:40:29Z 03F02484 BuildResidentialScheduleFile Schedule File Builder @@ -229,7 +229,7 @@ schedules.rb rb resource - F53FB2CB + F14B1337 shower_cluster_size_probability.csv diff --git a/BuildResidentialScheduleFile/resources/schedules.rb b/BuildResidentialScheduleFile/resources/schedules.rb index 4720b00c67..540d41b6eb 100644 --- a/BuildResidentialScheduleFile/resources/schedules.rb +++ b/BuildResidentialScheduleFile/resources/schedules.rb @@ -176,7 +176,7 @@ def create_stochastic_schedules(args:, plugload_tv_monthly_multiplier = Schedule.validate_values(schedules_csv_data[SchedulesFile::Columns[:PlugLoadsTV].name]['PlugLoadsTVMonthlyMultipliers'], 12, 'monthly') # American Time Use Survey ceiling_fan_weekday_sch = Schedule.validate_values(default_schedules_csv_data[SchedulesFile::Columns[:CeilingFan].name]['WeekdayScheduleFractions'], 24, 'weekday') # Table C.3(5) of ANSI/RESNET/ICC 301-2022 Addendum C ceiling_fan_weekend_sch = Schedule.validate_values(default_schedules_csv_data[SchedulesFile::Columns[:CeilingFan].name]['WeekendScheduleFractions'], 24, 'weekend') # Table C.3(5) of ANSI/RESNET/ICC 301-2022 Addendum C - ceiling_fan_monthly_multiplier = Schedule.validate_values(Defaults.get_ceiling_fan_months(weather).join(', '), 12, 'monthly') # based on monthly average outdoor temperatures per ANSI/RESNET/ICC 301-2019 + ceiling_fan_monthly_multiplier = Schedule.validate_values(Defaults.get_ceiling_fan_months(weather).join(', '), 12, 'monthly') # based on monthly average outdoor temperatures per ANSI/RESNET/ICC 301 sch = get_building_america_lighting_schedule(args[:time_zone_utc_offset], args[:latitude], args[:longitude], schedules_csv_data) interior_lighting_schedule = [] diff --git a/HPXMLtoOpenStudio/measure.xml b/HPXMLtoOpenStudio/measure.xml index b4ac7fd8f4..97654b0360 100644 --- a/HPXMLtoOpenStudio/measure.xml +++ b/HPXMLtoOpenStudio/measure.xml @@ -3,8 +3,8 @@ 3.1 hpxm_lto_openstudio b1543b30-9465-45ff-ba04-1d1f85e763bc - c578c384-3ac2-4bc5-8764-763873807874 - 2024-11-27T16:38:08Z + 15c2ce69-58f0-473c-b7a2-de7e8df08059 + 2024-12-09T21:53:03Z D8922A73 HPXMLtoOpenStudio HPXML to OpenStudio Translator @@ -189,7 +189,7 @@ airflow.rb rb resource - 9D8C57A2 + C329DF48 battery.rb @@ -327,7 +327,7 @@ defaults.rb rb resource - 3EB617E6 + CCB18256 energyplus.rb @@ -345,19 +345,19 @@ geometry.rb rb resource - C9E92EE9 + 58D1C43A hotwater_appliances.rb rb resource - FAD2E124 + 5F7B3A25 hpxml.rb rb resource - 30969918 + 270F2EEB hpxml_schema/HPXML.xsd @@ -387,13 +387,13 @@ hvac.rb rb resource - 901BE48C + 04AD7F98 hvac_sizing.rb rb resource - B1744A25 + E96BF50D internal_gains.rb @@ -627,7 +627,7 @@ waterheater.rb rb resource - 6D8EEB7D + DC762179 weather.rb diff --git a/HPXMLtoOpenStudio/resources/airflow.rb b/HPXMLtoOpenStudio/resources/airflow.rb index 7a2df1d5d2..99985b4f77 100644 --- a/HPXMLtoOpenStudio/resources/airflow.rb +++ b/HPXMLtoOpenStudio/resources/airflow.rb @@ -530,7 +530,7 @@ def self.apply_natural_ventilation_and_whole_house_fan(runner, model, spaces, hp neutral_level = 0.5 hor_lk_frac = 0.0 c_w, c_s = calc_wind_stack_coeffs(hpxml_bldg, hor_lk_frac, neutral_level, conditioned_space, infil_values[:height]) - max_oa_hr = 0.0115 # From ANSI 301-2022 + max_oa_hr = 0.0115 # From ANSI/RESNET/ICC 301-2022 # Program vent_program = Model.add_ems_program( @@ -1459,7 +1459,7 @@ def self.apply_duct_location(model, spaces, hpxml_bldg, ducts, object, i, duct_l duct_subroutine.addLine(' Set SupLatLkToDZ = sup_lk_mfr*h_fg*(AH_Wout-DZ_W)') # W duct_subroutine.addLine(' Set SupSensLkToDZ = SupTotLkToDZ-SupLatLkToDZ') # W - # Handle duct leakage imbalance induced infiltration (ANSI 301-2022 Addendum C Table 4.2.2(1c) + # Handle duct leakage imbalance induced infiltration (ANSI/RESNET/ICC 301-2022 Addendum C Table 4.2.2(1c) leakage_supply = leakage_fracs[HPXML::DuctTypeSupply].to_f + leakage_cfm25s[HPXML::DuctTypeSupply].to_f leakage_return = leakage_fracs[HPXML::DuctTypeReturn].to_f + leakage_cfm25s[HPXML::DuctTypeReturn].to_f if leakage_supply == leakage_return @@ -2007,7 +2007,7 @@ def self.apply_cfis(runner, infil_program, vent_mech_fans, cfis_data, cfis_fan_a # Calculate outdoor air ventilation infil_program.addLine('Set QWHV_cfis_sup = QWHV_cfis_sup + (oa_cfm_ah * f_operation)') - # Calculate fraction of the timestep with ventilation only mode runtime per ANSI 301-2022 Addendum E + # Calculate fraction of the timestep with ventilation only mode runtime per ANSI/RESNET/ICC 301-2022 Addendum E infil_program.addLine("Set #{f_vent_only_mode_var.name} = f_operation * (1 - fan_rtf_hvac)") # Calculate additional fan energy diff --git a/HPXMLtoOpenStudio/resources/defaults.rb b/HPXMLtoOpenStudio/resources/defaults.rb index 205e084b33..55f0778b2b 100644 --- a/HPXMLtoOpenStudio/resources/defaults.rb +++ b/HPXMLtoOpenStudio/resources/defaults.rb @@ -24,7 +24,7 @@ module Defaults # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit # @param weather [WeatherFile] Weather object containing EPW information # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files - # @param convert_shared_systems [Boolean] Whether to convert shared systems to equivalent in-unit systems per ANSI 301 + # @param convert_shared_systems [Boolean] Whether to convert shared systems to equivalent in-unit systems per ANSI/RESNET/ICC 301 # @return [Array] Maps of HPXML::Zones => DesignLoadValues object, HPXML::Spaces => DesignLoadValues object def self.apply(runner, hpxml, hpxml_bldg, weather, schedules_file: nil, convert_shared_systems: true) eri_version = hpxml.header.eri_calculation_version @@ -831,7 +831,7 @@ def self.apply_neighbor_buildings(hpxml_bldg) def self.apply_building_occupancy(hpxml_bldg, schedules_file) if not hpxml_bldg.building_occupancy.number_of_residents.nil? # Set equivalent number of bedrooms for operational calculation; this is an adjustment on - # ANSI 301 or Building America equations, which are based on number of bedrooms. + # ANSI/RESNET/ICC 301 or Building America equations, which are based on number of bedrooms. hpxml_bldg.building_construction.additional_properties.equivalent_number_of_bedrooms = get_equivalent_nbeds_for_operational_calculation(hpxml_bldg) else hpxml_bldg.building_construction.additional_properties.equivalent_number_of_bedrooms = hpxml_bldg.building_construction.number_of_bedrooms @@ -1828,7 +1828,7 @@ def self.apply_furniture_mass(hpxml_bldg) # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit # @param weather [WeatherFile] Weather object containing EPW information - # @param convert_shared_systems [Boolean] Whether to convert shared systems to equivalent in-unit systems per ANSI 301 + # @param convert_shared_systems [Boolean] Whether to convert shared systems to equivalent in-unit systems per ANSI/RESNET/ICC 301 # @param unit_num [Integer] Dwelling unit number # @return [nil] def self.apply_hvac(runner, hpxml_bldg, weather, convert_shared_systems, unit_num) @@ -4108,7 +4108,7 @@ def self.get_orientation_from_azimuth(azimuth) end # Gets the equivalent number of bedrooms for an operational calculation (i.e., when number - # of occupants are provided in the HPXML); this is an adjustment to the ANSI 301 or Building + # of occupants are provided in the HPXML); this is an adjustment to the ANSI/RESNET/ICC 301 or Building # America equations, which are based on number of bedrooms. # # This is used to adjust occupancy-driven end uses from asset calculations (based on number @@ -4582,11 +4582,13 @@ def self.get_clothes_washer_values(eri_version) # Gets the default piping length for a standard hot water distribution system. # - # Per ANSI 301-2022, the length of hot water piping from the hot water heater to the farthest + # The length of hot water piping from the hot water heater to the farthest # hot water fixture, measured longitudinally from plans, assuming the hot water piping does # not run diagonally, plus 10 feet of piping for each floor level, plus 5 feet of piping for # unconditioned basements (if any). # + # Source: ANSI/RESNET/ICC 301-2022 + # # @param has_uncond_bsmnt [Boolean] Whether the dwelling unit has an unconditioned basement # @param has_cond_bsmnt [Boolean] Whether the dwelling unit has a conditioned basement # @param cfa [Double] Conditioned floor area in the dwelling unit (ft2) @@ -4598,16 +4600,18 @@ def self.get_std_pipe_length(has_uncond_bsmnt, has_cond_bsmnt, cfa, ncfl) bsmnt = 1 end - return 2.0 * (cfa / ncfl)**0.5 + 10.0 * ncfl + 5.0 * bsmnt # PipeL in ANSI 301 + return 2.0 * (cfa / ncfl)**0.5 + 10.0 * ncfl + 5.0 * bsmnt # PipeL in ANSI/RESNET/ICC 301 end # Gets the default loop piping length for a recirculation hot water distribution system. # - # Per ANSI 301-2022, the recirculation loop length including both supply and return sides, + # The recirculation loop length including both supply and return sides, # measured longitudinally from plans, assuming the hot water piping does not run diagonally, # plus 20 feet of piping for each floor level greater than one plus 10 feet of piping for # unconditioned basements. # + # Source: ANSI/RESNET/ICC 301-2022 + # # @param has_uncond_bsmnt [Boolean] Whether the dwelling unit has an unconditioned basement # @param has_cond_bsmnt [Boolean] Whether the dwelling unit has a conditioned basement # @param cfa [Double] Conditioned floor area in the dwelling unit (ft2) @@ -4615,32 +4619,34 @@ def self.get_std_pipe_length(has_uncond_bsmnt, has_cond_bsmnt, cfa, ncfl) # @return [Double] Piping length (ft) def self.get_recirc_loop_length(has_uncond_bsmnt, has_cond_bsmnt, cfa, ncfl) std_pipe_length = get_std_pipe_length(has_uncond_bsmnt, has_cond_bsmnt, cfa, ncfl) - return 2.0 * std_pipe_length - 20.0 # refLoopL in ANSI 301 + return 2.0 * std_pipe_length - 20.0 # refLoopL in ANSI/RESNET/ICC 301 end # Gets the default branch piping length for a recirculation hot water distribution system. # - # Per ANSI 301-2022, the length of the branch hot water piping from the recirculation loop + # The length of the branch hot water piping from the recirculation loop # to the farthest hot water fixture from the recirculation loop, measured longitudinally # from plans, assuming the branch hot water piping does not run diagonally. # + # Source: ANSI/RESNET/ICC 301-2022 + # # @return [Double] Piping length (ft) def self.get_recirc_branch_length() - return 10.0 # See pRatio in ANSI 301 + return 10.0 # See pRatio in ANSI/RESNET/ICC 301 end # Gets the default pump power for a recirculation system. # # @return [Double] Pump power (W) def self.get_recirc_pump_power() - return 50.0 # See pumpW in ANSI 301 + return 50.0 # See pumpW in ANSI/RESNET/ICC 301 end # Gets the default pump power for a shared recirculation system. # # @return [Double] Pump power (W) def self.get_shared_recirc_pump_power() - # From ANSI/RESNET/ICC 301-2019 Equation 4.2-15b + # From ANSI/RESNET/ICC 301-2022 Eq. 4.2-43b pump_horsepower = 0.25 motor_efficiency = 0.85 pump_kw = pump_horsepower * 0.746 / motor_efficiency @@ -4674,9 +4680,11 @@ def self.get_freezer_or_extra_fridge_location(hpxml_bldg) # Window represents multiple windows, the value is calculated as the total window area # for any operable windows divided by the total window area. # + # Source: ANSI/RESNET/ICC 301-2025 + # # @return [Double] Operable fraction (frac) def self.get_fraction_of_windows_operable() - return 0.67 # 67% per ANSI 301-2025 + return 0.67 # 67% end # Gets the default specific leakage area (SLA) for a vented attic. @@ -4684,7 +4692,7 @@ def self.get_fraction_of_windows_operable() # # @return [Double] Specific leakage area (frac) def self.get_vented_attic_sla() - return (1.0 / 300.0).round(6) # ANSI 301, Table 4.2.2(1) - Attics + return (1.0 / 300.0).round(6) # ANSI/RESNET/ICC 301, Table 4.2.2(1) - Attics end # Gets the default specific leakage area (SLA) for a vented crawlspace. @@ -4692,7 +4700,7 @@ def self.get_vented_attic_sla() # # @return [Double] Specific leakage area (frac) def self.get_vented_crawl_sla() - return (1.0 / 150.0).round(6) # ANSI 301, Table 4.2.2(1) - Crawlspaces + return (1.0 / 150.0).round(6) # ANSI/RESNET/ICC 301, Table 4.2.2(1) - Crawlspaces end # Gets the default whole-home mechanical ventilation fan flow rate required to @@ -4707,7 +4715,7 @@ def self.get_vented_crawl_sla() # @param eri_version [String] Version of the ANSI/RESNET/ICC 301 Standard to use for equations/assumptions # @return [Double] Fan flow rate (cfm) def self.get_mech_vent_flow_rate_for_vent_fan(hpxml_bldg, vent_fan, weather, eri_version) - # Calculates Qfan cfm requirement per ASHRAE 62.2 / ANSI 301 + # Calculates Qfan cfm requirement per ASHRAE 62.2 / ANSI/RESNET/ICC 301 cfa = hpxml_bldg.building_construction.conditioned_floor_area nbeds = hpxml_bldg.building_construction.number_of_bedrooms infil_values = Airflow.get_values_from_air_infiltration_measurements(hpxml_bldg, weather) @@ -4727,10 +4735,11 @@ def self.get_mech_vent_flow_rate_for_vent_fan(hpxml_bldg, vent_fan, weather, eri # Gets the default whole-home mechanical ventilation fan efficiency. # + # Source: ANSI/RESNET/ICC 301 + # # @param vent_fan [HPXML::VentilationFan] The HPXML ventilation fan of interest # @return [Double] Fan efficiency (W/cfm) def self.get_mech_vent_fan_efficiency(vent_fan) - # Returns fan power in W/cfm, based on ANSI 301 if vent_fan.is_shared_system return 1.00 # Table 4.2.2(1) Note (n) end @@ -4880,7 +4889,7 @@ def self.get_infiltration_ach50(cfa, ncfl_ag, year_built, avg_ceiling_height, in # @param f_rect [Double] The fraction of duct length that is rectangular (not round) # @return [Double] Duct effective R-value (hr-ft2-F/Btu) def self.get_duct_effective_r_value(r_nominal, side, buried_level, f_rect) - # This methodology has been proposed by NREL for ANSI 301-2025. + # This methodology has been proposed by NREL for ANSI/RESNET/ICC 301-2025. if buried_level == HPXML::DuctBuriedInsulationNone if r_nominal <= 0 # Uninsulated ducts are set to R-1.7 based on ASHRAE HOF and the above paper. @@ -4984,9 +4993,9 @@ def self.get_water_heater_temperature(eri_version) def self.get_water_heater_performance_adjustment(water_heating_system) return unless water_heating_system.water_heater_type == HPXML::WaterHeaterTypeTankless if not water_heating_system.energy_factor.nil? - return 0.92 # Applies to EF, ANSI 301-2019 + return 0.92 # Applies to EF, ANSI/RESNET/ICC 301-2022 elsif not water_heating_system.uniform_energy_factor.nil? - return 0.94 # Applies to UEF, ANSI 301-2019 + return 0.94 # Applies to UEF, ANSI/RESNET/ICC 301-2022 end end @@ -5344,18 +5353,20 @@ def self.get_hvac_compressor_type(hvac_type, seer) # Gets the default fan power for a ceiling fan. # + # Source: ANSI/RESNET/ICC 301 + # # @return [Double] Fan power (W) def self.get_ceiling_fan_power() - # Per ANSI/RESNET/ICC 301 return 42.6 end # Gets the default quantity of ceiling fans. # + # Source: ANSI/RESNET/ICC 301 + # # @param nbeds [Integer] Number of bedrooms in the dwelling unit # @return [Integer] Number of ceiling fans def self.get_ceiling_fan_quantity(nbeds) - # Per ANSI/RESNET/ICC 301 return nbeds + 1 end @@ -5495,8 +5506,9 @@ def self.get_boiler_eae(heating_system) end end - # Gets the default interior/garage/exterior lighting fractions per ANSI/RESNET/ICC 301. - # Used by OS-ERI, OS-HEScore, etc. + # Gets the default interior/garage/exterior lighting fractions. Used by OS-ERI, OS-HEScore, etc. + # + # Source: ANSI/RESNET/ICC 301 # # @return [Hash] Map of [HPXML::LocationXXX, HPXML::LightingTypeXXX] => lighting fraction def self.get_lighting_fractions() @@ -5513,13 +5525,14 @@ def self.get_lighting_fractions() return ltg_fracs end - # Gets the default heating setpoints per ANSI/RESNET/ICC 301. + # Gets the default heating setpoints. + # + # Source: ANSI/RESNET/ICC 301 # # @param control_type [String] Thermostat control type (HPXML::HVACControlTypeXXX) # @param eri_version [String] Version of the ANSI/RESNET/ICC 301 Standard to use for equations/assumptions # @return [Array] 24 hourly comma-separated weekday and weekend setpoints def self.get_heating_setpoint(control_type, eri_version) - # Per ANSI/RESNET/ICC 301 htg_wd_setpoints = '68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68' htg_we_setpoints = '68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68' if control_type == HPXML::HVACControlTypeProgrammable @@ -5536,13 +5549,14 @@ def self.get_heating_setpoint(control_type, eri_version) return htg_wd_setpoints, htg_we_setpoints end - # Gets the default cooling setpoints per ANSI/RESNET/ICC 301. + # Gets the default cooling setpoints. + # + # Source: ANSI/RESNET/ICC 301 # # @param control_type [String] Thermostat control type (HPXML::HVACControlTypeXXX) # @param eri_version [String] Version of the ANSI/RESNET/ICC 301 Standard to use for equations/assumptions # @return [Array] 24 hourly comma-separated weekday and weekend setpoints def self.get_cooling_setpoint(control_type, eri_version) - # Per ANSI/RESNET/ICC 301 clg_wd_setpoints = '78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78' clg_we_setpoints = '78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78' if control_type == HPXML::HVACControlTypeProgrammable @@ -5576,15 +5590,16 @@ def self.get_heating_capacity_retention(compressor_type, hspf = nil) return retention_temp, retention_fraction end - # Gets a 12-element array of 1s and 0s that reflects months for which the ceiling fan operates - # (i.e., when the average drybulb temperature is greater than 63F, per ANSI/RESNET/ICC 301). + # Gets the monthly ceiling fan operation schedule. + # + # Source: ANSI/RESNET/ICC 301 # # @param weather [WeatherFile] Weather object containing EPW information # @return [Array] monthly array of 1s and 0s def self.get_ceiling_fan_months(weather) months = [0] * 12 weather.data.MonthlyAvgDrybulbs.each_with_index do |val, m| - next unless val > 63.0 # F + next unless val > 63.0 # Ceiling fan operates when average drybulb temperature is greater than 63F months[m] = 1 end diff --git a/HPXMLtoOpenStudio/resources/geometry.rb b/HPXMLtoOpenStudio/resources/geometry.rb index 3f6304e199..cc284bbddf 100644 --- a/HPXMLtoOpenStudio/resources/geometry.rb +++ b/HPXMLtoOpenStudio/resources/geometry.rb @@ -1124,7 +1124,7 @@ def self.get_surface_z_values(surfaceArray:) # @param nbeds [Integer] Number of bedrooms in the dwelling unit # @return [Double] Number of occupants in the dwelling unit def self.get_occupancy_default_num(nbeds:) - return Float(nbeds) # Per ANSI 301 for an asset calculation + return Float(nbeds) # Per ANSI/RESNET/ICC 301 for an asset calculation end # Creates a space and zone based on contents of spaces and value of location. @@ -1325,7 +1325,6 @@ def self.create_floor_vertices(length, width, z_origin, default_azimuths) end # Set calculated zone volumes for all HPXML locations on OpenStudio Thermal Zone and Space objects. - # TODO why? for reporting? # # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit diff --git a/HPXMLtoOpenStudio/resources/hotwater_appliances.rb b/HPXMLtoOpenStudio/resources/hotwater_appliances.rb index 0a57a99138..1b5aa56894 100644 --- a/HPXMLtoOpenStudio/resources/hotwater_appliances.rb +++ b/HPXMLtoOpenStudio/resources/hotwater_appliances.rb @@ -700,21 +700,23 @@ def self.calc_dishwasher_energy_gpd(runner, eri_version, nbeds, dishwasher, is_o return annual_kwh, frac_sens, frac_lat, gpd end - # Converts dishwasher rated annual use (kWh) to energy factor (EF). + # Calculates dishwasher rated energy factor (EF) from annual use (kWh). + # + # Source: ANSI/RESNET/ICC 301 # # @param annual_kwh [Double] Rated annual kWh # @return [Double] Energy factor def self.calc_dishwasher_ef_from_annual_kwh(annual_kwh) - # Per ANSI/RESNET/ICC 301 return 215.0 / annual_kwh end - # Converts dishwasher energy factor (EF) to rated annual use (kWh). + # Calculates dishwasher annual use (kWh) from rated energy factor (EF). + # + # Source: ANSI/RESNET/ICC 301 # # @param ef [Double] Energy factor # @return [Double] Rated annual use (kWh) def self.calc_dishwasher_annual_kwh_from_ef(ef) - # Per ANSI/RESNET/ICC 301 return 215.0 / ef end @@ -796,15 +798,23 @@ def self.calc_clothes_dryer_energy(runner, eri_version, nbeds, clothes_dryer, cl return annual_kwh, annual_therm, frac_sens, frac_lat end - # Converts clothes dryer energy factor (EF) to combined energy factor (CEF). + # Calculates clothes dryer combined energy factor (CEF) from energy factor (EF). + # + # Source: RESNET's Interpretation on Clothes Dryer CEF + # https://www.resnet.us/wp-content/uploads/No.-301-2014-10-Section-4.2.2.5.2.8-Clothes-Dryer-CEF-Rating.pdf + # Note that this is a regression based on products on the market, not a conversion. # # @param ef [Double] Energy factor # @return [Double] Combined energy factor def self.calc_clothes_dryer_cef_from_ef(ef) - return ef / 1.15 # Interpretation on ANSI/RESNET/ICC 301-2014 Clothes Dryer CEF + return ef / 1.15 end - # Converts clothes dryer combined energy factor (CEF) to energy factor (EF). + # Calculates clothes dryer energy factor (EF) from combined energy factor (CEF). + # + # Source: RESNET's Interpretation on Clothes Dryer CEF + # https://www.resnet.us/wp-content/uploads/No.-301-2014-10-Section-4.2.2.5.2.8-Clothes-Dryer-CEF-Rating.pdf + # Note that this is a regression based on products on the market, not a conversion. # # @param cef [Double] Combined energy factor # @return [Double] Energy factor @@ -869,20 +879,28 @@ def self.calc_clothes_washer_energy_gpd(runner, eri_version, nbeds, clothes_wash return annual_kwh, frac_sens, frac_lat, gpd end - # Converts clothes washer modified energy factor (MEF) to integrated modified energy factor (IMEF). + # Calculates clothes washer integrated modified energy factor (IMEF) from modified energy factor (MEF). + # + # Source: RESNET's Interpretation on Clothes Washer IMEF + # https://www.resnet.us/wp-content/uploads/No.-301-2014-08-sECTION-4.2.2.5.2.8-Clothes-Washers-Eq-4.2-6.pdf + # Note that this is a regression based on products on the market, not a conversion. # # @param mef [Double] Modified energy factor # @return [Double] Integrated modified energy factor def self.calc_clothes_washer_imef_from_mef(mef) - return (mef - 0.503) / 0.95 # Interpretation on ANSI/RESNET 301-2014 Clothes Washer IMEF + return (mef - 0.503) / 0.95 end - # Converts clothes washer integrated modified energy factor (IMEF) to modified energy factor (MEF). + # Calculates clothes washer modified energy factor (MEF) from integrated modified energy factor (IMEF). # - # @param mef [Double] Modified energy factor - # @return [Double] Integrated modified energy factor + # Source: RESNET's Interpretation on Clothes Washer IMEF + # https://www.resnet.us/wp-content/uploads/No.-301-2014-08-sECTION-4.2.2.5.2.8-Clothes-Washers-Eq-4.2-6.pdf + # Note that this is a regression based on products on the market, not a conversion. + # + # @param imef [Double] Integrated modified energy factor + # @return [Double] Modified energy factor def self.calc_clothes_washer_mef_from_imef(imef) - return 0.503 + 0.95 * imef # Interpretation on ANSI/RESNET 301-2014 Clothes Washer IMEF + return 0.503 + 0.95 * imef # Interpretation on ANSI/RESNET/ICC 301-2014 Clothes Washer IMEF end # Calculates refrigerator annual energy use. @@ -995,20 +1013,20 @@ def self.get_fridge_or_freezer_coefficients_schedule(model, col_name, obj_name, return schedule end - # Calculates Drain Water Heat Recovery (DWHR) factors per ANSI/RESNET/ICC 301. + # Calculates Drain Water Heat Recovery (DWHR) factors. + # + # Source: ANSI/RESNET/ICC 301 # # @param nbeds_eq [Integer] Number of bedrooms (or equivalent bedrooms, as adjusted by the number of occupants) in the dwelling unit # @param hot_water_distribution [HPXML::HotWaterDistribution] The HPXML hot water distribution system of interest # @param frac_low_flow_fixtures [Double] The fraction of fixtures considered low-flow # @return [Array] Effectiveness (frac), fraction of water impacted by DWHR, piping loss coefficient, location factor, fixture factor def self.get_dwhr_factors(nbeds_eq, hot_water_distribution, frac_low_flow_fixtures) - # ANSI/RESNET 301-2014 Addendum A-2015 - # Amendment on Domestic Hot Water (DHW) Systems - # Eq. 4.2-14 + # ANSI/RESNET/ICC 301-2022 Eq. 4.2-42 eff_adj = 1.0 + 0.082 * frac_low_flow_fixtures - iFrac = 0.56 + 0.015 * nbeds_eq - 0.0004 * nbeds_eq**2 # fraction of hot water use impacted by DWHR + i_frac = 0.56 + 0.015 * nbeds_eq - 0.0004 * nbeds_eq**2 # fraction of hot water use impacted by DWHR if hot_water_distribution.system_type == HPXML::DHWDistTypeRecirc pLength = hot_water_distribution.recirculation_branch_piping_length @@ -1019,19 +1037,19 @@ def self.get_dwhr_factors(nbeds_eq, hot_water_distribution, frac_low_flow_fixtur # Location factors for DWHR placement if hot_water_distribution.dwhr_equal_flow - locF = 1.000 + loc_f = 1.000 else - locF = 0.777 + loc_f = 0.777 end # Fixture Factor if hot_water_distribution.dwhr_facilities_connected == HPXML::DWHRFacilitiesConnectedAll - fixF = 1.0 + fix_f = 1.0 elsif hot_water_distribution.dwhr_facilities_connected == HPXML::DWHRFacilitiesConnectedOne - fixF = 0.5 + fix_f = 0.5 end - return eff_adj, iFrac, plc, locF, fixF + return eff_adj, i_frac, plc, loc_f, fix_f end # Calculates daily water heater inlet temperatures, which includes an adjustment if @@ -1151,10 +1169,8 @@ def self.get_fixtures_effectiveness(frac_low_flow_fixtures) # @return [Double] Mixed water use (gal/day) def self.get_fixtures_gpd(eri_version, nbeds, frac_low_flow_fixtures, daily_mw_fractions, fixtures_usage_multiplier = 1.0, n_occ = nil) if Constants::ERIVersions.index(eri_version) >= Constants::ERIVersions.index('2014A') - # ANSI/RESNET 301-2014 Addendum A-2015 - # Amendment on Domestic Hot Water (DHW) Systems if n_occ.nil? # Asset calculation - ref_f_gpd = 14.6 + 10.0 * nbeds # Eq. 4.2-2 (refFgpd) + ref_f_gpd = 14.6 + 10.0 * nbeds # ANSI/RESNET/ICC 301-2022 Eq. 4.2-29 (refFgpd) else # Operational calculation ref_f_gpd = [-4.84 + 18.6 * n_occ, 0.0].max # Eq. 14 from http://www.fsec.ucf.edu/en/publications/pdf/fsec-pf-464-15.pdf end @@ -1170,6 +1186,8 @@ def self.get_fixtures_gpd(eri_version, nbeds, frac_low_flow_fixtures, daily_mw_f # Calculates the equivalent daily mixed (not hot) water use associated with the distribution system. # + # Source: ANSI/RESNET/ICC 301 + # # @param eri_version [String] Version of the ANSI/RESNET/ICC 301 Standard to use for equations/assumptions # @param nbeds [Integer] Number of bedrooms in the dwelling unit # @param has_uncond_bsmnt [Boolean] Whether the dwelling unit has an unconditioned basement @@ -1187,11 +1205,9 @@ def self.get_dist_waste_gpd(eri_version, nbeds, has_uncond_bsmnt, has_cond_bsmnt return 0.0 end - # ANSI/RESNET 301-2014 Addendum A-2015 - # Amendment on Domestic Hot Water (DHW) Systems - # 4.2.2.5.2.11 Service Hot Water Use + # ANSI/RESNET/ICC 301-2022 Section 4.2.2.7.1.4 - # Table 4.2.2.5.2.11(2) Hot Water Distribution System Insulation Factors + # Table 4.2.2.7.2.11(2) Hot Water Distribution System Insulation Factors sys_factor = nil case hot_water_distribution.system_type when HPXML::DHWDistTypeRecirc @@ -1209,7 +1225,7 @@ def self.get_dist_waste_gpd(eri_version, nbeds, has_uncond_bsmnt, has_cond_bsmnt end if n_occ.nil? # Asset calculation - ref_w_gpd = 9.8 * (nbeds**0.43) # Eq. 4.2-2 (refWgpd) + ref_w_gpd = 9.8 * (nbeds**0.43) # ANSI/RESNET/ICC 301-2022 Eq. 4.2-29 (refWgpd) else # Operational calculation ref_w_gpd = 7.16 * (n_occ**0.7) # Eq. 14 from http://www.fsec.ucf.edu/en/publications/pdf/fsec-pf-464-15.pdf end diff --git a/HPXMLtoOpenStudio/resources/hpxml.rb b/HPXMLtoOpenStudio/resources/hpxml.rb index 02683908bb..b062fa4ca1 100644 --- a/HPXMLtoOpenStudio/resources/hpxml.rb +++ b/HPXMLtoOpenStudio/resources/hpxml.rb @@ -1848,7 +1848,7 @@ def has_walkout_basement end # Calculates above-grade and below-grade thermal boundary wall areas. - # Used to calculate the window area in the ERI Reference Home per ANSI 301. + # Used to calculate the window area in the ERI Reference Home. # # Thermal boundary wall is any wall that separates conditioned space from # unconditioned space, outside, or soil. Above-grade thermal boundary @@ -1856,6 +1856,8 @@ def has_walkout_basement # Below-grade thermal boundary wall is any portion of a thermal boundary # wall in contact with soil. # + # Source: ANSI/RESNET/ICC 301 + # # @return [Array] Above-grade and below-grade thermal boundary wall areas (ft2) def thermal_boundary_wall_areas ag_wall_area = 0.0 @@ -1901,8 +1903,9 @@ def above_grade_conditioned_volume return ag_cond_vol end - # Calculates common wall area. - # Used to calculate the window area in the ERI Reference Home per ANSI 301. + # Calculates common wall area. Used to calculate the window area in the ERI Reference Home. + # + # Source: ANSI/RESNET/ICC 301 # # Common wall is the total wall area of walls adjacent to other unit's # conditioned space, not including foundation walls. @@ -1924,12 +1927,14 @@ def common_wall_area # Returns the total and exterior compartmentalization boundary area. # Used to convert between total infiltration and exterior infiltration for - # SFA/MF dwelling units per ANSI 301. + # SFA/MF dwelling units. + # + # Source: ANSI/RESNET/ICC 301 # # @return [Array] Total and exterior compartmentalization areas (ft2) def compartmentalization_boundary_areas total_area = 0.0 # Total surface area that bounds the Infiltration Volume - exterior_area = 0.0 # Same as above excluding surfaces attached to garage, other housing units, or other multifamily spaces (see 301-2019 Addendum B) + exterior_area = 0.0 # Same as above excluding surfaces attached to garage, other housing units, or other multifamily spaces # Determine which spaces are within infiltration volume spaces_within_infil_volume = HPXML::conditioned_locations_this_unit diff --git a/HPXMLtoOpenStudio/resources/hvac.rb b/HPXMLtoOpenStudio/resources/hvac.rb index 6dad4bdfd5..b1248f89bd 100644 --- a/HPXMLtoOpenStudio/resources/hvac.rb +++ b/HPXMLtoOpenStudio/resources/hvac.rb @@ -10,7 +10,7 @@ module HVAC AirSourceCoolRatedIWB = 67.0 # degF, Rated indoor wetbulb for air-source systems, cooling CrankcaseHeaterTemp = 50.0 # degF - # TODO + # Adds any HVAC Systems to the OpenStudio model. # # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param model [OpenStudio::Model::Model] OpenStudio Model object @@ -727,7 +727,7 @@ def self.apply_ground_to_air_heat_pump(model, runner, weather, heat_pump, hvac_s add_pump_power_ems_program(model, pump_w, pump, air_loop_unitary) if heat_pump.is_shared_system - # Shared pump power per ANSI/RESNET/ICC 301-2019 Section 4.4.5.1 (pump runs 8760) + # Shared pump power per ANSI/RESNET/ICC 301-2022 Section 4.4.5.1 (pump runs 8760) design_level = heat_pump.shared_loop_watts / heat_pump.number_of_units_served.to_f equip = Model.add_electric_equipment( @@ -1293,14 +1293,14 @@ def self.apply_ceiling_fans(runner, model, spaces, weather, hpxml_bldg, hpxml_he ceiling_fan = hpxml_bldg.ceiling_fans[0] obj_name = Constants::ObjectTypeCeilingFan - hrs_per_day = 10.5 # From ANSI 301-2019 + hrs_per_day = 10.5 # From ANSI/RESNET/ICC 301-2022 cfm_per_w = ceiling_fan.efficiency label_energy_use = ceiling_fan.label_energy_use count = ceiling_fan.count if !label_energy_use.nil? # priority if both provided annual_kwh = UnitConversions.convert(count * label_energy_use * hrs_per_day * 365.0, 'Wh', 'kWh') elsif !cfm_per_w.nil? - medium_cfm = 3000.0 # cfm, per ANSI 301-2019 + medium_cfm = 3000.0 # cfm, per ANSI/RESNET/ICC 301-2019 annual_kwh = UnitConversions.convert(count * medium_cfm / cfm_per_w * hrs_per_day * 365.0, 'Wh', 'kWh') end @@ -5286,7 +5286,7 @@ def self.apply_shared_cooling_systems(hpxml_bldg) aux_dweq = cooling_system.fan_coil_watts end end - # ANSI/RESNET/ICC 301-2019 Equation 4.4-2 + # ANSI/RESNET/ICC 301-2022 Equation 4.4-2 seer_eq = (cap - 3.41 * aux - 3.41 * aux_dweq * n_dweq) / (chiller_input + aux + aux_dweq * n_dweq) elsif cooling_system.cooling_system_type == HPXML::HVACTypeCoolingTower @@ -5299,7 +5299,7 @@ def self.apply_shared_cooling_systems(hpxml_bldg) wlhp_input = wlhp_cap / wlhp.cooling_efficiency_eer end end - # ANSI/RESNET/ICC 301-2019 Equation 4.4-3 + # ANSI/RESNET/ICC 301-2022 Equation 4.4-3 seer_eq = (wlhp_cap - 3.41 * aux / n_dweq) / (wlhp_input + aux / n_dweq) else @@ -5392,7 +5392,7 @@ def self.apply_shared_heating_systems(hpxml_bldg) if heating_system.heating_system_type == HPXML::HVACTypeBoiler && hydronic_type.to_s == HPXML::HydronicTypeWaterLoop # Shared boiler w/ water loop heat pump - # Per ANSI/RESNET/ICC 301-2019 Section 4.4.7.2, model as: + # Per ANSI/RESNET/ICC 301-2022 Section 4.4.7.2, model as: # A) heat pump with constant efficiency and duct losses, fraction heat load served = 1/COP # B) boiler, fraction heat load served = 1-1/COP fraction_heat_load_served = heating_system.fraction_heat_load_served @@ -5584,7 +5584,7 @@ def self.apply_unit_multiplier(hpxml_bldg, hpxml_header) # Calculates rated SEER (older metric) from rated SEER2 (newer metric). # # Source: ANSI/RESNET/ICC 301 Table 4.4.4.1(1) SEER2/HSPF2 Conversion Factors - # This is based on a regression of products, not a translation. + # Note that this is a regression based on products on the market, not a conversion. # # @param seer2 [Double] Cooling efficiency (Btu/Wh) # @param is_ducted [Boolean] True if a ducted HVAC system diff --git a/HPXMLtoOpenStudio/resources/hvac_sizing.rb b/HPXMLtoOpenStudio/resources/hvac_sizing.rb index d2e76b1321..5772759da2 100644 --- a/HPXMLtoOpenStudio/resources/hvac_sizing.rb +++ b/HPXMLtoOpenStudio/resources/hvac_sizing.rb @@ -1989,11 +1989,11 @@ def self.apply_hvac_duct_loads(mj, zone, hvac_loads, zone_loads, all_space_loads end end - # TODO + # Calculates the duct loads using Manual J Table 7 default duct tables. # # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit # @param manualj_duct_load [HPXML::ManualJDuctLoad] Manual J duct load of interest - # @return [TODO] TODO + # @return [Array] Heating loss factor, Cooling sensible gain factor, Cooling latent gain Btuh def self.get_duct_table7_factors(hpxml_bldg, manualj_duct_load) # Gather values htg_oat = hpxml_bldg.header.manualj_heating_design_temp diff --git a/HPXMLtoOpenStudio/resources/waterheater.rb b/HPXMLtoOpenStudio/resources/waterheater.rb index 32c094ce64..02d78dccd7 100644 --- a/HPXMLtoOpenStudio/resources/waterheater.rb +++ b/HPXMLtoOpenStudio/resources/waterheater.rb @@ -4,7 +4,7 @@ module Waterheater DefaultTankHeight = 4.0 # ft, assumption from BEopt - # TODO + # Adds any water heaters and appliances to the OpenStudio Model. # # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param model [OpenStudio::Model::Model] OpenStudio Model object @@ -25,7 +25,7 @@ def self.apply_dhw_appliances(runner, model, weather, spaces, hpxml_bldg, hpxml_ when HPXML::WaterHeaterTypeTankless apply_tankless(model, runner, spaces, hpxml_bldg, hpxml_header, dhw_system, schedules_file, unavailable_periods, plantloop_map) when HPXML::WaterHeaterTypeHeatPump - apply_heatpump(model, runner, spaces, hpxml_bldg, hpxml_header, dhw_system, schedules_file, unavailable_periods, plantloop_map) + apply_hpwh(model, runner, spaces, hpxml_bldg, hpxml_header, dhw_system, schedules_file, unavailable_periods, plantloop_map) when HPXML::WaterHeaterTypeCombiStorage, HPXML::WaterHeaterTypeCombiTankless apply_combi(model, runner, spaces, hpxml_bldg, hpxml_header, dhw_system, schedules_file, unavailable_periods, plantloop_map) else @@ -36,12 +36,10 @@ def self.apply_dhw_appliances(runner, model, weather, spaces, hpxml_bldg, hpxml_ HotWaterAndAppliances.apply(runner, model, weather, spaces, hpxml_bldg, hpxml_header, schedules_file, plantloop_map) apply_solar_thermal(model, spaces, hpxml_bldg, plantloop_map) - - # Add combi-system EMS program with water use equipment information apply_combi_system_EMS(model, hpxml_bldg.water_heating_systems, plantloop_map) end - # TODO + # Adds a conventional storage tank water heater to the OpenStudio model. # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings @@ -58,33 +56,33 @@ def self.apply_tank(model, runner, spaces, hpxml_bldg, hpxml_header, water_heati unit_multiplier = hpxml_bldg.building_construction.number_of_units solar_fraction = get_water_heater_solar_fraction(water_heating_system, hpxml_bldg) t_set_c = get_t_set_c(water_heating_system.temperature, water_heating_system.water_heater_type) - loop = create_new_loop(model, t_set_c, hpxml_header.eri_calculation_version, unit_multiplier) + plant_loop = add_plant_loop(model, t_set_c, hpxml_header.eri_calculation_version, unit_multiplier) act_vol = calc_storage_tank_actual_vol(water_heating_system.tank_volume, water_heating_system.fuel_type) - u, ua, eta_c = calc_tank_UA(act_vol, water_heating_system, solar_fraction, hpxml_bldg.building_construction.number_of_bedrooms) - new_heater = create_new_heater(name: Constants::ObjectTypeWaterHeater, - water_heating_system: water_heating_system, - act_vol: act_vol, - t_set_c: t_set_c, - loc_space: loc_space, - loc_schedule: loc_schedule, - model: model, - runner: runner, - u: u, - ua: ua, - eta_c: eta_c, - schedules_file: schedules_file, - unavailable_periods: unavailable_periods, - unit_multiplier: unit_multiplier) - loop.addSupplyBranchForComponent(new_heater) - - add_ec_adj(model, hpxml_bldg, new_heater, loc_space, water_heating_system, unit_multiplier) - add_desuperheater(model, runner, water_heating_system, new_heater, loc_space, loc_schedule, loop, unit_multiplier) - - plantloop_map[water_heating_system.id] = loop + u, ua, eta_c = disaggregate_tank_losses_and_burner_efficiency(act_vol, water_heating_system, solar_fraction, hpxml_bldg.building_construction.number_of_bedrooms) + water_heater = apply_water_heater(name: Constants::ObjectTypeWaterHeater, + water_heating_system: water_heating_system, + act_vol: act_vol, + t_set_c: t_set_c, + loc_space: loc_space, + loc_schedule: loc_schedule, + model: model, + runner: runner, + u: u, + ua: ua, + eta_c: eta_c, + schedules_file: schedules_file, + unavailable_periods: unavailable_periods, + unit_multiplier: unit_multiplier) + plant_loop.addSupplyBranchForComponent(water_heater) + + apply_ec_adj_program(model, hpxml_bldg, water_heater, loc_space, water_heating_system, unit_multiplier) + apply_desuperheater(model, runner, water_heating_system, water_heater, loc_space, loc_schedule, plant_loop, unit_multiplier) + + plantloop_map[water_heating_system.id] = plant_loop end - # TODO + # Adds a tankless water heater to the OpenStudio model. # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings @@ -102,33 +100,33 @@ def self.apply_tankless(model, runner, spaces, hpxml_bldg, hpxml_header, water_h water_heating_system.heating_capacity = 100000000000.0 * unit_multiplier solar_fraction = get_water_heater_solar_fraction(water_heating_system, hpxml_bldg) t_set_c = get_t_set_c(water_heating_system.temperature, water_heating_system.water_heater_type) - loop = create_new_loop(model, t_set_c, hpxml_header.eri_calculation_version, unit_multiplier) + plant_loop = add_plant_loop(model, t_set_c, hpxml_header.eri_calculation_version, unit_multiplier) act_vol = 1.0 * unit_multiplier - _u, ua, eta_c = calc_tank_UA(act_vol, water_heating_system, solar_fraction, hpxml_bldg.building_construction.number_of_bedrooms) - new_heater = create_new_heater(name: Constants::ObjectTypeWaterHeater, - water_heating_system: water_heating_system, - act_vol: act_vol, - t_set_c: t_set_c, - loc_space: loc_space, - loc_schedule: loc_schedule, - model: model, - runner: runner, - ua: ua, - eta_c: eta_c, - schedules_file: schedules_file, - unavailable_periods: unavailable_periods, - unit_multiplier: unit_multiplier) - - loop.addSupplyBranchForComponent(new_heater) - - add_ec_adj(model, hpxml_bldg, new_heater, loc_space, water_heating_system, unit_multiplier) - add_desuperheater(model, runner, water_heating_system, new_heater, loc_space, loc_schedule, loop, unit_multiplier) - - plantloop_map[water_heating_system.id] = loop + _u, ua, eta_c = disaggregate_tank_losses_and_burner_efficiency(act_vol, water_heating_system, solar_fraction, hpxml_bldg.building_construction.number_of_bedrooms) + water_heater = apply_water_heater(name: Constants::ObjectTypeWaterHeater, + water_heating_system: water_heating_system, + act_vol: act_vol, + t_set_c: t_set_c, + loc_space: loc_space, + loc_schedule: loc_schedule, + model: model, + runner: runner, + ua: ua, + eta_c: eta_c, + schedules_file: schedules_file, + unavailable_periods: unavailable_periods, + unit_multiplier: unit_multiplier) + + plant_loop.addSupplyBranchForComponent(water_heater) + + apply_ec_adj_program(model, hpxml_bldg, water_heater, loc_space, water_heating_system, unit_multiplier) + apply_desuperheater(model, runner, water_heating_system, water_heater, loc_space, loc_schedule, plant_loop, unit_multiplier) + + plantloop_map[water_heating_system.id] = plant_loop end - # TODO + # Adds a heat pump water heater to the OpenStudio model. # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings @@ -140,14 +138,13 @@ def self.apply_tankless(model, runner, spaces, hpxml_bldg, hpxml_header, water_h # @param unavailable_periods [HPXML::UnavailablePeriods] Object that defines periods for, e.g., power outages or vacancies # @param plantloop_map [Hash] Map of HPXML System ID => OpenStudio PlantLoop objects # @return [nil] - def self.apply_heatpump(model, runner, spaces, hpxml_bldg, hpxml_header, water_heating_system, schedules_file, unavailable_periods, plantloop_map) + def self.apply_hpwh(model, runner, spaces, hpxml_bldg, hpxml_header, water_heating_system, schedules_file, unavailable_periods, plantloop_map) loc_space, loc_schedule = Geometry.get_space_or_schedule_from_location(water_heating_system.location, model, spaces) unit_multiplier = hpxml_bldg.building_construction.number_of_units obj_name = Constants::ObjectTypeWaterHeater - conditioned_zone = spaces[HPXML::LocationConditionedSpace].thermalZone.get solar_fraction = get_water_heater_solar_fraction(water_heating_system, hpxml_bldg) t_set_c = get_t_set_c(water_heating_system.temperature, water_heating_system.water_heater_type) - loop = create_new_loop(model, t_set_c, hpxml_header.eri_calculation_version, unit_multiplier) + plant_loop = add_plant_loop(model, t_set_c, hpxml_header.eri_calculation_version, unit_multiplier) # Add in schedules for Tamb, RHamb, and the compressor hpwh_tamb = Model.add_schedule_constant( @@ -178,22 +175,22 @@ def self.apply_heatpump(model, runner, spaces, hpxml_bldg, hpxml_header, water_h limits: EPlus::ScheduleTypeLimitsTemperature ) - setpoint_schedule = nil + # To handle variable setpoints, need one schedule that gets sensed and a new schedule that gets actuated + sensed_setpoint_schedule = nil if not schedules_file.nil? - # To handle variable setpoints, need one schedule that gets sensed and a new schedule that gets actuated # Sensed schedule - setpoint_schedule = schedules_file.create_schedule_file(model, col_name: SchedulesFile::Columns[:WaterHeaterSetpoint].name, schedule_type_limits_name: EPlus::ScheduleTypeLimitsTemperature) - if not setpoint_schedule.nil? + sensed_setpoint_schedule = schedules_file.create_schedule_file(model, col_name: SchedulesFile::Columns[:WaterHeaterSetpoint].name, schedule_type_limits_name: EPlus::ScheduleTypeLimitsTemperature) + if not sensed_setpoint_schedule.nil? # Actuated schedule control_setpoint_schedule = ScheduleConstant.new(model, "#{obj_name} ControlSetpoint", 0.0, EPlus::ScheduleTypeLimitsTemperature, unavailable_periods: unavailable_periods) control_setpoint_schedule = control_setpoint_schedule.schedule end end - if setpoint_schedule.nil? - setpoint_schedule = ScheduleConstant.new(model, Constants::ObjectTypeWaterHeaterSetpoint, t_set_c, EPlus::ScheduleTypeLimitsTemperature, unavailable_periods: unavailable_periods) - setpoint_schedule = setpoint_schedule.schedule + if sensed_setpoint_schedule.nil? + sensed_setpoint_schedule = ScheduleConstant.new(model, Constants::ObjectTypeWaterHeaterSetpoint, t_set_c, EPlus::ScheduleTypeLimitsTemperature, unavailable_periods: unavailable_periods) + sensed_setpoint_schedule = sensed_setpoint_schedule.schedule - control_setpoint_schedule = setpoint_schedule + control_setpoint_schedule = sensed_setpoint_schedule else runner.registerWarning("Both '#{SchedulesFile::Columns[:WaterHeaterSetpoint].name}' schedule file and setpoint temperature provided; the latter will be ignored.") if !t_set_c.nil? end @@ -203,13 +200,13 @@ def self.apply_heatpump(model, runner, spaces, hpxml_bldg, hpxml_header, water_h max_temp = 120.0 # F # Coil:WaterHeating:AirToWaterHeatPump:Wrapped - coil = setup_hpwh_dxcoil(model, runner, water_heating_system, hpxml_bldg.elevation, obj_name, airflow_rate, unit_multiplier) + coil = apply_hpwh_dxcoil(model, runner, water_heating_system, hpxml_bldg.elevation, obj_name, airflow_rate, unit_multiplier) # WaterHeater:Stratified - tank = setup_hpwh_stratified_tank(model, water_heating_system, obj_name, solar_fraction, hpwh_tamb, bottom_element_sp, top_element_sp, unit_multiplier, hpxml_bldg.building_construction.number_of_bedrooms) - loop.addSupplyBranchForComponent(tank) + tank = apply_hpwh_stratified_tank(model, water_heating_system, obj_name, solar_fraction, hpwh_tamb, top_element_sp, bottom_element_sp, unit_multiplier, hpxml_bldg.building_construction.number_of_bedrooms) + plant_loop.addSupplyBranchForComponent(tank) - add_desuperheater(model, runner, water_heating_system, tank, loc_space, loc_schedule, loop, unit_multiplier) + apply_desuperheater(model, runner, water_heating_system, tank, loc_space, loc_schedule, plant_loop, unit_multiplier) # Fan:SystemModel fan_power = 0.0462 # W/cfm, Based on 1st gen AO Smith HPWH, could be updated but pretty minor impact @@ -224,30 +221,33 @@ def self.apply_heatpump(model, runner, spaces, hpxml_bldg, hpxml_header, water_h fan.additionalProperties.setFeature('ObjectType', Constants::ObjectTypeWaterHeater) # Used by reporting measure # WaterHeater:HeatPump:WrappedCondenser - hpwh = setup_hpwh_wrapped_condenser(model, obj_name, coil, tank, fan, airflow_rate, hpwh_tamb, hpwh_rhamb, min_temp, max_temp, control_setpoint_schedule, unit_multiplier) + hpwh = apply_hpwh_wrapped_condenser(model, obj_name, coil, tank, fan, airflow_rate, hpwh_tamb, hpwh_rhamb, min_temp, max_temp, control_setpoint_schedule, unit_multiplier) # Amb temp & RH sensors, temp sensor shared across programs - amb_temp_sensor, amb_rh_sensors = get_loc_temp_rh_sensors(model, obj_name, loc_space, loc_schedule, conditioned_zone) - hpwh_inlet_air_program = add_hpwh_inlet_air_and_zone_heat_gain_program(model, obj_name, loc_space, hpwh_tamb, hpwh_rhamb, tank, coil, fan, amb_temp_sensor, amb_rh_sensors, unit_multiplier) + amb_temp_sensor, amb_rh_sensors = apply_hpwh_loc_temp_rh_sensors(model, obj_name, loc_space, loc_schedule, spaces) + hpwh_zone_heat_gain_program = apply_hpwh_zone_heat_gain_program(model, obj_name, loc_space, hpwh_tamb, hpwh_rhamb, tank, coil, fan, amb_temp_sensor, amb_rh_sensors, unit_multiplier) # EMS for the HPWH control logic - op_mode = water_heating_system.operating_mode - hpwh_ctrl_program = add_hpwh_control_program(model, runner, obj_name, amb_temp_sensor, top_element_sp, bottom_element_sp, min_temp, max_temp, op_mode, setpoint_schedule, control_setpoint_schedule, schedules_file) + hpwh_ctrl_program = apply_hpwh_control_program(model, runner, obj_name, water_heating_system, amb_temp_sensor, top_element_sp, bottom_element_sp, min_temp, max_temp, sensed_setpoint_schedule, control_setpoint_schedule, schedules_file) # ProgramCallingManagers Model.add_ems_program_calling_manager( model, name: "#{obj_name} ProgramManager", calling_point: 'InsideHVACSystemIterationLoop', - ems_programs: [hpwh_ctrl_program, hpwh_inlet_air_program] + ems_programs: [hpwh_ctrl_program, hpwh_zone_heat_gain_program] ) - add_ec_adj(model, hpxml_bldg, hpwh, loc_space, water_heating_system, unit_multiplier) + apply_ec_adj_program(model, hpxml_bldg, hpwh, loc_space, water_heating_system, unit_multiplier) - plantloop_map[water_heating_system.id] = loop + plantloop_map[water_heating_system.id] = plant_loop end - # TODO + # Adds a combination boiler water heater to the OpenStudio model. + # + # Note that we model two separate boilers (one for just space heating and one for just water heating). + # This makes the code much simpler and makes the EnergyPlus results more robust. + # # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings @@ -278,35 +278,35 @@ def self.apply_combi(model, runner, spaces, hpxml_bldg, hpxml_header, water_heat act_vol = calc_storage_tank_actual_vol(water_heating_system.tank_volume, nil) a_side = calc_tank_areas(act_vol)[1] - ua = calc_indirect_ua_with_standbyloss(act_vol, water_heating_system, a_side, solar_fraction, hpxml_bldg.building_construction.number_of_bedrooms) + ua = calc_combi_tank_losses(act_vol, water_heating_system, a_side, solar_fraction, hpxml_bldg.building_construction.number_of_bedrooms) else ua = 0.0 act_vol = 1.0 end t_set_c = get_t_set_c(water_heating_system.temperature, water_heating_system.water_heater_type) - loop = create_new_loop(model, t_set_c, hpxml_header.eri_calculation_version, unit_multiplier) + plant_loop = add_plant_loop(model, t_set_c, hpxml_header.eri_calculation_version, unit_multiplier) # Create water heater - new_heater = create_new_heater(name: obj_name_combi, - water_heating_system: water_heating_system, - act_vol: act_vol, - t_set_c: t_set_c, - loc_space: loc_space, - loc_schedule: loc_schedule, - model: model, - runner: runner, - ua: ua, - is_combi: true, - schedules_file: schedules_file, - unavailable_periods: unavailable_periods, - unit_multiplier: unit_multiplier) - new_heater.setSourceSideDesignFlowRate(100 * unit_multiplier) # set one large number, override by EMS + water_heater = apply_water_heater(name: obj_name_combi, + water_heating_system: water_heating_system, + act_vol: act_vol, + t_set_c: t_set_c, + loc_space: loc_space, + loc_schedule: loc_schedule, + model: model, + runner: runner, + ua: ua, + is_combi: true, + schedules_file: schedules_file, + unavailable_periods: unavailable_periods, + unit_multiplier: unit_multiplier) + water_heater.setSourceSideDesignFlowRate(100 * unit_multiplier) # set one large number, override by EMS # Create alternate setpoint schedule for source side flow request - alternate_stp_sch = new_heater.setpointTemperatureSchedule.get.clone(model).to_Schedule.get + alternate_stp_sch = water_heater.setpointTemperatureSchedule.get.clone(model).to_Schedule.get alternate_stp_sch.setName("#{obj_name_combi} Alt Spt") - new_heater.setIndirectAlternateSetpointTemperatureSchedule(alternate_stp_sch) + water_heater.setIndirectAlternateSetpointTemperatureSchedule(alternate_stp_sch) # Create setpoint schedule to specify source side temperature # tank source side inlet temperature, degree C @@ -324,28 +324,31 @@ def self.apply_combi(model, runner, spaces, hpxml_bldg, hpxml_header, water_heat # change loop equipment operation scheme to heating load scheme_dhw = OpenStudio::Model::PlantEquipmentOperationHeatingLoad.new(model) - scheme_dhw.addEquipment(1000000000, new_heater) - loop.setPrimaryPlantEquipmentOperationScheme(scheme_dhw) + scheme_dhw.addEquipment(1000000000, water_heater) + plant_loop.setPrimaryPlantEquipmentOperationScheme(scheme_dhw) # Add dhw boiler to the load distribution scheme scheme = OpenStudio::Model::PlantEquipmentOperationHeatingLoad.new(model) scheme.addEquipment(1000000000, boiler) boiler_plant_loop.setPrimaryPlantEquipmentOperationScheme(scheme) - boiler_plant_loop.addDemandBranchForComponent(new_heater) + boiler_plant_loop.addDemandBranchForComponent(water_heater) boiler_plant_loop.setPlantLoopVolume(0.001 * unit_multiplier) # Cannot be auto-calculated because of large default tank source side mfr(set to be overwritten by EMS) - loop.addSupplyBranchForComponent(new_heater) + plant_loop.addSupplyBranchForComponent(water_heater) - add_ec_adj(model, hpxml_bldg, new_heater, loc_space, water_heating_system, unit_multiplier, boiler) + apply_ec_adj_program(model, hpxml_bldg, water_heater, loc_space, water_heating_system, unit_multiplier, boiler) - plantloop_map[water_heating_system.id] = loop + plantloop_map[water_heating_system.id] = plant_loop end - # TODO + # Calculates the water heating energy consumption adjustment factor (EC_adj) due to the effectiveness of + # the hot water distribution system. + # + # Source: ANSI/RESNET/ICC 301 # # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit # @param water_heating_system [HPXML::WaterHeatingSystem] The HPXML water heating system of interest - # @return [TODO] TODO + # @return [Double] Hot water energy consumption multiplier def self.get_dist_energy_consumption_adjustment(hpxml_bldg, water_heating_system) if water_heating_system.fraction_dhw_load_served <= 0 # No fixtures; not accounting for distribution system @@ -359,9 +362,7 @@ def self.get_dist_energy_consumption_adjustment(hpxml_bldg, water_heating_system cfa = hpxml_bldg.building_construction.conditioned_floor_area ncfl = hpxml_bldg.building_construction.number_of_conditioned_floors - # ANSI/RESNET 301-2014 Addendum A-2015 - # Amendment on Domestic Hot Water (DHW) Systems - # Eq. 4.2-16 + # ANSI/RESNET/ICC 301-2022 Eq. 4.2-44 ew_fact = get_dist_energy_waste_factor(hot_water_distribution) o_frac = 0.25 # fraction of hot water waste from standard operating conditions oew_fact = ew_fact * o_frac # standard operating condition portion of hot water energy waste @@ -378,14 +379,14 @@ def self.get_dist_energy_consumption_adjustment(hpxml_bldg, water_heating_system return (e_waste + 128.0) / 160.0 end - # TODO + # Retrieves the hot water distribution system relative annual energy waste factors. # - # @param hot_water_distribution [TODO] TODO - # @return [TODO] TODO + # Source: ANSI/RESNET/ICC 301 + # + # @param hot_water_distribution [HPXML::HotWaterDistribution] The HPXML hot water distribution system of interest + # @return [Double] Energy waste factor def self.get_dist_energy_waste_factor(hot_water_distribution) - # ANSI/RESNET 301-2014 Addendum A-2015 - # Amendment on Domestic Hot Water (DHW) Systems - # Table 4.2.2.5.2.11(6) Hot water distribution system relative annual energy waste factors + # ANSI/RESNET/ICC 301-2022 Table 4.2.2.7.2.11(6) if hot_water_distribution.system_type == HPXML::DHWDistTypeRecirc case hot_water_distribution.recirculation_control_type when HPXML::DHWRecircControlTypeNone, HPXML::DHWRecircControlTypeTimer @@ -423,7 +424,9 @@ def self.get_dist_energy_waste_factor(hot_water_distribution) fail 'Unexpected hot water distribution system.' end - # TODO + # Adds an EMS program to control the boiler operation based on domestic hot water demand. + # The program modulates the source side mass flow rate to achieve better control and accuracy + # compared to not having the EMS program. See https://github.com/NREL/OpenStudio-HPXML/pull/225. # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param water_heating_systems [Array] The HPXML water heaters of interest @@ -595,7 +598,7 @@ def self.apply_combi_system_EMS(model, water_heating_systems, plantloop_map) end end - # TODO + # Adds an HPXML Solar Thermal System to the OpenStudio model. # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects @@ -827,7 +830,7 @@ def self.apply_solar_thermal(model, spaces, hpxml_bldg, plantloop_map) storage_tank.setHeaterFuelType(EPlus::FuelTypeElectricity) storage_tank.setHeaterThermalEfficiency(1) storage_tank.ambientTemperatureSchedule.get.remove - set_wh_ambient(loc_space, loc_schedule, storage_tank) + apply_ambient_temperature(loc_space, loc_schedule, storage_tank) storage_tank.setSkinLossFractiontoZone(1.0 / unit_multiplier) # Tank losses are multiplied by E+ zone multiplier, so need to compensate here storage_tank.setOffCycleFlueLossFractiontoZone(1.0 / unit_multiplier) storage_tank.setUseSideEffectiveness(1) @@ -841,7 +844,7 @@ def self.apply_solar_thermal(model, spaces, hpxml_bldg, plantloop_map) storage_tank.setOnCycleParasiticFuelConsumptionRate(0) storage_tank.setOffCycleParasiticFuelConsumptionRate(0) storage_tank.setUseSideDesignFlowRate(UnitConversions.convert(storage_volume, 'gal', 'm^3') / 60.1) # Sized to ensure that E+ never autosizes the design flow rate to be larger than the tank volume getting drawn out in a hour (60 minutes) - set_stratified_tank_ua(storage_tank, u_tank, unit_multiplier) + apply_stratified_tank_losses(storage_tank, u_tank, unit_multiplier) storage_tank.additionalProperties.setFeature('HPXML_ID', solar_thermal_system.water_heating_system.id) # Used by reporting measure storage_tank.additionalProperties.setFeature('ObjectType', Constants::ObjectTypeSolarHotWater) # Used by reporting measure @@ -900,23 +903,23 @@ def self.apply_solar_thermal(model, spaces, hpxml_bldg, plantloop_map) ) end - # TODO + # Adds a WaterHeaterHeatPumpWrappedCondenser object for the HPWH to the OpenStudio model. # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param obj_name [String] Name for the OpenStudio object - # @param coil [TODO] TODO - # @param tank [TODO] TODO - # @param fan [TODO] TODO - # @param airflow_rate [TODO] TODO - # @param hpwh_tamb [TODO] TODO - # @param hpwh_rhamb [TODO] TODO - # @param min_temp [TODO] TODO - # @param max_temp [TODO] TODO - # @param setpoint_schedule [TODO] TODO + # @param coil [OpenStudio::Model::CoilWaterHeatingAirToWaterHeatPumpWrapped] The HPWH DX coil + # @param tank [OpenStudio::Model::WaterHeaterStratified] The HPWH storage tank + # @param fan [OpenStudio::Model::FanSystemModel] The HPWH fan + # @param airflow_rate [Double] HPWH fan airflow rate (cfm) + # @param hpwh_tamb [OpenStudio::Model::ScheduleConstant] HPWH ambient temperature (C) + # @param hpwh_rhamb [OpenStudio::Model::ScheduleConstant] HPWH ambient relative humidity + # @param min_temp [Double] Minimum temperature for compressor operation (F) + # @param max_temp [Double] Maximum temperature for compressor operation (F) + # @param control_setpoint_schedule [OpenStudio::Model::ScheduleConstant or OpenStudio::Model::ScheduleRuleset] Setpoint temperature schedule (controlled) # @param unit_multiplier [Integer] Number of similar dwelling units - # @return [TODO] TODO - def self.setup_hpwh_wrapped_condenser(model, obj_name, coil, tank, fan, airflow_rate, hpwh_tamb, hpwh_rhamb, min_temp, max_temp, setpoint_schedule, unit_multiplier) - hpwh = OpenStudio::Model::WaterHeaterHeatPumpWrappedCondenser.new(model, coil, tank, fan, setpoint_schedule, model.alwaysOnDiscreteSchedule) + # @return [OpenStudio::Model::WaterHeaterHeatPumpWrappedCondenser] The HPWH object + def self.apply_hpwh_wrapped_condenser(model, obj_name, coil, tank, fan, airflow_rate, hpwh_tamb, hpwh_rhamb, min_temp, max_temp, control_setpoint_schedule, unit_multiplier) + hpwh = OpenStudio::Model::WaterHeaterHeatPumpWrappedCondenser.new(model, coil, tank, fan, control_setpoint_schedule, model.alwaysOnDiscreteSchedule) hpwh.setName("#{obj_name} hpwh") hpwh.setDeadBandTemperatureDifference(3.89) hpwh.setCondenserBottomLocation((1.0 - (12 - 0.5) / 12.0) * tank.tankHeight.get) # in the 12th node of a 12-node tank (counting from top) @@ -941,17 +944,17 @@ def self.setup_hpwh_wrapped_condenser(model, obj_name, coil, tank, fan, airflow_ return hpwh end - # TODO + # Adds a CoilWaterHeatingAirToWaterHeatPumpWrapped object for the HPWH to the OpenStudio model. # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param water_heating_system [HPXML::WaterHeatingSystem] The HPXML water heating system of interest # @param elevation [Double] Elevation of the building site (ft) # @param obj_name [String] Name for the OpenStudio object - # @param airflow_rate [TODO] TODO + # @param airflow_rate [Double] HPWH fan airflow rate (cfm) # @param unit_multiplier [Integer] Number of similar dwelling units - # @return [TODO] TODO - def self.setup_hpwh_dxcoil(model, runner, water_heating_system, elevation, obj_name, airflow_rate, unit_multiplier) + # @return [OpenStudio::Model::CoilWaterHeatingAirToWaterHeatPumpWrapped] The HPWH DX coil + def self.apply_hpwh_dxcoil(model, runner, water_heating_system, elevation, obj_name, airflow_rate, unit_multiplier) # Curves hpwh_cap = Model.add_curve_biquadratic( model, @@ -1028,19 +1031,19 @@ def self.set_heat_pump_cop(water_heating_system) water_heating_system.additional_properties.cop = cop end - # TODO + # Adds a WaterHeaterStratified object for the HPWH to the OpenStudio model. # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param water_heating_system [HPXML::WaterHeatingSystem] The HPXML water heating system of interest # @param obj_name [String] Name for the OpenStudio object - # @param solar_fraction [TODO] TODO - # @param hpwh_tamb [TODO] TODO - # @param hpwh_bottom_element_sp [TODO] TODO - # @param hpwh_top_element_sp [TODO] TODO + # @param solar_fraction [Double] Portion of hot water load served by an attached solar thermal system + # @param hpwh_tamb [OpenStudio::Model::ScheduleConstant] HPWH ambient temperature (C) + # @param hpwh_top_element_sp [OpenStudio::Model::ScheduleConstant] HPWH top element setpoint schedule + # @param hpwh_bottom_element_sp [OpenStudio::Model::ScheduleConstant] HPWH bottom element setpoint schedule # @param unit_multiplier [Integer] Number of similar dwelling units # @param nbeds [Integer] Number of bedrooms in the dwelling unit - # @return [TODO] TODO - def self.setup_hpwh_stratified_tank(model, water_heating_system, obj_name, solar_fraction, hpwh_tamb, hpwh_bottom_element_sp, hpwh_top_element_sp, unit_multiplier, nbeds) + # @return [OpenStudio::Model::WaterHeaterStratified] The HPWH storage tank + def self.apply_hpwh_stratified_tank(model, water_heating_system, obj_name, solar_fraction, hpwh_tamb, hpwh_top_element_sp, hpwh_bottom_element_sp, unit_multiplier, nbeds) h_tank = 0.0188 * water_heating_system.tank_volume + 0.0935 # m; Linear relationship that gets GE height at 50 gal and AO Smith height at 80 gal # Calculate some geometry parameters for UA, the location of sensors and heat sources in the tank @@ -1099,21 +1102,23 @@ def self.setup_hpwh_stratified_tank(model, water_heating_system, obj_name, solar tank.setSourceSideOutletHeight(0) tank.setSkinLossFractiontoZone(1.0 / unit_multiplier) # Tank losses are multiplied by E+ zone multiplier, so need to compensate here tank.setOffCycleFlueLossFractiontoZone(1.0 / unit_multiplier) - set_stratified_tank_ua(tank, u_tank, unit_multiplier) + apply_stratified_tank_losses(tank, u_tank, unit_multiplier) tank.additionalProperties.setFeature('HPXML_ID', water_heating_system.id) # Used by reporting measure return tank end - # TODO + # Adds EMS sensors for the HPWH ambient temperature and ambient relative humidity. # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param obj_name [String] Name for the OpenStudio object # @param loc_space [OpenStudio::Model::Space] The space where the water heater is located - # @param loc_schedule [OpenStudio::Model::ScheduleConstant] The temperature schedule for where the water heater is located, if not in a space - # @param conditioned_zone [TODO] TODO - # @return [TODO] TODO - def self.get_loc_temp_rh_sensors(model, obj_name, loc_space, loc_schedule, conditioned_zone) + # @param loc_schedule [OpenStudio::Model::ScheduleConstant] The temperature schedule, if not located in a space + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @return [Array>] HPWH ambient temperature sensor, One or more HPWH ambient RH sensors + def self.apply_hpwh_loc_temp_rh_sensors(model, obj_name, loc_space, loc_schedule, spaces) + conditioned_zone = spaces[HPXML::LocationConditionedSpace].thermalZone.get + rh_sensors = [] if not loc_schedule.nil? amb_temp_sensor = Model.add_ems_sensor( @@ -1183,21 +1188,21 @@ def self.get_loc_temp_rh_sensors(model, obj_name, loc_space, loc_schedule, condi return amb_temp_sensor, rh_sensors end - # TODO + # Adds an EMS program to add sensible/latent gains to the thermal zone where the HPWH is located. # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param obj_name [String] Name for the OpenStudio object # @param loc_space [OpenStudio::Model::Space] The space where the water heater is located - # @param hpwh_tamb [TODO] TODO - # @param hpwh_rhamb [TODO] TODO - # @param tank [TODO] TODO - # @param coil [TODO] TODO - # @param fan [TODO] TODO - # @param amb_temp_sensor [TODO] TODO - # @param amb_rh_sensors [TODO] TODO + # @param hpwh_tamb [OpenStudio::Model::ScheduleConstant] HPWH ambient temperature (C) + # @param hpwh_rhamb [OpenStudio::Model::ScheduleConstant] HPWH ambient relative humidity + # @param tank [OpenStudio::Model::WaterHeaterStratified] The HPWH storage tank + # @param coil [OpenStudio::Model::CoilWaterHeatingAirToWaterHeatPumpWrapped] The HPWH DX coil + # @param fan [OpenStudio::Model::FanSystemModel] The HPWH fan + # @param amb_temp_sensor [OpenStudio::Model::EnergyManagementSystemSensor] HPWH ambient temperature sensor + # @param amb_rh_sensors [Array] One or more HPWH ambient RH sensors # @param unit_multiplier [Integer] Number of similar dwelling units - # @return [TODO] TODO - def self.add_hpwh_inlet_air_and_zone_heat_gain_program(model, obj_name, loc_space, hpwh_tamb, hpwh_rhamb, tank, coil, fan, amb_temp_sensor, amb_rh_sensors, unit_multiplier) + # @return [OpenStudio::Model::EnergyManagementSystemProgram] The HPWH heat gain program + def self.apply_hpwh_zone_heat_gain_program(model, obj_name, loc_space, hpwh_tamb, hpwh_rhamb, tank, coil, fan, amb_temp_sensor, amb_rh_sensors, unit_multiplier) # EMS Actuators: Inlet T & RH, sensible and latent gains to the space tamb_act_actuator = Model.add_ems_actuator( name: "#{obj_name} Tamb act", @@ -1279,41 +1284,41 @@ def self.add_hpwh_inlet_air_and_zone_heat_gain_program(model, obj_name, loc_spac key_name: fan.name ) - hpwh_inlet_air_program = Model.add_ems_program( + hpwh_zone_heat_gain_program = Model.add_ems_program( model, name: "#{obj_name} InletAir" ) - hpwh_inlet_air_program.addLine("Set #{tamb_act_actuator.name} = #{amb_temp_sensor.name}") + hpwh_zone_heat_gain_program.addLine("Set #{tamb_act_actuator.name} = #{amb_temp_sensor.name}") # Average relative humidity for mf spaces: other multifamily buffer space & other heated space - hpwh_inlet_air_program.addLine("Set #{rhamb_act_actuator.name} = 0") + hpwh_zone_heat_gain_program.addLine("Set #{rhamb_act_actuator.name} = 0") amb_rh_sensors.each do |amb_rh_sensor| - hpwh_inlet_air_program.addLine("Set #{rhamb_act_actuator.name} = #{rhamb_act_actuator.name} + (#{amb_rh_sensor.name} / 100) / #{amb_rh_sensors.size}") + hpwh_zone_heat_gain_program.addLine("Set #{rhamb_act_actuator.name} = #{rhamb_act_actuator.name} + (#{amb_rh_sensor.name} / 100) / #{amb_rh_sensors.size}") end if not loc_space.nil? # Sensible/latent heat gain to the space # Tank losses are multiplied by E+ zone multiplier, so need to compensate here - hpwh_inlet_air_program.addLine("Set #{sens_act_actuator.name} = (0 - #{sens_cool_sensor.name} - (#{tl_sensor.name} + #{fan_power_sensor.name})) / #{unit_multiplier}") - hpwh_inlet_air_program.addLine("Set #{lat_act_actuator.name} = (0 - #{lat_cool_sensor.name}) / #{unit_multiplier}") + hpwh_zone_heat_gain_program.addLine("Set #{sens_act_actuator.name} = (0 - #{sens_cool_sensor.name} - (#{tl_sensor.name} + #{fan_power_sensor.name})) / #{unit_multiplier}") + hpwh_zone_heat_gain_program.addLine("Set #{lat_act_actuator.name} = (0 - #{lat_cool_sensor.name}) / #{unit_multiplier}") end - return hpwh_inlet_air_program + return hpwh_zone_heat_gain_program end - # TODO + # Adds an EMS program to control the HPWH upper and lower elements. # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param obj_name [String] Name for the OpenStudio object - # @param amb_temp_sensor [TODO] TODO - # @param hpwh_top_element_sp [TODO] TODO - # @param hpwh_bottom_element_sp [TODO] TODO - # @param min_temp [TODO] TODO - # @param max_temp [TODO] TODO - # @param op_mode [TODO] TODO - # @param setpoint_schedule [TODO] TODO - # @param control_setpoint_schedule [TODO] TODO + # @param water_heating_system [HPXML::WaterHeatingSystem] The HPXML water heating system of interest + # @param amb_temp_sensor [OpenStudio::Model::EnergyManagementSystemSensor] HPWH ambient temperature sensor + # @param hpwh_top_element_sp [OpenStudio::Model::ScheduleConstant] HPWH top element setpoint schedule + # @param hpwh_bottom_element_sp [OpenStudio::Model::ScheduleConstant] HPWH bottom element setpoint schedule + # @param min_temp [Double] Minimum temperature for compressor operation (F) + # @param max_temp [Double] Maximum temperature for compressor operation (F) + # @param sensted_setpoint_schedule [OpenStudio::Model::ScheduleConstant or OpenStudio::Model::ScheduleRuleset] Setpoint temperature schedule (sensed) + # @param control_setpoint_schedule [OpenStudio::Model::ScheduleConstant or OpenStudio::Model::ScheduleRuleset] Setpoint temperature schedule (controlled) # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files - # @return [TODO] TODO - def self.add_hpwh_control_program(model, runner, obj_name, amb_temp_sensor, hpwh_top_element_sp, hpwh_bottom_element_sp, min_temp, max_temp, op_mode, setpoint_schedule, control_setpoint_schedule, schedules_file) + # @return [OpenStudio::Model::EnergyManagementSystemProgram] The HPWH control program + def self.apply_hpwh_control_program(model, runner, obj_name, water_heating_system, amb_temp_sensor, hpwh_top_element_sp, hpwh_bottom_element_sp, min_temp, max_temp, sensted_setpoint_schedule, control_setpoint_schedule, schedules_file) # Lower element is enabled if the ambient air temperature prevents the HP from running leschedoverride_actuator = Model.add_ems_actuator( name: "#{obj_name} LESchedOverride", @@ -1345,7 +1350,7 @@ def self.add_hpwh_control_program(model, runner, obj_name, amb_temp_sensor, hpwh model, name: "#{obj_name} T_set", output_var_or_meter_name: 'Schedule Value', - key_name: setpoint_schedule.name + key_name: sensted_setpoint_schedule.name ) op_mode_schedule = nil @@ -1375,42 +1380,42 @@ def self.add_hpwh_control_program(model, runner, obj_name, amb_temp_sensor, hpwh ) hpwh_ctrl_program.addLine("Set #{hpwhschedoverride_actuator.name} = #{t_set_sensor.name}") # If in HP only mode: still enable elements if ambient temperature is out of bounds, otherwise disable elements - if op_mode == HPXML::WaterHeaterOperatingModeHeatPumpOnly + if water_heating_system.operating_mode == HPXML::WaterHeaterOperatingModeHeatPumpOnly hpwh_ctrl_program.addLine("If (#{amb_temp_sensor.name}<#{min_temp_c}) || (#{amb_temp_sensor.name}>#{max_temp_c})") - hpwh_ctrl_program.addLine("Set #{leschedoverride_actuator.name} = #{t_set_sensor.name}") - hpwh_ctrl_program.addLine("Set #{ueschedoverride_actuator.name} = #{t_set_sensor.name}") + hpwh_ctrl_program.addLine(" Set #{leschedoverride_actuator.name} = #{t_set_sensor.name}") + hpwh_ctrl_program.addLine(" Set #{ueschedoverride_actuator.name} = #{t_set_sensor.name}") hpwh_ctrl_program.addLine('Else') - hpwh_ctrl_program.addLine("Set #{leschedoverride_actuator.name} = 0") - hpwh_ctrl_program.addLine("Set #{ueschedoverride_actuator.name} = 0") + hpwh_ctrl_program.addLine(" Set #{leschedoverride_actuator.name} = 0") + hpwh_ctrl_program.addLine(" Set #{ueschedoverride_actuator.name} = 0") hpwh_ctrl_program.addLine('EndIf') else # First, check if ambient temperature is out of bounds for HP operation, if so enable lower element hpwh_ctrl_program.addLine("If (#{amb_temp_sensor.name}<#{min_temp_c}) || (#{amb_temp_sensor.name}>#{max_temp_c})") - hpwh_ctrl_program.addLine("Set #{ueschedoverride_actuator.name} = #{t_set_sensor.name}") - hpwh_ctrl_program.addLine("Set #{leschedoverride_actuator.name} = #{t_set_sensor.name}") + hpwh_ctrl_program.addLine(" Set #{ueschedoverride_actuator.name} = #{t_set_sensor.name}") + hpwh_ctrl_program.addLine(" Set #{leschedoverride_actuator.name} = #{t_set_sensor.name}") hpwh_ctrl_program.addLine('Else') - hpwh_ctrl_program.addLine("Set #{ueschedoverride_actuator.name} = #{t_set_sensor.name} - #{t_offset}") - hpwh_ctrl_program.addLine("Set #{leschedoverride_actuator.name} = 0") + hpwh_ctrl_program.addLine(" Set #{ueschedoverride_actuator.name} = #{t_set_sensor.name} - #{t_offset}") + hpwh_ctrl_program.addLine(" Set #{leschedoverride_actuator.name} = 0") hpwh_ctrl_program.addLine('EndIf') # Scheduled operating mode: if in HP only mode, disable both elements (this will override prior logic) if not op_mode_schedule.nil? hpwh_ctrl_program.addLine("If #{op_mode_sensor.name} == 1") - hpwh_ctrl_program.addLine("Set #{ueschedoverride_actuator.name} = 0") + hpwh_ctrl_program.addLine(" Set #{ueschedoverride_actuator.name} = 0") hpwh_ctrl_program.addLine('Else') - hpwh_ctrl_program.addLine("Set #{ueschedoverride_actuator.name} = #{t_set_sensor.name} - #{t_offset}") + hpwh_ctrl_program.addLine(" Set #{ueschedoverride_actuator.name} = #{t_set_sensor.name} - #{t_offset}") hpwh_ctrl_program.addLine('EndIf') end end return hpwh_ctrl_program end - # TODO + # Sets the tank losses for a stratified water heater tank. # - # @param tank [TODO] TODO - # @param u_tank [TODO] TODO + # @param tank [OpenStudio::Model::WaterHeaterStratified] The stratified tank + # @param u_tank [Double] The heat loss U-factor (Btu/hr-ft^2-F) # @param unit_multiplier [Integer] Number of similar dwelling units # @return [nil] - def self.set_stratified_tank_ua(tank, u_tank, unit_multiplier) + def self.apply_stratified_tank_losses(tank, u_tank, unit_multiplier) node_ua = [0] * 12 # Max number of nodes in E+ stratified tank model if unit_multiplier == 1 tank.setUniformSkinLossCoefficientperUnitAreatoAmbientTemperature(u_tank) @@ -1452,11 +1457,11 @@ def self.set_stratified_tank_ua(tank, u_tank, unit_multiplier) tank.setNode12AdditionalLossCoefficient(node_ua[11]) end - # TODO + # Gets the combination boiler and plant loop OpenStudio objects associated with the HPXML water heating system. # # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param heating_source_id [TODO] TODO - # @return [TODO] TODO + # @param heating_source_id [String] ID of the HPXML water heating system + # @return [Array] Boiler object, PlantLoop object def self.get_combi_boiler_and_plant_loop(model, heating_source_id) # Search for the right boiler OS object boiler_hw = nil @@ -1482,38 +1487,34 @@ def self.get_combi_boiler_and_plant_loop(model, heating_source_id) return boiler_hw, plant_loop_hw end - # TODO + # Adds a desuperheater for the given HPXML water heating system to the OpenStudio model. # - # @param water_heating_system [HPXML::WaterHeatingSystem] The HPXML water heating system of interest # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @return [TODO] TODO - def self.get_desuperheatercoil(water_heating_system, model) + # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings + # @param water_heating_system [HPXML::WaterHeatingSystem] The HPXML water heating system of interest + # @param tank [OpenStudio::Model::WaterHeaterMixed or OpenStudio::Model::WaterHeaterStratified] The water heater tank + # @param loc_space [OpenStudio::Model::Space] The space where the water heater is located + # @param loc_schedule [OpenStudio::Model::ScheduleConstant] The temperature schedule, if not located in a space + # @param plant_loop [OpenStudio::Model::PlantLoop] The DHW plant loop + # @param unit_multiplier [Integer] Number of similar dwelling units + # @return [nil] + def self.apply_desuperheater(model, runner, water_heating_system, tank, loc_space, loc_schedule, plant_loop, unit_multiplier) + return unless water_heating_system.uses_desuperheater + + # Get the HVAC cooling coil for the desuperheater + desuperheater_clg_coil = nil (model.getCoilCoolingDXSingleSpeeds + model.getCoilCoolingDXMultiSpeeds + model.getCoilCoolingWaterToAirHeatPumpEquationFits).each do |clg_coil| sys_id = clg_coil.additionalProperties.getFeatureAsString('HPXML_ID') if sys_id.is_initialized && sys_id.get == water_heating_system.related_hvac_idref - return clg_coil + desuperheater_clg_coil = clg_coil end end - fail "RelatedHVACSystem '#{water_heating_system.related_hvac_idref}' for water heating system '#{water_heating_system.id}' is not currently supported for desuperheaters." - end - - # TODO - # - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings - # @param water_heating_system [HPXML::WaterHeatingSystem] The HPXML water heating system of interest - # @param tank [TODO] TODO - # @param loc_space [OpenStudio::Model::Space] The space where the water heater is located - # @param loc_schedule [OpenStudio::Model::ScheduleConstant] The temperature schedule for where the water heater is located, if not in a space - # @param loop [TODO] TODO - # @param unit_multiplier [Integer] Number of similar dwelling units - # @return [TODO] TODO - def self.add_desuperheater(model, runner, water_heating_system, tank, loc_space, loc_schedule, loop, unit_multiplier) - return unless water_heating_system.uses_desuperheater + if desuperheater_clg_coil.nil? + fail "RelatedHVACSystem '#{water_heating_system.related_hvac_idref}' for water heating system '#{water_heating_system.id}' is not currently supported for desuperheaters." + end - desuperheater_clg_coil = get_desuperheatercoil(water_heating_system, model) reclaimed_efficiency = 0.25 # default desuperheater_name = "#{tank.name} desuperheater" @@ -1522,25 +1523,26 @@ def self.add_desuperheater(model, runner, water_heating_system, tank, loc_space, storage_vol_actual = calc_storage_tank_actual_vol(vol, nil) assumed_ua = 6.0 # Btu/hr-F, tank ua calculated based on 1.0 standby_loss and 50gal nominal vol storage_tank_name = "#{tank.name} storage tank" - # reduce tank setpoint to enable desuperheater setpoint at t_set + if water_heating_system.temperature.nil? fail "Detailed setpoints for water heating system '#{water_heating_system.id}' is not currently supported for desuperheaters." else - tank_setpoint = get_t_set_c(water_heating_system.temperature - 5.0, HPXML::WaterHeaterTypeStorage) + # reduce tank setpoint to enable desuperheater setpoint at t_set + t_set_c = get_t_set_c(water_heating_system.temperature - 5.0, HPXML::WaterHeaterTypeStorage) end - storage_tank = create_new_heater(name: storage_tank_name, - act_vol: storage_vol_actual, - t_set_c: tank_setpoint, - loc_space: loc_space, - loc_schedule: loc_schedule, - model: model, - runner: runner, - ua: assumed_ua, - is_dsh_storage: true, - unit_multiplier: unit_multiplier) - - loop.addSupplyBranchForComponent(storage_tank) + storage_tank = apply_water_heater(name: storage_tank_name, + act_vol: storage_vol_actual, + t_set_c: t_set_c, + loc_space: loc_space, + loc_schedule: loc_schedule, + model: model, + runner: runner, + ua: assumed_ua, + is_dsh_storage: true, + unit_multiplier: unit_multiplier) + + plant_loop.addSupplyBranchForComponent(storage_tank) tank.addToNode(storage_tank.supplyOutletModelObject.get.to_Node.get) # Create a schedule for desuperheater @@ -1553,27 +1555,28 @@ def self.add_desuperheater(model, runner, water_heating_system, tank, loc_space, limits: EPlus::ScheduleTypeLimitsTemperature ) - # create a desuperheater object + # Create desuperheater object desuperheater = OpenStudio::Model::CoilWaterHeatingDesuperheater.new(model, new_schedule) desuperheater.setName(desuperheater_name) desuperheater.setMaximumInletWaterTemperatureforHeatReclaim(100) desuperheater.setDeadBandTemperatureDifference(0.2) desuperheater.setRatedHeatReclaimRecoveryEfficiency(reclaimed_efficiency) desuperheater.addToHeatRejectionTarget(storage_tank) - # FUTURE: Desuperheater pump power? - desuperheater.setWaterPumpPower(0) - # attach to the clg coil source + desuperheater.setWaterPumpPower(0) # FUTURE: Desuperheater pump power? desuperheater.setHeatingSource(desuperheater_clg_coil) desuperheater.setWaterFlowRate(0.0001 * unit_multiplier) desuperheater.additionalProperties.setFeature('HPXML_ID', water_heating_system.id) # Used by reporting measure end - # TODO + # Calculates an Energy Factor (EF) from a Uniform Energy Factor (UEF). + # + # Source: RESNET's Interpretation on Water Heater UEF + # https://www.resnet.us/wp-content/uploads/Interpretation-301-2014-012-Water-Heater-UEF.pdf + # Note that this is a regression based on products on the market, not a conversion. # # @param water_heating_system [HPXML::WaterHeatingSystem] The HPXML water heating system of interest - # @return [TODO] TODO + # @return [Double] The Energy Factor def self.calc_ef_from_uef(water_heating_system) - # Interpretation on Water Heater UEF if water_heating_system.fuel_type == HPXML::FuelTypeElectricity case water_heating_system.water_heater_type when HPXML::WaterHeaterTypeStorage @@ -1594,11 +1597,11 @@ def self.calc_ef_from_uef(water_heating_system) fail 'Unexpected water heater.' end - # TODO + # Calculates water heater storage tank surface areas. # - # @param act_vol [TODO] TODO - # @param height [TODO] TODO - # @return [TODO] TODO + # @param act_vol [Double] Actual tank volume (gal) + # @param height [Double] Tank height (ft) + # @return [Array] Tank total surface area (ft), Tank side surface area (ft) def self.calc_tank_areas(act_vol, height = nil) if height.nil? height = DefaultTankHeight @@ -1606,20 +1609,20 @@ def self.calc_tank_areas(act_vol, height = nil) diameter = 2.0 * (UnitConversions.convert(act_vol, 'gal', 'ft^3') / (height * Math::PI))**0.5 # feet a_top = Math::PI * diameter**2.0 / 4.0 # sqft a_side = Math::PI * diameter * height # sqft - surface_area = 2.0 * a_top + a_side # sqft + a_total = 2.0 * a_top + a_side # sqft - return surface_area, a_side + return a_total, a_side end - # TODO + # Calculates tank losses for the combination boiler given its standby loss value. # - # @param act_vol [TODO] TODO + # @param act_vol [Double] Actual tank volume (gal) # @param water_heating_system [HPXML::WaterHeatingSystem] The HPXML water heating system of interest - # @param a_side [TODO] TODO - # @param solar_fraction [TODO] TODO + # @param a_side [Double] Tank side surface area (ft^3) + # @param solar_fraction [Double] Portion of hot water load served by an attached solar thermal system # @param nbeds [Integer] Number of bedrooms in the dwelling unit - # @return [TODO] TODO - def self.calc_indirect_ua_with_standbyloss(act_vol, water_heating_system, a_side, solar_fraction, nbeds = nil) + # @return [Double] Tank loss UA factor (Btu/hr-F) + def self.calc_combi_tank_losses(act_vol, water_heating_system, a_side, solar_fraction, nbeds = nil) standby_loss_units = water_heating_system.standby_loss_units standby_loss_value = water_heating_system.standby_loss_value @@ -1647,17 +1650,18 @@ def self.calc_indirect_ua_with_standbyloss(act_vol, water_heating_system, a_side return ua end - # TODO + # Adds an EMS program to increase/decrease the energy consumption of the water heater based on + # the energy consumption adjustment factor (EC_adj). # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit - # @param heater [TODO] TODO + # @param water_heater [OpenStudio::Model::WaterHeaterMixed or OpenStudio::Model::WaterHeaterStratified or OpenStudio::Model::WaterHeaterHeatPumpWrappedCondenser] The water heater object # @param loc_space [OpenStudio::Model::Space] The space where the water heater is located # @param water_heating_system [HPXML::WaterHeatingSystem] The HPXML water heating system of interest # @param unit_multiplier [Integer] Number of similar dwelling units - # @param combi_boiler [TODO] TODO - # @return [TODO] TODO - def self.add_ec_adj(model, hpxml_bldg, heater, loc_space, water_heating_system, unit_multiplier, combi_boiler = nil) + # @param combi_boiler [OpenStudio::Model::BoilerHotWater] The boiler object if the HPXML water heating system is a combi boiler + # @return [nil] + def self.apply_ec_adj_program(model, hpxml_bldg, water_heater, loc_space, water_heating_system, unit_multiplier, combi_boiler = nil) ec_adj = get_dist_energy_consumption_adjustment(hpxml_bldg, water_heating_system) adjustment = ec_adj - 1.0 @@ -1666,9 +1670,9 @@ def self.add_ec_adj(model, hpxml_bldg, heater, loc_space, water_heating_system, end if water_heating_system.water_heater_type == HPXML::WaterHeaterTypeHeatPump - tank = heater.tank + tank = water_heater.tank else - tank = heater + tank = water_heater end if [HPXML::WaterHeaterTypeCombiStorage, HPXML::WaterHeaterTypeCombiTankless].include? water_heating_system.water_heater_type fuel_type = water_heating_system.related_hvac_system.heating_system_fuel @@ -1712,15 +1716,15 @@ def self.add_ec_adj(model, hpxml_bldg, heater, loc_space, water_heating_system, if water_heating_system.water_heater_type == HPXML::WaterHeaterTypeHeatPump ec_adj_hp_sensor = Model.add_ems_sensor( model, - name: "#{heater.dXCoil.name} energy", + name: "#{water_heater.dXCoil.name} energy", output_var_or_meter_name: "Cooling Coil Water Heating #{EPlus::FuelTypeElectricity} Rate", - key_name: heater.dXCoil.name + key_name: water_heater.dXCoil.name ) ec_adj_fan_sensor = Model.add_ems_sensor( model, - name: "#{heater.fan.name} energy", + name: "#{water_heater.fan.name} energy", output_var_or_meter_name: "Fan #{EPlus::FuelTypeElectricity} Rate", - key_name: heater.fan.name + key_name: water_heater.fan.name ) end end @@ -1740,7 +1744,7 @@ def self.add_ec_adj(model, hpxml_bldg, heater, loc_space, water_heating_system, # Actuators ec_adj_actuator = Model.add_ems_actuator( - name: "#{heater.name} ec adj act", + name: "#{water_heater.name} ec adj act", model_object: ec_adj_object, comp_type_and_control: EPlus::EMSActuatorOtherEquipmentPower ) @@ -1748,7 +1752,7 @@ def self.add_ec_adj(model, hpxml_bldg, heater, loc_space, water_heating_system, # Program ec_adj_program = Model.add_ems_program( model, - name: "#{heater.name} EC_adj" + name: "#{water_heater.name} EC_adj" ) ec_adj_program.addLine('If WarmupFlag == 0') # Prevent a non-zero adjustment in the first hour because of the warmup period if [HPXML::WaterHeaterTypeCombiStorage, HPXML::WaterHeaterTypeCombiTankless].include? water_heating_system.water_heater_type @@ -1769,31 +1773,30 @@ def self.add_ec_adj(model, hpxml_bldg, heater, loc_space, water_heating_system, # Program Calling Manager Model.add_ems_program_calling_manager( model, - name: "#{heater.name} EC_adj ProgramManager", + name: "#{water_heater.name} EC_adj ProgramManager", calling_point: 'EndOfSystemTimestepBeforeHVACReporting', ems_programs: [ec_adj_program] ) end - # TODO + # Gets the water heater setpoint temperature deadband. # - # @param wh_type [TODO] TODO - # @return [TODO] TODO - def self.deadband(wh_type) + # @param wh_type [String] Type of water heater (HPXML::WaterHeaterTypeXXX) + # @return [Double] Temperature deadband (C) + def self.get_deadband(wh_type) if [HPXML::WaterHeaterTypeStorage, HPXML::WaterHeaterTypeCombiStorage].include? wh_type - return 2.0 # C + return 2.0 else - return 0.0 # C + return 0.0 end end - # TODO + # Calculates the water heater actual volume from its nominal/rated volume. # - # @param vol [TODO] TODO - # @param fuel [TODO] TODO - # @return [TODO] TODO + # @param vol [Double] Nominal tank volume (gal) + # @param fuel [String] Water heater fuel type (HPXML::FuelTypeXXX) + # @return [Double] Actual tank volume (gal) def self.calc_storage_tank_actual_vol(vol, fuel) - # Convert the nominal tank volume to an actual volume if fuel.nil? act_vol = 0.95 * vol # indirect tank else @@ -1806,22 +1809,21 @@ def self.calc_storage_tank_actual_vol(vol, fuel) return act_vol end - # TODO + # Disaggregates the water heater's (uniform) energy factor into tank losses and burner efficiency. # - # @param act_vol [TODO] TODO + # If using EF: + # Calculations based on the Energy Factor and Recovery Efficiency of the tank + # Source: Burch and Erickson 2004 - http://www.nrel.gov/docs/gen/fy04/36035.pdf + # IF using UEF: + # Calculations based on the Uniform Energy Factor, First Hour Rating, and Recovery Efficiency of the tank + # Source: Maguire and Roberts 2020 - https://www.ashrae.org/file%20library/conferences/specialty%20conferences/2020%20building%20performance/papers/d-bsc20-c039.pdf + # + # @param act_vol [Double] Actual tank volume (gal) # @param water_heating_system [HPXML::WaterHeatingSystem] The HPXML water heating system of interest - # @param solar_fraction [TODO] TODO + # @param solar_fraction [Double] Portion of hot water load served by an attached solar thermal system # @param nbeds [Integer] Number of bedrooms in the dwelling unit - # @return [TODO] TODO - def self.calc_tank_UA(act_vol, water_heating_system, solar_fraction, nbeds) - # If using EF: - # Calculates the U value, UA of the tank and conversion efficiency (eta_c) - # based on the Energy Factor and recovery efficiency of the tank - # Source: Burch and Erickson 2004 - http://www.nrel.gov/docs/gen/fy04/36035.pdf - # IF using UEF: - # Calculates the U value, UA of the tank and conversion efficiency (eta_c) - # based on the Uniform Energy Factor, First Hour Rating, and Recovery Efficiency of the tank - # Source: Maguire and Roberts 2020 - https://www.ashrae.org/file%20library/conferences/specialty%20conferences/2020%20building%20performance/papers/d-bsc20-c039.pdf + # @return [Array] Tank loss U-factor (Btu/hr-ft^2-F), tank loss UA factor (Btu/hr-F), burner efficiency (frac) + def self.disaggregate_tank_losses_and_burner_efficiency(act_vol, water_heating_system, solar_fraction, nbeds) if water_heating_system.water_heater_type == HPXML::WaterHeaterTypeTankless if not water_heating_system.energy_factor.nil? eta_c = water_heating_system.energy_factor * water_heating_system.performance_adjustment @@ -1888,13 +1890,14 @@ def self.calc_tank_UA(act_vol, water_heating_system, solar_fraction, nbeds) return u, ua, eta_c end - # TODO + # Calculates an adjustment to the tank loss UA factor to account for the presence + # of a water heater jacket (insulation). # # @param water_heating_system [HPXML::WaterHeatingSystem] The HPXML water heating system of interest - # @param ua_pre [TODO] TODO - # @param a_side [TODO] TODO - # @return [TODO] TODO - def self.apply_tank_jacket(water_heating_system, ua_pre, a_side) + # @param ua [Double] Tank loss UA factor (Btu/hr-F) + # @param a_side [Double] Tank side surface area (ft^3) + # @return [Double] Adjusted tank loss UA factor (Btu/hr-F) + def self.apply_tank_jacket(water_heating_system, ua, a_side) if not water_heating_system.jacket_r_value.nil? skin_insulation_R = 5.0 # R5 if water_heating_system.fuel_type.nil? # indirect water heater, etc. Assume 2 inch skin insulation @@ -1905,58 +1908,59 @@ def self.apply_tank_jacket(water_heating_system, ua_pre, a_side) ef = calc_ef_from_uef(water_heating_system) end if ef < 0.7 - skin_insulation_t = 1.0 # inch + skin_insulation_t = 1.0 # inch, assumed else - skin_insulation_t = 2.0 # inch + skin_insulation_t = 2.0 # inch, assumed end else # electric - skin_insulation_t = 2.0 # inch + skin_insulation_t = 2.0 # inch, assumed end # water heater wrap calculation based on: # Modeling Water Heat Wraps in BEopt DRAFT Technical Note # Authors: Ben Polly and Jay Burch (NREL) u_pre_skin = 1.0 / (skin_insulation_t * skin_insulation_R + 1.0 / 1.3 + 1.0 / 52.8) # Btu/hr-ft^2-F = (1 / hout + kins / tins + t / hin)^-1 - ua = ua_pre - water_heating_system.jacket_r_value / (1.0 / u_pre_skin + water_heating_system.jacket_r_value) * u_pre_skin * a_side + ua_adj = ua - water_heating_system.jacket_r_value / (1.0 / u_pre_skin + water_heating_system.jacket_r_value) * u_pre_skin * a_side else - ua = ua_pre + ua_adj = ua end - return ua + return ua_adj end - # TODO + # Calculates an adjustment to the tank loss UA factor for a shared water heater, in which we + # apportion the tank losses to the dwelling unit. # # @param water_heating_system [HPXML::WaterHeatingSystem] The HPXML water heating system of interest - # @param ua [TODO] TODO + # @param ua [Double] Tank loss UA factor (Btu/hr-F) # @param nbeds [Integer] Number of bedrooms in the dwelling unit - # @return [TODO] TODO + # @return [Double] Adjusted tank loss UA factor (Btu/hr-F) def self.apply_shared_adjustment(water_heating_system, ua, nbeds) if water_heating_system.is_shared_system - # Apportion shared water heater energy use due to tank losses to the dwelling unit - ua = ua * [nbeds.to_f, 1.0].max / water_heating_system.number_of_bedrooms_served.to_f + return ua * [nbeds.to_f, 1.0].max / water_heating_system.number_of_bedrooms_served.to_f + else + return ua end - return ua end - # TODO + # Adds a water heater object to the OpenStudio model. # - # @param name [TODO] TODO + # @param name [String] Name for the OpenStudio object # @param water_heating_system [HPXML::WaterHeatingSystem] The HPXML water heating system of interest - # @param act_vol [TODO] TODO - # @param t_set_c [TODO] TODO + # @param act_vol [Double] Actual tank volume (gal) + # @param t_set_c [Double] Water heater setpoint including deadband (C) # @param loc_space [OpenStudio::Model::Space] The space where the water heater is located - # @param loc_schedule [OpenStudio::Model::ScheduleConstant] The temperature schedule for where the water heater is located, if not in a space + # @param loc_schedule [OpenStudio::Model::ScheduleConstant] The temperature schedule, if not located in a space # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings - # @param u [TODO] TODO - # @param ua [TODO] TODO - # @param eta_c [TODO] TODO - # @param is_dsh_storage [TODO] TODO - # @param is_combi [TODO] TODO + # @param u [Double] Tank loss coefficient (FIXME) + # @param ua [Double] Tank loss UA factor (Btu/hr-F) + # @param eta_c [Double] Burner efficiency (frac) + # @param is_dsh_storage [Boolean] True if this is a desuperheater storage tank + # @param is_combi [Boolean] True if this is a combination boiler # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files # @param unavailable_periods [HPXML::UnavailablePeriods] Object that defines periods for, e.g., power outages or vacancies # @param unit_multiplier [Integer] Number of similar dwelling units - # @return [TODO] TODO - def self.create_new_heater(name:, water_heating_system: nil, act_vol:, t_set_c: nil, loc_space:, loc_schedule: nil, model:, runner:, u: nil, ua:, eta_c: nil, is_dsh_storage: false, is_combi: false, schedules_file: nil, unavailable_periods: [], unit_multiplier: 1.0) + # @return [OpenStudio::Model::WaterHeaterMixed or OpenStudio::Model::WaterHeaterStratified] Water heater object + def self.apply_water_heater(name:, water_heating_system: nil, act_vol:, t_set_c: nil, loc_space:, loc_schedule: nil, model:, runner:, u: nil, ua:, eta_c: nil, is_dsh_storage: false, is_combi: false, schedules_file: nil, unavailable_periods: [], unit_multiplier: 1.0) # storage tank doesn't require water_heating_system class argument being passed if is_dsh_storage || is_combi fuel = nil @@ -1981,44 +1985,44 @@ def self.create_new_heater(name:, water_heating_system: nil, act_vol:, t_set_c: h_tank = UnitConversions.convert(DefaultTankHeight, 'ft', 'm') # Add a WaterHeater:Stratified to the model - new_heater = OpenStudio::Model::WaterHeaterStratified.new(model) - new_heater.setEndUseSubcategory('Domestic Hot Water') - new_heater.setTankVolume(UnitConversions.convert(act_vol, 'gal', 'm^3')) - new_heater.setTankHeight(h_tank) - new_heater.setMaximumTemperatureLimit(90) - new_heater.setHeaterPriorityControl('MasterSlave') - new_heater.setHeater1Capacity(UnitConversions.convert(cap, 'kBtu/hr', 'W')) - new_heater.setHeater1Height((1.0 - (4 - 0.5) / 15) * h_tank) # in the 4th node of a 15-node tank (counting from top); height of upper element based on TRNSYS assumptions for an ERWH - new_heater.setHeater1DeadbandTemperatureDifference(5.556) - new_heater.setHeater2Capacity(UnitConversions.convert(cap, 'kBtu/hr', 'W')) - new_heater.setHeater2Height((1.0 - (13 - 0.5) / 15) * h_tank) # in the 13th node of a 15-node tank (counting from top); height of upper element based on TRNSYS assumptions for an ERWH - new_heater.setHeater2DeadbandTemperatureDifference(5.556) - new_heater.setHeaterThermalEfficiency(1.0) - new_heater.setNumberofNodes(12) - new_heater.setAdditionalDestratificationConductivity(0) - new_heater.setUseSideDesignFlowRate(UnitConversions.convert(act_vol, 'gal', 'm^3') / 60.1) - new_heater.setSourceSideDesignFlowRate(0) - new_heater.setSourceSideFlowControlMode('') - new_heater.setSourceSideInletHeight((1.0 - (1 - 0.5) / 15) * h_tank) # in the 1st node of a 15-node tank (counting from top) - new_heater.setSourceSideOutletHeight((1.0 - (15 - 0.5) / 15) * h_tank) # in the 15th node of a 15-node tank (counting from top) - new_heater.setSkinLossFractiontoZone(1.0 / unit_multiplier) # Tank losses are multiplied by E+ zone multiplier, so need to compensate here - new_heater.setOffCycleFlueLossFractiontoZone(1.0 / unit_multiplier) - set_stratified_tank_ua(new_heater, u, unit_multiplier) + water_heater = OpenStudio::Model::WaterHeaterStratified.new(model) + water_heater.setEndUseSubcategory('Domestic Hot Water') + water_heater.setTankVolume(UnitConversions.convert(act_vol, 'gal', 'm^3')) + water_heater.setTankHeight(h_tank) + water_heater.setMaximumTemperatureLimit(90) + water_heater.setHeaterPriorityControl('MasterSlave') + water_heater.setHeater1Capacity(UnitConversions.convert(cap, 'kBtu/hr', 'W')) + water_heater.setHeater1Height((1.0 - (4 - 0.5) / 15) * h_tank) # in the 4th node of a 15-node tank (counting from top); height of upper element based on TRNSYS assumptions for an ERWH + water_heater.setHeater1DeadbandTemperatureDifference(5.556) + water_heater.setHeater2Capacity(UnitConversions.convert(cap, 'kBtu/hr', 'W')) + water_heater.setHeater2Height((1.0 - (13 - 0.5) / 15) * h_tank) # in the 13th node of a 15-node tank (counting from top); height of upper element based on TRNSYS assumptions for an ERWH + water_heater.setHeater2DeadbandTemperatureDifference(5.556) + water_heater.setHeaterThermalEfficiency(1.0) + water_heater.setNumberofNodes(12) + water_heater.setAdditionalDestratificationConductivity(0) + water_heater.setUseSideDesignFlowRate(UnitConversions.convert(act_vol, 'gal', 'm^3') / 60.1) + water_heater.setSourceSideDesignFlowRate(0) + water_heater.setSourceSideFlowControlMode('') + water_heater.setSourceSideInletHeight((1.0 - (1 - 0.5) / 15) * h_tank) # in the 1st node of a 15-node tank (counting from top) + water_heater.setSourceSideOutletHeight((1.0 - (15 - 0.5) / 15) * h_tank) # in the 15th node of a 15-node tank (counting from top) + water_heater.setSkinLossFractiontoZone(1.0 / unit_multiplier) # Tank losses are multiplied by E+ zone multiplier, so need to compensate here + water_heater.setOffCycleFlueLossFractiontoZone(1.0 / unit_multiplier) + apply_stratified_tank_losses(water_heater, u, unit_multiplier) else - new_heater = OpenStudio::Model::WaterHeaterMixed.new(model) - new_heater.setTankVolume(UnitConversions.convert(act_vol, 'gal', 'm^3')) - new_heater.setHeaterThermalEfficiency(eta_c) unless eta_c.nil? - new_heater.setMaximumTemperatureLimit(99.0) + water_heater = OpenStudio::Model::WaterHeaterMixed.new(model) + water_heater.setTankVolume(UnitConversions.convert(act_vol, 'gal', 'm^3')) + water_heater.setHeaterThermalEfficiency(eta_c) unless eta_c.nil? + water_heater.setMaximumTemperatureLimit(99.0) if [HPXML::WaterHeaterTypeTankless, HPXML::WaterHeaterTypeCombiTankless].include? tank_type - new_heater.setHeaterControlType('Modulate') + water_heater.setHeaterControlType('Modulate') else - new_heater.setHeaterControlType('Cycle') + water_heater.setHeaterControlType('Cycle') end - new_heater.setDeadbandTemperatureDifference(deadband(tank_type)) + water_heater.setDeadbandTemperatureDifference(get_deadband(tank_type)) # Capacity, storage tank to be 0 - new_heater.setHeaterMaximumCapacity(UnitConversions.convert(cap, 'kBtu/hr', 'W')) - new_heater.setHeaterMinimumCapacity(0.0) + water_heater.setHeaterMaximumCapacity(UnitConversions.convert(cap, 'kBtu/hr', 'W')) + water_heater.setHeaterMinimumCapacity(0.0) # Set fraction of heat loss from tank to ambient (vs out flue) # Based on lab testing done by LBNL @@ -2039,68 +2043,68 @@ def self.create_new_heater(name:, water_heating_system: nil, act_vol:, t_set_c: skinlossfrac = 0.96 # Condensing end end - new_heater.setOffCycleLossFractiontoThermalZone(skinlossfrac / unit_multiplier) # Tank losses are multiplied by E+ zone multiplier, so need to compensate here - new_heater.setOnCycleLossFractiontoThermalZone(1.0 / unit_multiplier) # Tank losses are multiplied by E+ zone multiplier, so need to compensate here + water_heater.setOffCycleLossFractiontoThermalZone(skinlossfrac / unit_multiplier) # Tank losses are multiplied by E+ zone multiplier, so need to compensate here + water_heater.setOnCycleLossFractiontoThermalZone(1.0 / unit_multiplier) # Tank losses are multiplied by E+ zone multiplier, so need to compensate here ua_w_k = UnitConversions.convert(ua, 'Btu/(hr*F)', 'W/K') - new_heater.setOnCycleLossCoefficienttoAmbientTemperature(ua_w_k) - new_heater.setOffCycleLossCoefficienttoAmbientTemperature(ua_w_k) + water_heater.setOnCycleLossCoefficienttoAmbientTemperature(ua_w_k) + water_heater.setOffCycleLossCoefficienttoAmbientTemperature(ua_w_k) end - assign_water_heater_setpoint(runner, model, new_heater, schedules_file, t_set_c, unavailable_periods) + apply_setpoint(runner, model, water_heater, schedules_file, t_set_c, unavailable_periods) if not water_heating_system.nil? - new_heater.additionalProperties.setFeature('HPXML_ID', water_heating_system.id) # Used by reporting measure + water_heater.additionalProperties.setFeature('HPXML_ID', water_heating_system.id) # Used by reporting measure end if is_combi - new_heater.additionalProperties.setFeature('IsCombiBoiler', true) # Used by reporting measure + water_heater.additionalProperties.setFeature('IsCombiBoiler', true) # Used by reporting measure end - new_heater.setName(name) - new_heater.setHeaterFuelType(EPlus.fuel_type(fuel)) unless fuel.nil? - set_wh_ambient(loc_space, loc_schedule, new_heater) + water_heater.setName(name) + water_heater.setHeaterFuelType(EPlus.fuel_type(fuel)) unless fuel.nil? + apply_ambient_temperature(loc_space, loc_schedule, water_heater) # FUTURE: These are always zero right now; develop smart defaults. - new_heater.setOffCycleParasiticFuelType(EPlus::FuelTypeElectricity) - new_heater.setOffCycleParasiticFuelConsumptionRate(0.0) - new_heater.setOffCycleParasiticHeatFractiontoTank(0) - new_heater.setOnCycleParasiticFuelType(EPlus::FuelTypeElectricity) - new_heater.setOnCycleParasiticFuelConsumptionRate(0.0) - new_heater.setOnCycleParasiticHeatFractiontoTank(0) - - return new_heater + water_heater.setOffCycleParasiticFuelType(EPlus::FuelTypeElectricity) + water_heater.setOffCycleParasiticFuelConsumptionRate(0.0) + water_heater.setOffCycleParasiticHeatFractiontoTank(0) + water_heater.setOnCycleParasiticFuelType(EPlus::FuelTypeElectricity) + water_heater.setOnCycleParasiticFuelConsumptionRate(0.0) + water_heater.setOnCycleParasiticHeatFractiontoTank(0) + + return water_heater end - # TODO + # Applies the ambient temperature conditions to the water heater. # # @param loc_space [OpenStudio::Model::Space] The space where the water heater is located - # @param loc_schedule [OpenStudio::Model::ScheduleConstant] The temperature schedule for where the water heater is located, if not in a space - # @param wh_obj [TODO] TODO + # @param loc_schedule [OpenStudio::Model::ScheduleConstant] The temperature schedule, if not located in a space + # @param water_heater [OpenStudio::Model::WaterHeaterMixed or OpenStudio::Model::WaterHeaterStratified] Water heater object # @return [nil] - def self.set_wh_ambient(loc_space, loc_schedule, wh_obj) - if wh_obj.ambientTemperatureSchedule.is_initialized - wh_obj.ambientTemperatureSchedule.get.remove + def self.apply_ambient_temperature(loc_space, loc_schedule, water_heater) + if water_heater.ambientTemperatureSchedule.is_initialized + water_heater.ambientTemperatureSchedule.get.remove end if not loc_schedule.nil? # Temperature schedule indicator - wh_obj.setAmbientTemperatureSchedule(loc_schedule) + water_heater.setAmbientTemperatureSchedule(loc_schedule) elsif not loc_space.nil? - wh_obj.setAmbientTemperatureIndicator('ThermalZone') - wh_obj.setAmbientTemperatureThermalZone(loc_space.thermalZone.get) + water_heater.setAmbientTemperatureIndicator('ThermalZone') + water_heater.setAmbientTemperatureThermalZone(loc_space.thermalZone.get) else # Located outside - wh_obj.setAmbientTemperatureIndicator('Outdoors') + water_heater.setAmbientTemperatureIndicator('Outdoors') end end - # TODO + # Applies the temperature setpoint to the water heater. # # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param water_heater [TODO] TODO + # @param water_heater [OpenStudio::Model::WaterHeaterMixed or OpenStudio::Model::WaterHeaterStratified] Water heater object # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files - # @param t_set_c [TODO] TODO + # @param t_set_c [Double] Water heater setpoint including deadband (C) # @param unavailable_periods [HPXML::UnavailablePeriods] Object that defines periods for, e.g., power outages or vacancies # @return [nil] - def self.assign_water_heater_setpoint(runner, model, water_heater, schedules_file, t_set_c, unavailable_periods) + def self.apply_setpoint(runner, model, water_heater, schedules_file, t_set_c, unavailable_periods) setpoint_sch = nil if not schedules_file.nil? setpoint_sch = schedules_file.create_schedule_file(model, col_name: SchedulesFile::Columns[:WaterHeaterSetpoint].name) @@ -2124,26 +2128,26 @@ def self.assign_water_heater_setpoint(runner, model, water_heater, schedules_fil end end - # TODO + # Returns the water heater setpoint, accounting for any deadband, in deg-C. The deadband is currently + # centered, not single-sided; see https://github.com/NREL/OpenStudio-HPXML/issues/642. # - # @param t_set [TODO] TODO - # @param wh_type [TODO] TODO - # @return [TODO] TODO + # @param t_set [Double] Water heater setpoint (F) + # @param wh_type [String] Type of water heater (HPXML::WaterHeaterTypeXXX) + # @return [Double] Water heater setpoint including deadband (C) def self.get_t_set_c(t_set, wh_type) return if t_set.nil? - return UnitConversions.convert(t_set, 'F', 'C') + deadband(wh_type) / 2.0 # Half the deadband to account for E+ deadband + return UnitConversions.convert(t_set, 'F', 'C') + get_deadband(wh_type) / 2.0 # Half the deadband to account for E+ deadband end - # TODO + # Adds a plant loop for the water heater to the OpenStudio model. # # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param t_set_c [TODO] TODO + # @param t_set_c [Double] Water heater setpoint including deadband (C) # @param eri_version [String] Version of the ANSI/RESNET/ICC 301 Standard to use for equations/assumptions # @param unit_multiplier [Integer] Number of similar dwelling units - # @return [TODO] TODO - def self.create_new_loop(model, t_set_c, eri_version, unit_multiplier) - # Create a new plant loop for the water heater + # @return [OpenStudio::Model::PlantLoop] The plant loop + def self.add_plant_loop(model, t_set_c, eri_version, unit_multiplier) name = 'dhw loop' if t_set_c.nil? @@ -2181,11 +2185,18 @@ def self.create_new_loop(model, t_set_c, eri_version, unit_multiplier) return loop end - # TODO + # Gets the solar fraction, which is defined as the portion of total conventional hot water heating + # load (delivered energy plus tank standby losses) served by the solar thermal system. + # + # A value of zero will be returned unless all of these conditions are met: + # 1. There is a solar thermal system + # 2. The solar thermal system is attached to this water heater + # 3. The solar thermal system has simple inputs (i.e., solar fraction), as opposed to detailed + # inputs that would result in the solar thermal system being modeled explicitly in EnergyPlus. # # @param water_heating_system [HPXML::WaterHeatingSystem] The HPXML water heating system of interest # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit - # @return [TODO] TODO + # @return [Double] Solar fraction or zero def self.get_water_heater_solar_fraction(water_heating_system, hpxml_bldg) return 0.0 if hpxml_bldg.solar_thermal_systems.size == 0 From 2b64d9f800df1b91b98acff8fde817b4f1c28665 Mon Sep 17 00:00:00 2001 From: Scott Horowitz Date: Mon, 9 Dec 2024 15:49:48 -0700 Subject: [PATCH 2/3] Bugfix. --- HPXMLtoOpenStudio/measure.xml | 6 +++--- HPXMLtoOpenStudio/resources/waterheater.rb | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/HPXMLtoOpenStudio/measure.xml b/HPXMLtoOpenStudio/measure.xml index fe19c4b8b5..598f6cdcd7 100644 --- a/HPXMLtoOpenStudio/measure.xml +++ b/HPXMLtoOpenStudio/measure.xml @@ -3,8 +3,8 @@ 3.1 hpxm_lto_openstudio b1543b30-9465-45ff-ba04-1d1f85e763bc - 74dbb4e9-cf99-40f2-8ae8-f51374a2667b - 2024-12-09T21:55:17Z + 62ead610-24c2-4bb7-807b-9a7c572f76ba + 2024-12-09T22:48:31Z D8922A73 HPXMLtoOpenStudio HPXML to OpenStudio Translator @@ -627,7 +627,7 @@ waterheater.rb rb resource - DC762179 + B2C6B3DB weather.rb diff --git a/HPXMLtoOpenStudio/resources/waterheater.rb b/HPXMLtoOpenStudio/resources/waterheater.rb index 02d78dccd7..995b939740 100644 --- a/HPXMLtoOpenStudio/resources/waterheater.rb +++ b/HPXMLtoOpenStudio/resources/waterheater.rb @@ -1367,7 +1367,9 @@ def self.apply_hpwh_control_program(model, runner, obj_name, water_heating_syste key_name: op_mode_schedule.name ) - runner.registerWarning("Both '#{SchedulesFile::Columns[:WaterHeaterOperatingMode].name}' schedule file and operating mode provided; the latter will be ignored.") if !op_mode.nil? + if not water_heating_system.operating_mode.nil? + runner.registerWarning("Both '#{SchedulesFile::Columns[:WaterHeaterOperatingMode].name}' schedule file and operating mode provided; the latter will be ignored.") + end end t_offset = 9.0 # C From 8068f153d26181b2181065bbd2775851c7676f92 Mon Sep 17 00:00:00 2001 From: Scott Horowitz Date: Mon, 9 Dec 2024 16:46:20 -0700 Subject: [PATCH 3/3] Add Model.add_plant_loop() method. --- HPXMLtoOpenStudio/measure.xml | 10 ++--- HPXMLtoOpenStudio/resources/hvac.rb | 32 +++++++--------- HPXMLtoOpenStudio/resources/model.rb | 29 ++++++++++++++ HPXMLtoOpenStudio/resources/waterheater.rb | 44 ++++++++++------------ 4 files changed, 68 insertions(+), 47 deletions(-) diff --git a/HPXMLtoOpenStudio/measure.xml b/HPXMLtoOpenStudio/measure.xml index 598f6cdcd7..3869fb75a3 100644 --- a/HPXMLtoOpenStudio/measure.xml +++ b/HPXMLtoOpenStudio/measure.xml @@ -3,8 +3,8 @@ 3.1 hpxm_lto_openstudio b1543b30-9465-45ff-ba04-1d1f85e763bc - 62ead610-24c2-4bb7-807b-9a7c572f76ba - 2024-12-09T22:48:31Z + a96e00a3-319b-4f82-a104-9013094367f5 + 2024-12-09T23:44:23Z D8922A73 HPXMLtoOpenStudio HPXML to OpenStudio Translator @@ -387,7 +387,7 @@ hvac.rb rb resource - 04AD7F98 + 0F2DDAA0 hvac_sizing.rb @@ -447,7 +447,7 @@ model.rb rb resource - 8B91AD7C + 56A086CD output.rb @@ -627,7 +627,7 @@ waterheater.rb rb resource - B2C6B3DB + B6FE7ABC weather.rb diff --git a/HPXMLtoOpenStudio/resources/hvac.rb b/HPXMLtoOpenStudio/resources/hvac.rb index b1248f89bd..6e91052f60 100644 --- a/HPXMLtoOpenStudio/resources/hvac.rb +++ b/HPXMLtoOpenStudio/resources/hvac.rb @@ -663,20 +663,19 @@ def self.apply_ground_to_air_heat_pump(model, runner, weather, heat_pump, hvac_s xing.setAverageSoilSurfaceTemperature(ground_heat_exch_vert.groundTemperature.get) # Plant Loop - plant_loop = OpenStudio::Model::PlantLoop.new(model) - plant_loop.setName(obj_name + ' condenser loop') - plant_loop.setFluidType(hp_ap.fluid_type) - if hp_ap.fluid_type != EPlus::FluidWater - plant_loop.setGlycolConcentration((hp_ap.frac_glycol * 100).to_i) - end - plant_loop.setMaximumLoopTemperature(48.88889) - plant_loop.setMinimumLoopTemperature(UnitConversions.convert(hp_ap.design_hw, 'F', 'C')) - plant_loop.setMinimumLoopFlowRate(0) - plant_loop.setLoadDistributionScheme('SequentialLoad') + plant_loop = Model.add_plant_loop( + model, + name: "#{obj_name} condenser loop", + fluid_type: hp_ap.fluid_type, + glycol_concentration: (hp_ap.frac_glycol * 100).to_i, + min_temp: UnitConversions.convert(hp_ap.design_hw, 'F', 'C'), + max_temp: 48.88889, + max_flow_rate: UnitConversions.convert(geothermal_loop.loop_flow, 'gal/min', 'm^3/s') + ) + plant_loop.addSupplyBranchForComponent(ground_heat_exch_vert) plant_loop.addDemandBranchForComponent(htg_coil) plant_loop.addDemandBranchForComponent(clg_coil) - plant_loop.setMaximumLoopFlowRate(UnitConversions.convert(geothermal_loop.loop_flow, 'gal/min', 'm^3/s')) sizing_plant = plant_loop.sizingPlant sizing_plant.setLoopType('Condenser') @@ -839,13 +838,10 @@ def self.apply_boiler(model, runner, heating_system, hvac_sequential_load_fracs, end # Plant Loop - plant_loop = OpenStudio::Model::PlantLoop.new(model) - plant_loop.setName(obj_name + ' hydronic heat loop') - plant_loop.setFluidType(EPlus::FluidWater) - plant_loop.setMaximumLoopTemperature(100) - plant_loop.setMinimumLoopTemperature(0) - plant_loop.setMinimumLoopFlowRate(0) - plant_loop.autocalculatePlantLoopVolume() + plant_loop = Model.add_plant_loop( + model, + name: "#{obj_name} hydronic heat loop" + ) loop_sizing = plant_loop.sizingPlant loop_sizing.setLoopType('Heating') diff --git a/HPXMLtoOpenStudio/resources/model.rb b/HPXMLtoOpenStudio/resources/model.rb index 537e2eabf6..5e2ba31032 100644 --- a/HPXMLtoOpenStudio/resources/model.rb +++ b/HPXMLtoOpenStudio/resources/model.rb @@ -353,6 +353,35 @@ def self.add_fan_system_model(model, name:, end_use:, power_per_flow:, max_flow_ return fan end + # Adds a PlantLoop object to the OpenStudio model. + # + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param name [String] Name for the OpenStudio object + # @param fluid_type [String] Fluid type (Eplus::FluidXXX) + # @param glycol_concentration [Integer] Percent glycol concentration, only used if fluid is propylene glycol + # @param volume [Double] Volume of the entire loop, both demand and supply side (m^3) + # @param min_temp [Double] Minimum loop temperature (C) + # @param max_temp [Double] Maximum loop temperature (C) + # @param max_flow_rate [Double] Maximum loop flow rate (m^3/s) + # @return [OpenStudio::Model::PlantLoop] The model object + def self.add_plant_loop(model, name:, fluid_type: EPlus::FluidWater, glycol_concentration: 50, volume: nil, min_temp: nil, max_temp: nil, max_flow_rate: nil) + plant_loop = OpenStudio::Model::PlantLoop.new(model) + plant_loop.setName(name) + plant_loop.setFluidType(fluid_type) + if fluid_type == EPlus::FluidPropyleneGlycol + plant_loop.setGlycolConcentration(glycol_concentration) + end + plant_loop.setMinimumLoopTemperature(min_temp) unless min_temp.nil? + plant_loop.setMaximumLoopTemperature(max_temp) unless max_temp.nil? + plant_loop.setMaximumLoopFlowRate(max_flow_rate) unless max_flow_rate.nil? + if not volume.nil? + plant_loop.setPlantLoopVolume(volume) + else + plant_loop.autocalculatePlantLoopVolume() + end + return plant_loop + end + # Adds a PumpVariableSpeed object to the OpenStudio model. # # @param model [OpenStudio::Model::Model] OpenStudio Model object diff --git a/HPXMLtoOpenStudio/resources/waterheater.rb b/HPXMLtoOpenStudio/resources/waterheater.rb index 995b939740..3d9ec8a762 100644 --- a/HPXMLtoOpenStudio/resources/waterheater.rb +++ b/HPXMLtoOpenStudio/resources/waterheater.rb @@ -686,19 +686,11 @@ def self.apply_solar_thermal(model, spaces, hpxml_bldg, plantloop_map) end end - plant_loop = OpenStudio::Model::PlantLoop.new(model) - plant_loop.setName('solar hot water loop') - if fluid_type == EPlus::FluidWater - plant_loop.setFluidType(EPlus::FluidWater) - else - plant_loop.setFluidType(EPlus::FluidPropyleneGlycol) - plant_loop.setGlycolConcentration(50) - end - plant_loop.setMaximumLoopTemperature(100) - plant_loop.setMinimumLoopTemperature(0) - plant_loop.setMinimumLoopFlowRate(0) - plant_loop.setLoadDistributionScheme('Optimal') - plant_loop.setPlantEquipmentOperationHeatingLoadSchedule(model.alwaysOnDiscreteSchedule) + plant_loop = Model.add_plant_loop( + model, + name: 'solar hot water loop', + fluid_type: fluid_type + ) sizing_plant = plant_loop.sizingPlant sizing_plant.setLoopType('Heating') @@ -2156,25 +2148,29 @@ def self.add_plant_loop(model, t_set_c, eri_version, unit_multiplier) t_set_c = UnitConversions.convert(Defaults.get_water_heater_temperature(eri_version), 'F', 'C') end - loop = OpenStudio::Model::PlantLoop.new(model) - loop.setName(name) - loop.sizingPlant.setDesignLoopExitTemperature(t_set_c) - loop.sizingPlant.setLoopDesignTemperatureDifference(UnitConversions.convert(10.0, 'deltaF', 'deltaC')) - loop.setPlantLoopVolume(0.003 * unit_multiplier) # ~1 gal - loop.setMaximumLoopFlowRate(0.01 * unit_multiplier) # This size represents the physical limitations to flow due to losses in the piping system. We assume that the pipes are always adequately sized. + plant_loop = Model.add_plant_loop( + model, + name: name, + volume: 0.003 * unit_multiplier, # ~1 gal + max_flow_rate: 0.01 * unit_multiplier # This size represents the physical limitations to flow due to losses in the piping system. We assume that the pipes are always adequately sized. + ) + + sizing_plant = plant_loop.sizingPlant + sizing_plant.setDesignLoopExitTemperature(t_set_c) + sizing_plant.setLoopDesignTemperatureDifference(UnitConversions.convert(10.0, 'deltaF', 'deltaC')) bypass_pipe = Model.add_pipe_adiabatic(model) out_pipe = Model.add_pipe_adiabatic(model) - loop.addSupplyBranchForComponent(bypass_pipe) - out_pipe.addToNode(loop.supplyOutletNode) + plant_loop.addSupplyBranchForComponent(bypass_pipe) + out_pipe.addToNode(plant_loop.supplyOutletNode) pump = Model.add_pump_variable_speed( model, name: "#{name} pump", rated_power: 0 ) - pump.addToNode(loop.supplyInletNode) + pump.addToNode(plant_loop.supplyInletNode) temp_schedule = Model.add_schedule_constant( model, @@ -2182,9 +2178,9 @@ def self.add_plant_loop(model, t_set_c, eri_version, unit_multiplier) value: t_set_c ) setpoint_manager = OpenStudio::Model::SetpointManagerScheduled.new(model, temp_schedule) - setpoint_manager.addToNode(loop.supplyOutletNode) + setpoint_manager.addToNode(plant_loop.supplyOutletNode) - return loop + return plant_loop end # Gets the solar fraction, which is defined as the portion of total conventional hot water heating