Skip to content
Merged
2 changes: 2 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
## OpenStudio-HPXML v1.12.0

__New Features__
- Output updates:
- **Breaking change**: Replaces "UnitX" prefixes with Building IDs in whole SFA/MF building timeseries outputs.

__Bugfixes__
- Only issue a warning about adjusting inverted setpoints when heating setpoint is greater than cooling setpoint during overlapping heating/cooling seasons.
Expand Down
2 changes: 1 addition & 1 deletion HPXMLtoOpenStudio/measure.rb
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ def create_unit_model(hpxml, hpxml_bldg, runner, model, weather, schedules_file)
Geometry.apply_thermal_mass(model, spaces, hpxml_bldg, hpxml.header)
Geometry.set_zone_volumes(spaces, hpxml_bldg, hpxml.header)
Geometry.explode_surfaces(model, hpxml_bldg)
Geometry.apply_building_unit(model, hpxml, hpxml_bldg)
Geometry.apply_building_unit(model, hpxml)

# HVAC
airloop_map = HVAC.apply_hvac_systems(runner, model, weather, spaces, hpxml_bldg, hpxml.header, schedules_file, hvac_days)
Expand Down
8 changes: 4 additions & 4 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>73a71e63-8dc6-4568-a9a4-398efebfda0c</version_id>
<version_modified>2026-01-20T22:30:24Z</version_modified>
<version_id>977c1cef-1979-4589-8fc0-ca07c4a548f0</version_id>
<version_modified>2026-01-21T17:43:00Z</version_modified>
<xml_checksum>D8922A73</xml_checksum>
<class_name>HPXMLtoOpenStudio</class_name>
<display_name>HPXML to OpenStudio Translator</display_name>
Expand Down Expand Up @@ -192,7 +192,7 @@
<filename>measure.rb</filename>
<filetype>rb</filetype>
<usage_type>script</usage_type>
<checksum>4FAA1035</checksum>
<checksum>68C4EB21</checksum>
</file>
<file>
<filename>airflow.rb</filename>
Expand Down Expand Up @@ -372,7 +372,7 @@
<filename>geometry.rb</filename>
<filetype>rb</filetype>
<usage_type>resource</usage_type>
<checksum>5EA7867A</checksum>
<checksum>6229B9E7</checksum>
</file>
<file>
<filename>hotwater_appliances.rb</filename>
Expand Down
15 changes: 7 additions & 8 deletions HPXMLtoOpenStudio/resources/geometry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -861,13 +861,16 @@ def self.get_occupancy_default_num(nbeds)
# @param model [OpenStudio::Model::Model] OpenStudio Model object
# @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects
# @param location [String] The location of interest (HPXML::LocationXXX)
# @param zone_multiplier [Integer] the number of similar zones represented
# @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit
# @return [OpenStudio::Model::Space, nil] updated spaces hash if location is not already a key
def self.create_space_and_zone(model, spaces, location, zone_multiplier)
def self.create_space_and_zone(model, spaces, location, hpxml_bldg)
zone_multiplier = hpxml_bldg.building_construction.number_of_units
building_id = hpxml_bldg.building_id
if not spaces.keys.include? location
thermal_zone = OpenStudio::Model::ThermalZone.new(model)
thermal_zone.setName(location)
thermal_zone.additionalProperties.setFeature('ObjectType', location)
thermal_zone.additionalProperties.setFeature('BuildingID', building_id) # Used by reporting measure
thermal_zone.setMultiplier(zone_multiplier)

space = OpenStudio::Model::Space.new(model)
Expand Down Expand Up @@ -1749,7 +1752,7 @@ def self.calculate_subsurface_parent_buffer(length, width)
# @return [OpenStudio::Model::Space] the OpenStudio::Model::Space object corresponding to HPXML::LocationXXX
def self.create_or_get_space(model, spaces, location, hpxml_bldg)
if spaces[location].nil?
create_space_and_zone(model, spaces, location, hpxml_bldg.building_construction.number_of_units)
create_space_and_zone(model, spaces, location, hpxml_bldg)
end
return spaces[location]
end
Expand All @@ -1758,15 +1761,11 @@ def self.create_or_get_space(model, spaces, location, hpxml_bldg)
#
# @param model [OpenStudio::Model::Model] OpenStudio Model object
# @param hpxml [HPXML] HPXML object
# @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit
# @return [nil]
def self.apply_building_unit(model, hpxml, hpxml_bldg)
def self.apply_building_unit(model, hpxml)
return if hpxml.buildings.size == 1

