Skip to content
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
8758181
Add new sample files for heat pump with hvac seasons, power outage.
joseph-robertson Jan 21, 2026
aae60b1
Add crankcase ems program, and use hvac seasons and unavailability pe…
joseph-robertson Jan 21, 2026
e1c4950
Update tests and docs.
joseph-robertson Jan 21, 2026
0ff6650
Merge branch 'master' into address-crankcase-defrost-pan-warning
joseph-robertson Jan 22, 2026
1a40627
Crankcase into its own program, and remove season from pan and crankc…
joseph-robertson Jan 22, 2026
554afd1
Update defrost/pan/crankcase ems test for crankcase changes.
joseph-robertson Jan 22, 2026
2873982
Remove old crankcase unit test.
joseph-robertson Jan 22, 2026
e526e25
Deal with crankcase allocation to heating or cooling.
joseph-robertson Jan 22, 2026
6eeaa29
Latest results.
Jan 23, 2026
3316eab
Crankcase allows zero capacity; slight program fix.
joseph-robertson Jan 26, 2026
51bb6a7
Latest results.
Jan 26, 2026
897e276
Merge branch 'master' into address-crankcase-defrost-pan-warning
joseph-robertson Jan 26, 2026
a215af2
Use heating and/or cooling availability sensors for crankcase operation.
joseph-robertson Jan 26, 2026
8ba115f
Latest results.
Jan 26, 2026
3a05c70
Use heating coil runtime fraction for crankcase calculation.
joseph-robertson Jan 27, 2026
8c263cb
Revise and simplify ems logic for unavailable periods.
joseph-robertson Jan 27, 2026
61a3792
Revert clg_coil addition in hvac test.
joseph-robertson Jan 27, 2026
c1c8046
Latest results.
Jan 27, 2026
9345759
Set additional properties for hp heating tout and rtf sensors.
joseph-robertson Feb 2, 2026
2abcd8e
Update measure xml.
joseph-robertson Feb 2, 2026
cd0d7ab
Merge branch 'master' into address-crankcase-defrost-pan-warning
joseph-robertson Feb 2, 2026
641094c
Put hp heating tout and rtf sensors upstream of defrost and crankcase.
joseph-robertson Feb 2, 2026
5e0541d
Introduce a new sensor for unitary system.
joseph-robertson Feb 2, 2026
ecdc565
Bring cycling ratio into the calculation for multispeed objects.
joseph-robertson Feb 2, 2026
c8dd252
Update measure xml file.
joseph-robertson Feb 2, 2026
f1da956
Organize ems sensors.
joseph-robertson Feb 3, 2026
b85e3c4
Introduce the cooling coil runtime fraction sensor.
joseph-robertson Feb 3, 2026
5147828
Latest results.
Feb 3, 2026
ba58f27
Merge branch 'master' into address-crankcase-defrost-pan-warning
joseph-robertson Feb 4, 2026
a4f9f01
Latest results.
Feb 5, 2026
13fab78
Merge branch 'master' into address-crankcase-defrost-pan-warning
joseph-robertson Feb 9, 2026
fd4513d
Latest results.
Feb 9, 2026
63dd002
Merge branch 'master' into address-crankcase-defrost-pan-warning
joseph-robertson Mar 6, 2026
439a213
Remove model from being passed into get_object_outputs_by_key.
joseph-robertson Mar 6, 2026
76cddff
Latest results.
Mar 7, 2026
5c2f362
Merge branch 'master' into address-crankcase-defrost-pan-warning
joseph-robertson Mar 12, 2026
54238a3
Latest results.
Mar 12, 2026
2afbc95
Change calling point InsideHVACSystemIterationLoop -> EndOfSystemTime…
joseph-robertson Mar 13, 2026
44ea2e1
Latest results.
Mar 13, 2026
5e820e6
Replace a few remaining TODO comments. [ci skip]
joseph-robertson Mar 16, 2026
c585f32
Update the changelog. [ci skip]
joseph-robertson Mar 16, 2026
5e8f794
Merge branch 'master' into address-crankcase-defrost-pan-warning
joseph-robertson Mar 31, 2026
69e7703
Remove passing model into get_object_outputs_by_key.
joseph-robertson Mar 31, 2026
a6d7be1
Latest results.
Mar 31, 2026
546ddbb
Merge branch 'master' of https://github.com/NatLabRockies/OpenStudio-…
shorowit Apr 6, 2026
9c4b80b
Latest results.
Apr 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions HPXMLtoOpenStudio/measure.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
<schema_version>3.1</schema_version>
<name>hpxm_lto_openstudio</name>
<uid>b1543b30-9465-45ff-ba04-1d1f85e763bc</uid>
<version_id>6f3658e4-715f-4ef2-aa75-2e82ec6dcbef</version_id>
<version_modified>2026-01-26T16:58:29Z</version_modified>
<version_id>4fc5923a-28ca-4ba5-b750-aceb525de9ff</version_id>
<version_modified>2026-01-27T18:32:29Z</version_modified>
<xml_checksum>D8922A73</xml_checksum>
<class_name>HPXMLtoOpenStudio</class_name>
<display_name>HPXML to OpenStudio Translator</display_name>
Expand Down Expand Up @@ -216,7 +216,7 @@
<filename>constants.rb</filename>
<filetype>rb</filetype>
<usage_type>resource</usage_type>
<checksum>C1C12650</checksum>
<checksum>52C8B837</checksum>
</file>
<file>
<filename>constructions.rb</filename>
Expand Down Expand Up @@ -414,7 +414,7 @@
<filename>hvac.rb</filename>
<filetype>rb</filetype>
<usage_type>resource</usage_type>
<checksum>6B182496</checksum>
<checksum>59FE4272</checksum>
</file>
<file>
<filename>hvac_sizing.rb</filename>
Expand Down Expand Up @@ -654,7 +654,7 @@
<filename>schedules.rb</filename>
<filetype>rb</filetype>
<usage_type>resource</usage_type>
<checksum>662E62CE</checksum>
<checksum>D064707B</checksum>
</file>
<file>
<filename>simcontrols.rb</filename>
Expand Down Expand Up @@ -762,7 +762,7 @@
<filename>test_hvac.rb</filename>
<filetype>rb</filetype>
<usage_type>test</usage_type>
<checksum>E70D77B1</checksum>
<checksum>2F1F54D9</checksum>
</file>
<file>
<filename>test_hvac_sizing.rb</filename>
Expand Down Expand Up @@ -810,7 +810,7 @@
<filename>test_validation.rb</filename>
<filetype>rb</filetype>
<usage_type>test</usage_type>
<checksum>1A14E141</checksum>
<checksum>4ACBE620</checksum>
</file>
<file>
<filename>test_vehicle.rb</filename>
Expand Down
1 change: 1 addition & 0 deletions HPXMLtoOpenStudio/resources/constants.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ module Constants
ObjectTypeNaturalVentilation = 'natural vent'
ObjectTypeNeighbors = 'neighbors'
ObjectTypeOccupants = 'occupants'
ObjectTypeCrankcaseHeater = 'crankcase heater'
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the new "end use" for actuated other equipment objects.

