diff --git a/HPXMLtoOpenStudio/resources/hpxml_schematron/EPvalidator.xml b/HPXMLtoOpenStudio/resources/hpxml_schematron/EPvalidator.xml
index 2cb6b07991..4f6c78ab44 100644
--- a/HPXMLtoOpenStudio/resources/hpxml_schematron/EPvalidator.xml
+++ b/HPXMLtoOpenStudio/resources/hpxml_schematron/EPvalidator.xml
@@ -338,7 +338,7 @@
[HVACSizingControl]
Expected 0 or 1 element(s) for xpath: HeatPumpSizingMethodology
- Expected HeatPumpSizingMethodology to be 'ACCA' or 'HERS' or 'MaxLoad'
+ Expected HeatPumpSizingMethodology to be 'ACCA2023' or 'ACCA' or 'HERS' or 'MaxLoad'
Expected 0 or 1 element(s) for xpath: HeatPumpBackupSizingMethodology
Expected HeatPumpBackupSizingMethodology to be 'emergency' or 'supplemental'
Expected 0 or 1 element(s) for xpath: AllowIncreasedFixedCapacities
diff --git a/HPXMLtoOpenStudio/resources/hvac_sizing.rb b/HPXMLtoOpenStudio/resources/hvac_sizing.rb
index 832db9cec8..a6724ba4d6 100644
--- a/HPXMLtoOpenStudio/resources/hvac_sizing.rb
+++ b/HPXMLtoOpenStudio/resources/hvac_sizing.rb
@@ -1711,26 +1711,105 @@ def self.apply_fractions_load_served(hvac_heating, hvac_loads, frac_zone_heat_lo
end
# Returns the ACCA Manual S sizing allowances for a given type of HVAC equipment.
- # These sizing allowances are used in the logic that determines how to convert heating/cooling
+ # These sizing allowances are used in the logic that determines how to convert heating/cooling
# design loads into corresponding equipment capacities.
- #
- # @param hvac_cooling [HPXML::CoolingSystem or HPXML::HeatPump] The cooling portion of the current HPXML HVAC system
- # @return [Array] Oversize fraction (frac), oversize delta (Btu/hr), undersize fraction (frac)
- def self.get_hvac_size_limits(hvac_cooling)
- oversize_limit = 1.15
- oversize_delta = 15000.0
- undersize_limit = 0.9
- if not hvac_cooling.nil?
- if hvac_cooling.compressor_type == HPXML::HVACCompressorTypeTwoStage
- oversize_limit = 1.2
- elsif hvac_cooling.compressor_type == HPXML::HVACCompressorTypeVariableSpeed
- oversize_limit = 1.3
- end
- end
+ # New ACCA Manual S specifies different size limits for clg only/HPs depending on
+ # the sizing condition (Standard, Dry, Variable Speed, etc.)
+ # and total cooling load (24,000 BTU/hr cutoff)
- return oversize_limit, oversize_delta, undersize_limit
- end
+ # get_hvac_size_limits() does not address section/table N2.3.3 Two-Speed Heat Pump Sizing Condition
+ # or section/table N2.3.4 Variable-Capacity Equipment Sizing Condition
+ # since these size conditions also include heating size factors and minimum compressor heating size factors
+ # To maintain consistency w/ previous implementation in ACCA Man. S 2014, only cooling size limits are returned.
+ # Therefore, only sections/tables N2.3.1 and N2.3.2 from ACCA Man. S 2023 are addressed in this method.
+
+ # @param hvac_cooling [HPXML::CoolingSystem or HPXML::HeatPump] The cooling portion of the current HPXML HVAC system
+ # @param hvac_sizings [HVACSizingValues] Object with sizing values for a given HVAC system
+ # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit
+ # @param weather [WeatherFile] Weather object containing EPW information
+ # @return [Array] total clg oversize limit (frac),
+ # total clg undersize limit (frac), sens. clg undersize limit (frac), lat. clg undersize limit (frac)
+
+ def self.get_hvac_size_limits(hvac_cooling, hvac_sizings, hpxml_bldg, weather)
+
+ load_shr = hvac_sizings.Cool_Load_Sens / hvac_sizings.Cool_Load_Tot
+ # calculate Climate JSHR to determine standard vs dry sizing condition (new ACCA)
+
+ if hpxml_bldg.header.heat_pump_sizing_methodology == HPXML::HeatPumpSizingACCA2023
+ if not hvac_cooling.nil?
+ if load_shr < 0.95
+ # larger latent load
+ # Section N.2.3.1 ACCA Man. S 2023, Standard Sizing Condition
+ total_clg_undersize_limit = 0.90
+ sens_clg_undersize_limit = 0.90
+ lat_clg_undersize_limit = 1.00
+ if hvac_cooling.compressor_type == HPXML::HVACCompressorTypeSingleStage && hvac_sizings.Cool_Load_Tot <= 24000 # BTU/hr
+ total_clg_oversize_limit = 1.20
+ elsif hvac_cooling.compressor_type == HPXML::HVACCompressorTypeSingleStage && hvac_sizings.Cool_Load_Tot > 24000 # BTU/hr
+ total_clg_oversize_limit = 1.15
+ elsif hvac_cooling.compressor_type == HPXML::HVACCompressorTypeTwoStage
+ total_clg_oversize_limit = 1.25
+ end
+ elsif load_shr >= 0.95 # Section N.2.3.2 ACCA Man. S 2023, Dry Sizing Condition
+ if hvac_cooling.compressor_type == HPXML::HVACCompressorTypeSingleStage
+ # Total Cooling Capacity / (Total Cooling Load + 6 kBtuh) <= 1.00
+ # 1.00 = non-load adjusted oversize limit
+ # goal --> determine "load adjusted" oversize limit
+ # Total Cooling Capacity <= 1.00 * (Total Cooling Load + 6 kBtuh)
+ # Total Cooling Capacity <= 1.00 * Total Cooling Load + 1.00 * 6 kBtuh
+ # Total Cooling Capacity / Total Cooling Load <= 1.00 + (1.00 * 6 kBtuh) / Total Cooling Load
+ # Total Cooling Capacity Factor <= 1.00 + 6 kBtuh / Total Cooling Load
+ # Effective Total Cooling Oversize Limit = 1.00 + 6 kBtuh / Total Cooling Load
+ total_clg_oversize_limit = 1 + 6000 / hvac_sizings.Cool_Load_Tot
+ total_clg_undersize_limit = 0.90
+ sens_clg_undersize_limit = 0.90
+ lat_clg_undersize_limit = 1.00
+ elsif hvac_cooling.compressor_type == HPXML::HVACCompressorTypeTwoStage
+ total_clg_oversize_limit = 1.15 # minimum compressor speed! TODO: incorporate minimum speed logic elsewhere
+ total_clg_undersize_limit = 0.90 # total clg undersize limit for dry climate, 2-stage compressor is as specified in ACCA 2014
+ # ACCA 2023 Table N2.3.2 does not specify total cooling undersize limit.
+ sens_clg_undersize_limit = 0.90
+ lat_clg_undersize_limit = 1.00
+ end
+ end
+ end
+ elsif hpxml_bldg.header.heat_pump_sizing_methodology == HPXML::HeatPumpSizingACCA
+ # References "Overview of Size Limits for Residential HVAC Equipment" from Man. S 2014
+ if ((weather.data.HDD65F / weather.data.CDD50F) < 2.0) || (load_shr < 0.95)
+ # mild winter or has latent cooling load
+ if not hvac_cooling.nil?
+ total_clg_undersize_limit = 0.90
+ sens_clg_undersize_limit = 0.90
+ lat_clg_undersize_limit = 1.00
+ if hvac_cooling.compressor_type == HPXML::HVACCompressorTypeSingleStage
+ total_clg_oversize_limit = 1.15
+ elsif hvac_cooling.compressor_type == HPXML::HVACCompressorTypeTwoStage
+ total_clg_oversize_limit = 1.20
+ elsif hvac_cooling.compressor_type == HPXML::HVACCompressorTypeVariableSpeed
+ total_clg_oversize_limit = 1.30
+ end
+ end
+ elsif ((weather.data.HDD65F / weather.data.CDD50F) >= 2.0) && (load_shr >= 0.95)
+ # cold winter and no latent cooling load
+ # Manual S 2023 doesn't specify latent/sensible capacity undersize limits in this situation.
+ # from software perspective, could either return zero, nil, or the same latent/sensible
+ # undersize limits as the situation with latent cooling load/mild winter.
+ total_clg_undersize_limit = 0.90
+ sens_clg_undersize_limit = nil
+ lat_clg_undersize_limit = nil
+ # Max Total Cooling Capacity = Total Cooling Load + 15 kBtuh
+ # i.e. Total Cooling Capacity <= Total Cooling Load + 15 kBtuh
+ # <= means solve for oversize limit, size factor (capacity/load) <= oversize limit
+ # Total Cooling Capacity / Total Cooling Load <= (Total Cooling Load + 15 kBtuh) / Total Cooling Load
+ # Total Cooling Oversize Limit = (Total Cooling Load + 15 kBtuh) / Total Cooling Load
+ total_clg_oversize_limit = (hvac_sizings.Cool_Load_Tot + 15000) / hvac_sizings.Cool_Load_Tot
+ end
+ end
+
+ return total_clg_oversize_limit, total_clg_undersize_limit, sens_clg_undersize_limit, lat_clg_undersize_limit
+
+ end
# Transfers the design load totals from the HVAC loads object to the HVAC sizings object.
#
@@ -2108,7 +2187,7 @@ def self.apply_hvac_equipment_adjustments(mj, runner, hvac_sizings, weather, hva
hvac_cooling_ap = hvac_cooling.additional_properties
is_ducted = !hvac_cooling.distribution_system.nil?
cooling_delta_t = mj.cool_setpoint - hvac_cooling_ap.leaving_air_temp
- oversize_limit, oversize_delta, undersize_limit = get_hvac_size_limits(hvac_cooling)
+ tot_clg_oversize_limit, tot_clg_undersize_limit, sens_clg_undersize_limit, lat_clg_undersize_limit = get_hvac_size_limits(hvac_cooling)
end
if hvac_sizings.Cool_Load_Tot <= 0
@@ -2218,11 +2297,11 @@ def self.apply_hvac_equipment_adjustments(mj, runner, hvac_sizings, weather, hva
# If the adjusted equipment size is negative (occurs at altitude), use oversize limit (the adjustment
# almost always hits the oversize limit in this case, making this a safe assumption)
if (cool_cap_design < 0) || (cool_sens_cap_design < 0)
- cool_cap_design = oversize_limit * hvac_sizings.Cool_Load_Tot
+ cool_cap_design = tot_clg_oversize_limit * hvac_sizings.Cool_Load_Tot
end
# Limit total capacity to oversize limit
- cool_cap_design = [cool_cap_design, oversize_limit * hvac_sizings.Cool_Load_Tot].min
+ cool_cap_design = [cool_cap_design, tot_clg_oversize_limit * hvac_sizings.Cool_Load_Tot].min
# Determine rated capacities
hvac_sizings.Cool_Capacity = cool_cap_design / total_cap_curve_value
@@ -2231,7 +2310,7 @@ def self.apply_hvac_equipment_adjustments(mj, runner, hvac_sizings, weather, hva
# Determine the final sensible capacity at design using the SHR
cool_sens_cap_design = cool_cap_design * design_shr
- elsif cool_sens_cap_design < undersize_limit * hvac_sizings.Cool_Load_Sens
+ elsif cool_sens_cap_design < sens_undersize_limit * hvac_sizings.Cool_Load_Sens
# Size by MJ8 Sensible Load, return to rated conditions, find rated sensible capacity with SHRRated. Limit total
# capacity to oversizing limit.
@@ -2241,7 +2320,7 @@ def self.apply_hvac_equipment_adjustments(mj, runner, hvac_sizings, weather, hva
cool_cap_design = cool_sens_cap_design / design_shr
# Limit total capacity to oversize limit
- cool_cap_design = [cool_cap_design, oversize_limit * hvac_sizings.Cool_Load_Tot].min
+ cool_cap_design = [cool_cap_design, tot_clg_oversize_limit * hvac_sizings.Cool_Load_Tot].min
# rated capacities
hvac_sizings.Cool_Capacity = cool_cap_design / total_cap_curve_value
@@ -2344,7 +2423,7 @@ def self.apply_hvac_equipment_adjustments(mj, runner, hvac_sizings, weather, hva
cool_cap_design = cool_load_sens_cap_design + cool_load_lat_cap_design
# Limit total capacity via oversizing limit
- cool_cap_design = [cool_cap_design, oversize_limit * hvac_sizings.Cool_Load_Tot].min
+ cool_cap_design = [cool_cap_design, tot_clg_oversize_limit * hvac_sizings.Cool_Load_Tot].min
hvac_sizings.Cool_Capacity = cool_cap_design / total_cap_curve_value
hvac_sizings.Cool_Capacity_Sens = hvac_sizings.Cool_Capacity * hvac_cooling_shr
end
@@ -3168,8 +3247,10 @@ def self.calculate_heat_pump_adj_factor_at_outdoor_temperature(mj, hvac_heating,
# @return [Double] Heat pump backup load (Btu/hr)
def self.calculate_heat_pump_backup_load(mj, hvac_heating, heating_load, hp_nominal_heating_capacity, hvac_heating_speed, hpxml_bldg)
if hpxml_bldg.header.heat_pump_backup_sizing_methodology == HPXML::HeatPumpBackupSizingEmergency
- # Size backup to meet full design load in case heat pump fails
- return heating_load
+ # Size backup to meet 85% of design load in case heat pump fails
+ # New ACCA Man S (2024)--> emergency heating load is 85% of heating load
+ # See Table N1.16.3.2 Electric Resistance Emergency Heat
+ return 0.85*heating_load
elsif hpxml_bldg.header.heat_pump_backup_sizing_methodology == HPXML::HeatPumpBackupSizingSupplemental
if not hvac_heating.backup_heating_switchover_temp.nil?
min_compressor_temp = hvac_heating.backup_heating_switchover_temp