unit_num = hpxml.buildings.index(hpxml_bldg) + 1

unit = OpenStudio::Model::BuildingUnit.new(model)
unit.additionalProperties.setFeature('unit_num', unit_num)
model.getSpaces.each do |s|
s.setBuildingUnit(unit)
end
Expand Down
104 changes: 63 additions & 41 deletions ReportSimulationOutput/measure.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1108,30 +1108,42 @@ def get_outputs(runner, args)
end

# Get zones of interest
zone_names = []
bldg_id_zone_name_map = {}
@model.getThermalZones.each do |zone|
next unless zone.floorArea > 1 # Skip e.g. plenum zone for duct model

zone_names << zone.name.to_s.upcase
zone_name = zone.name.to_s.upcase
bldg_id = nil
if @hpxml_bldgs.size > 1
bldg_id = zone.additionalProperties.getFeatureAsString('BuildingID').get
end
bldg_id_zone_name_map[bldg_id] = [] unless bldg_id_zone_name_map.keys.include?(bldg_id)
bldg_id_zone_name_map[bldg_id] << zone_name
end
zone_names.sort!
bldg_id_zone_name_map = bldg_id_zone_name_map.sort_by { |k, v| [k, v] }.to_h

# Returns a user-friendly version of the object name for output.
# Returns a user-friendly version of bldg_id + object_name for output.
# UNITX will be stripped from the object_name, if it exists.
#
# @param bldg_id [String or nil] The HPXML Building ID for the dwelling unit (if a whole SFA/MF building simulation)
# @param object_name [String] OpenStudio object name
# @return [String] Output name
def sanitize_name(object_name)
return object_name.gsub('_', ' ').split.map(&:capitalize).join(' ')
def sanitize_name(bldg_id, object_name)
sstrs = object_name.gsub('_', ' ').split
sstrs.delete(sstrs[0]) if sstrs[0].include?('UNIT')
return "#{bldg_id} #{sstrs.map(&:capitalize).join(' ')}".strip
end

# Zone temperatures
if args[:include_timeseries_zone_temperatures]

zone_names.each do |zone_name|
@zone_temps[zone_name] = ZoneTemp.new
@zone_temps[zone_name].name = "Temperature: #{sanitize_name(zone_name)}"
@zone_temps[zone_name].timeseries_units = 'F'
@zone_temps[zone_name].timeseries_output = get_report_variable_data_timeseries([zone_name], ['Zone Mean Air Temperature'], 9.0 / 5.0, 32.0, args[:timeseries_frequency])
bldg_id_zone_name_map.each do |bldg_id, zone_names|
zone_names.each do |zone_name|
@zone_temps[zone_name] = ZoneTemp.new
@zone_temps[zone_name].name = "Temperature: #{sanitize_name(bldg_id, zone_name)}"
@zone_temps[zone_name].timeseries_units = 'F'
@zone_temps[zone_name].timeseries_output = get_report_variable_data_timeseries([zone_name], ['Zone Mean Air Temperature'], 9.0 / 5.0, 32.0, args[:timeseries_frequency])
end
end

# Scheduled temperatures
Expand All @@ -1143,7 +1155,7 @@ def sanitize_name(object_name)

sch_name = schedule.name.to_s.upcase
@zone_temps[sch_name] = ZoneTemp.new
@zone_temps[sch_name].name = "Temperature: #{sanitize_name(sch_name)}"
@zone_temps[sch_name].name = "Temperature: #{sanitize_name(nil, sch_name)}"
@zone_temps[sch_name].timeseries_units = 'F'
@zone_temps[sch_name].timeseries_output = get_report_variable_data_timeseries([sch_name], ['Schedule Value'], 9.0 / 5.0, 32.0, args[:timeseries_frequency])