ObjectTypePanHeater = 'pan heater'
ObjectTypePhotovoltaics = 'photovoltaics'
ObjectTypePTAC = 'packaged terminal air conditioner'
Expand Down
126 changes: 116 additions & 10 deletions HPXMLtoOpenStudio/resources/hvac.rb
Original file line number Diff line number Diff line change
Expand Up @@ -482,11 +482,12 @@ def self.apply_air_source_hvac_systems(runner, model, weather, hpxml_bldg, hpxml

add_variable_speed_power_ems_program(runner, model, air_loop_unitary, control_zone, heating_system, cooling_system, htg_supp_coil, clg_coil, htg_coil, schedules_file)

if is_heatpump
ems_program = apply_defrost_ems_program(model, htg_coil, control_zone.spaces[0], cooling_system, hpxml_bldg.building_construction.number_of_units)
if cooling_system.pan_heater_watts.to_f > 0
if not cooling_system.nil?
if is_heatpump
ems_program = apply_defrost_ems_program(model, htg_coil, control_zone.spaces[0], cooling_system, hpxml_bldg.building_construction.number_of_units)
apply_pan_heater_ems_program(model, ems_program, htg_coil, control_zone.spaces[0], cooling_system, htg_ap.hp_min_temp)
end
apply_crankcase_heater_ems_program(model, htg_coil, clg_coil, control_zone.spaces[0], cooling_system)
end
return air_loop
end
Expand Down Expand Up @@ -1583,9 +1584,6 @@ def self.apply_setpoints(runner, model, weather, spaces, hpxml_bldg, hpxml_heade
hvac_control.seasons_heating_end_month, hvac_control.seasons_heating_end_day)
hvac_season_days[:clg] = Calendar.get_daily_season(hpxml_header.sim_calendar_year, hvac_control.seasons_cooling_begin_month, hvac_control.seasons_cooling_begin_day,
hvac_control.seasons_cooling_end_month, hvac_control.seasons_cooling_end_day)
if hvac_season_days[:htg].include?(0) || hvac_season_days[:clg].include?(0)
runner.registerWarning('It is not possible to eliminate all HVAC energy use (e.g. crankcase/defrost energy) in EnergyPlus outside of an HVAC season.')
end

heating_sch = nil
cooling_sch = nil
Expand Down Expand Up @@ -3104,7 +3102,7 @@ def self.create_dx_cooling_coil(model, obj_name, cooling_system, weather_max_dry

clg_coil.setName(coil_name)
clg_coil.setCondenserType('AirCooled')
clg_coil.setCrankcaseHeaterCapacity(cooling_system.crankcase_heater_watts)
clg_coil.setCrankcaseHeaterCapacity(0) # We model crankcase heater via EMS.
clg_coil.additionalProperties.setFeature('HPXML_ID', cooling_system.id) # Used by reporting measure
if has_deadband_control
# Apply startup capacity degradation
Expand Down Expand Up @@ -3272,7 +3270,7 @@ def self.create_dx_heating_coil(model, obj_name, heating_system, weather_min_dry

# Per E+ documentation, if an air-to-air heat pump, the crankcase heater defined for the DX cooling coil is ignored and the crankcase heater power defined for the DX heating coil is used
htg_coil.setMaximumOutdoorDryBulbTemperatureforCrankcaseHeaterOperation(UnitConversions.convert(CrankcaseHeaterTemp, 'F', 'C'))
htg_coil.setCrankcaseHeaterCapacity(heating_system.crankcase_heater_watts)
htg_coil.setCrankcaseHeaterCapacity(0) # We model crankcase heater via EMS.
htg_coil.additionalProperties.setFeature('HPXML_ID', heating_system.id) # Used by reporting measure
htg_coil.additionalProperties.setFeature('FractionHeatLoadServed', heating_system.fraction_heat_load_served) # Used by reporting measure
if has_deadband_control
Expand Down Expand Up @@ -4679,6 +4677,113 @@ def self.apply_installation_quality_ems_program(model, heating_system, cooling_s
)
end

# Creates an EMS program to add crankcase heater energy use for a heat pump.
# A crankcase heater ...
#
# @param model [OpenStudio::Model::Model] OpenStudio Model object
# @param htg_coil [TODO] TODO
# @param clg_coil [OpenStudio::Model::CoilCoolingDXSingleSpeed or OpenStudio::Model::CoilCoolingDXMultiSpeed] OpenStudio Cooling Coil object
# @param conditioned_space [OpenStudio::Model::Space] OpenStudio Space object for conditioned zone
# @param heat_pump [HPXML::HeatPump] The HPXML heat pump of interest
# @return [nil]
def self.apply_crankcase_heater_ems_program(model, htg_coil, clg_coil, conditioned_space, heat_pump)
return unless heat_pump.crankcase_heater_watts.to_f > 0

coil_name = clg_coil.name.to_s
if (heat_pump.is_a? HPXML::HeatPump) && (heat_pump.fraction_heat_load_served > 0)
coil_name = htg_coil.name.to_s
else
htg_coil = nil
end
Comment on lines +4729 to +4734
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By default, use the cooling coil name in the actuated object and program names. Unless it's a heat pump that provides heating -- then use the heating coil name.


# Other equipment/actuator
cnt = model.getOtherEquipments.count { |e| e.endUseSubcategory.start_with? Constants::ObjectTypeCrankcaseHeater } # Ensure unique meter for each heat pump
crankcase_heater_energy_oe = Model.add_other_equipment(
model,
name: "#{coil_name} crankcase heater energy",
end_use: "#{Constants::ObjectTypeCrankcaseHeater}#{cnt + 1}",
space: conditioned_space,
design_level: 0,
frac_radiant: 0,
frac_latent: 0,
frac_lost: 1,
schedule: model.alwaysOnDiscreteSchedule,
fuel_type: HPXML::FuelTypeElectricity
)
crankcase_heater_energy_oe.additionalProperties.setFeature('HPXML_ID', heat_pump.id) # Used by reporting measure
if heat_pump.is_a? HPXML::CoolingSystem
crankcase_heater_energy_oe.additionalProperties.setFeature('FractionHeatLoadServed', 0.0) # Used by reporting measure
elsif heat_pump.is_a? HPXML::HeatPump
crankcase_heater_energy_oe.additionalProperties.setFeature('FractionHeatLoadServed', heat_pump.fraction_heat_load_served) # Used by reporting measure
end
Comment on lines +4751 to +4755
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Used by the reporting measure for allocating crankcase either to heating or cooling end use.


crankcase_heater_energy_oe_act = Model.add_ems_actuator(
name: "#{crankcase_heater_energy_oe.name} act",
model_object: crankcase_heater_energy_oe,
comp_type_and_control: EPlus::EMSActuatorOtherEquipmentPower
)

# Sensors
tout_db_sensor = Model.add_ems_sensor(
model,
name: "#{clg_coil.name} tout s",
output_var_or_meter_name: 'Site Outdoor Air Drybulb Temperature',
key_name: 'Environment'
)

htg_avail_sensor = model.getEnergyManagementSystemSensors.find { |s| s.additionalProperties.getFeatureAsString('ObjectType').to_s == Constants::ObjectTypeHeatingAvailabilitySensor }
clg_avail_sensor = model.getEnergyManagementSystemSensors.find { |s| s.additionalProperties.getFeatureAsString('ObjectType').to_s == Constants::ObjectTypeCoolingAvailabilitySensor }

if not htg_coil.nil?
htg_coil_rtf_sensor = Model.add_ems_sensor(
model,
name: "#{htg_coil.name} rtf s",
output_var_or_meter_name: 'Heating Coil Runtime Fraction',
key_name: htg_coil.name
)
end

# EMS program
if clg_coil.is_a? OpenStudio::Model::CoilCoolingDXSingleSpeed
max_oat_crankcase = clg_coil.maximumOutdoorDryBulbTemperatureForCrankcaseHeaterOperation
elsif clg_coil.is_a? OpenStudio::Model::CoilCoolingDXMultiSpeed
max_oat_crankcase = clg_coil.maximumOutdoorDryBulbTemperatureforCrankcaseHeaterOperation
end
program = Model.add_ems_program(
model,
name: "#{coil_name} crankcase program"
)
program.addLine("Set T_out = #{tout_db_sensor.name}")
if not htg_coil_rtf_sensor.nil?
program.addLine("Set frac_htg = #{htg_coil_rtf_sensor.name}")
else
program.addLine('Set frac_htg = 0.0')
end
temp_criteria = "If (T_out < #{max_oat_crankcase})"
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Crankcase heater only operates when the compressor is not running -- how do we incorporate that here?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 minus the coil runtime fraction?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

e.g., crankcase_power * (1 - coil_rtf)

You don't incorporate it in the criteria, you incorporate it in the energy use calculation.

# Don't run crankcase heater during heating/cooling unavailable periods either
if heat_pump.is_a? HPXML::CoolingSystem
temp_criteria += " && (#{clg_avail_sensor.name} == 1)" if not clg_avail_sensor.nil?
elsif heat_pump.is_a? HPXML::HeatPump
if heat_pump.fraction_heat_load_served > 0
temp_criteria += " && (#{htg_avail_sensor.name} == 1)" if not htg_avail_sensor.nil?
else
temp_criteria += " && (#{clg_avail_sensor.name} == 1)" if not clg_avail_sensor.nil?
end
end
program.addLine(temp_criteria)
program.addLine(" Set #{crankcase_heater_energy_oe_act.name} = #{heat_pump.crankcase_heater_watts} * (1 - frac_htg)")
program.addLine('Else')
program.addLine(" Set #{crankcase_heater_energy_oe_act.name} = 0.0")
program.addLine('EndIf')

Model.add_ems_program_calling_manager(
model,
name: "#{program.name} calling manager",
calling_point: 'InsideHVACSystemIterationLoop',
ems_programs: [program]
)
end

# Creates an EMS program to add pan heater energy use for a heat pump.
# A pan heater ensures that water melted during the defrost cycle does not refreeze into ice and
# result in fan obstruction or coil damage.
Expand All @@ -4691,6 +4796,8 @@ def self.apply_installation_quality_ems_program(model, heating_system, cooling_s
# @param hp_min_temp [Double] Minimum heat pump compressor operating temperature for heating
# @return [nil]
def self.apply_pan_heater_ems_program(model, ems_program, htg_coil, conditioned_space, heat_pump, hp_min_temp)
return unless heat_pump.pan_heater_watts.to_f > 0

# Other equipment/actuator
cnt = model.getOtherEquipments.count { |e| e.endUseSubcategory.start_with? Constants::ObjectTypePanHeater } # Ensure unique meter for each heat pump
pan_heater_energy_oe = Model.add_other_equipment(
Expand Down Expand Up @@ -4732,6 +4839,7 @@ def self.apply_pan_heater_ems_program(model, ems_program, htg_coil, conditioned_
ems_program.addLine('Else')
ems_program.addLine(" Set #{pan_heater_energy_oe_act.name} = 0.0")
ems_program.addLine('EndIf')
return ems_program
end

# Create EMS program and Other equipment objects to account for delivered cooling load and supplemental heating energy during defrost.
Expand Down Expand Up @@ -5026,7 +5134,6 @@ def self.apply_unit_multiplier(hpxml_bldg, hpxml_header)
clg_sys.cooling_capacity *= unit_multiplier
clg_sys.cooling_design_airflow_cfm *= unit_multiplier
clg_ap.cooling_actual_airflow_cfm *= unit_multiplier
clg_sys.crankcase_heater_watts *= unit_multiplier unless clg_sys.crankcase_heater_watts.nil?
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Meters incoporate thermal zone multipliers.

clg_sys.integrated_heating_system_capacity *= unit_multiplier unless clg_sys.integrated_heating_system_capacity.nil?
clg_sys.integrated_heating_system_airflow_cfm *= unit_multiplier unless clg_sys.integrated_heating_system_airflow_cfm.nil?
clg_sys.cooling_detailed_performance_data.each do |dp|
Expand All @@ -5043,7 +5150,6 @@ def self.apply_unit_multiplier(hpxml_bldg, hpxml_header)
hp_ap.heating_actual_airflow_cfm *= unit_multiplier
hp_sys.heating_capacity_17F *= unit_multiplier unless hp_sys.heating_capacity_17F.nil?
hp_sys.backup_heating_capacity *= unit_multiplier unless hp_sys.backup_heating_capacity.nil?
hp_sys.crankcase_heater_watts *= unit_multiplier unless hp_sys.crankcase_heater_watts.nil?
hpxml_header.heat_pump_backup_heating_capacity_increment *= unit_multiplier unless hpxml_header.heat_pump_backup_heating_capacity_increment.nil?
hp_sys.heating_detailed_performance_data.each do |dp|
dp.capacity *= unit_multiplier unless dp.capacity.nil?
Expand Down
4 changes: 1 addition & 3 deletions HPXMLtoOpenStudio/resources/schedules.rb
Original file line number Diff line number Diff line change
Expand Up @@ -822,9 +822,7 @@ def self.unavailable_period_applies(runner, schedule_name, col_name)
end
if applies == 1
if not runner.nil?
if [SchedulesFile::Columns[:SpaceHeating].name, SchedulesFile::Columns[:SpaceCooling].name].include?(schedule_name)
runner.registerWarning('It is not possible to eliminate all HVAC energy use (e.g. crankcase/defrost energy) in EnergyPlus during an unavailable period.')
elsif schedule_name == SchedulesFile::Columns[:WaterHeater].name
if schedule_name == SchedulesFile::Columns[:WaterHeater].name
runner.registerWarning('It is not possible to eliminate all DHW energy use (e.g. water heater parasitics) in EnergyPlus during an unavailable period.')
end
end
Expand Down
Loading