diff --git a/measures/comstock_sensitivity_reports/measure.rb b/measures/comstock_sensitivity_reports/measure.rb
index d8fee1fe1..3f236aa06 100644
--- a/measures/comstock_sensitivity_reports/measure.rb
+++ b/measures/comstock_sensitivity_reports/measure.rb
@@ -2994,6 +2994,31 @@ def system_type_for(pump)
runner.registerValue('com_report_unitary_sys_cycling_excess_electricity_heating_pcnt',
com_report_unitary_sys_cycling_excess_electricity_heating_pcnt)
+ # calculate DX heating load during hybrid heating from EMS output variables
+ # this only gets a value when HPRTU measure is applied in the model
+ # which creates an EMS variable to report this value
+ # this variable is to support the difference between two operating scenarios
+ # 1) simulataneous DX heating and gas coil heating
+ # 2) gas coil heating only and DX compressor locked out
+ # suffix used below is hard-coded in HPRTU measure
+ # timestep of 'Hourly' is also hard-coded in HPRTU measure
+ suffix = "_dx_load_during_hybrid_heating"
+ com_report_hvac_dx_heating_load_during_hybrid_heating_j = 0.0
+ model.getEnergyManagementSystemOutputVariables.each do |ems_var|
+ name = ems_var.name.to_s
+ next unless name.downcase.include?(suffix)
+ ts_opt = sql.timeSeries(ann_env_pd, 'Hourly', name, 'EMS')
+ if ts_opt.is_initialized
+ ts = ts_opt.get
+ values = ts.values
+ annual_j = values.sum * 3600 # Convert from Wh to J (Wh * 3600 s/h)
+ com_report_hvac_dx_heating_load_during_hybrid_heating_j += annual_j
+ else
+ runner.registerError("No time series found for EMS variable #{name} for calculating DX heating load during hybrid heating.")
+ end
+ end
+ runner.registerValue('com_report_hvac_dx_heating_load_during_hybrid_heating_j', com_report_hvac_dx_heating_load_during_hybrid_heating_j)
+
# Get the outdoor air temp timeseries and calculate heating and cooling degree days
# Per ISO 15927-6, "Accumulated hourly temperature differences shall be calculated according to 4.4 when hourly data are available. When hourly data are not available, the approximate method given in 4.5, based on the maximum and minimum temperatures each day, may be used."
# Method 4.4 is used here, summing hour values over/under a threshold and then dividing by 24
diff --git a/measures/comstock_sensitivity_reports/measure.xml b/measures/comstock_sensitivity_reports/measure.xml
index cc2148c4b..35f00c478 100644
--- a/measures/comstock_sensitivity_reports/measure.xml
+++ b/measures/comstock_sensitivity_reports/measure.xml
@@ -3,8 +3,8 @@
3.1
com_stock_sensitivity_reports
aeb81242-de7e-4613-af47-c1faf19d286a
- 0955d5f5-3a15-4c10-9151-5ea649038f6b
- 2025-09-17T17:07:23Z
+ 462e4ec4-a241-4650-9690-6823be753fe5
+ 2026-01-27T16:45:14Z
A0069B90
ComStockSensitivityReports
ComStock_Sensitivity_Reports
@@ -76,13 +76,13 @@
measure.rb
rb
script
- 58DAA5CC
+ 26CFC767
Variable List.xlsx
xlsx
resource
- 3B3560C3
+ D877DBB2
8172d17_0000008.osm
@@ -238,7 +238,7 @@
comstock_sensitivity_reports_test.rb
rb
test
- BA366C89
+ 887C00B2
ground_heat_exchanger.osm
diff --git a/postprocessing/comstockpostproc/resources/comstock_column_definitions.csv b/postprocessing/comstockpostproc/resources/comstock_column_definitions.csv
index b0211464a..efc724549 100644
--- a/postprocessing/comstockpostproc/resources/comstock_column_definitions.csv
+++ b/postprocessing/comstockpostproc/resources/comstock_column_definitions.csv
@@ -533,6 +533,7 @@ results.csv,com_stock_sensitivity_reports.com_report_hvac_dx_heating_total_suppl
results.csv,com_stock_sensitivity_reports.com_report_hvac_dx_heating_total_supplemental_load_electric_j,out.params.dx_heating_total_supplemental_load_electric,TRUE,FALSE,float,j,j,total dx heating equipment supplemental load served by electric
results.csv,com_stock_sensitivity_reports.com_report_hvac_dx_heating_total_supplemental_load_gas_j,out.params.dx_heating_total_supplemental_load_gas,TRUE,FALSE,float,j,j,total dx heating equipment supplemental load served by gas
results.csv,com_stock_sensitivity_reports.com_report_hvac_dx_heating_total_supplemental_load_j,out.params.dx_heating_total_supplemental_load,TRUE,FALSE,float,j,j,total dx heating equipment supplemental load served
+results.csv,com_stock_sensitivity_reports.com_report_hvac_dx_heating_load_during_hybrid_heating_j,out.params.dx_heating_load_during_hybrid_heating,TRUE,FALSE,float,j,j,total dx heating load during hybrid heating with gas coil
results.csv,com_stock_sensitivity_reports.com_report_hvac_economizer_control_type,out.params.economizer_control_type,TRUE,FALSE,string,,,most prevalent economizer control type by total annual airflow
results.csv,com_stock_sensitivity_reports.com_report_hvac_economizer_high_limit_enthalpy_j_per_kg,out.params.economizer_high_limit_enthalpy,TRUE,FALSE,float,j_per_kg,j_per_kg,"average economizer high limited enthalpy, weighted by total annual airflow"
results.csv,com_stock_sensitivity_reports.com_report_hvac_economizer_high_limit_temperature_c,out.params.economizer_high_limit_temperature,TRUE,FALSE,float,c,c,"average economizer high limit temperature, weighted by total annual airflow"
diff --git a/resources/measures/upgrade_hvac_add_heat_pump_rtu/README.md b/resources/measures/upgrade_hvac_add_heat_pump_rtu/README.md
index b8a7295e2..5c2d88f5d 100644
--- a/resources/measures/upgrade_hvac_add_heat_pump_rtu/README.md
+++ b/resources/measures/upgrade_hvac_add_heat_pump_rtu/README.md
@@ -27,7 +27,7 @@ Specifies if the backup heat fuel type is a gas furnace or electric resistance c
**Required:** true,
**Model Dependent:** false
-**Choice Display Names** ["match_original_primary_heating_fuel", "electric_resistance_backup"]
+**Choice Display Names** ["match_original_primary_heating_fuel", "electric_resistance_backup", "dual_fuel_gas_furnace_backup"]
### Maximum Performance Oversizing Factor
@@ -85,7 +85,7 @@ Determines performance assumptions. two_speed_standard_eff is a standard efficie
**Required:** true,
**Model Dependent:** false
-**Choice Display Names** ["two_speed_standard_eff", "two_speed_lab_data", "variable_speed_high_eff", "cchpc_2027_spec"]
+**Choice Display Names** ["two_speed_standard_eff", "two_speed_lab_data", "variable_speed_high_eff", "cchpc_2027_spec", "carrier_48qe_dualfuel"]
### Add Energy Recovery?
diff --git a/resources/measures/upgrade_hvac_add_heat_pump_rtu/measure.rb b/resources/measures/upgrade_hvac_add_heat_pump_rtu/measure.rb
index 41b7963a2..e5c567473 100644
--- a/resources/measures/upgrade_hvac_add_heat_pump_rtu/measure.rb
+++ b/resources/measures/upgrade_hvac_add_heat_pump_rtu/measure.rb
@@ -10,7 +10,10 @@
# start the measure
class AddHeatPumpRtu < OpenStudio::Measure::ModelMeasure
+
+ # ---------------------------------------------------------
# defining global variable
+ # ---------------------------------------------------------
# adding tolerance because EnergyPlus unit conversion differs from manual conversion
# reference: https://github.com/NREL/EnergyPlus/blob/337bfbadf019a80052578d1bad6112dca43036db/src/EnergyPlus/DataHVACGlobals.hh#L362-L368
CFM_PER_TON_MIN_RATED = 300 # hard limit of 300
@@ -19,6 +22,10 @@ class AddHeatPumpRtu < OpenStudio::Measure::ModelMeasure
# CFM_PER_TON_MAX_OPERATIONAL_HEATING = 600 # hard limit of 600 for operational maximum threshold for both heating
# CFM_PER_TON_MAX_OPERATIONAL_COOLING = 500 # hard limit of 500 for operational maximum threshold for both cooling
+ # ---------------------------------------------------------
+ # required methods
+ # ---------------------------------------------------------
+
# human readable name
def name
# Measure name should be the title case of the class name.
@@ -40,7 +47,7 @@ def arguments(_model)
args = OpenStudio::Measure::OSArgumentVector.new
# make list of backup heat options
- li_backup_heat_options = %w[match_original_primary_heating_fuel electric_resistance_backup]
+ li_backup_heat_options = %w[match_original_primary_heating_fuel electric_resistance_backup dual_fuel_gas_furnace_backup]
v_backup_heat_options = OpenStudio::StringVector.new
li_backup_heat_options.each do |option|
v_backup_heat_options << option
@@ -97,7 +104,7 @@ def arguments(_model)
args << hp_min_comp_lockout_temp_f
# make list of cchpc scenarios
- li_hprtu_scenarios = %w[two_speed_standard_eff two_speed_lab_data variable_speed_high_eff cchpc_2027_spec]
+ li_hprtu_scenarios = %w[two_speed_standard_eff two_speed_lab_data variable_speed_high_eff cchpc_2027_spec carrier_48qe_dualfuel]
v_li_hprtu_scenarios = OpenStudio::StringVector.new
li_hprtu_scenarios.each do |option|
v_li_hprtu_scenarios << option
@@ -156,7 +163,7 @@ def arguments(_model)
# modify setbacks or not
modify_setbacks = OpenStudio::Measure::OSArgument.makeBoolArgument('modify_setbacks', false)
modify_setbacks.setDisplayName('Modify setbacks in heating mode? True will adjust setbacks, according to value in setback value argument.')
- modify_setbacks.setDefaultValue(true)
+ modify_setbacks.setDefaultValue(false)
args << modify_setbacks
# setback value
@@ -181,8 +188,16 @@ def outputs
result
end
- #### Predefined functions
+ # ---------------------------------------------------------
+ # supporting methods
+ # ---------------------------------------------------------
+
# determine if the air loop is residential (checks to see if there is outdoor air system object)
+ # Determines if an air loop is a residential system based on its components.
+ # A system is considered residential if it does NOT contain an outdoor air system component.
+ #
+ # @param air_loop_hvac [OpenStudio::Model::AirLoopHVAC] the air loop to check
+ # @return [Boolean] true if the air loop is a residential system (no outdoor air system), false otherwise
def air_loop_res?(air_loop_hvac)
is_res_system = true
air_loop_hvac.supplyComponents.each do |component|
@@ -196,6 +211,13 @@ def air_loop_res?(air_loop_hvac)
end
# Determine if is evaporative cooler
+ # Checks if an air loop contains any evaporative cooler components.
+ #
+ # This method iterates through all supply components of the given air loop
+ # and determines if any evaporative cooling equipment is present.
+ #
+ # @param air_loop_hvac [OpenStudio::Model::AirLoopHVAC] the air loop to check for evaporative coolers
+ # @return [Boolean] true if the air loop contains any evaporative cooler component, false otherwise
def air_loop_evaporative_cooler?(air_loop_hvac)
is_evap = false
air_loop_hvac.supplyComponents.each do |component|
@@ -210,6 +232,14 @@ def air_loop_evaporative_cooler?(air_loop_hvac)
# Determine if the air loop is a unitary system
# @return [Bool] Returns true if a unitary system is present, false if not.
+ # Determines if an air loop HVAC system contains a unitary system component.
+ #
+ # This method checks the supply components of an air loop to identify if any
+ # of them are unitary system types, including standard unitary systems,
+ # air-to-air heat pumps, multi-speed heat pumps, or VAV changeover bypass systems.
+ #
+ # @param air_loop_hvac [OpenStudio::Model::AirLoopHVAC] the air loop HVAC system to check
+ # @return [Boolean] true if the air loop contains a unitary system component, false otherwise
def air_loop_hvac_unitary_system?(air_loop_hvac)
is_unitary_system = false
air_loop_hvac.supplyComponents.each do |component|
@@ -224,6 +254,16 @@ def air_loop_hvac_unitary_system?(air_loop_hvac)
# Load curve to model from json
# modified version from OS Standards to read from custom json file
+ # Adds a performance curve to the OpenStudio model based on curve data from standards.
+ # First checks if the curve already exists in the model and returns it if found.
+ # Otherwise, creates a new curve object of the appropriate type and configures it
+ # with coefficients and limits from the standards data.
+ #
+ # @param model [OpenStudio::Model::Model] the OpenStudio model object
+ # @param curve_name [String] the name of the curve to add
+ # @param standards_data_curve [Hash] hash containing the 'tables' key with curve data
+ # @param std [Standard] the standards object used to find curve data
+ # @return [OpenStudio::Model::Curve, OpenStudio::Model::TableLookup, nil] the curve object if found/created, nil if curve data not found
def model_add_curve(model, curve_name, standards_data_curve, std)
# First check model and return curve if it already exists
existing_curves = []
@@ -404,6 +444,32 @@ def model_add_curve(model, curve_name, standards_data_curve, std)
end
# Assign staging data from json
+ # Extracts and returns heat pump staging configuration data from a JSON data structure.
+ # Parses performance parameters including stage counts, capacity fractions, flow fractions,
+ # COP fractions, and other staging-related settings for both heating and cooling operations.
+ #
+ # @param staging_data_json [Hash] JSON hash containing staging data in 'tables']['curves'] structure
+ # @param std [Standard] OpenStudio Standards object used for data lookup
+ # @return [Array]
+ # Returns an array containing:
+ # - num_heating_stages: number of heating stages
+ # - num_cooling_stages: number of cooling stages
+ # - rated_stage_num_heating: rated heating stage number
+ # - rated_stage_num_cooling: rated cooling stage number
+ # - final_rated_cooling_cop: rated cooling COP
+ # - final_rated_heating_cop: rated heating COP
+ # - stage_cap_fractions_heating: heating capacity fractions by stage
+ # - stage_flow_fractions_heating: heating flow fractions by stage
+ # - stage_cap_fractions_cooling: cooling capacity fractions by stage
+ # - stage_flow_fractions_cooling: cooling flow fractions by stage
+ # - stage_rated_cop_frac_heating: heating COP fractions by stage
+ # - stage_rated_cop_frac_cooling: cooling COP fractions by stage
+ # - boost_stage_num_and_max_temp_tuple: boost stage configuration
+ # - stage_gross_rated_sensible_heat_ratio_cooling: sensible heat ratios for cooling stages
+ # - enable_cycling_losses_above_lowest_speed: flag for cycling losses
+ # - reference_cooling_cfm_per_ton: reference cooling airflow per ton
+ # - reference_heating_cfm_per_ton: reference heating airflow per ton
+ # @return [nil] returns nil if staging data cannot be found in the JSON structure
def assign_staging_data(staging_data_json, std)
# Parse the JSON string into a Ruby hash
# Find curve data
@@ -438,6 +504,9 @@ def assign_staging_data(staging_data_json, std)
# Get rated cooling COP from fitted regression
# based on actual product performances (Carrier/Lennox) which meet 2023 federal minimum efficiency requirements
# reflecting rated COP without blower power and blower heat gain
+ #
+ # @param rated_capacity_w [Float] the rated cooling capacity in watts
+ # @return [Float] the rated cooling COP, clamped between min_cop (3.02) and max_cop (3.97)
def get_rated_cop_cooling(rated_capacity_w)
intercept = 3.881009
coef_1 = -0.01034
@@ -451,6 +520,12 @@ def get_rated_cop_cooling(rated_capacity_w)
# Get rated heating COP from fitted regression
# based on actual product performances (Carrier/Lennox) which meet 2023 federal minimum efficiency requirements
# reflecting rated COP without blower power and blower heat gain
+ # Calculates the rated Coefficient of Performance (COP) for heating based on the equipment's rated capacity.
+ # The calculation uses a linear regression model with capacity-based coefficients and applies
+ # minimum and maximum COP constraints to ensure the result falls within acceptable performance bounds.
+ #
+ # @param rated_capacity_w [Numeric] The rated heating capacity in Watts
+ # @return [Float] The rated heating COP, constrained between 3.46 and 3.99
def get_rated_cop_heating(rated_capacity_w)
intercept = 3.957724
coef_1 = -0.008502
@@ -462,6 +537,14 @@ def get_rated_cop_heating(rated_capacity_w)
end
# Get rated cooling COP from fitted regression - for advanced HP RTU (from Daikin Rebel data)
+ # Calculates the rated Coefficient of Performance (COP) for cooling in advanced mode
+ # based on the rated capacity of the equipment.
+ #
+ # The COP is calculated using a linear regression model with capacity as the independent variable.
+ # The result is clamped between minimum and maximum COP values to ensure realistic performance bounds.
+ #
+ # @param rated_capacity_w [Float] The rated cooling capacity in watts (W)
+ # @return [Float] The rated cooling COP, clamped between 3.34 and 4.29
def get_rated_cop_cooling_adv(rated_capacity_w)
intercept = 4.140806
coef_1 = -0.007577
@@ -473,6 +556,12 @@ def get_rated_cop_cooling_adv(rated_capacity_w)
end
# Get rated heating COP from fitted regression - for advanced HP RTU (from Daikin Rebel data)
+ # Calculates the rated coefficient of performance (COP) for heating in advanced heat pump systems
+ # based on the rated capacity. The COP is determined using a linear regression model with
+ # capacity-based adjustments and is clamped between minimum and maximum values.
+ #
+ # @param rated_capacity_w [Float] the rated heating capacity in watts (W)
+ # @return [Float] the rated heating COP, clamped between 3.5 and 3.87
def get_rated_cop_heating_adv(rated_capacity_w)
intercept = 3.861114
coef_1 = -0.003304
@@ -483,17 +572,79 @@ def get_rated_cop_heating_adv(rated_capacity_w)
rated_cop_heating.clamp(min_cop, max_cop)
end
- # Convert unit
+ # Get rated cooling COP from fitted regression - for Carrier dual fuel RTU (48QE)
+ # Calculates the rated coefficient of performance (COP) for cooling in Carrier's 48QE dual fuel RTU
+ # based on the rated capacity of the equipment.
+ #
+ # The COP is calculated using a linear regression model with capacity as the independent variable.
+ # The result is clamped between minimum and maximum COP values to ensure realistic performance bounds.
+ #
+ # @param rated_capacity_w [Float] The rated cooling capacity in watts (W)
+ # @return [Float] The rated cooling COP, clamped between 3.07 and 3.91
+ def get_rated_cop_cooling_dualfuelrtu(rated_capacity_w)
+ intercept = 3.99207113
+ coef_1 = -0.00000969
+ min_cop = 3.07
+ max_cop = 3.91
+ rated_cop_cooling = intercept + (coef_1 * rated_capacity_w)
+ rated_cop_cooling.clamp(min_cop, max_cop)
+ end
+
+ # Get rated heating COP from fitted regression - for Carrier dual fuel RTU (48QE)
+ # Calculates the rated coefficient of performance (COP) for heating in a dual fuel RTU
+ # based on the rated capacity using a linear regression model.
+ #
+ # The COP is calculated using the formula: COP = intercept + (coefficient * capacity)
+ # The result is clamped between minimum and maximum COP values to ensure realistic performance.
+ #
+ # @param rated_capacity_w [Float] The rated heating capacity in watts
+ # @return [Float] The rated heating COP, clamped between 3.57 and 3.89
+ def get_rated_cop_heating_dualfuelrtu(rated_capacity_w)
+ intercept = 3.83411768
+ coef_1 = -0.00000337
+ min_cop = 3.57
+ max_cop = 3.89
+ rated_cop_heating = intercept + (coef_1 * rated_capacity_w)
+ rated_cop_heating.clamp(min_cop, max_cop)
+ end
+
+ # Converts airflow per cooling capacity from CFM per ton to cubic meters per second per watt.
+ #
+ # This conversion is used when transitioning between imperial and metric units for HVAC sizing calculations.
+ # The conversion accounts for both volumetric flow rate (CFM to m³/s) and capacity (tons to watts).
+ #
+ # @param cfm_per_ton [Float] airflow rate in cubic feet per minute per ton of cooling capacity
+ # @return [Float] airflow rate in cubic meters per second per watt of cooling capacity
def cfm_per_ton_to_m_3_per_sec_watts(cfm_per_ton)
OpenStudio.convert(OpenStudio.convert(cfm_per_ton, 'cfm', 'm^3/s').get, 'W', 'ton').get
end
- # Convert unit
+ # Converts airflow per cooling capacity from cubic meters per second per watt to CFM per ton.
+ #
+ # This conversion is used when transitioning between metric and imperial units for HVAC sizing calculations.
+ # The conversion accounts for both volumetric flow rate (m³/s to CFM) and capacity (watts to tons).
+ #
+ # @param m_3_per_sec_watts [Float] airflow rate in cubic meters per second per watt of cooling capacity
+ # @return [Float] airflow rate in cubic feet per minute per ton of cooling capacity
def m_3_per_sec_watts_to_cfm_per_ton(m_3_per_sec_watts)
OpenStudio.convert(OpenStudio.convert(m_3_per_sec_watts, 'm^3/s', 'cfm').get, 'ton', 'W').get
end
- # Adjust rated COP based on reference CFM/ton
+ # Adjusts the rated Coefficient of Performance (COP) based on reference airflow per ton of capacity.
+ #
+ # This method calculates an adjusted COP by evaluating how the actual sized airflow compares to
+ # a reference airflow rate (specified in CFM per ton). The adjustment uses an Energy Input Ratio (EIR)
+ # modifier curve as a function of flow fraction to determine the performance impact.
+ #
+ # @param runner [OpenStudio::Measure::OSRunner] the measure runner for logging
+ # @param airflow_sized_m_3_per_s [Float] the actual sized airflow rate in cubic meters per second
+ # @param reference_cfm_per_ton [Float] the reference airflow rate in cubic feet per minute per ton of capacity
+ # @param rated_capacity_w [Float] the rated capacity in watts
+ # @param original_rated_cop [Float] the original rated Coefficient of Performance before adjustment
+ # @param eir_modifier_curve_flow [OpenStudio::Model::Curve] the EIR modifier curve as a function of flow fraction
+ # (supports CurveBiquadratic, CurveQuadratic, or CurveCubic types)
+ # @return [Float] the adjusted rated COP accounting for the difference between sized and reference airflow
+ # @raise [RuntimeError] if the eir_modifier_curve_flow is not a supported curve type
def adjust_rated_cop_from_ref_cfm_per_ton(runner, airflow_sized_m_3_per_s, reference_cfm_per_ton, rated_capacity_w,
original_rated_cop, eir_modifier_curve_flow)
# get reference airflow
@@ -518,7 +669,35 @@ def adjust_rated_cop_from_ref_cfm_per_ton(runner, airflow_sized_m_3_per_s, refer
original_rated_cop * (1.0 / modifier_eir)
end
- # Adjust CFM/ton based on limits
+ # Adjusts airflow and capacity for each stage to ensure CFM per ton ratios stay within acceptable bounds.
+ # This method validates and adjusts stage-level airflows and capacities to maintain CFM/ton ratios
+ # between minimum (300) and maximum (450) limits. Lower speed stages that cannot meet these limits
+ # may be disabled. If the rated/highest stage violates limits, airflow is adjusted to comply.
+ #
+ # The method:
+ # - Calculates flow per ton for each stage
+ # - Adjusts airflow or capacity if outside CFM/ton bounds
+ # - May disable lower stages if they cannot meet minimum airflow requirements
+ # - Ensures at least 2 stages remain active when possible
+ # - Updates stage flow fractions based on terminal supply airflow
+ #
+ # @param stage_cap_fractions [Hash] Hash of stage number to capacity fraction (relative to rated capacity)
+ # @param stage_flows [Hash] Hash of stage number to airflow in m³/s
+ # @param stage_flow_fractions [Hash] Hash of stage number to flow fraction (relative to design flow)
+ # @param dx_rated_cap_applied [Float] Applied rated DX capacity in watts after any upsizing
+ # @param rated_stage_num [Integer] The stage number that represents rated conditions
+ # @param old_terminal_sa_flow_m3_per_s [Float] Original terminal supply air flow rate in m³/s
+ # @param min_airflow_ratio [Float] Minimum allowable airflow ratio to maintain ventilation requirements
+ # @param air_loop_hvac [OpenStudio::Model::AirLoopHVAC] The air loop being modified
+ # @param heating_or_cooling [String] Either 'heating' or 'cooling' to identify which mode is being adjusted
+ # @param runner [OpenStudio::Measure::OSRunner] The measure runner for logging
+ # @param debug_verbose [Boolean] Flag to enable detailed debug logging
+ # @return [Array] Returns array containing:
+ # - stage_flows: updated hash of stage flows in m³/s
+ # - stage_caps: updated hash of stage capacities in watts
+ # - stage_flow_fractions: updated hash of stage flow fractions
+ # - stage_cap_fractions: updated hash of stage capacity fractions
+ # - num_stages: final number of active stages after adjustments
def adjust_cfm_per_ton_per_limits(stage_cap_fractions, stage_flows, stage_flow_fractions, dx_rated_cap_applied,
rated_stage_num, old_terminal_sa_flow_m3_per_s, min_airflow_ratio, air_loop_hvac, heating_or_cooling, runner, debug_verbose)
# determine capacities for each stage
@@ -635,7 +814,33 @@ def adjust_cfm_per_ton_per_limits(stage_cap_fractions, stage_flows, stage_flow_f
[stage_flows, stage_caps, stage_flow_fractions, stage_cap_fractions, num_stages]
end
- # Set coling coil stages in relevant objects/fields
+ # Sets up cooling coil configuration with appropriate number of stages and performance curves.
+ # Creates either a single-speed or multi-speed DX cooling coil based on the number of stages,
+ # and assigns performance curves, rated capacities, airflows, and other operating parameters
+ # to each stage. Handles stage-specific capacity fractions, flow rates, COP values, and
+ # sensible heat ratios.
+ #
+ # @param model [OpenStudio::Model::Model] the OpenStudio model object
+ # @param runner [OpenStudio::Measure::OSRunner] the measure runner for logging
+ # @param stage_flows_cooling [Hash] hash mapping stage number to design airflow rate (m³/s)
+ # @param stage_caps_cooling [Hash] hash mapping stage number to cooling capacity (W)
+ # @param num_cooling_stages [Integer] total number of cooling stages
+ # @param final_rated_cooling_cop [Float] the rated cooling coefficient of performance
+ # @param cool_cap_ft_curve_stages [Hash] hash mapping stage number to capacity modifier curve (function of temperature)
+ # @param cool_eir_ft_curve_stages [Hash] hash mapping stage number to EIR modifier curve (function of temperature)
+ # @param cool_cap_ff_curve_stages [Hash] hash mapping stage number to capacity modifier curve (function of flow fraction)
+ # @param cool_eir_ff_curve_stages [Hash] hash mapping stage number to EIR modifier curve (function of flow fraction)
+ # @param cool_plf_fplr1 [OpenStudio::Model::Curve] part load fraction curve as function of part load ratio
+ # @param stage_rated_cop_frac_cooling [Hash] hash mapping stage number to COP fraction relative to rated COP
+ # @param stage_gross_rated_sensible_heat_ratio_cooling [Hash] hash mapping stage number to sensible heat ratio
+ # @param rated_stage_num_cooling [Integer] the stage number representing rated conditions
+ # @param enable_cycling_losses_above_lowest_speed [Boolean] flag to enable part load losses for speeds above stage 1
+ # @param air_loop_hvac [OpenStudio::Model::AirLoopHVAC] the air loop being modified
+ # @param always_on [OpenStudio::Model::ScheduleConstant] always-on schedule for availability
+ # @param _stage_caps_heating [Hash] hash of heating stage capacities (unused parameter)
+ # @param debug_verbose [Boolean] flag to enable detailed debug logging
+ # @return [OpenStudio::Model::CoilCoolingDXSingleSpeed, OpenStudio::Model::CoilCoolingDXMultiSpeed]
+ # the configured cooling coil object (single-speed for 1 stage, multi-speed for multiple stages)
def set_cooling_coil_stages(model, runner, stage_flows_cooling, stage_caps_cooling, num_cooling_stages, final_rated_cooling_cop, cool_cap_ft_curve_stages, cool_eir_ft_curve_stages,
cool_cap_ff_curve_stages, cool_eir_ff_curve_stages, cool_plf_fplr1, stage_rated_cop_frac_cooling, stage_gross_rated_sensible_heat_ratio_cooling,
rated_stage_num_cooling, enable_cycling_losses_above_lowest_speed, air_loop_hvac, always_on, _stage_caps_heating, debug_verbose)
@@ -728,7 +933,36 @@ def set_cooling_coil_stages(model, runner, stage_flows_cooling, stage_caps_cooli
new_dx_cooling_coil
end
- # Set coling coil stages in relevant objects/fields
+ # Sets up heating coil stages for a heat pump RTU system
+ #
+ # This method configures either a single-speed or multi-speed DX heating coil based on the number
+ # of heating stages defined. It validates that the number of capacity stages matches the number
+ # of flow stages, then creates and configures the appropriate coil type with performance curves,
+ # defrost settings, and crankcase heater specifications.
+ #
+ # @param model [OpenStudio::Model::Model] The OpenStudio model object
+ # @param runner [OpenStudio::Measure::OSRunner] The measure runner for logging
+ # @param stage_flows_heating [Hash] Hash of heating airflow rates by stage number
+ # @param stage_caps_heating [Hash] Hash of heating capacities by stage number
+ # @param num_heating_stages [Integer] Number of heating stages
+ # @param final_rated_heating_cop [Float] Rated heating coefficient of performance
+ # @param heat_cap_ft_curve_stages [Hash] Hash of heating capacity function of temperature curves by stage
+ # @param heat_eir_ft_curve_stages [Hash] Hash of heating EIR function of temperature curves by stage
+ # @param heat_cap_ff_curve_stages [Hash] Hash of heating capacity function of flow fraction curves by stage
+ # @param heat_eir_ff_curve_stages [Hash] Hash of heating EIR function of flow fraction curves by stage
+ # @param heat_plf_fplr1 [OpenStudio::Model::Curve] Part load fraction correlation curve
+ # @param defrost_eir [OpenStudio::Model::Curve] Defrost energy input ratio curve
+ # @param _stage_rated_cop_frac_heating [Hash] Hash of COP fractions by stage (for multi-speed only)
+ # @param rated_stage_num_heating [Integer] The rated stage number for heating
+ # @param air_loop_hvac [OpenStudio::Model::AirLoopHVAC] The air loop HVAC system
+ # @param hp_min_comp_lockout_temp_f [Float] Minimum outdoor temperature for compressor operation in Fahrenheit
+ # @param enable_cycling_losses_above_lowest_speed [Boolean] Whether to apply part load fraction to speeds > 1
+ # @param always_on [OpenStudio::Model::ScheduleConstant] Schedule that is always on
+ # @param _stage_caps_cooling [Hash] Hash of cooling capacities by stage (unused but kept for compatibility)
+ # @param debug_verbose [Boolean] Flag to enable verbose debug logging
+ #
+ # @return [OpenStudio::Model::CoilHeatingDXSingleSpeed, OpenStudio::Model::CoilHeatingDXMultiSpeed]
+ # Returns the newly created heating coil object (single-speed or multi-speed depending on num_heating_stages)
def set_heating_coil_stages(model, runner, stage_flows_heating, stage_caps_heating, num_heating_stages, final_rated_heating_cop, heat_cap_ft_curve_stages, heat_eir_ft_curve_stages,
heat_cap_ff_curve_stages, heat_eir_ff_curve_stages, heat_plf_fplr1, defrost_eir, _stage_rated_cop_frac_heating, rated_stage_num_heating, air_loop_hvac, hp_min_comp_lockout_temp_f,
enable_cycling_losses_above_lowest_speed, always_on, _stage_caps_cooling, debug_verbose)
@@ -823,8 +1057,24 @@ def set_heating_coil_stages(model, runner, stage_flows_heating, stage_caps_heati
end
new_dx_heating_coil
end
-
- # Get tabular data from sql file
+
+ # Retrieves a specific numeric value from the OpenStudio SQL tabular data output.
+ #
+ # This method queries the TabularDataWithStrings table in the SQL file to extract
+ # a single double value based on the provided report structure identifiers.
+ #
+ # @param runner [OpenStudio::Measure::OSRunner] The measure runner for logging
+ # @param _model [OpenStudio::Model::Model] The OpenStudio model (unused parameter)
+ # @param sql [OpenStudio::SqlFile] The SQL file object containing simulation results
+ # @param report_name [String] The name of the report (e.g., 'AnnualBuildingUtilityPerformanceSummary')
+ # @param report_for_string [String] The report scope identifier (e.g., 'Entire Facility')
+ # @param table_name [String] The name of the table within the report
+ # @param row_name [String] The row identifier in the table
+ # @param column_name [String] The column identifier in the table
+ #
+ # @return [OpenStudio::OptionalDouble] An OptionalDouble containing the queried value if found,
+ # or an uninitialized OptionalDouble if the query fails. Registers an error with the runner
+ # if the value cannot be retrieved.
def get_tabular_data(runner, _model, sql, report_name, report_for_string, table_name, row_name, column_name)
result = OpenStudio::OptionalDouble.new
var_val_query = "SELECT Value FROM TabularDataWithStrings WHERE ReportName = '#{report_name}' AND ReportForString = '#{report_for_string}' AND TableName = '#{table_name}' AND RowName = '#{row_name}' AND ColumnName = '#{column_name}'"
@@ -836,7 +1086,23 @@ def get_tabular_data(runner, _model, sql, report_name, report_for_string, table_
end
result
end
-
+
+ # Retrieves a dependent variable value from a 2D lookup table using bilinear interpolation.
+ #
+ # This method performs bilinear interpolation on a TableLookup object with exactly two
+ # independent variables. It extracts the independent variable arrays and dependent variable
+ # values, clamps input values to the table bounds, and interpolates between grid points.
+ #
+ # @param runner [Object] The runner object used for logging warnings and errors
+ # @param lookup_table [Object] A TableLookup object containing:
+ # - independentVariables: Array of two independent variable arrays
+ # - outputValues: Flattened array of dependent variable values
+ # @param input1 [Numeric] The first input value to interpolate (corresponds to first independent variable)
+ # @param input2 [Numeric] The second input value to interpolate (corresponds to second independent variable)
+ #
+ # @return [Numeric, false] The interpolated dependent variable value, or false if:
+ # - The table doesn't have exactly two independent variables
+ # - Table dimensions don't match output size
def self.get_dep_var_from_lookup_table_with_interpolation(runner, lookup_table, input1, input2)
if lookup_table.independentVariables.size == 2
# Extract independent variable arrays
@@ -920,6 +1186,16 @@ def self.get_dep_var_from_lookup_table_with_interpolation(runner, lookup_table,
end
end
+ # Determines if a thermostat schedule contains part of an optimum start sequence at a given index.
+ # Optimum start is identified when the zone will be occupied in the next 1-2 time steps
+ # and the heating schedule value falls within the specified min/max range.
+ #
+ # @param sch_zone_occ_annual_profile [Array] Annual occupancy schedule profile (0 = unoccupied, 1 = occupied)
+ # @param htg_schedule_annual_profile [Array] Annual heating schedule profile with temperature setpoints
+ # @param min_value [Float] Minimum threshold value for heating schedule to be considered optimum start
+ # @param max_value [Float] Maximum threshold value for heating schedule to be considered optimum start
+ # @param idx [Integer] Index position in the annual profile arrays to evaluate
+ # @return [Boolean, nil] Returns true if optimum start conditions are met, nil otherwise
def opt_start?(sch_zone_occ_annual_profile, htg_schedule_annual_profile, min_value, max_value, idx)
# method to determine if a thermostat schedule contains part of an optimum start sequence at a given index
if (sch_zone_occ_annual_profile[idx + 1] == 1 || sch_zone_occ_annual_profile[idx + 2] == 1) &&
@@ -928,7 +1204,501 @@ def opt_start?(sch_zone_occ_annual_profile, htg_schedule_annual_profile, min_val
end
end
- #### End predefined functions
+ # Creates a two-stage dual fuel gas backup heating coil with Energy Management System (EMS) controls
+ # for hybrid heat pump systems.
+ #
+ # The method creates a user-defined coil with EMS actuators and sensors to control:
+ # - Two-stage gas heating operation (stage 1 and stage 2 with different capacities)
+ # - Integration with existing heat pump for dual fuel operation
+ # - Part load ratio calculations for each heating stage
+ # - Gas consumption tracking and metering for both stages
+ # - Dynamic adjustment of outlet temperature, humidity ratio, and mass flow rate
+ # - Fuel usage metering and reporting for each stage
+ #
+ # @param model [OpenStudio::Model::Model] The OpenStudio model object
+ # @param runner [OpenStudio::Measure::OSRunner] The measure runner for logging and error handling
+ # @param air_loop_hvac [OpenStudio::Model::AirLoopHVAC] The air loop to which the coil will be added
+ # @param new_dx_heating_coil [OpenStudio::Model::CoilHeatingDXSingleSpeed] The existing DX heating coil (heat pump)
+ # @param orig_htg_coil_gross_cap_old [Float] Original gross heating capacity in watts for stage 2 sizing
+ # @param new_air_to_air_heatpump [OpenStudio::Model::AirLoopHVACUnitarySystem] The heat pump unitary system
+ # @param hp_min_comp_lockout_temp_f [Float] Minimum outdoor temperature in Fahrenheit for heat pump operation
+ # @param dx_rated_htg_cap_applied [Float] Rated heating capacity of the DX coil after adjustments
+ # @return [void] Creates and configures the dual fuel gas coil system with EMS controls
+ def create_two_stage_dual_fuel_gas_coil_with_ems(
+ model, runner, air_loop_hvac, new_dx_heating_coil, orig_htg_coil_gross_cap_old,
+ new_air_to_air_heatpump, hp_min_comp_lockout_temp_f,
+ dx_rated_htg_cap_applied
+ )
+
+ # get simulation timestep
+ num_steps_per_hr = model.getSimulationControl.timestep.get.numberOfTimestepsPerHour
+
+ # initialize constants
+ heating_capacity_stage_1_w, heating_capacity_stage_2_w = get_dual_fuel_gas_coil_capacity(dx_rated_htg_cap_applied)
+
+ # replace airloop name based on this hash and create Erl friendly name
+ label_map = {
+ 'wholebuilding' => 'wb',
+ 'office' => 'off',
+ 'zone' => 'zn',
+ 'story' => 'stry',
+ 'ground' => 'grnd',
+ 'psz-ac' => '',
+ 'fullservicerestaurant' => 'fsr',
+ 'dining' => 'din',
+ }
+ ems_name_airloop = air_loop_hvac.name.to_s.downcase
+
+ # Replace longer keys first to avoid partial collisions
+ label_map.sort_by { |k, _| -k.length }.each do |key, value|
+ ems_name_airloop.gsub!(key, value)
+ end
+
+ # Replace non-alphanumeric with underscore
+ ems_name_airloop.gsub!(/[^a-z0-9]/, '_')
+
+ # Collapse underscores
+ ems_name_airloop.gsub!(/_+/, '_')
+ ems_name_airloop.gsub!(/^_|_$/, '')
+
+ # Ensure valid EMS identifier (must start with letter)
+ unless ems_name_airloop =~ /^[a-z]/
+ ems_name_airloop = "a_#{ems_name_airloop}"
+ end
+
+ # -------------------------------------------------------------------------------
+ # Create user-defined backup heating coil and attach early
+ # -------------------------------------------------------------------------------
+
+ new_backup_heating_coil = OpenStudio::Model::CoilUserDefined.new(model)
+ new_backup_heating_coil.setName("#{ems_name_airloop}_hybrid_gas_heating_coil")
+ new_backup_heating_coil.addToNode(new_air_to_air_heatpump.outletNode.get)
+
+ # -------------------------------------------------------------------------------
+ # EMS actuators
+ # -------------------------------------------------------------------------------
+
+ a_coil_outlet_t = OpenStudio::Model::EnergyManagementSystemActuator.new(
+ new_backup_heating_coil, "Air Connection 1", "Outlet Temperature"
+ )
+ a_coil_outlet_t.setName("#{ems_name_airloop}_a_coil_outlet_t")
+
+ a_coil_outlet_hr = OpenStudio::Model::EnergyManagementSystemActuator.new(
+ new_backup_heating_coil, "Air Connection 1", "Outlet Humidity Ratio"
+ )
+ a_coil_outlet_hr.setName("#{ems_name_airloop}_a_coil_outlet_hr")
+
+ a_coil_outlet_mdot = OpenStudio::Model::EnergyManagementSystemActuator.new(
+ new_backup_heating_coil, "Air Connection 1", "Mass Flow Rate"
+ )
+ a_coil_outlet_mdot.setName("#{ems_name_airloop}_a_coil_outlet_mdot")
+
+ # -------------------------------------------------------------------------------
+ # EMS sensors
+ # -------------------------------------------------------------------------------
+
+ s_coil_inlet_t = OpenStudio::Model::EnergyManagementSystemInternalVariable.new(
+ model, "Inlet Temperature for Air Connection 1"
+ )
+ s_coil_inlet_t.setName("#{ems_name_airloop}_s_coil_outlet_t")
+ s_coil_inlet_t.setInternalDataIndexKeyName(new_backup_heating_coil.name.to_s)
+
+ s_coil_inlet_hr = OpenStudio::Model::EnergyManagementSystemInternalVariable.new(
+ model, "Inlet Humidity Ratio for Air Connection 1"
+ )
+ s_coil_inlet_hr.setName("#{ems_name_airloop}_s_coil_outlet_hr")
+ s_coil_inlet_hr.setInternalDataIndexKeyName(new_backup_heating_coil.name.to_s)
+
+ s_coil_inlet_mdot = OpenStudio::Model::EnergyManagementSystemInternalVariable.new(
+ model, "Inlet Mass Flow Rate for Air Connection 1"
+ )
+ s_coil_inlet_mdot.setName("#{ems_name_airloop}_s_coil_outlet_mdot")
+ s_coil_inlet_mdot.setInternalDataIndexKeyName(new_backup_heating_coil.name.to_s)
+
+ s_dx_runtime_frac = OpenStudio::Model::EnergyManagementSystemSensor.new(
+ model, "Heating Coil Runtime Fraction"
+ )
+ s_dx_runtime_frac.setName("#{ems_name_airloop}_s_dx_runtime_frac")
+ s_dx_runtime_frac.setKeyName(new_dx_heating_coil.name.to_s)
+
+ s_dx_heating_load = OpenStudio::Model::EnergyManagementSystemSensor.new(
+ model, "Heating Coil Heating Rate"
+ )
+ s_dx_heating_load.setName("#{ems_name_airloop}_s_dx_heating_load")
+ s_dx_heating_load.setKeyName(new_dx_heating_coil.name.to_s)
+
+ s_airloop_setpoint_t = OpenStudio::Model::EnergyManagementSystemSensor.new(
+ model, "System Node Setpoint Temperature"
+ )
+ s_airloop_setpoint_t.setName("#{ems_name_airloop}_s_airloop_setpoint_t")
+ s_airloop_setpoint_t.setKeyName(air_loop_hvac.supplyOutletNode.name.to_s)
+
+ s_oat_t = OpenStudio::Model::EnergyManagementSystemSensor.new(
+ model, "Site Outdoor Air Drybulb Temperature"
+ )
+ s_oat_t.setName("#{ems_name_airloop}_s_oat_t")
+ s_oat_t.setKeyName(air_loop_hvac.supplyOutletNode.name.to_s)
+
+ # -------------------------------------------------------------------------------
+ # EMS global variables
+ # -------------------------------------------------------------------------------
+
+ g_stage_1 = OpenStudio::Model::EnergyManagementSystemGlobalVariable.new(
+ model, "#{ems_name_airloop}_g_stage_1"
+ )
+ g_stage_2 = OpenStudio::Model::EnergyManagementSystemGlobalVariable.new(
+ model, "#{ems_name_airloop}_g_stage_2"
+ )
+ g_part_load_ratio_1 = OpenStudio::Model::EnergyManagementSystemGlobalVariable.new(
+ model, "#{ems_name_airloop}_g_part_load_ratio_1"
+ )
+ g_part_load_ratio_2 = OpenStudio::Model::EnergyManagementSystemGlobalVariable.new(
+ model, "#{ems_name_airloop}_g_part_load_ratio_2"
+ )
+ g_fuel_usage_1 = OpenStudio::Model::EnergyManagementSystemGlobalVariable.new(
+ model, "#{ems_name_airloop}_g_fuel_usage_1_w"
+ )
+ g_fuel_usage_2 = OpenStudio::Model::EnergyManagementSystemGlobalVariable.new(
+ model, "#{ems_name_airloop}_g_fuel_usage_2_w"
+ )
+ g_dx_load_during_hybrid_heating = OpenStudio::Model::EnergyManagementSystemGlobalVariable.new(
+ model, "#{ems_name_airloop}_g_dx_load_during_hybrid_heating_w"
+ )
+
+ # -------------------------------------------------------------------------------
+ # EMS program
+ # -------------------------------------------------------------------------------
+
+ # ems program for two stage gas coil control
+ ems_program = OpenStudio::Model::EnergyManagementSystemProgram.new(model)
+ ems_program.setName("#{ems_name_airloop}_p_two_stage_gas_coil")
+
+ # Initialize flags, PLRs, actuator passthroughs, constants
+ ems_program.addLine("SET #{g_stage_1.name} = 0")
+ ems_program.addLine("SET #{g_stage_2.name} = 0")
+ ems_program.addLine("SET #{g_part_load_ratio_1.name} = 0")
+ ems_program.addLine("SET #{g_part_load_ratio_2.name} = 0")
+ ems_program.addLine("SET #{a_coil_outlet_t.name} = #{s_coil_inlet_t.name}")
+ ems_program.addLine("SET #{a_coil_outlet_hr.name} = #{s_coil_inlet_hr.name}")
+ ems_program.addLine("SET #{a_coil_outlet_mdot.name} = #{s_coil_inlet_mdot.name}")
+ ems_program.addLine("SET T_set = #{s_airloop_setpoint_t.name}")
+ ems_program.addLine("SET cp = @CpAirFnW #{s_coil_inlet_hr.name}")
+ ems_program.addLine("SET cap_stage_2 = #{heating_capacity_stage_2_w}")
+ ems_program.addLine("SET cap_stage_1 = #{heating_capacity_stage_1_w}")
+ ems_program.addLine("SET burner_eff = 0.8")
+ ems_program.addLine("SET #{g_fuel_usage_1.name} = 0.0")
+ ems_program.addLine("SET #{g_fuel_usage_2.name} = 0.0")
+ ems_program.addLine("SET #{g_dx_load_during_hybrid_heating.name} = 0.0")
+ ems_program.addLine("SET mech_heat_enable = 0")
+
+ # Mechanical heating must be active
+ ems_program.addLine("IF #{s_dx_runtime_frac.name} > 0.0")
+ ems_program.addLine(" SET mech_heat_enable = 1")
+ ems_program.addLine("ELSEIF #{s_oat_t.name} < #{hp_min_comp_lockout_temp_f}")
+ ems_program.addLine(" SET mech_heat_enable = 1")
+ ems_program.addLine("ENDIF")
+ ems_program.addLine("IF mech_heat_enable == 1")
+ ems_program.addLine(" IF #{s_coil_inlet_t.name} < T_set")
+ ems_program.addLine(" IF #{s_coil_inlet_mdot.name} > 0.0")
+
+ # Stage 1
+ ems_program.addLine(" SET #{g_stage_1.name} = 1")
+ ems_program.addLine(" SET dT1_full = cap_stage_1 / #{s_coil_inlet_mdot.name} / cp")
+ ems_program.addLine(" SET #{g_dx_load_during_hybrid_heating.name} = #{s_dx_heating_load.name}")
+
+ # Check if Stage 1 alone meets setpoint
+ ems_program.addLine(" IF (#{s_coil_inlet_t.name} + dT1_full) >= T_set")
+ ems_program.addLine(" SET dT_used_1 = T_set - #{s_coil_inlet_t.name}")
+ ems_program.addLine(" SET #{g_part_load_ratio_1.name} = dT_used_1 / dT1_full")
+ ems_program.addLine(" SET #{a_coil_outlet_t.name} = T_set")
+
+ # Fuel usage for Stage 1 only when Stage 2 OFF
+ ems_program.addLine(" SET #{g_fuel_usage_1.name} = #{s_coil_inlet_mdot.name} * cp * dT_used_1 * #{g_part_load_ratio_1.name} / burner_eff")
+ ems_program.addLine(" SET #{g_fuel_usage_2.name} = 0.0")
+ ems_program.addLine(" ELSE") # Stage 1 not enough → Stage 2 needed
+ ems_program.addLine(" SET #{g_part_load_ratio_1.name} = 1.0")
+ ems_program.addLine(" SET T_after_stage_1 = #{s_coil_inlet_t.name} + dT1_full")
+ ems_program.addLine(" SET #{a_coil_outlet_t.name} = T_after_stage_1") # temp after stage 1
+ ems_program.addLine(" SET #{g_fuel_usage_1.name} = #{s_coil_inlet_mdot.name} * cp * dT1_full * #{g_part_load_ratio_1.name} / burner_eff")
+
+ # Stage 2 assist
+ ems_program.addLine(" SET #{g_stage_2.name} = 1")
+ ems_program.addLine(" SET dT2_full = cap_stage_2 / #{s_coil_inlet_mdot.name} / cp")
+ ems_program.addLine(" IF (T_after_stage_1 + dT2_full) >= T_set")
+ ems_program.addLine(" SET dT_used_2 = T_set - T_after_stage_1")
+ ems_program.addLine(" SET #{g_part_load_ratio_2.name} = dT_used_2 / dT2_full")
+ ems_program.addLine(" SET #{a_coil_outlet_t.name} = T_set")
+ ems_program.addLine(" ELSE")
+ ems_program.addLine(" SET dT_used_2 = dT2_full")
+ ems_program.addLine(" SET #{g_part_load_ratio_2.name} = 1.0")
+ ems_program.addLine(" SET #{a_coil_outlet_t.name} = T_after_stage_1 + dT2_full")
+ ems_program.addLine(" ENDIF")
+
+ # Stage 2 fuel
+ ems_program.addLine(" SET #{g_fuel_usage_2.name} = #{s_coil_inlet_mdot.name} * cp * dT_used_2 * #{g_part_load_ratio_2.name} / burner_eff")
+ ems_program.addLine(" ENDIF") # Stage 1 sufficient?
+ ems_program.addLine(" ENDIF") # mdot > 0
+ ems_program.addLine(" ENDIF") # inlet < setpoint
+ ems_program.addLine("ENDIF") # mech_heat_enable
+
+ # Convert power to energy
+ ems_program.addLine("SET dt = 60 / #{num_steps_per_hr} * 60") # in seconds
+ ems_program.addLine("SET #{ems_name_airloop}_E_fuel_1 = #{g_fuel_usage_1.name} * dt") # Joules
+ ems_program.addLine("SET #{ems_name_airloop}_E_fuel_2 = #{g_fuel_usage_2.name} * dt") # Joules
+
+ # ems program for initialization
+ ems_program_initialization = OpenStudio::Model::EnergyManagementSystemProgram.new(model)
+ ems_program_initialization.setName("#{ems_name_airloop}_p_two_stage_gas_coil_initialization")
+ ems_program_initialization.addLine("SET #{g_stage_1.name} = 0")
+ ems_program_initialization.addLine("SET #{g_stage_2.name} = 0")
+ ems_program_initialization.addLine("SET #{g_part_load_ratio_1.name} = 0")
+ ems_program_initialization.addLine("SET #{g_part_load_ratio_2.name} = 0")
+ ems_program_initialization.addLine("SET #{g_fuel_usage_1.name} = 0")
+ ems_program_initialization.addLine("SET #{g_fuel_usage_2.name} = 0")
+ ems_program_initialization.addLine("SET #{g_dx_load_during_hybrid_heating.name} = 0.0")
+ ems_program_initialization.addLine("SET #{a_coil_outlet_t.name} = #{s_coil_inlet_t.name}")
+
+ # -------------------------------------------------------------------------------
+ # Program calling manager
+ # -------------------------------------------------------------------------------
+
+ ems_pcm = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model)
+ ems_pcm.setName("#{ems_name_airloop}_pcm_gas_coil")
+ ems_pcm.setCallingPoint("UserDefinedComponentModel")
+ ems_pcm.addProgram(ems_program)
+
+ ems_pcm_initialization = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model)
+ ems_pcm_initialization.setName("#{ems_name_airloop}_pcm_gas_coil_initialization")
+ ems_pcm_initialization.setCallingPoint("UserDefinedComponentModel")
+ ems_pcm_initialization.addProgram(ems_program_initialization)
+
+ # -------------------------------------------------------------------------------
+ # Attach EMS objects to coil
+ # -------------------------------------------------------------------------------
+
+ new_backup_heating_coil.setOverallSimulationProgram(ems_program)
+ new_backup_heating_coil.setInitializationSimulationProgram(ems_program_initialization)
+ new_backup_heating_coil.setOverallModelSimulationProgramCallingManager(ems_pcm)
+ new_backup_heating_coil.setModelSetupandSizingProgramCallingManager(ems_pcm_initialization)
+ new_backup_heating_coil.setAirOutletTemperatureActuator(a_coil_outlet_t)
+ new_backup_heating_coil.setAirOutletHumidityRatioActuator(a_coil_outlet_hr)
+ new_backup_heating_coil.setAirMassFlowRateActuator(a_coil_outlet_mdot)
+
+ # -------------------------------------------------------------------------------
+ # Output variable
+ # -------------------------------------------------------------------------------
+
+ ems_ov_status_heating_stage_1 = OpenStudio::Model::EnergyManagementSystemOutputVariable.new(model,g_stage_1)
+ ems_ov_status_heating_stage_1.setName("#{ems_name_airloop}_ov_status_heating_stage_1")
+ ems_ov_status_heating_stage_1.setEMSVariableName("#{g_stage_1.name}")
+ ems_ov_status_heating_stage_1.setTypeOfDataInVariable("Averaged")
+ ems_ov_status_heating_stage_1.setUpdateFrequency("SystemTimeStep")
+ output_var = OpenStudio::Model::OutputVariable.new("#{ems_ov_status_heating_stage_1.name}", model)
+ output_var.setKeyValue("*")
+ output_var.setReportingFrequency("Timestep")
+
+ ems_ov_status_heating_stage_2 = OpenStudio::Model::EnergyManagementSystemOutputVariable.new(model,g_stage_2)
+ ems_ov_status_heating_stage_2.setName("#{ems_name_airloop}_ov_status_heating_stage_2")
+ ems_ov_status_heating_stage_2.setEMSVariableName("#{g_stage_2.name}")
+ ems_ov_status_heating_stage_2.setTypeOfDataInVariable("Averaged")
+ ems_ov_status_heating_stage_2.setUpdateFrequency("SystemTimeStep")
+ output_var = OpenStudio::Model::OutputVariable.new("#{ems_ov_status_heating_stage_2.name}", model)
+ output_var.setKeyValue("*")
+ output_var.setReportingFrequency("Timestep")
+
+ ems_ov_status_heating_plr_1 = OpenStudio::Model::EnergyManagementSystemOutputVariable.new(model,g_part_load_ratio_1)
+ ems_ov_status_heating_plr_1.setName("#{ems_name_airloop}_ov_status_heating_plr_1")
+ ems_ov_status_heating_plr_1.setEMSVariableName("#{g_part_load_ratio_1.name}")
+ ems_ov_status_heating_plr_1.setTypeOfDataInVariable("Averaged")
+ ems_ov_status_heating_plr_1.setUpdateFrequency("SystemTimeStep")
+ output_var = OpenStudio::Model::OutputVariable.new("#{ems_ov_status_heating_plr_1.name}", model)
+ output_var.setKeyValue("*")
+ output_var.setReportingFrequency("Timestep")
+
+ ems_ov_status_heating_plr_2 = OpenStudio::Model::EnergyManagementSystemOutputVariable.new(model,g_part_load_ratio_2)
+ ems_ov_status_heating_plr_2.setName("#{ems_name_airloop}_ov_status_heating_plr_2")
+ ems_ov_status_heating_plr_2.setEMSVariableName("#{g_part_load_ratio_2.name}")
+ ems_ov_status_heating_plr_2.setTypeOfDataInVariable("Averaged")
+ ems_ov_status_heating_plr_2.setUpdateFrequency("SystemTimeStep")
+ output_var = OpenStudio::Model::OutputVariable.new("#{ems_ov_status_heating_plr_2.name}", model)
+ output_var.setKeyValue("*")
+ output_var.setReportingFrequency("Timestep")
+
+ ems_ov_fuel_usage_1 = OpenStudio::Model::EnergyManagementSystemOutputVariable.new(model,g_fuel_usage_1)
+ ems_ov_fuel_usage_1.setName("#{ems_name_airloop}_ov_fuel_usage_1")
+ ems_ov_fuel_usage_1.setEMSVariableName("#{g_fuel_usage_1.name}")
+ ems_ov_fuel_usage_1.setTypeOfDataInVariable("Averaged")
+ ems_ov_fuel_usage_1.setUpdateFrequency("SystemTimeStep")
+ output_var = OpenStudio::Model::OutputVariable.new("#{ems_ov_fuel_usage_1.name}", model)
+ output_var.setKeyValue("*")
+ output_var.setReportingFrequency("Timestep")
+
+ ems_ov_fuel_usage_2 = OpenStudio::Model::EnergyManagementSystemOutputVariable.new(model,g_fuel_usage_2)
+ ems_ov_fuel_usage_2.setName("#{ems_name_airloop}_ov_fuel_usage_2")
+ ems_ov_fuel_usage_2.setEMSVariableName("#{g_fuel_usage_2.name}")
+ ems_ov_fuel_usage_2.setTypeOfDataInVariable("Averaged")
+ ems_ov_fuel_usage_2.setUpdateFrequency("SystemTimeStep")
+ output_var = OpenStudio::Model::OutputVariable.new("#{ems_ov_fuel_usage_2.name}", model)
+ output_var.setKeyValue("*")
+ output_var.setReportingFrequency("Timestep")
+
+ ems_dx_load_during_hybrid_heating = OpenStudio::Model::EnergyManagementSystemOutputVariable.new(model,g_dx_load_during_hybrid_heating)
+ ems_dx_load_during_hybrid_heating.setName("#{ems_name_airloop}_dx_load_during_hybrid_heating")
+ ems_dx_load_during_hybrid_heating.setEMSVariableName("#{g_dx_load_during_hybrid_heating.name}")
+ ems_dx_load_during_hybrid_heating.setTypeOfDataInVariable("Averaged")
+ ems_dx_load_during_hybrid_heating.setUpdateFrequency("SystemTimeStep")
+ ems_dx_load_during_hybrid_heating.setUnits("W")
+ output_var = OpenStudio::Model::OutputVariable.new("#{ems_dx_load_during_hybrid_heating.name}", model)
+ output_var.setKeyValue("*")
+ output_var.setReportingFrequency("Hourly")
+
+ # -------------------------------------------------------------------------------
+ # Define meter
+ # -------------------------------------------------------------------------------
+
+ gas_mtr_out_var_1 =
+ OpenStudio::Model::EnergyManagementSystemMeteredOutputVariable.new(
+ model,
+ "#{ems_name_airloop} Gas Heating Stage 1"
+ )
+
+ gas_mtr_out_var_1.setName("#{ems_name_airloop} Gas Heating Stage 1")
+ gas_mtr_out_var_1.setEMSVariableName("#{ems_name_airloop}_E_fuel_1")
+ gas_mtr_out_var_1.setUpdateFrequency('SystemTimestep')
+ gas_mtr_out_var_1.setString(4, ems_program.handle.to_s)
+ gas_mtr_out_var_1.setResourceType('NaturalGas')
+ gas_mtr_out_var_1.setGroupType('HVAC')
+ gas_mtr_out_var_1.setEndUseCategory('Heating')
+ gas_mtr_out_var_1.setEndUseSubcategory('Hybrid gas stage 1')
+ gas_mtr_out_var_1.setUnits('J')
+
+ gas_mtr_out_var_2 =
+ OpenStudio::Model::EnergyManagementSystemMeteredOutputVariable.new(
+ model,
+ "#{ems_name_airloop} Gas Heating Stage 2"
+ )
+
+ gas_mtr_out_var_2.setName("#{ems_name_airloop} Gas Heating Stage 2")
+ gas_mtr_out_var_2.setEMSVariableName("#{ems_name_airloop}_E_fuel_2")
+ gas_mtr_out_var_2.setUpdateFrequency('SystemTimestep')
+ gas_mtr_out_var_2.setString(4, ems_program.handle.to_s)
+ gas_mtr_out_var_2.setResourceType('NaturalGas')
+ gas_mtr_out_var_2.setGroupType('HVAC')
+ gas_mtr_out_var_2.setEndUseCategory('Heating')
+ gas_mtr_out_var_2.setEndUseSubcategory('Hybrid gas stage 2')
+ gas_mtr_out_var_2.setUnits('J')
+
+ # -------------------------------------------------------------------------------
+ # Dummy plant actuators (created but intentionally unused)
+ # Required to satisfy OpenStudio CoilUserDefined internal checks
+ # -------------------------------------------------------------------------------
+
+ a_plant_mdot = OpenStudio::Model::EnergyManagementSystemActuator.new(
+ new_backup_heating_coil,
+ "Plant Connection",
+ "Mass Flow Rate"
+ )
+ a_plant_mdot.setName("#{ems_name_airloop}_a_plant_mdot")
+
+ a_plant_min_mdot = OpenStudio::Model::EnergyManagementSystemActuator.new(
+ new_backup_heating_coil,
+ "Plant Connection",
+ "Minimum Mass Flow Rate"
+ )
+ a_plant_min_mdot.setName("#{ems_name_airloop}_a_plant_min_mdot")
+
+ a_plant_max_mdot = OpenStudio::Model::EnergyManagementSystemActuator.new(
+ new_backup_heating_coil,
+ "Plant Connection",
+ "Maximum Mass Flow Rate"
+ )
+ a_plant_max_mdot.setName("#{ems_name_airloop}_a_plant_max_mdot")
+
+ a_plant_outlet_t = OpenStudio::Model::EnergyManagementSystemActuator.new(
+ new_backup_heating_coil,
+ "Plant Connection",
+ "Outlet Temperature"
+ )
+ a_plant_outlet_t.setName("#{ems_name_airloop}_a_plant_outlet_t")
+
+ a_plant_design_vdot = OpenStudio::Model::EnergyManagementSystemActuator.new(
+ new_backup_heating_coil,
+ "Plant Connection",
+ "Design Volume Flow Rate"
+ )
+ a_plant_design_vdot.setName("#{ems_name_airloop}_a_plant_design_vdot")
+
+ # Attach plant actuators to the coil (never referenced in EMS programs)
+ new_backup_heating_coil.setPlantMinimumMassFlowRateActuator(a_plant_min_mdot)
+ new_backup_heating_coil.setPlantMaximumMassFlowRateActuator(a_plant_max_mdot)
+ new_backup_heating_coil.setPlantOutletTemperatureActuator(a_plant_outlet_t)
+ new_backup_heating_coil.setPlantDesignVolumeFlowRateActuator(a_plant_design_vdot)
+ new_backup_heating_coil.setPlantMassFlowRateActuator(a_plant_mdot)
+
+ # -------------------------------------------------------------------------------
+ # Patch fix for removing redundant EMS objects from model
+ # -------------------------------------------------------------------------------
+ model.getEnergyManagementSystemPrograms.sort.each do |program|
+ if program.name.to_s.include?('initializationSimulationProgram')
+ program.remove()
+ elsif program.name.to_s.include?('overallSimulationProgram')
+ program.remove()
+ end
+ end
+ model.getEnergyManagementSystemProgramCallingManagers.sort.each do |pcm|
+ if pcm.name.to_s.include?('modelSetupandSizingProgramCallingManager')
+ pcm.remove()
+ elsif pcm.name.to_s.include?('overallModelSimulationProgramCallingManager')
+ pcm.remove()
+ end
+ end
+ model.getEnergyManagementSystemActuators.sort.each do |actuator|
+ if actuator.name.to_s.include?('plantDesignVolumeFlowRate')
+ actuator.remove()
+ elsif actuator.name.to_s.include?('plantMassFlowRate')
+ actuator.remove()
+ elsif actuator.name.to_s.include?('plantMaximumMassFlowRate')
+ actuator.remove()
+ elsif actuator.name.to_s.include?('plantMinimumMassFlowRate')
+ actuator.remove()
+ elsif actuator.name.to_s.include?('plantOutletTemperature')
+ actuator.remove()
+ end
+ end
+
+ end
+
+ # Calculates the dual fuel gas coil capacities for a heating system based on the
+ # given DX coil heating capacity. The method uses regression equations derived
+ # from catalog data to estimate the capacities for two stages of operation.
+ # Additionally, it enforces minimum capacity thresholds based on available data.
+ #
+ # @param [Float] dx_coil_heating_capacity_w The heating capacity of the DX coil in watts.
+ # @return [Array] An array containing two elements:
+ # - The capacity for stage 1 in watts, with a minimum value of 19052.0 W.
+ # - The capacity for stage 2 in watts, with a minimum value of 25793.0 W.
+ def get_dual_fuel_gas_coil_capacity(dx_coil_heating_capacity_w)
+
+ # calculate capacities with regression (from catalog data)
+ capacity_stage_1_w = 0.7353 * dx_coil_heating_capacity_w + 20245.0
+ capacity_stage_2_w = 0.6305 * dx_coil_heating_capacity_w + 12484.0
+
+ # cap minimum value of capacity_stage_1_w to 19052 (based on available data)
+ if capacity_stage_1_w < 19052.0
+ capacity_stage_1_w = 19052.0
+ end
+
+ # cap minimum value of capacity_stage_2_w to 25793 (based on available data)
+ if capacity_stage_2_w < 25793.0
+ capacity_stage_2_w = 25793.0
+ end
+
+ return capacity_stage_1_w, capacity_stage_2_w
+ end
+
+ # ---------------------------------------------------------
+ # main measure code
+ # ---------------------------------------------------------
# define what happens when the measure is run
def run(model, runner, user_arguments)
@@ -959,6 +1729,7 @@ def run(model, runner, user_arguments)
setback_value = runner.getDoubleArgumentValue('setback_value', user_arguments)
modify_setbacks = runner.getBoolArgumentValue('modify_setbacks', user_arguments)
+ # ---------------------------------------------------------
# build standard to use OS standards methods
# ---------------------------------------------------------
template = 'ComStock 90.1-2019'
@@ -1121,7 +1892,9 @@ def run(model, runner, user_arguments)
return true
end
+ # ---------------------------------------------------------
# call roof and/or window upgrades based on user input
+ # ---------------------------------------------------------
condition_initial_roof = ''
condition_final_roof = ''
condition_initial_window = ''
@@ -1169,7 +1942,9 @@ def run(model, runner, user_arguments)
sql = sql.get if sql.is_initialized
end
- #########################################################################################################
+ # ---------------------------------------------------------
+ # Temporary section
+ # ---------------------------------------------------------
### This section includes temporary code to remove units with high OA fractiosn and night cycling
### This code should be removed when fix is initiated
# add systems with high outdoor air ratios to a list for non-applicability
@@ -1250,8 +2025,6 @@ def run(model, runner, user_arguments)
applicable_area_m2 -= thermal_zone.floorArea * thermal_zone.multiplier
# remove area served by air loop from applicability
end
- ### End of temp section
- #########################################################################################################
# ---------------------------------------------------------
# check if any air loops are applicable to measure
@@ -1321,8 +2094,9 @@ def run(model, runner, user_arguments)
[15.5556, 19.4444, 'HRV']
end
+
# ---------------------------------------------------------
- # load performance data for standard performance units
+ # load performance data from json files
# ---------------------------------------------------------
custom_data_json = nil
# if cchpc scenarios are set, use those curves. else, use the standard performance curves
@@ -1343,6 +2117,10 @@ def run(model, runner, user_arguments)
# read performance data
path_data_curve = "#{File.dirname(__FILE__)}/resources/performance_maps_hprtu_lab_data.json"
custom_data_json = JSON.parse(File.read(path_data_curve))
+ when 'carrier_48qe_dualfuel'
+ # read performance data
+ path_data_curve = "#{File.dirname(__FILE__)}/resources/performance_maps_carrier_48qe_dualfuel.json"
+ custom_data_json = JSON.parse(File.read(path_data_curve))
end
# ---------------------------------------------------------
@@ -1370,6 +2148,10 @@ def run(model, runner, user_arguments)
cool_cap_ft3 = model_add_curve(model, 'cool_cap_ft3', custom_data_json, std)
cool_cap_ft4 = model_add_curve(model, 'cool_cap_ft4', custom_data_json, std)
cool_cap_ft_curve_stages = { 1 => cool_cap_ft1, 2 => cool_cap_ft2, 3 => cool_cap_ft3, 4 => cool_cap_ft4 }
+ when 'carrier_48qe_dualfuel'
+ cool_cap_ft1 = model_add_curve(model, 'cap_mod_cooling_low_t', custom_data_json, std)
+ cool_cap_ft2 = model_add_curve(model, 'cap_mod_cooling_high_t', custom_data_json, std)
+ cool_cap_ft_curve_stages = { 1 => cool_cap_ft1, 2 => cool_cap_ft2 }
end
# Curve Import - Cooling efficiency as a function of temperature
@@ -1394,6 +2176,10 @@ def run(model, runner, user_arguments)
cool_eir_ft3 = model_add_curve(model, 'cool_eir_ft3', custom_data_json, std)
cool_eir_ft4 = model_add_curve(model, 'cool_eir_ft4', custom_data_json, std)
cool_eir_ft_curve_stages = { 1 => cool_eir_ft1, 2 => cool_eir_ft2, 3 => cool_eir_ft3, 4 => cool_eir_ft4 }
+ when 'carrier_48qe_dualfuel'
+ cool_eir_ft1 = model_add_curve(model, 'eir_mod_cooling_low_t', custom_data_json, std)
+ cool_eir_ft2 = model_add_curve(model, 'eir_mod_cooling_high_t', custom_data_json, std)
+ cool_eir_ft_curve_stages = { 1 => cool_eir_ft1, 2 => cool_eir_ft2 }
end
# Curve Import - Cooling capacity as a function of flow rate
@@ -1412,6 +2198,10 @@ def run(model, runner, user_arguments)
when 'cchpc_2027_spec'
cool_cap_ff1 = model_add_curve(model, 'cool_cap_ff1', custom_data_json, std)
cool_cap_ff_curve_stages = { 1 => cool_cap_ff1, 2 => cool_cap_ff1, 3 => cool_cap_ff1, 4 => cool_cap_ff1 }
+ when 'carrier_48qe_dualfuel'
+ cool_cap_ff1 = model_add_curve(model, 'cap_mod_cooling_low_ff', custom_data_json, std)
+ cool_cap_ff2 = model_add_curve(model, 'cap_mod_cooling_high_ff', custom_data_json, std)
+ cool_cap_ff_curve_stages = { 1 => cool_cap_ff1, 2 => cool_cap_ff2 }
end
# Curve Import - Cooling efficiency as a function of flow rate
@@ -1430,6 +2220,10 @@ def run(model, runner, user_arguments)
when 'cchpc_2027_spec'
cool_eir_ff1 = model_add_curve(model, 'cool_eir_ff1', custom_data_json, std)
cool_eir_ff_curve_stages = { 1 => cool_eir_ff1, 2 => cool_eir_ff1, 3 => cool_eir_ff1, 4 => cool_eir_ff1 }
+ when 'carrier_48qe_dualfuel'
+ cool_eir_ff1 = model_add_curve(model, 'eir_mod_cooling_low_ff', custom_data_json, std)
+ cool_eir_ff2 = model_add_curve(model, 'eir_mod_cooling_high_ff', custom_data_json, std)
+ cool_eir_ff_curve_stages = { 1 => cool_eir_ff1, 2 => cool_eir_ff2 }
end
# Curve Import - Cooling efficiency as a function of part load ratio
@@ -1442,6 +2236,8 @@ def run(model, runner, user_arguments)
cool_plf_fplr1 = model_add_curve(model, 'cool_plf_plr1', custom_data_json, std)
when 'cchpc_2027_spec'
cool_plf_fplr1 = model_add_curve(model, 'cool_plf_plr1', custom_data_json, std)
+ when 'carrier_48qe_dualfuel'
+ cool_plf_fplr1 = model_add_curve(model, 'plf_na_cooling_na_plr', custom_data_json, std)
end
# ---------------------------------------------------------
@@ -1467,6 +2263,9 @@ def run(model, runner, user_arguments)
heat_cap_ft3 = model_add_curve(model, 'h_cap_high', custom_data_json, std)
heat_cap_ft4 = model_add_curve(model, 'h_cap_boost', custom_data_json, std)
heat_cap_ft_curve_stages = { 1 => heat_cap_ft1, 2 => heat_cap_ft2, 3 => heat_cap_ft3, 4 => heat_cap_ft4 }
+ when 'carrier_48qe_dualfuel'
+ heat_cap_ft1 = model_add_curve(model, 'cap_mod_heating_high_t', custom_data_json, std)
+ heat_cap_ft_curve_stages = { 1 => heat_cap_ft1 }
end
# Curve Import - Heating efficiency as a function of temperature
@@ -1489,6 +2288,9 @@ def run(model, runner, user_arguments)
heat_eir_ft3 = model_add_curve(model, 'h_eir_high', custom_data_json, std)
heat_eir_ft4 = model_add_curve(model, 'h_eir_boost', custom_data_json, std)
heat_eir_ft_curve_stages = { 1 => heat_eir_ft1, 2 => heat_eir_ft2, 3 => heat_eir_ft3, 4 => heat_eir_ft4 }
+ when 'carrier_48qe_dualfuel'
+ heat_eir_ft1 = model_add_curve(model, 'eir_mod_heating_high_t', custom_data_json, std)
+ heat_eir_ft_curve_stages = { 1 => heat_eir_ft1 }
end
# Curve Import - Heating capacity as a function of flow rate
@@ -1505,6 +2307,9 @@ def run(model, runner, user_arguments)
when 'cchpc_2027_spec'
heat_cap_ff1 = model_add_curve(model, 'h_cap_allstages_ff', custom_data_json, std)
heat_cap_ff_curve_stages = { 1 => heat_cap_ff1, 2 => heat_cap_ff1, 3 => heat_cap_ff1, 4 => heat_cap_ff1 }
+ when 'carrier_48qe_dualfuel'
+ heat_cap_ff1 = model_add_curve(model, 'cap_mod_heating_high_ff', custom_data_json, std)
+ heat_cap_ff_curve_stages = { 1 => heat_cap_ff1 }
end
# Curve Import - Heating efficiency as a function of flow rate
@@ -1521,6 +2326,9 @@ def run(model, runner, user_arguments)
when 'cchpc_2027_spec'
heat_eir_ff1 = model_add_curve(model, 'h_eir_allstages_ff', custom_data_json, std)
heat_eir_ff_curve_stages = { 1 => heat_eir_ff1, 2 => heat_eir_ff1, 3 => heat_eir_ff1, 4 => heat_eir_ff1 }
+ when 'carrier_48qe_dualfuel'
+ heat_eir_ff1 = model_add_curve(model, 'eir_mod_heating_high_ff', custom_data_json, std)
+ heat_eir_ff_curve_stages = { 1 => heat_eir_ff1 }
end
# Curve Import - Heating efficiency as a function of part load ratio
@@ -1534,6 +2342,8 @@ def run(model, runner, user_arguments)
heat_plf_fplr1 = model_add_curve(model, 'heat_plf_plr1', custom_data_json, std)
when 'cchpc_2027_spec'
heat_plf_fplr1 = model_add_curve(model, 'heat_plf_plr1', custom_data_json, std)
+ when 'carrier_48qe_dualfuel'
+ heat_plf_fplr1 = model_add_curve(model, 'plf_na_heating_na_plr', custom_data_json, std)
end
# Curve Import - Defrost energy as a function of temperature
@@ -1547,14 +2357,18 @@ def run(model, runner, user_arguments)
defrost_eir = model_add_curve(model, 'defrost_eir', custom_data_json, std)
when 'cchpc_2027_spec'
defrost_eir = model_add_curve(model, 'defrost_eir', custom_data_json, std)
+ when 'carrier_48qe_dualfuel'
+ defrost_eir = model_add_curve(model, 'eir_mod_defrost_na_na', custom_data_json, std)
end
# ---------------------------------------------------------
# replace existing applicable air loops with new heat pump rtu air loops
# ---------------------------------------------------------
selected_air_loops.sort.each do |air_loop_hvac|
- # get necessary schedules, etc. from unitary system object
+
+ # *********************************************************
# initialize variables before loop
+ # *********************************************************
hvac_operation_sched = air_loop_hvac.availabilitySchedule
unitary_availability_sched = 'tmp'
control_zone = 'tmp'
@@ -1566,9 +2380,7 @@ def run(model, runner, user_arguments)
fan_static_pressure = 'tmp'
orig_clg_coil_gross_cap = nil
orig_htg_coil_gross_cap = nil
-
equip_to_delete = []
-
space_types_no_setback = [
# 'Kitchen',
# 'kitchen',
@@ -1600,9 +2412,12 @@ def run(model, runner, user_arguments)
'Guest Room',
'guest room'
]
-
setback_value_c = setback_value * 5 / 9 # convert to c
+ always_on = model.alwaysOnDiscreteSchedule
+ # *********************************************************
+ # modify zone thermostats for setbacks
+ # *********************************************************
if modify_setbacks # modify setbacks if argument set to true
zones = air_loop_hvac.thermalZones
zones.sort.each do |thermal_zone|
@@ -1732,8 +2547,10 @@ def run(model, runner, user_arguments)
end
end
end
- # end of setback modification
+ # *********************************************************
+ # gather information from existing air loop
+ # *********************************************************
# for unitary systems
if air_loop_hvac_unitary_system?(air_loop_hvac)
@@ -1894,34 +2711,21 @@ def run(model, runner, user_arguments)
end
end
+ # *********************************************************
# delete equipment from original loop
- equip_to_delete.each(&:remove)
-
- # set always on schedule; this will be used in other object definitions
- always_on = model.alwaysOnDiscreteSchedule
-
- # get thermal zone
- thermal_zone = air_loop_hvac.thermalZones[0]
-
- # Get the min OA flow rate from the OA; this is used below
- oa_system = air_loop_hvac.airLoopHVACOutdoorAirSystem.get
- controller_oa = oa_system.getControllerOutdoorAir
- oa_flow_m3_per_s = nil
- if controller_oa.minimumOutdoorAirFlowRate.is_initialized
- oa_flow_m3_per_s = controller_oa.minimumOutdoorAirFlowRate.get
- elsif controller_oa.autosizedMinimumOutdoorAirFlowRate.is_initialized
- oa_flow_m3_per_s = controller_oa.autosizedMinimumOutdoorAirFlowRate.get
- else
- runner.registerError("No outdoor air sizing information was found for #{controller_oa.name}, which is required for setting ERV wheel power consumption.")
- return false
- end
+ # *********************************************************
+ equip_to_delete.each(&:remove)
+ # *********************************************************
# change sizing parameter to vav
+ # *********************************************************
sizing = air_loop_hvac.sizingSystem
sizing.setCentralCoolingCapacityControlMethod('VAV') # CC-TMP
- # replace any CV terminal box with no reheat VAV terminal box
+ # *********************************************************
# get old terminal box
+ # *********************************************************
+ thermal_zone = air_loop_hvac.thermalZones[0]
if thermal_zone.airLoopHVACTerminal.get.to_AirTerminalSingleDuctConstantVolumeReheat.is_initialized
old_terminal = thermal_zone.airLoopHVACTerminal.get.to_AirTerminalSingleDuctConstantVolumeReheat.get
elsif thermal_zone.airLoopHVACTerminal.get.to_AirTerminalSingleDuctConstantVolumeNoReheat.is_initialized
@@ -1939,7 +2743,9 @@ def run(model, runner, user_arguments)
return false
end
+ # *********************************************************
# get design supply air flow rate
+ # *********************************************************
old_terminal_sa_flow_m3_per_s = nil
if air_loop_hvac.designSupplyAirFlowRate.is_initialized
old_terminal_sa_flow_m3_per_s = air_loop_hvac.designSupplyAirFlowRate.get
@@ -1949,24 +2755,45 @@ def run(model, runner, user_arguments)
runner.registerError("No sizing data available for air loop #{air_loop_hvac.name} zone terminal box.")
end
+ # *********************************************************
+ # get the min OA flow rate from the OA
+ # *********************************************************
+ oa_system = air_loop_hvac.airLoopHVACOutdoorAirSystem.get
+ controller_oa = oa_system.getControllerOutdoorAir
+ oa_flow_m3_per_s = nil
+ if controller_oa.minimumOutdoorAirFlowRate.is_initialized
+ oa_flow_m3_per_s = controller_oa.minimumOutdoorAirFlowRate.get
+ elsif controller_oa.autosizedMinimumOutdoorAirFlowRate.is_initialized
+ oa_flow_m3_per_s = controller_oa.autosizedMinimumOutdoorAirFlowRate.get
+ else
+ runner.registerError("No outdoor air sizing information was found for #{controller_oa.name}, which is required for setting ERV wheel power consumption.")
+ return false
+ end
+
+ # *********************************************************
# define minimum flow rate needed to maintain ventilation - add in max fraction if in model
+ # *********************************************************
if controller_oa.maximumFractionofOutdoorAirSchedule.is_initialized
controller_oa.resetMaximumFractionofOutdoorAirSchedule
end
min_oa_flow_ratio = (oa_flow_m3_per_s / old_terminal_sa_flow_m3_per_s)
- # remove old equipment
+ # *********************************************************
+ # remove old air terminals
+ # *********************************************************
old_terminal.remove
air_loop_hvac.removeBranchForZone(thermal_zone)
- # define new terminal box
- # new_terminal = OpenStudio::Model::AirTerminalSingleDuctConstantVolumeNoReheat.new(model, always_on)
+
+ # *********************************************************
+ # define new air terminals
+ # *********************************************************
new_terminal = OpenStudio::Model::AirTerminalSingleDuctVAVHeatAndCoolNoReheat.new(model)
- # set name of terminal box and add
new_terminal.setName("#{thermal_zone.name} VAV Terminal")
air_loop_hvac.addBranchForZone(thermal_zone, new_terminal.to_StraightComponent)
- #################################### Start Sizing Logic
-
+ # *********************************************************
+ # sizing: get heating sizing temperature
+ # *********************************************************
# get heating design day temperatures into list
li_design_days = model.getDesignDays
li_htg_dsgn_day_temps = []
@@ -2007,13 +2834,17 @@ def run(model, runner, user_arguments)
end
end
- ## define number of stages, and capacity/airflow fractions for each stage
+ # *********************************************************
+ # sizing: get system specifications from custom data
+ # *********************************************************
(_, _, rated_stage_num_heating, rated_stage_num_cooling, final_rated_cooling_cop, final_rated_heating_cop, stage_cap_fractions_heating,
stage_flow_fractions_heating, stage_cap_fractions_cooling, stage_flow_fractions_cooling, stage_rated_cop_frac_heating,
stage_rated_cop_frac_cooling, _, stage_gross_rated_sensible_heat_ratio_cooling, enable_cycling_losses_above_lowest_speed, reference_cooling_cfm_per_ton,
reference_heating_cfm_per_ton) = assign_staging_data(custom_data_json, std)
- # get appropriate design heating load
+ # *********************************************************
+ # sizing: get appropriate design heating load
+ # *********************************************************
orig_htg_coil_gross_cap_old = orig_htg_coil_gross_cap
design_air_flow_from_zone_sizing_heating_m_3_per_s = old_terminal_sa_flow_m3_per_s
if sizing_run
@@ -2071,11 +2902,16 @@ def run(model, runner, user_arguments)
end
end
- # determine heating load curve; y=mx+b
+ # *********************************************************
+ # sizing: determine heating load curve; y=mx+b
+ # *********************************************************
# assumes 0 load at 60F (15.556 C)
htg_load_slope = (0 - orig_htg_coil_gross_cap) / (15.5556 - wntr_design_day_temp_c)
htg_load_intercept = orig_htg_coil_gross_cap - (htg_load_slope * wntr_design_day_temp_c)
+ # *********************************************************
+ # sizing: get rated heating capacity with heating derating factor
+ # *********************************************************
# calculate heat pump design load, derate factors, and required rated capacities (at stage 4) for different OA temperatures; assumes 75F interior temp (23.8889C)
ia_temp_c = 23.8889
@@ -2091,6 +2927,9 @@ def run(model, runner, user_arguments)
end
req_rated_hp_cap_at_user_dsn_to_meet_load_at_user_dsn = dns_htg_load_at_user_dsn_temp / hp_derate_factor_at_user_dsn
+ # *********************************************************
+ # sizing: get upsized heating/cooling capacities based on user inputs
+ # *********************************************************
# determine heat pump system sizing based on user-specified sizing temperature and user-specified maximum upsizing limits
# upsize total cooling capacity using user-specified factor
autosized_tot_clg_cap_upsized = orig_clg_coil_gross_cap * clg_oversizing_estimate
@@ -2099,7 +2938,9 @@ def run(model, runner, user_arguments)
# get maximum heating capacity based on max cooling capacity and heating-to-cooling ratio
max_heat_cap_w_upsize = autosized_tot_clg_cap_upsized * (performance_oversizing_factor + 1) * htg_to_clg_hp_ratio
- # Sizing decision based on heating load level
+ # *********************************************************
+ # sizing: sizing decision based on heating load level
+ # *********************************************************
heating_load_category = ''
# If ratio of required heating capacity at rated conditions to cooling capacity is less than specified heating to cooling ratio, then size everything based on cooling
# If heating load requires upsizing, but is below user-input cooling upsizing limit, then size based on design heating load
@@ -2155,6 +2996,9 @@ def run(model, runner, user_arguments)
runner.registerInfo("sizing summary: sizing air loop (#{air_loop_hvac.name}): final upsizing percentage % = #{((dx_rated_htg_cap_applied - orig_clg_coil_gross_cap) / orig_clg_coil_gross_cap * 100).round(2)}")
end
+ # *********************************************************
+ # sizing: get final upsizing factor
+ # *********************************************************
# calculate applied upsizing factor
upsize_factor = (dx_rated_htg_cap_applied - orig_clg_coil_gross_cap) / orig_clg_coil_gross_cap
@@ -2172,7 +3016,9 @@ def run(model, runner, user_arguments)
runner.registerInfo("sizing summary: cfm/ton heating = #{m_3_per_sec_watts_to_cfm_per_ton(design_cooling_airflow_m_3_per_s / dx_rated_clg_cap_applied)}")
end
- # adjust if rated/highest stage cfm/ton is violated
+ # *********************************************************
+ # sizing: adjust if rated/highest stage cfm/ton is violated
+ # *********************************************************
cfm_per_ton_rated_heating = m_3_per_sec_watts_to_cfm_per_ton(design_heating_airflow_m_3_per_s / dx_rated_htg_cap_applied)
cfm_per_ton_rated_cooling = m_3_per_sec_watts_to_cfm_per_ton(design_cooling_airflow_m_3_per_s / dx_rated_clg_cap_applied)
if cfm_per_ton_rated_heating < CFM_PER_TON_MIN_RATED
@@ -2198,7 +3044,9 @@ def run(model, runner, user_arguments)
runner.registerInfo("sizing summary: heating_load_category = #{heating_load_category}")
end
- # set airloop design airflow based on the maximum of heating and cooling design flow
+ # *********************************************************
+ # sizing: set airloop design airflow based on the maximum of heating and cooling design flow
+ # *********************************************************
design_airflow_for_sizing_m_3_per_s = if design_cooling_airflow_m_3_per_s < design_heating_airflow_m_3_per_s
design_heating_airflow_m_3_per_s
else
@@ -2234,7 +3082,9 @@ def run(model, runner, user_arguments)
runner.registerInfo("sizing summary: min_airflow_m3_per_s = #{min_airflow_m3_per_s}")
end
- # determine airflows for each stage of heating
+ # *********************************************************
+ # sizing: determine airflows for each stage of heating and cooling
+ # *********************************************************
# airflow for each stage will be the higher of the user-input stage ratio or the minimum OA
# lower stages may be removed later if cfm/ton bounds cannot be maintained due to minimum OA limits
# if oversizing is not specified (upsize_factor = 0.0), then use cooling design airflow
@@ -2248,7 +3098,6 @@ def run(model, runner, user_arguments)
stage_flows_heating[stage] = airflow >= min_airflow_m3_per_s ? airflow : min_airflow_m3_per_s
end
- # determine airflows for each stage of cooling
# airflow for each stage will be the higher of the user-input stage ratio or the minimum OA
# lower stages may be removed later if cfm/ton bounds cannot be maintained due to minimum OA limits
stage_flows_cooling = {}
@@ -2265,7 +3114,9 @@ def run(model, runner, user_arguments)
runner.registerInfo("sizing summary: stage_flows_cooling = #{stage_flows_cooling}")
end
- # heating - align stage CFM/ton bounds where possible
+ # *********************************************************
+ # sizing: align stage CFM/ton bounds where possible for heating/cooling
+ # *********************************************************
# this may remove some lower stages
stage_flows_heating, stage_caps_heating, _, _, num_heating_stages = adjust_cfm_per_ton_per_limits(
stage_cap_fractions_heating,
@@ -2302,19 +3153,27 @@ def run(model, runner, user_arguments)
runner.registerInfo("sizing summary: stage_flows_heating = #{stage_flows_heating}")
runner.registerInfo("sizing summary: stage_flows_cooling = #{stage_flows_cooling}")
end
- #################################### Start performance curve assignment
- # ---------------------------------------------------------
- # cooling curve assignments
- # ---------------------------------------------------------
+ # *********************************************************
+ # sizing: cooling curve assignments
+ # *********************************************************
# adjust rated cooling cop
if final_rated_cooling_cop == false
+ if hprtu_scenario == 'two_speed_standard_eff'
+ rated_cooling_cop = get_rated_cop_cooling(stage_caps_cooling[rated_stage_num_cooling])
+ elsif hprtu_scenario == 'variable_speed_high_eff'
+ rated_cooling_cop = get_rated_cop_cooling_adv(stage_caps_cooling[rated_stage_num_cooling])
+ elsif hprtu_scenario == 'carrier_48qe_dualfuel'
+ rated_cooling_cop = get_rated_cop_cooling_dualfuelrtu(stage_caps_cooling[rated_stage_num_cooling])
+ else
+ rated_cooling_cop = get_rated_cop_cooling_adv(stage_caps_cooling[rated_stage_num_cooling])
+ end
final_rated_cooling_cop = adjust_rated_cop_from_ref_cfm_per_ton(runner, stage_flows_cooling[rated_stage_num_cooling],
reference_cooling_cfm_per_ton,
stage_caps_cooling[rated_stage_num_cooling],
- get_rated_cop_cooling(stage_caps_cooling[rated_stage_num_cooling]),
+ rated_cooling_cop,
cool_eir_ff_curve_stages[rated_stage_num_cooling])
- runner.registerInfo("sizing summary: rated cooling COP adjusted from #{get_rated_cop_cooling(stage_caps_cooling[rated_stage_num_cooling]).round(3)} to #{final_rated_cooling_cop.round(3)} based on reference cfm/ton of #{reference_cooling_cfm_per_ton.round(0)} (i.e., average value of actual products)")
+ runner.registerInfo("sizing summary: rated cooling COP adjusted from #{rated_cooling_cop.round(3)} to #{final_rated_cooling_cop.round(3)} based on reference cfm/ton of #{reference_cooling_cfm_per_ton.round(0)} (i.e., average value of actual products)")
runner.registerInfo("sizing summary: sizing air loop (#{air_loop_hvac.name}): final rated cooling COP = #{final_rated_cooling_cop.round(3)}")
end
@@ -2342,17 +3201,26 @@ def run(model, runner, user_arguments)
debug_verbose
)
- # ---------------------------------------------------------
- # heating curve assignments
- # ---------------------------------------------------------
+ # *********************************************************
+ # sizing: heating curve assignments
+ # *********************************************************
# adjust rated heating cop
if final_rated_heating_cop == false
+ if hprtu_scenario == 'two_speed_standard_eff'
+ rated_heating_cop = get_rated_cop_heating(stage_caps_heating[rated_stage_num_heating])
+ elsif hprtu_scenario == 'variable_speed_high_eff'
+ rated_heating_cop = get_rated_cop_heating_adv(stage_caps_heating[rated_stage_num_heating])
+ elsif hprtu_scenario == 'carrier_48qe_dualfuel'
+ rated_heating_cop = get_rated_cop_heating_dualfuelrtu(stage_caps_heating[rated_stage_num_heating])
+ else
+ rated_heating_cop = get_rated_cop_heating_adv(stage_caps_heating[rated_stage_num_heating])
+ end
final_rated_heating_cop = adjust_rated_cop_from_ref_cfm_per_ton(runner, stage_flows_heating[rated_stage_num_heating],
reference_heating_cfm_per_ton,
stage_caps_heating[rated_stage_num_heating],
- get_rated_cop_heating(stage_caps_heating[rated_stage_num_heating]),
+ rated_heating_cop,
heat_eir_ff_curve_stages[rated_stage_num_heating])
- runner.registerInfo("sizing summary: rated heating COP adjusted from #{get_rated_cop_heating(stage_caps_heating[rated_stage_num_heating]).round(3)} to #{final_rated_heating_cop.round(3)} based on reference cfm/ton of #{reference_heating_cfm_per_ton.round(0)} (i.e., average value of actual products)")
+ runner.registerInfo("sizing summary: rated heating COP adjusted from #{rated_heating_cop.round(3)} to #{final_rated_heating_cop.round(3)} based on reference cfm/ton of #{reference_heating_cfm_per_ton.round(0)} (i.e., average value of actual products)")
runner.registerInfo("sizing summary: sizing air loop (#{air_loop_hvac.name}): final rated heating COP = #{final_rated_heating_cop.round(3)}")
end
@@ -2381,33 +3249,55 @@ def run(model, runner, user_arguments)
debug_verbose
)
- #################################### End performance curve assignment
-
+ # *********************************************************
# add new supplemental heating coil
+ # *********************************************************
new_backup_heating_coil = nil
- # define backup heat source TODO: set capacity to equal full heating capacity
- if (prim_ht_fuel_type == 'electric') || (backup_ht_fuel_scheme == 'electric_resistance_backup')
+
+ case backup_ht_fuel_scheme
+ when "electric_resistance_backup"
+ coil_type = :electric
+
+ when "match_original_primary_heating_fuel"
+ if prim_ht_fuel_type == "electric"
+ coil_type = :electric
+ elsif prim_ht_fuel_type == "gas"
+ coil_type = :gas
+ end
+
+ when "dual_fuel_gas_furnace_backup"
+ coil_type = nil
+ end
+
+ if coil_type == :electric
new_backup_heating_coil = OpenStudio::Model::CoilHeatingElectric.new(model)
new_backup_heating_coil.setEfficiency(1.0)
new_backup_heating_coil.setName("#{air_loop_hvac.name} electric resistance backup coil")
- else
+
+ elsif coil_type == :gas
new_backup_heating_coil = OpenStudio::Model::CoilHeatingGas.new(model)
new_backup_heating_coil.setGasBurnerEfficiency(0.80)
new_backup_heating_coil.setName("#{air_loop_hvac.name} gas backup coil")
end
- # set availability schedule
- new_backup_heating_coil.setAvailabilitySchedule(always_on)
- # set capacity of backup heat to meet full heating load
- new_backup_heating_coil.setNominalCapacity(orig_htg_coil_gross_cap_old)
+ # shared capacity logic
+ if new_backup_heating_coil
+ new_backup_heating_coil.setNominalCapacity(orig_htg_coil_gross_cap_old)
+ new_backup_heating_coil.setAvailabilitySchedule(always_on)
+ end
+
+ # *********************************************************
# add new fan
+ # *********************************************************
new_fan = OpenStudio::Model::FanVariableVolume.new(model, always_on)
new_fan.setAvailabilitySchedule(supply_fan_avail_sched)
new_fan.setName("#{air_loop_hvac.name} VFD Fan")
new_fan.setMotorEfficiency(fan_mot_eff) # from Daikin Rebel E+ file
new_fan.setFanPowerMinimumFlowRateInputMethod('Fraction')
+ # *********************************************************
# set fan total efficiency, which determines fan power
+ # *********************************************************
if hprtu_scenario == 'variable_speed_high_eff'
# new_fan.setFanTotalEfficiency(0.57) # from PNNL
std.fan_change_motor_efficiency(new_fan, fan_mot_eff)
@@ -2428,13 +3318,17 @@ def run(model, runner, user_arguments)
end
new_fan.setPressureRise(fan_static_pressure) # set from origial fan power; 0.5in will be added later if adding HR
- # add new unitary system object
+ # *********************************************************
+ # add and configure new unitary system object
+ # *********************************************************
new_air_to_air_heatpump = OpenStudio::Model::AirLoopHVACUnitarySystem.new(model)
new_air_to_air_heatpump.setName("#{air_loop_hvac.name} Unitary Heat Pump System")
new_air_to_air_heatpump.setSupplyFan(new_fan)
new_air_to_air_heatpump.setHeatingCoil(new_dx_heating_coil)
new_air_to_air_heatpump.setCoolingCoil(new_dx_cooling_coil)
- new_air_to_air_heatpump.setSupplementalHeatingCoil(new_backup_heating_coil)
+ if new_backup_heating_coil
+ new_air_to_air_heatpump.setSupplementalHeatingCoil(new_backup_heating_coil)
+ end
new_air_to_air_heatpump.addToNode(air_loop_hvac.supplyOutletNode)
# set other features
@@ -2460,7 +3354,25 @@ def run(model, runner, user_arguments)
# set no load design flow rate
new_air_to_air_heatpump.setSupplyAirFlowRateWhenNoCoolingorHeatingisRequired(min_airflow_m3_per_s)
+ # *********************************************************
+ # add dual fuel gas coil via ems
+ # *********************************************************
+ if (backup_ht_fuel_scheme == 'dual_fuel_gas_furnace_backup')
+ create_two_stage_dual_fuel_gas_coil_with_ems(
+ model,
+ runner,
+ air_loop_hvac,
+ new_dx_heating_coil,
+ orig_htg_coil_gross_cap_old,
+ new_air_to_air_heatpump,
+ hp_min_comp_lockout_temp_f,
+ dx_rated_htg_cap_applied
+ )
+ end
+
+ # *********************************************************
# add dcv to air loop if dcv flag is true
+ # *********************************************************
if dcv == true
oa_system = air_loop_hvac.airLoopHVACOutdoorAirSystem.get
controller_oa = oa_system.getControllerOutdoorAir
@@ -2468,7 +3380,9 @@ def run(model, runner, user_arguments)
controller_mv.setDemandControlledVentilation(true)
end
+ # *********************************************************
# add economizer
+ # *********************************************************
if econ == true
# set parameters
oa_system = air_loop_hvac.airLoopHVACOutdoorAirSystem.get
@@ -2489,7 +3403,9 @@ def run(model, runner, user_arguments)
controller_oa = oa_system.getControllerOutdoorAir
controller_oa.setLockoutType('LockoutWithHeating') unless controller_oa.getEconomizerControlType == 'NoEconomizer'
- # Energy recovery
+ # *********************************************************
+ # add energy recovery
+ # *********************************************************
# check for ERV, and get components
# ERV components will be removed and replaced if ERV flag was selected
# If ERV flag was not selected, ERV equipment will remain in place as-is
@@ -2507,7 +3423,9 @@ def run(model, runner, user_arguments)
# add energy recovery if specified by user and if the building type is applicable
next unless (hr == true) && (btype_erv_applicable == true)
+ # *********************************************************
# check for space type applicability
+ # *********************************************************
thermal_zone_names_to_exclude = %w[Kitchen kitchen KITCHEN Dining dining DINING]
# skip air loops that serve non-applicable space types and warn user
if thermal_zone_names_to_exclude.any? { |word| thermal_zone.name.to_s.include?(word) }
@@ -2617,7 +3535,9 @@ def run(model, runner, user_arguments)
end
end
+ # ---------------------------------------------------------
# report final condition of model
+ # ---------------------------------------------------------
condition_final_hprtu = "The building finished with heat pump RTUs replacing the HVAC equipment for #{selected_air_loops.size} air loops."
condition_final = [condition_final_hprtu, condition_final_roof, condition_final_window].reject(&:empty?).join(' | ')
runner.registerFinalCondition(condition_final)
diff --git a/resources/measures/upgrade_hvac_add_heat_pump_rtu/measure.xml b/resources/measures/upgrade_hvac_add_heat_pump_rtu/measure.xml
index 0a120175d..289f59315 100644
--- a/resources/measures/upgrade_hvac_add_heat_pump_rtu/measure.xml
+++ b/resources/measures/upgrade_hvac_add_heat_pump_rtu/measure.xml
@@ -3,8 +3,8 @@
3.1
add_heat_pump_rtu
f4567a68-27f2-4a15-ae91-ba0f35cd08c7
- c9f84023-5c70-46a1-9a0f-d41872955f62
- 2025-10-06T15:04:54Z
+ f7097b04-da40-47d6-8730-757183017f27
+ 2026-01-27T16:25:44Z
5E2576E4
AddHeatPumpRtu
add_heat_pump_rtu
@@ -28,6 +28,10 @@
electric_resistance_backup
electric_resistance_backup
+
+ dual_fuel_gas_furnace_backup
+ dual_fuel_gas_furnace_backup
+
@@ -118,6 +122,10 @@
cchpc_2027_spec
cchpc_2027_spec
+
+ carrier_48qe_dualfuel
+ carrier_48qe_dualfuel
+
@@ -254,7 +262,7 @@
Boolean
false
false
- true
+ false
true
@@ -318,7 +326,7 @@
README.md
md
readme
- 39FE80C4
+ C7988B04
README.md.erb
@@ -341,7 +349,7 @@
measure.rb
rb
script
- F4B40836
+ 373D2716
call_other_measures.rb
@@ -355,6 +363,12 @@
resource
A388157B
+
+ performance_maps_carrier_48qe_dualfuel.json
+ json
+ resource
+ C28E21E3
+
performance_maps_hprtu_lab_data.json
json
@@ -389,7 +403,7 @@
measure_test.rb
rb
test
- 6660D57F
+ 5CEE508F
diff --git a/resources/measures/upgrade_hvac_add_heat_pump_rtu/resources/performance_maps_carrier_48qe_dualfuel.json b/resources/measures/upgrade_hvac_add_heat_pump_rtu/resources/performance_maps_carrier_48qe_dualfuel.json
new file mode 100644
index 000000000..9068816f1
--- /dev/null
+++ b/resources/measures/upgrade_hvac_add_heat_pump_rtu/resources/performance_maps_carrier_48qe_dualfuel.json
@@ -0,0 +1,457 @@
+{
+ "tables": {
+ "curves": {
+ "table": [
+ {
+ "name": "cap_mod_cooling_high_t",
+ "category": "cap_mod_cooling_high_t",
+ "form": "MultiVariableLookupTable",
+ "number_independent_variables": 2,
+ "interpolation_method": "LinearInterpolationOfTable",
+ "number_of_interpolation_points": 2,
+ "curve_type": "Biquadratic",
+ "normalization_reference": 1.0,
+ "output_unit_type": "Dimensionless",
+ "minimum_independent_variable_1": 14.444444444444445,
+ "maximum_independent_variable_1": 24.444444444444443,
+ "input_unit_type_x1": "Temperature",
+ "minimum_independent_variable_2": 29.444444444444443,
+ "maximum_independent_variable_2": 51.666666666666664,
+ "input_unit_type_x2": "Temperature",
+ "notes": "cooling capacity modifier function of temperatures for high stage",
+ "data_point1": "14.44,29.44,0.9526",
+ "data_point2": "14.44,35.0,0.9039",
+ "data_point3": "14.44,40.56,0.8515",
+ "data_point4": "14.44,46.11,0.7952",
+ "data_point5": "14.44,51.67,0.7349",
+ "data_point6": "16.67,29.44,0.9711",
+ "data_point7": "16.67,35.0,0.9171",
+ "data_point8": "16.67,40.56,0.859",
+ "data_point9": "16.67,46.11,0.8011",
+ "data_point10": "16.67,51.67,0.7375",
+ "data_point11": "19.44,29.44,1.0618",
+ "data_point12": "19.44,35.0,1.0",
+ "data_point13": "19.44,40.56,0.9337",
+ "data_point14": "19.44,46.11,0.8631",
+ "data_point15": "19.44,51.67,0.7874",
+ "data_point16": "22.22,29.44,1.1718",
+ "data_point17": "22.22,35.0,1.1055",
+ "data_point18": "22.22,40.56,1.0349",
+ "data_point19": "22.22,46.11,0.9591",
+ "data_point20": "22.22,51.67,0.8787",
+ "data_point21": "24.44,29.44,1.2656",
+ "data_point22": "24.44,35.0,1.1958",
+ "data_point23": "24.44,40.56,1.1213",
+ "data_point24": "24.44,46.11,1.0414",
+ "data_point25": "24.44,51.67,0.9535"
+ },
+ {
+ "name": "cap_mod_cooling_low_t",
+ "category": "cap_mod_cooling_low_t",
+ "form": "MultiVariableLookupTable",
+ "number_independent_variables": 2,
+ "interpolation_method": "LinearInterpolationOfTable",
+ "number_of_interpolation_points": 2,
+ "curve_type": "Biquadratic",
+ "normalization_reference": 1.0,
+ "output_unit_type": "Dimensionless",
+ "minimum_independent_variable_1": 14.444444444444445,
+ "maximum_independent_variable_1": 24.444444444444443,
+ "input_unit_type_x1": "Temperature",
+ "minimum_independent_variable_2": 29.444444444444443,
+ "maximum_independent_variable_2": 51.666666666666664,
+ "input_unit_type_x2": "Temperature",
+ "notes": "cooling capacity modifier function of temperatures for low stage",
+ "data_point1": "14.44,29.44,0.9486",
+ "data_point2": "14.44,35.0,0.9013",
+ "data_point3": "14.44,40.56,0.8502",
+ "data_point4": "14.44,46.11,0.7961",
+ "data_point5": "14.44,51.67,0.7367",
+ "data_point6": "16.67,29.44,0.964",
+ "data_point7": "16.67,35.0,0.914",
+ "data_point8": "16.67,40.56,0.8561",
+ "data_point9": "16.67,46.11,0.7983",
+ "data_point10": "16.67,51.67,0.7377",
+ "data_point11": "19.44,29.44,1.0603",
+ "data_point12": "19.44,35.0,1.0",
+ "data_point13": "19.44,40.56,0.9357",
+ "data_point14": "19.44,46.11,0.8675",
+ "data_point15": "19.44,51.67,0.7939",
+ "data_point16": "22.22,29.44,1.1709",
+ "data_point17": "22.22,35.0,1.1066",
+ "data_point18": "22.22,40.56,1.0385",
+ "data_point19": "22.22,46.11,0.9663",
+ "data_point20": "22.22,51.67,0.8883",
+ "data_point21": "24.44,29.44,1.2668",
+ "data_point22": "24.44,35.0,1.1982",
+ "data_point23": "24.44,40.56,1.1275",
+ "data_point24": "24.44,46.11,1.0511",
+ "data_point25": "24.44,51.67,0.9692"
+ },
+ {
+ "name": "cap_mod_heating_high_t",
+ "category": "cap_mod_heating_high_t",
+ "form": "MultiVariableLookupTable",
+ "number_independent_variables": 2,
+ "interpolation_method": "LinearInterpolationOfTable",
+ "number_of_interpolation_points": 2,
+ "curve_type": "Biquadratic",
+ "normalization_reference": 1.0,
+ "output_unit_type": "Dimensionless",
+ "minimum_independent_variable_1": 12.777777777777779,
+ "maximum_independent_variable_1": 26.666666666666668,
+ "input_unit_type_x1": "Temperature",
+ "minimum_independent_variable_2": -23.333333333333332,
+ "maximum_independent_variable_2": 15.555555555555555,
+ "input_unit_type_x2": "Temperature",
+ "notes": "heating capacity modifier function of temperatures",
+ "data_point1": "12.78,-23.33,0.3272",
+ "data_point2": "12.78,-17.78,0.4217",
+ "data_point3": "12.78,-12.22,0.5333",
+ "data_point4": "12.78,-8.33,0.6232",
+ "data_point5": "12.78,-1.11,0.7712",
+ "data_point6": "12.78,4.44,0.9234",
+ "data_point7": "12.78,8.33,1.0446",
+ "data_point8": "12.78,10.0,1.0833",
+ "data_point9": "12.78,15.56,1.2376",
+ "data_point10": "21.11,-23.33,0.2941",
+ "data_point11": "21.11,-17.78,0.3859",
+ "data_point12": "21.11,-12.22,0.5058",
+ "data_point13": "21.11,-8.33,0.59",
+ "data_point14": "21.11,-1.11,0.7416",
+ "data_point15": "21.11,4.44,0.8845",
+ "data_point16": "21.11,8.33,1.0",
+ "data_point17": "21.11,10.0,1.04",
+ "data_point18": "21.11,15.56,1.1881",
+ "data_point19": "26.67,-23.33,0.2529",
+ "data_point20": "26.67,-17.78,0.3586",
+ "data_point21": "26.67,-12.22,0.4838",
+ "data_point22": "26.67,-8.33,0.5649",
+ "data_point23": "26.67,-1.11,0.7171",
+ "data_point24": "26.67,4.44,0.8574",
+ "data_point25": "26.67,8.33,0.9661",
+ "data_point26": "26.67,10.0,1.0061",
+ "data_point27": "26.67,15.56,1.1568"
+ },
+ {
+ "name": "eir_mod_cooling_high_t",
+ "category": "c_eir_high_T",
+ "form": "MultiVariableLookupTable",
+ "number_independent_variables": 2,
+ "interpolation_method": "LinearInterpolationOfTable",
+ "number_of_interpolation_points": 2,
+ "curve_type": "Biquadratic",
+ "normalization_reference": 1.0,
+ "output_unit_type": "Dimensionless",
+ "minimum_independent_variable_1": 13.88888888888889,
+ "maximum_independent_variable_1": 22.22222222222222,
+ "input_unit_type_x1": "Temperature",
+ "minimum_independent_variable_2": 23.88888888888889,
+ "maximum_independent_variable_2": 51.666666666666664,
+ "input_unit_type_x2": "Temperature",
+ "notes": "cooling EIR modifier function of temperatures for high stage",
+ "data_point1": "13.89,23.89,0.7441",
+ "data_point2": "13.89,29.44,0.8835",
+ "data_point3": "13.89,35.0,1.0421",
+ "data_point4": "13.89,40.56,1.2617",
+ "data_point5": "13.89,46.11,1.5177",
+ "data_point6": "13.89,51.67,1.813",
+ "data_point7": "16.67,23.89,0.7349",
+ "data_point8": "16.67,29.44,0.8833",
+ "data_point9": "16.67,35.0,1.0582",
+ "data_point10": "16.67,40.56,1.2784",
+ "data_point11": "16.67,46.11,1.5353",
+ "data_point12": "16.67,51.67,1.8279",
+ "data_point13": "17.22,23.89,0.7225",
+ "data_point14": "17.22,29.44,0.8696",
+ "data_point15": "17.22,35.0,1.0461",
+ "data_point16": "17.22,40.56,1.2779",
+ "data_point17": "17.22,46.11,1.59",
+ "data_point18": "17.22,51.67,1.8299",
+ "data_point19": "19.44,23.89,0.6948",
+ "data_point20": "19.44,29.44,0.8274",
+ "data_point21": "19.44,35.0,1.0",
+ "data_point22": "19.44,40.56,1.2287",
+ "data_point23": "19.44,46.11,1.5162",
+ "data_point24": "19.44,51.67,1.7874",
+ "data_point25": "21.67,23.89,0.6644",
+ "data_point26": "21.67,29.44,0.7745",
+ "data_point27": "21.67,35.0,0.9426",
+ "data_point28": "21.67,40.56,1.1551",
+ "data_point29": "21.67,46.11,1.4395",
+ "data_point30": "21.67,51.67,1.7321",
+ "data_point31": "22.22,23.89,0.6589",
+ "data_point32": "22.22,29.44,0.7917",
+ "data_point33": "22.22,35.0,0.9438",
+ "data_point34": "22.22,40.56,1.1528",
+ "data_point35": "22.22,46.11,1.3977",
+ "data_point36": "22.22,51.67,1.6876"
+ },
+ {
+ "name": "eir_mod_cooling_low_t",
+ "category": "c_eir_low_T",
+ "form": "MultiVariableLookupTable",
+ "number_independent_variables": 2,
+ "interpolation_method": "LinearInterpolationOfTable",
+ "number_of_interpolation_points": 2,
+ "curve_type": "Biquadratic",
+ "normalization_reference": 1.0,
+ "output_unit_type": "Dimensionless",
+ "minimum_independent_variable_1": 17.22222222222222,
+ "maximum_independent_variable_1": 21.666666666666668,
+ "input_unit_type_x1": "Temperature",
+ "minimum_independent_variable_2": 29.444444444444443,
+ "maximum_independent_variable_2": 46.11111111111111,
+ "input_unit_type_x2": "Temperature",
+ "notes": "cooling EIR modifier function of temperatures for low stage",
+ "data_point1": "17.22,29.44,0.8782",
+ "data_point2": "17.22,35.0,1.0352",
+ "data_point3": "17.22,40.56,1.2391",
+ "data_point4": "17.22,46.11,1.504",
+ "data_point5": "19.44,29.44,0.848",
+ "data_point6": "19.44,35.0,1.0",
+ "data_point7": "19.44,40.56,1.1996",
+ "data_point8": "19.44,46.11,1.4705",
+ "data_point9": "21.67,29.44,0.8062",
+ "data_point10": "21.67,35.0,0.9501",
+ "data_point11": "21.67,40.56,1.1312",
+ "data_point12": "21.67,46.11,1.3759"
+ },
+ {
+ "name": "eir_mod_heating_high_t",
+ "category": "h_eir_T",
+ "form": "MultiVariableLookupTable",
+ "number_independent_variables": 2,
+ "interpolation_method": "LinearInterpolationOfTable",
+ "number_of_interpolation_points": 2,
+ "curve_type": "Biquadratic",
+ "normalization_reference": 1.0,
+ "output_unit_type": "Dimensionless",
+ "minimum_independent_variable_1": 12.777777777777777,
+ "maximum_independent_variable_1": 26.666666666666664,
+ "input_unit_type_x1": "Temperature",
+ "minimum_independent_variable_2": -23.333333333333332,
+ "maximum_independent_variable_2": 15.555555555555555,
+ "input_unit_type_x2": "Temperature",
+ "notes": "heating EIR modifier function of temperatures",
+ "data_point1": "12.78,-23.33,3.1502",
+ "data_point2": "12.78,-17.78,1.9067",
+ "data_point3": "12.78,-12.22,1.4123",
+ "data_point4": "12.78,-6.67,1.1518",
+ "data_point5": "12.78,-1.11,0.987",
+ "data_point6": "12.78,4.44,0.8718",
+ "data_point7": "12.78,10.0,0.7904",
+ "data_point8": "12.78,15.56,0.7232",
+ "data_point9": "21.11,-23.33,5.2578",
+ "data_point10": "21.11,-17.78,2.6433",
+ "data_point11": "21.11,-12.22,1.87",
+ "data_point12": "21.11,-6.67,1.4798",
+ "data_point13": "21.11,-1.11,1.242",
+ "data_point14": "21.11,4.44,1.0845",
+ "data_point15": "21.11,10.0,0.9678",
+ "data_point16": "21.11,15.56,0.8789",
+ "data_point17": "26.67,-23.33,7.4729",
+ "data_point18": "26.67,-17.78,3.3174",
+ "data_point19": "26.67,-12.22,2.2634",
+ "data_point20": "26.67,-6.67,1.7578",
+ "data_point21": "26.67,-1.11,1.459",
+ "data_point22": "26.67,4.44,1.2582",
+ "data_point23": "26.67,10.0,1.1137",
+ "data_point24": "26.67,15.56,1.0078"
+ },
+ {
+ "name": "cap_mod_cooling_high_ff",
+ "category": "c_cap_high_ff",
+ "form": "Quadratic",
+ "notes": "cooling capacity modifier function of flow rate, high cooling stage",
+ "dependent_variable": "CapRatio",
+ "independent_variable_1": "FlowFraction",
+ "independent_variable_2": null,
+ "coeff_1": 0.68667,
+ "coeff_2": 0.44817,
+ "coeff_3": -0.13504,
+ "minimum_independent_variable_1": 0.682,
+ "maximum_independent_variable_1": 1.333,
+ "minimum_independent_variable_2": null,
+ "maximum_independent_variable_2": null,
+ "minimum_dependent_variable_output": 0.929,
+ "maximum_dependent_variable_output": 1.044
+ },
+ {
+ "name": "cap_mod_cooling_low_ff",
+ "category": "c_cap_low_ff",
+ "form": "Quadratic",
+ "notes": "cooling capacity modifier function of flow rate, low cooling stage",
+ "dependent_variable": "CapRatio",
+ "independent_variable_1": "FlowFraction",
+ "independent_variable_2": null,
+ "coeff_1": 0.69109,
+ "coeff_2": 0.42775,
+ "coeff_3": -0.11933,
+ "minimum_independent_variable_1": 0.682,
+ "maximum_independent_variable_1": 1.328,
+ "minimum_independent_variable_2": null,
+ "maximum_independent_variable_2": null,
+ "minimum_dependent_variable_output": 0.927,
+ "maximum_dependent_variable_output": 1.049
+ },
+ {
+ "name": "cap_mod_heating_high_ff",
+ "category": "h_cap_allstages_ff",
+ "form": "Quadratic",
+ "notes": "heating capacity function of flow rate",
+ "dependent_variable": "CapRatio",
+ "independent_variable_1": "FlowFraction",
+ "independent_variable_2": null,
+ "coeff_1": 0.80813,
+ "coeff_2": 0.26394,
+ "coeff_3": -0.07145,
+ "minimum_independent_variable_1": 0.682,
+ "maximum_independent_variable_1": 1.333,
+ "minimum_independent_variable_2": null,
+ "maximum_independent_variable_2": null,
+ "minimum_dependent_variable_output": 0.955,
+ "maximum_dependent_variable_output": 1.033
+ },
+ {
+ "name": "eir_mod_cooling_high_ff",
+ "category": "c_eir_high_ff",
+ "form": "Quadratic",
+ "notes": "cooling eir modifier function of flow rate, high cooling stage",
+ "dependent_variable": "EirRatio",
+ "independent_variable_1": "FlowFraction",
+ "independent_variable_2": null,
+ "coeff_1": 1.170145,
+ "coeff_2": -0.163166,
+ "coeff_3": 0,
+ "minimum_independent_variable_1": 0.57,
+ "maximum_independent_variable_1": 1.52,
+ "minimum_independent_variable_2": null,
+ "maximum_independent_variable_2": null,
+ "minimum_dependent_variable_output": 0.88,
+ "maximum_dependent_variable_output": 1.13
+ },
+ {
+ "name": "eir_mod_cooling_low_ff",
+ "category": "c_eir_low_ff",
+ "form": "Quadratic",
+ "notes": "cooling eir modifier function of flow rate, low cooling stage",
+ "dependent_variable": "EirRatio",
+ "independent_variable_1": "FlowFraction",
+ "independent_variable_2": null,
+ "coeff_1": 1.094569,
+ "coeff_2": -0.093388,
+ "coeff_3": 0,
+ "minimum_independent_variable_1": 0.91,
+ "maximum_independent_variable_1": 1.41,
+ "minimum_independent_variable_2": null,
+ "maximum_independent_variable_2": null,
+ "minimum_dependent_variable_output": 0.96,
+ "maximum_dependent_variable_output": 1.02
+ },
+ {
+ "name": "eir_mod_heating_high_ff",
+ "category": "h_eir_allstages_ff",
+ "form": "Quadratic",
+ "notes": "heating efficiency function of flow rate",
+ "dependent_variable": "EirRatio",
+ "independent_variable_1": "FlowFraction",
+ "independent_variable_2": null,
+ "coeff_1": 1.349981,
+ "coeff_2": -0.325294,
+ "coeff_3": 0,
+ "minimum_independent_variable_1": 0.68,
+ "maximum_independent_variable_1": 1.52,
+ "minimum_independent_variable_2": null,
+ "maximum_independent_variable_2": null,
+ "minimum_dependent_variable_output": 0.77,
+ "maximum_dependent_variable_output": 1.54
+ },
+ {
+ "name": "plf_na_cooling_na_plr",
+ "category": "cool_plf_plr1",
+ "form": "Quadratic",
+ "notes": "Part Load Fraction Correlation Curve",
+ "dependent_variable": "PLF",
+ "independent_variable_1": "PLR",
+ "independent_variable_2": null,
+ "coeff_1": 0.75,
+ "coeff_2": 0.25,
+ "coeff_3": 0,
+ "minimum_independent_variable_1": 0,
+ "maximum_independent_variable_1": 1,
+ "minimum_independent_variable_2": null,
+ "maximum_independent_variable_2": null,
+ "minimum_dependent_variable_output": 0.7,
+ "maximum_dependent_variable_output": 1
+ },
+ {
+ "name": "plf_na_heating_na_plr",
+ "category": "heat_plf_plr1",
+ "form": "MultiVariableLookupTable",
+ "number_independent_variables": 1,
+ "interpolation_method": "LinearInterpolationOfTable",
+ "number_of_interpolation_points": 1,
+ "curve_type": "Quadratic",
+ "normalization_reference": 1.0,
+ "output_unit_type": "Dimensionless",
+ "minimum_independent_variable_1": 0.02,
+ "maximum_independent_variable_1": 1,
+ "input_unit_type_x1": "Dimensionless",
+ "notes": "Part Load Fraction Correlation Curve, all stages",
+ "data_point1": "0.02,0.37540472",
+ "data_point2": "0.128888889,0.639547532",
+ "data_point3": "0.237777778,0.795243399",
+ "data_point4": "0.346666667,0.854899893",
+ "data_point5": "0.455555556,0.895125481",
+ "data_point6": "0.564444444,0.928934175",
+ "data_point7": "0.673333333,0.956325973",
+ "data_point8": "0.782222222,0.977300877",
+ "data_point9": "0.891111111,0.991858886",
+ "data_point10": "1,1"
+ },
+ {
+ "name": "eir_mod_defrost_na_na",
+ "category": "defrost_eir",
+ "form": "BiQuadratic",
+ "notes": "Defrost Energy Input Ratio Function of Temperature Curve",
+ "dependent_variable": "Power_compressor/Rated_Capacity",
+ "independent_variable_1": "ID WB",
+ "independent_variable_2": "OD DB",
+ "coeff_1": 0.137458424283,
+ "coeff_2": -0.002674448387,
+ "coeff_3": 0.000143460534,
+ "coeff_4": -0.000640671969,
+ "coeff_5": -9.434242e-06,
+ "coeff_6": -4.6933266e-05,
+ "minimum_independent_variable_1": 3.722222,
+ "maximum_independent_variable_1": 19.444444,
+ "minimum_independent_variable_2": -22.444444,
+ "maximum_independent_variable_2": 2.333333,
+ "minimum_dependent_variable_output": 0.122898,
+ "maximum_dependent_variable_output": 0.151752
+ },
+ {
+ "name": "staging_data",
+ "num_heating_stages": 1,
+ "num_cooling_stages": 2,
+ "rated_stage_num_heating": 1,
+ "rated_stage_num_cooling": 2,
+ "final_rated_cooling_cop": false,
+ "final_rated_heating_cop": false,
+ "stage_cap_fractions_heating": "{1=>1}",
+ "stage_flow_fractions_heating": "{1=>1}",
+ "stage_cap_fractions_cooling": "{2=>1, 1=>0.66}",
+ "stage_flow_fractions_cooling": "{2=>1, 1=>0.65}",
+ "stage_rated_cop_frac_heating": "{1 => 1}",
+ "stage_rated_cop_frac_cooling": "{2 => 1, 1 => 1}",
+ "boost_stage_num_and_max_temp_tuple": "[]",
+ "stage_gross_rated_sensible_heat_ratio_cooling": "{2 => 0.77, 1 => 0.82}",
+ "enable_cycling_losses_above_lowest_speed": true,
+ "reference_cooling_cfm_per_ton": 413.0,
+ "reference_heating_cfm_per_ton": 433.0
+ }
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/resources/measures/upgrade_hvac_add_heat_pump_rtu/tests/measure_test.rb b/resources/measures/upgrade_hvac_add_heat_pump_rtu/tests/measure_test.rb
index d055289de..58569d16a 100644
--- a/resources/measures/upgrade_hvac_add_heat_pump_rtu/tests/measure_test.rb
+++ b/resources/measures/upgrade_hvac_add_heat_pump_rtu/tests/measure_test.rb
@@ -297,6 +297,7 @@ def test_table_lookup_format
path_to_jsons = "#{__dir__}/../resources/*.json"
json_files = Dir.glob(path_to_jsons)
json_files.each do |file_path|
+ puts("### checking json file: #{file_path}")
begin
content = File.read(file_path)
hash = JSON.parse(content, symbolize_names: true)
@@ -392,6 +393,7 @@ def test_biquadratic_format
rescue JSON::ParserError => e
flunk "JSON parsing failed for #{file_path}: #{e.message}"
end
+ end
end
def calc_cfm_per_ton_singlespdcoil_heating(model, cfm_per_ton_min, cfm_per_ton_max)
@@ -504,6 +506,11 @@ def verify_cfm_per_ton(model, result)
calc_cfm_per_ton_multispdcoil_cooling(model, cfm_per_ton_min, cfm_per_ton_max)
calc_cfm_per_ton_singlespdcoil_heating(model, cfm_per_ton_min, cfm_per_ton_max)
+ elsif performance_category.include?('dualfuel')
+
+ calc_cfm_per_ton_multispdcoil_cooling(model, cfm_per_ton_min, cfm_per_ton_max)
+ calc_cfm_per_ton_singlespdcoil_heating(model, cfm_per_ton_min, cfm_per_ton_max)
+
end
end
@@ -976,6 +983,12 @@ def calc_cfm_per_ton_singlespdcoil_heating(model, cfm_per_ton_min, cfm_per_ton_m
rated_airflow_cfm = OpenStudio.convert(rated_airflow_m_3_per_sec, 'm^3/s', 'cfm').get
cfm_per_ton = rated_airflow_cfm / rated_capacity_ton
+ # puts("### checking coil")
+ # puts("heating_coil.name = #{heating_coil.name}")
+ # puts("rated_capacity_w = #{rated_capacity_w}")
+ # puts("rated_airflow_cfm = #{rated_airflow_cfm}")
+ # puts("cfm_per_ton = #{cfm_per_ton}")
+
# check if resultant cfm/ton is violating min/max bounds
assert_equal(cfm_per_ton.round(0) >= cfm_per_ton_min, true, "cfm_per_ton (#{cfm_per_ton}) is not larger than the threshold of cfm_per_ton_min (#{cfm_per_ton_min}) | heating_coil = #{heating_coil.name}")
assert_equal(cfm_per_ton.round(0) <= cfm_per_ton_max, true, "cfm_per_ton (#{cfm_per_ton}) is not smaller than the threshold of cfm_per_ton_max (#{cfm_per_ton_max}) | heating_coil = #{heating_coil.name}")
@@ -1028,47 +1041,18 @@ def calc_cfm_per_ton_multispdcoil_cooling(model, cfm_per_ton_min, cfm_per_ton_ma
rated_airflow_cfm = OpenStudio.convert(rated_airflow_m_3_per_sec, 'm^3/s', 'cfm').get
cfm_per_ton = rated_airflow_cfm / rated_capacity_ton
+ # puts("### checking coil")
+ # puts("cooling_coil.name = #{cooling_coil.name}")
+ # puts("rated_capacity_w = #{rated_capacity_w}")
+ # puts("rated_airflow_cfm = #{rated_airflow_cfm}")
+ # puts("cfm_per_ton = #{cfm_per_ton}")
+
# check if resultant cfm/ton is violating min/max bounds
assert_equal(cfm_per_ton.round(0) >= cfm_per_ton_min, true, "cfm_per_ton (#{cfm_per_ton}) is not larger than the threshold of cfm_per_ton_min (#{cfm_per_ton_min}) | cooling_coil = #{cooling_coil.name}")
assert_equal(cfm_per_ton.round(0) <= cfm_per_ton_max, true, "cfm_per_ton (#{cfm_per_ton}) is not smaller than the threshold of cfm_per_ton_max (#{cfm_per_ton_max}) | cooling_coil = #{cooling_coil.name}")
end
end
- def verify_cfm_per_ton(model, result)
- # define min and max limits of cfm/ton
- cfm_per_ton_min = 300
- cfm_per_ton_max = 450
-
- # Create an instance of the measure
- measure = AddHeatPumpRtu.new
-
- # initialize parameters
- performance_category = nil
-
- # check performance category
- result.stepValues.each do |input_arg|
- next unless input_arg.name == 'hprtu_scenario'
-
- performance_category = input_arg.valueAsString
-
- puts performance_category
- end
- refute_equal(performance_category, nil)
-
- # loop through coils and check cfm/ton values
- if performance_category.include?('high_eff')
-
- calc_cfm_per_ton_multispdcoil_cooling(model, cfm_per_ton_min, cfm_per_ton_max)
- calc_cfm_per_ton_multispdcoil_heating(model, cfm_per_ton_min, cfm_per_ton_max)
-
- elsif performance_category.include?('standard')
-
- calc_cfm_per_ton_multispdcoil_cooling(model, cfm_per_ton_min, cfm_per_ton_max)
- calc_cfm_per_ton_singlespdcoil_heating(model, cfm_per_ton_min, cfm_per_ton_max)
-
- end
- end
-
# ##########################################################################
# # Single building result examples
# def test_single_building_result_examples
@@ -1752,6 +1736,144 @@ def test_380_full_service_restaurant_psz_gas_coil_std_perf
verify_cfm_per_ton(model, result)
end
+ ###########################################################################
+ # This test is for dual fuel RTU unit
+ def test_380_full_service_restaurant_psz_gas_coil_dual_fuel_rtu
+ osm_name = '380_full_service_restaurant_psz_gas_coil.osm'
+ epw_name = 'GA_ROBINS_AFB_722175_12.epw'
+
+ puts "\n######\nTEST:#{osm_name}\n######\n"
+
+ lookup_table_tests = [
+ {
+ 'table_name': 'cap_mod_cooling_high_t',
+ 'ind1': 19.44,
+ 'ind2': 46.11,
+ 'dep': 0.8631
+ },
+ {
+ 'table_name': 'cap_mod_cooling_low_t',
+ 'ind1': 22.22,
+ 'ind2': 40.56,
+ 'dep': 1.0385
+ },
+ {
+ 'table_name': 'cap_mod_heating_high_t',
+ 'ind1': 12.78,
+ 'ind2': 10.0,
+ 'dep': 1.0833
+ },
+ {
+ 'table_name': 'eir_mod_cooling_high_t',
+ 'ind1': 16.67,
+ 'ind2': 40.56,
+ 'dep': 1.2784
+ },
+ {
+ 'table_name': 'eir_mod_cooling_low_t',
+ 'ind1': 21.67,
+ 'ind2': 46.11,
+ 'dep': 1.3759
+ },
+ {
+ 'table_name': 'eir_mod_heating_high_t',
+ 'ind1': 12.78,
+ 'ind2': -23.33,
+ 'dep': 3.1502
+ }
+ ]
+
+ osm_path = model_input_path(osm_name)
+ epw_path = epw_input_path(epw_name)
+
+ # Create an instance of the measure
+ measure = AddHeatPumpRtu.new
+
+ # Load the model; only used here for populating arguments
+ model = load_model(osm_path)
+
+ # get arguments
+ arguments = measure.arguments(model)
+ argument_map = OpenStudio::Measure.convertOSArgumentVectorToMap(arguments)
+
+ # populate argument with specified hash value if specified
+ arguments.each_with_index do |arg, idx|
+ temp_arg_var = arg.clone
+ if arg.name == 'backup_ht_fuel_scheme'
+ backup_ht_fuel_scheme = arguments[idx].clone
+ backup_ht_fuel_scheme.setValue('dual_fuel_gas_furnace_backup')
+ argument_map[arg.name] = backup_ht_fuel_scheme
+ elsif arg.name == 'hprtu_scenario'
+ hprtu_scenario = arguments[idx].clone
+ hprtu_scenario.setValue('carrier_48qe_dualfuel') # carrier_48qe_dualfuel, two_speed_standard_eff
+ argument_map[arg.name] = hprtu_scenario
+ else
+ argument_map[arg.name] = temp_arg_var
+ end
+ end
+
+ # Apply the measure to the model and optionally run the model
+ result = set_weather_and_apply_measure_and_run(__method__, measure, argument_map, osm_path, epw_path, run_model: false, apply: true)
+ assert_equal('Success', result.value.valueName)
+ model = load_model(model_output_path(__method__))
+
+ # check performance category
+ performance_category = nil
+ result.stepValues.each do |input_arg|
+ next unless input_arg.name == 'hprtu_scenario'
+ performance_category = input_arg.valueAsString
+ end
+
+ # check performance category
+ back_up_type = nil
+ result.stepValues.each do |input_arg|
+ next unless input_arg.name == 'backup_ht_fuel_scheme'
+ back_up_type = input_arg.valueAsString
+ end
+
+ # test lookup table values
+ runner = OpenStudio::Measure::OSRunner.new(OpenStudio::WorkflowJSON.new)
+ if performance_category == 'carrier_48qe_dualfuel'
+ lookup_table_tests.each do |lookup_table_test|
+ # Check if lookup table is available
+ lookup_table_name = lookup_table_test[:table_name]
+ #table_multivar_lookups = model.getTableMultiVariableLookups
+ table_multivar_lookups = model.getTableLookups
+ lookup_table = table_multivar_lookups.find { |table| table.name.to_s == lookup_table_name }
+ refute_nil(lookup_table, "Cannot find table named #{lookup_table_name} from model.")
+
+ # Compare table lookup value against hard-coded values
+ dep_var_ref = lookup_table_test[:dep]
+ dep_var = AddHeatPumpRtu.get_dep_var_from_lookup_table_with_interpolation(runner, lookup_table, lookup_table_test[:ind1], lookup_table_test[:ind2])
+ # puts("### lookup table test")
+ # puts("--- lookup_table_name = #{lookup_table_name}")
+ # puts("--- input_var1 = #{lookup_table_test[:ind1]} | input_var2 = #{lookup_table_test[:ind2]}")
+ # puts("--- dep_var reference = #{dep_var_ref} | dep_var from model = #{dep_var}")
+ assert_in_epsilon(dep_var_ref, dep_var, 0.001, "Table lookup value test didn't pass: table name = #{lookup_table_name} | ind_var1 = #{lookup_table_test[:ind1]} | ind_var2 = #{lookup_table_test[:ind2]} | expected #{dep_var_ref} but got #{dep_var}")
+ end
+ end
+ if back_up_type == 'dual_fuel_gas_furnace_backup'
+ number_of_applicable_airloops = 6 # hard-coded based on example model
+
+ # count energymanagementsystem:program objects with specific naming patterns
+ count_ems_prgm_init = model.getEnergyManagementSystemPrograms.select { |prgm| prgm.name.to_s.end_with?('_initialization') }.size
+ count_ems_prgm = model.getEnergyManagementSystemPrograms.select { |prgm| prgm.name.to_s.end_with?('_two_stage_gas_coil') }.size
+
+ # count energymanagementsystem:programcallingmanager objects with specific naming patterns
+ count_ems_pcm_init = model.getEnergyManagementSystemProgramCallingManagers.select { |pcm| pcm.name.to_s.end_with?('_initialization') }.size
+ count_ems_pcm = model.getEnergyManagementSystemProgramCallingManagers.select { |pcm| pcm.name.to_s.end_with?('_pcm_gas_coil') }.size
+
+ # assert counts
+ assert_equal(number_of_applicable_airloops, count_ems_prgm_init, "expected #{number_of_applicable_airloops} ems programs with '_initialization' but got #{count_ems_prgm_init}")
+ assert_equal(number_of_applicable_airloops, count_ems_prgm, "expected #{number_of_applicable_airloops} ems programs with '_two_stage_gas_coil' but got #{count_ems_prgm}")
+ assert_equal(number_of_applicable_airloops, count_ems_pcm_init, "expected #{number_of_applicable_airloops} ems program calling managers with '_initialization' but got #{count_ems_pcm_init}")
+ assert_equal(number_of_applicable_airloops, count_ems_pcm, "expected #{number_of_applicable_airloops} ems program calling managers with '_two_stage_gas_coil' but got #{count_ems_pcm}")
+ end
+
+ # assert cfm/ton violation
+ verify_cfm_per_ton(model, result)
+ end
+
###########################################################################
# This test is for cfm/ton check for upsized unit
def test_380_full_service_restaurant_psz_gas_coil_upsizing
@@ -2185,6 +2307,10 @@ def test_confirm_heating_setback_change_square_wave
setback_value_arg = arguments[idx].clone
setback_value_arg.setValue(setback_val) # set setback value
argument_map[arg.name] = setback_value_arg
+ elsif arg.name == 'modify_setbacks'
+ modify_setbacks = arguments[idx].clone
+ modify_setbacks.setValue(true)
+ argument_map[arg.name] = modify_setbacks
else
argument_map[arg.name] = temp_arg_var
end
@@ -2233,7 +2359,7 @@ def test_confirm_heating_setback_change_square_wave
# Make sure no deltas are greater than the expected setback value
deltas_out_of_range = schedule_deltas.any? { |x| x > setback_value_c }
- puts("Temperature deltas in schedule match expected values: #{(deltas_out_of_range == false)}")
+ puts("Temperature deltas in schedule match expected values: #{(deltas_out_of_range == false)}")
assert_equal(deltas_out_of_range, false)
@@ -2275,6 +2401,10 @@ def test_confirm_heating_setback_change_opt_start
setback_value_arg = arguments[idx].clone
setback_value_arg.setValue(setback_val) # set setback value
argument_map[arg.name] = setback_value_arg
+ elsif arg.name == 'modify_setbacks'
+ modify_setbacks = arguments[idx].clone
+ modify_setbacks.setValue(true)
+ argument_map[arg.name] = modify_setbacks
else
argument_map[arg.name] = temp_arg_var
end