Expand All @@ -1156,8 +1168,8 @@ def sanitize_name(object_name)
heated_zones.each do |heated_zone|
var_name = 'Temperature: Heating Setpoint'
if @hpxml_header.whole_sfa_or_mf_building_sim
unit_num = @model.getThermalZones.find { |z| z.name.to_s == heated_zone }.spaces[0].buildingUnit.get.additionalProperties.getFeatureAsInteger('unit_num').get
var_name = "Temperature: Unit#{unit_num} Heating Setpoint"
building_id = @model.getThermalZones.find { |z| z.name.to_s == heated_zone }.additionalProperties.getFeatureAsString('BuildingID').get
var_name = "Temperature: #{building_id} Heating Setpoint"
end
@zone_temps["#{heated_zone} Heating Setpoint"] = ZoneTemp.new
@zone_temps["#{heated_zone} Heating Setpoint"].name = var_name
Expand All @@ -1170,8 +1182,8 @@ def sanitize_name(object_name)
cooled_zones.each do |cooled_zone|
var_name = 'Temperature: Cooling Setpoint'
if @hpxml_header.whole_sfa_or_mf_building_sim
unit_num = @model.getThermalZones.find { |z| z.name.to_s == cooled_zone }.spaces[0].buildingUnit.get.additionalProperties.getFeatureAsInteger('unit_num').get
var_name = "Temperature: Unit#{unit_num} Cooling Setpoint"
building_id = @model.getThermalZones.find { |z| z.name.to_s == cooled_zone }.additionalProperties.getFeatureAsString('BuildingID').get
var_name = "Temperature: #{building_id} Cooling Setpoint"
end
@zone_temps["#{cooled_zone} Cooling Setpoint"] = ZoneTemp.new
@zone_temps["#{cooled_zone} Cooling Setpoint"].name = var_name
Expand All @@ -1184,43 +1196,53 @@ def sanitize_name(object_name)
if args[:include_timeseries_zone_conditions]

# Zone humidity ratios
zone_names.each do |zone_name|
@zone_conds["#{zone_name} Humidity Ratio"] = ZoneCond.new
@zone_conds["#{zone_name} Humidity Ratio"].name = "Humidity Ratio: #{sanitize_name(zone_name)}"
@zone_conds["#{zone_name} Humidity Ratio"].timeseries_units = 'fraction'
@zone_conds["#{zone_name} Humidity Ratio"].timeseries_output = get_report_variable_data_timeseries([zone_name], ['Zone Air Humidity Ratio'], 1, 0, args[:timeseries_frequency])
bldg_id_zone_name_map.each do |bldg_id, zone_names|
zone_names.each do |zone_name|
@zone_conds["#{zone_name} Humidity Ratio"] = ZoneCond.new
@zone_conds["#{zone_name} Humidity Ratio"].name = "Humidity Ratio: #{sanitize_name(bldg_id, zone_name)}"
@zone_conds["#{zone_name} Humidity Ratio"].timeseries_units = 'fraction'
@zone_conds["#{zone_name} Humidity Ratio"].timeseries_output = get_report_variable_data_timeseries([zone_name], ['Zone Air Humidity Ratio'], 1, 0, args[:timeseries_frequency])
end
end

# Zone relative humidities
zone_names.each do |zone_name|
@zone_conds["#{zone_name} Relative Humidity"] = ZoneCond.new
@zone_conds["#{zone_name} Relative Humidity"].name = "Relative Humidity: #{sanitize_name(zone_name)}"
@zone_conds["#{zone_name} Relative Humidity"].timeseries_units = '%'
@zone_conds["#{zone_name} Relative Humidity"].timeseries_output = get_report_variable_data_timeseries([zone_name], ['Zone Air Relative Humidity'], 1, 0, args[:timeseries_frequency])
bldg_id_zone_name_map.each do |bldg_id, zone_names|
zone_names.each do |zone_name|
@zone_conds["#{zone_name} Relative Humidity"] = ZoneCond.new
@zone_conds["#{zone_name} Relative Humidity"].name = "Relative Humidity: #{sanitize_name(bldg_id, zone_name)}"
@zone_conds["#{zone_name} Relative Humidity"].timeseries_units = '%'
@zone_conds["#{zone_name} Relative Humidity"].timeseries_output = get_report_variable_data_timeseries([zone_name], ['Zone Air Relative Humidity'], 1, 0, args[:timeseries_frequency])
end
end

# Zone dewpoint temperatures
zone_names.each do |zone_name|
@zone_conds["#{zone_name} Dewpoint Temperature"] = ZoneCond.new
@zone_conds["#{zone_name} Dewpoint Temperature"].name = "Dewpoint Temperature: #{sanitize_name(zone_name)}"
@zone_conds["#{zone_name} Dewpoint Temperature"].timeseries_units = 'F'
@zone_conds["#{zone_name} Dewpoint Temperature"].timeseries_output = get_report_variable_data_timeseries([zone_name], ['Zone Mean Air Dewpoint Temperature'], 9.0 / 5.0, 32.0, args[:timeseries_frequency])
bldg_id_zone_name_map.each do |bldg_id, zone_names|
zone_names.each do |zone_name|
@zone_conds["#{zone_name} Dewpoint Temperature"] = ZoneCond.new
@zone_conds["#{zone_name} Dewpoint Temperature"].name = "Dewpoint Temperature: #{sanitize_name(bldg_id, zone_name)}"
@zone_conds["#{zone_name} Dewpoint Temperature"].timeseries_units = 'F'
@zone_conds["#{zone_name} Dewpoint Temperature"].timeseries_output = get_report_variable_data_timeseries([zone_name], ['Zone Mean Air Dewpoint Temperature'], 9.0 / 5.0, 32.0, args[:timeseries_frequency])
end
end

# Zone mean radiant temperatures
zone_names.each do |zone_name|
@zone_conds["#{zone_name} Radiant Temperature"] = ZoneCond.new
@zone_conds["#{zone_name} Radiant Temperature"].name = "Radiant Temperature: #{sanitize_name(zone_name)}"
@zone_conds["#{zone_name} Radiant Temperature"].timeseries_units = 'F'
@zone_conds["#{zone_name} Radiant Temperature"].timeseries_output = get_report_variable_data_timeseries([zone_name], ['Zone Mean Radiant Temperature'], 9.0 / 5.0, 32.0, args[:timeseries_frequency])
bldg_id_zone_name_map.each do |bldg_id, zone_names|
zone_names.each do |zone_name|
@zone_conds["#{zone_name} Radiant Temperature"] = ZoneCond.new
@zone_conds["#{zone_name} Radiant Temperature"].name = "Radiant Temperature: #{sanitize_name(bldg_id, zone_name)}"
@zone_conds["#{zone_name} Radiant Temperature"].timeseries_units = 'F'
@zone_conds["#{zone_name} Radiant Temperature"].timeseries_output = get_report_variable_data_timeseries([zone_name], ['Zone Mean Radiant Temperature'], 9.0 / 5.0, 32.0, args[:timeseries_frequency])
end
end

# Zone operative temperatures
zone_names.each do |zone_name|
@zone_conds["#{zone_name} Operative Temperature"] = ZoneCond.new
@zone_conds["#{zone_name} Operative Temperature"].name = "Operative Temperature: #{sanitize_name(zone_name)}"
@zone_conds["#{zone_name} Operative Temperature"].timeseries_units = 'F'
@zone_conds["#{zone_name} Operative Temperature"].timeseries_output = get_report_variable_data_timeseries([zone_name], ['Zone Operative Temperature'], 9.0 / 5.0, 32.0, args[:timeseries_frequency])
bldg_id_zone_name_map.each do |bldg_id, zone_names|
zone_names.each do |zone_name|
@zone_conds["#{zone_name} Operative Temperature"] = ZoneCond.new
@zone_conds["#{zone_name} Operative Temperature"].name = "Operative Temperature: #{sanitize_name(bldg_id, zone_name)}"
@zone_conds["#{zone_name} Operative Temperature"].timeseries_units = 'F'
@zone_conds["#{zone_name} Operative Temperature"].timeseries_output = get_report_variable_data_timeseries([zone_name], ['Zone Operative Temperature'], 9.0 / 5.0, 32.0, args[:timeseries_frequency])
end
end
end

Expand Down
8 changes: 4 additions & 4 deletions ReportSimulationOutput/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>report_simulation_output</name>
<uid>df9d170c-c21a-4130-866d-0d46b06073fd</uid>
<version_id>f11d5b77-4320-47e3-b4cf-60f70702476f</version_id>
<version_modified>2025-12-11T00:16:13Z</version_modified>
<version_id>e7883514-bf4b-41be-a7ee-bd601f8ec28c</version_id>
<version_modified>2026-01-21T20:56:09Z</version_modified>
<xml_checksum>9BF1E6AC</xml_checksum>
<class_name>ReportSimulationOutput</class_name>
<display_name>HPXML Simulation Output Report</display_name>
Expand Down Expand Up @@ -1991,13 +1991,13 @@
<filename>measure.rb</filename>
<filetype>rb</filetype>
<usage_type>script</usage_type>
<checksum>1A026522</checksum>
<checksum>8F9212E4</checksum>
</file>
<file>
<filename>test_report_sim_output.rb</filename>
<filetype>rb</filetype>
<usage_type>test</usage_type>
<checksum>C7F737E4</checksum>
<checksum>D953D5A4</checksum>
</file>
</files>
</measure>
11 changes: 6 additions & 5 deletions ReportSimulationOutput/tests/test_report_sim_output.rb
Original file line number Diff line number Diff line change
Expand Up @@ -990,14 +990,15 @@ def test_timeseries_hourly_zone_temperatures_whole_mf_building
actual_timeseries_cols = File.readlines(timeseries_csv)[0].strip.split(',')
expected_timeseries_cols = ['Time']
for i in 1..6
expected_timeseries_cols << "Temperature: Unit#{i} Conditioned Space"
i == 1 ? building_id = 'MyBuilding' : building_id = "MyBuilding_#{i}"
expected_timeseries_cols << "Temperature: #{building_id} Conditioned Space"
if i <= 2
expected_timeseries_cols << "Temperature: Unit#{i} Basement Unconditioned"
expected_timeseries_cols << "Temperature: #{building_id} Basement Unconditioned"
elsif i >= 5
expected_timeseries_cols << "Temperature: Unit#{i} Attic Vented"
expected_timeseries_cols << "Temperature: #{building_id} Attic Vented"
end
expected_timeseries_cols << "Temperature: Unit#{i} Heating Setpoint"
expected_timeseries_cols << "Temperature: Unit#{i} Cooling Setpoint"
expected_timeseries_cols << "Temperature: #{building_id} Heating Setpoint"
expected_timeseries_cols << "Temperature: #{building_id} Cooling Setpoint"
end
assert_equal(expected_timeseries_cols.sort, actual_timeseries_cols.sort)
timeseries_rows = CSV.read(timeseries_csv)
Expand Down
4 changes: 2 additions & 2 deletions ReportUtilityBills/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>report_utility_bills</name>
<uid>ca88a425-e59a-4bc4-af51-c7e7d1e960fe</uid>
<version_id>b0f44e4b-fc76-4a7b-b41b-ff2d6e693009</version_id>
<version_modified>2025-12-30T21:33:00Z</version_modified>
<version_id>04e940f7-5852-4ead-8d84-8387da3fe56b</version_id>
<version_modified>2026-01-21T20:34:49Z</version_modified>
<xml_checksum>15BF4E57</xml_checksum>
<class_name>ReportUtilityBills</class_name>
<display_name>Utility Bills Report</display_name>
Expand Down
2 changes: 1 addition & 1 deletion docs/source/workflow_outputs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -656,7 +656,7 @@ Depending on the outputs requested, the file may include:
Component Loads ``componentloads`` Heating and cooling loads (in kBtu) disaggregated by component (e.g., Walls, Windows, Infiltration, Ducts, etc.).
Unmet Hours ``unmethours`` Heating, cooling, and EV driving unmet hours.
Zone Temperatures ``temperatures`` Zone temperatures (in deg-F) for each space (e.g., conditioned space, attic, garage, basement, crawlspace, etc.) plus heating/cooling setpoints.
Zone Conditions ``conditions`` Zone conditions (humidity ratio and relative humidity and dewpoint, radiant, and operative temperatures)
Zone Conditions ``conditions`` Zone conditions (humidity ratio and relative humidity and dewpoint, radiant, and operative temperatures).
Airflows ``airflows`` Airflow rates (in cfm) for infiltration, mechanical ventilation (including clothes dryer exhaust), natural ventilation, whole house fans.
Weather ``weather`` Weather file data including outdoor temperatures, relative humidity, wind speed, and solar.
Resilience ``resilience`` Resilience outputs (currently only average resilience hours for battery storage).
Expand Down