Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
189a8ba
Add the check for whole building with vehicles.
joseph-robertson Dec 3, 2025
859aca5
Add validation test for whole building with vehicles.
joseph-robertson Dec 3, 2025
3ac83ca
Fix for handling schedule file with no electric_vehicle column.
joseph-robertson Dec 3, 2025
0b00cf3
Add sample file for whole building with vehicles.
joseph-robertson Dec 4, 2025
246d66d
Move vehicle ems from vehicle resource file to output resource file.
joseph-robertson Dec 4, 2025
9cc77fd
Remove obsolete validation test and update docs.
joseph-robertson Dec 4, 2025
ee3a631
Add actuators to unit models before merge.
joseph-robertson Dec 4, 2025
f6af3d5
Return from vehicle ems program method if no vehicles.
joseph-robertson Dec 4, 2025
14adadf
Latest results.
Dec 5, 2025
87338ad
Merge branch 'master' into vehicles-whole-building2
joseph-robertson Dec 5, 2025
f6f8264
Remove fixme comment, and refactor a bit.
joseph-robertson Dec 5, 2025
5777c07
Only write vehicle EMS program if at least one vehicle and charger, a…
joseph-robertson Dec 5, 2025
0c206e3
Latest results.
Dec 5, 2025
a9e8feb
Merge branch 'master' into vehicles-whole-building2
joseph-robertson Dec 5, 2025
905b77f
Latest results.
Dec 6, 2025
4c4801b
Merge branch 'master' of https://github.com/NREL/OpenStudio-HPXML int…
shorowit Dec 9, 2025
81316cf
Latest results.
Dec 9, 2025
449bfd9
Minor fix and cleanup. [ci skip]
shorowit Dec 9, 2025
cfd581e
Merge branch 'vehicles-whole-building2' of https://github.com/NREL/Op…
shorowit Dec 9, 2025
4b13e23
Separate EMS programs for charging/discharging power and calculating …
joseph-robertson Dec 9, 2025
699c46c
Update reporting measure to retrieve unmet driving hours program.
joseph-robertson Dec 9, 2025
842c2b5
Update the changelog.
joseph-robertson Dec 9, 2025
0c95008
Fix and update some method descriptions.
joseph-robertson Dec 9, 2025
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
2 changes: 2 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ __New Features__
- For storm windows, removes minimum base window U-factor limit and throws a warning instead if the base window U-factor is below 0.3.
- Whole SFA/MF buildings:
- Allows modeling inter-unit heat transfer using the `@sameas` attribute.
- Allows modeling detailed electric vehicles.
- Documents a workaround for modeling common spaces (conditioned or unconditioned).
- See the [documentation](https://openstudio-hpxml.readthedocs.io/en/latest/workflow_inputs.html#whole-sfa-mf-buildings) for more information.

Expand All @@ -35,6 +36,7 @@ __Bugfixes__
- Fixes requested EnergyPlus timeseries output variables/meters not displayed in DView if they don't have units.
- Fixes possible errors when small water flow rates for variable-speed experimental ground-source heat pump model.
- Fixes possible ground-source heat pump sizing error if the heating or cooling design temperature differences are zero.
- Fixes EMS discharge power program and assignment of default discharging schedule for detailed electric vehicles.

## OpenStudio-HPXML v1.10.0

Expand Down
10 changes: 5 additions & 5 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>3a95928b-a02e-48b4-aefa-bac6bce64107</version_id>
<version_modified>2025-12-08T19:49:33Z</version_modified>
<version_id>fc398e25-7dc2-455b-922a-037474c951a6</version_id>
<version_modified>2025-12-09T21:14:52Z</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>DB20CDBC</checksum>
<checksum>0DE5B248</checksum>
</file>
<file>
<filename>constructions.rb</filename>
Expand Down Expand Up @@ -480,7 +480,7 @@
<filename>output.rb</filename>
<filetype>rb</filetype>
<usage_type>resource</usage_type>
<checksum>8A1049F9</checksum>
<checksum>02203D44</checksum>
</file>
<file>
<filename>psychrometrics.rb</filename>
Expand Down Expand Up @@ -684,7 +684,7 @@
<filename>vehicle.rb</filename>
<filetype>rb</filetype>
<usage_type>resource</usage_type>
<checksum>2F0FD377</checksum>
<checksum>1497784B</checksum>
</file>
<file>
<filename>version.rb</filename>
Expand Down
3 changes: 2 additions & 1 deletion HPXMLtoOpenStudio/resources/constants.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ module Constants

# Object types
ObjectTypeAirSourceHeatPump = 'air source heat pump'
ObjectTypeBEVDischargeProgram = 'bev discharge program'
ObjectTypeBattery = 'battery'
ObjectTypeBatteryLossesAdjustment = 'battery losses adjustment'
ObjectTypeBoiler = 'boiler'
Expand Down Expand Up @@ -84,6 +83,8 @@ module Constants
ObjectTypeUnitHeater = 'unit heater'
ObjectTypeUnmetHoursProgram = 'unmet hours program'
ObjectTypeVehicle = 'vehicle'
ObjectTypeVehicleDischargeScheduleSensor = 'vehicle discharge schedule sensor'
ObjectTypeVehicleUnmetHoursProgram = 'vehicle unmet hours program'
ObjectTypeWaterHeater = 'water heater'
ObjectTypeWaterHeaterSetpoint = 'water heater setpoint'
ObjectTypeWaterHeaterAdjustment = 'water heater energy adjustment'
Expand Down
62 changes: 62 additions & 0 deletions HPXMLtoOpenStudio/resources/output.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ module Outputs
# @return [nil]
def self.apply_ems_programs(model, hpxml_osm_map, hpxml_header, add_component_loads)
season_day_nums = apply_unmet_hours_ems_program(model, hpxml_osm_map, hpxml_header)
apply_unmet_driving_hours_ems_program(model, hpxml_osm_map)
loads_data = apply_total_loads_ems_program(model, hpxml_osm_map, hpxml_header)
if add_component_loads
apply_component_loads_ems_program(model, hpxml_osm_map, loads_data, season_day_nums)
Expand Down Expand Up @@ -172,6 +173,67 @@ def self.apply_unmet_hours_ems_program(model, hpxml_osm_map, hpxml_header)
return season_day_nums
end

# Creates an EMS program that calculates driving unmet hours (number
# of hours where the EV driving demand is not met).
#
# @param model [OpenStudio::Model::Model] OpenStudio Model object
# @param hpxml_osm_map [Hash] Map of HPXML::Building objects => OpenStudio Model objects for each dwelling unit
# @return [nil]
def self.apply_unmet_driving_hours_ems_program(model, hpxml_osm_map)
return if hpxml_osm_map.keys.map { |hpxml_bldg| hpxml_bldg.vehicles.map { |vehicle| vehicle.vehicle_type == HPXML::VehicleTypeBEV && !vehicle.ev_charger_idref.nil? }.size }.sum == 0

# EMS program
driving_hrs = 'unmet_driving_hours'
unit_driving_hrs = 'unit_driving_unmet_hours'
program = Model.add_ems_program(
model,
name: 'unmet driving hours program'
)
program.additionalProperties.setFeature('ObjectType', Constants::ObjectTypeVehicleUnmetHoursProgram)
program.addLine("Set #{driving_hrs} = 0")

hpxml_osm_map.each do |hpxml_bldg, unit_model|
vehicle = hpxml_bldg.vehicles.find { |vehicle| vehicle.vehicle_type == HPXML::VehicleTypeBEV && !vehicle.ev_charger_idref.nil? }
next if vehicle.nil?

ev_elcd = unit_model.getElectricLoadCenterDistributions.find { |elcd| elcd.name.to_s.include?(vehicle.id) }
discharge_sch_sensor = unit_model.getEnergyManagementSystemSensors.find { |s| s.additionalProperties.getFeatureAsString('ObjectType').to_s == Constants::ObjectTypeVehicleDischargeScheduleSensor }

unit_model.getElectricLoadCenterStorageLiIonNMCBatterys.each do |elcs|
next unless elcs.name.to_s.include? vehicle.id

min_soc = ev_elcd.minimumStorageStateofChargeFraction

soc_sensor = Model.add_ems_sensor(
model,
name: "#{elcs.name} soc_sensor",
output_var_or_meter_name: 'Electric Storage Charge Fraction',
key_name: elcs.name.to_s
)

program.addLine(" If #{discharge_sch_sensor.name} > 0.0")
program.addLine(" If #{soc_sensor.name} <= #{min_soc}")
program.addLine(" Set #{unit_driving_hrs} = #{discharge_sch_sensor.name}")
program.addLine(' Else')
program.addLine(" Set #{unit_driving_hrs} = 0")
program.addLine(' EndIf')
program.addLine(' Else')
program.addLine(" Set #{unit_driving_hrs} = 0")
program.addLine(' EndIf')
program.addLine(" If #{unit_driving_hrs} > #{driving_hrs}") # Use max hourly value across all units
program.addLine(" Set #{driving_hrs} = #{unit_driving_hrs}")
program.addLine(' EndIf')
end
end

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

# Creates an EMS program that calculates total heating and cooling loads delivered
# by the HVAC system(s).
#
Expand Down
161 changes: 72 additions & 89 deletions HPXMLtoOpenStudio/resources/vehicle.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ def self.apply(runner, model, spaces, hpxml_bldg, hpxml_header, schedules_file)
end

# Apply an electric vehicle to the model using the battery.rb Battery class, which assigns OpenStudio ElectricLoadCenterStorageLiIonNMCBattery and ElectricLoadCenterDistribution objects.
# An EMS program models the effect of ambient temperature on the effective power output, scales power with the fraction charged at home, and calculates the unmet driving hours.
# Bi-directional charging is not currently implemented
# An EMS program models the effect of ambient temperature on the effective power output and scales power with the fraction charged at home.
# Bi-directional charging is not currently implemented.
#
# @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings
# @param model [OpenStudio::Model::Model] OpenStudio Model object
Expand Down Expand Up @@ -67,7 +67,9 @@ def self.apply_electric_vehicle(runner, model, spaces, hpxml_bldg, hpxml_header,
if not schedules_file.nil?
charging_schedule = schedules_file.create_schedule_file(model, col_name: charging_col_name)
discharging_schedule = schedules_file.create_schedule_file(model, col_name: discharging_col_name)
eff_discharge_power = schedules_file.calc_design_level_from_daily_kwh(col_name: discharging_schedule.name.to_s, daily_kwh: ev_annl_energy / 365)
if not discharging_schedule.nil?
eff_discharge_power = schedules_file.calc_design_level_from_daily_kwh(col_name: discharging_schedule.name.to_s, daily_kwh: ev_annl_energy / 365)
end
end
if charging_schedule.nil? && discharging_schedule.nil?
charging_unavailable_periods = Schedule.get_unavailable_periods(runner, charging_col_name, hpxml_header.unavailable_periods)
Expand All @@ -89,106 +91,87 @@ def self.apply_electric_vehicle(runner, model, spaces, hpxml_bldg, hpxml_header,
# Scale the effective discharge power by 2.25 to assign the rated discharge power.
# This value reflects the maximum power adjustment allowed in the EMS EV discharge program at -17.8 C.
vehicle.additional_properties.rated_power_output = eff_discharge_power * 2.25
vehicle.additional_properties.eff_discharge_power = eff_discharge_power

# Apply vehicle battery to model
Battery.apply_battery(runner, model, spaces, hpxml_bldg, vehicle, charging_schedule, discharging_schedule)

temp_sensor = Model.add_ems_sensor(
model,
name: 'site_temp',
output_var_or_meter_name: 'Site Outdoor Air Drybulb Temperature',
key_name: 'Environment'
)

# Power adjustment vs ambient temperature curve; derived from most recent data in Figure 9 of https://www.nrel.gov/docs/fy23osti/83916.pdf
# This adjustment scales power demand based on ambient temperature, and encompasses losses due to battery and space conditioning (i.e., discharging losses), as well as charging losses.
coefs = [1.412768, -3.910397E-02, 9.408235E-04, 8.971560E-06, -7.699244E-07, 1.265614E-08]
power_curve = ''
coefs.each_with_index do |coef, i|
power_curve += "+(#{coef}*(site_temp_adj^#{i}))"
end
power_curve = power_curve[1..]

# Apply EMS program to adjust discharge power based on ambient temperature.
ev_discharge_program = Model.add_ems_program(
model,
name: 'ev_discharge_program'
)
ev_discharge_program.addLine(" Set site_temp_adj = #{temp_sensor.name}")
ev_discharge_program.addLine(" If #{temp_sensor.name} < #{UnitConversions.convert(0, 'F', 'C').round(3)}")
ev_discharge_program.addLine(" Set site_temp_adj = #{UnitConversions.convert(0, 'F', 'C').round(3)}")
ev_discharge_program.addLine(" ElseIf #{temp_sensor.name} > #{UnitConversions.convert(100, 'F', 'C').round(3)}")
ev_discharge_program.addLine(" Set site_temp_adj = #{UnitConversions.convert(100, 'F', 'C').round(3)}")
ev_discharge_program.addLine(' EndIf')
ev_discharge_program.addLine(" Set power_mult = #{power_curve}")

ev_elcd = model.getElectricLoadCenterDistributions.find { |elcd| elcd.name.to_s.include?(vehicle.id) }

eff_charge_power = ev_elcd.designStorageControlChargePower
discharging_schedule = ev_elcd.storageDischargePowerFractionSchedule.get
charging_schedule = ev_elcd.storageChargePowerFractionSchedule.get

discharge_sch_sensor = Model.add_ems_sensor(
model,
name: "#{discharging_schedule.name} discharge_sch_sensor",
output_var_or_meter_name: 'Schedule Value',
key_name: discharging_schedule.name.to_s
)
discharge_sch_sensor.additionalProperties.setFeature('ObjectType', Constants::ObjectTypeVehicleDischargeScheduleSensor)
charge_sch_sensor = Model.add_ems_sensor(
model,
name: "#{charging_schedule.name} charge_sch_sensor",
output_var_or_meter_name: 'Schedule Value',
key_name: charging_schedule.name.to_s
)
discharge_power_act = Model.add_ems_actuator(
name: 'battery_discharge_power_act',
model_object: ev_elcd,
comp_type_and_control: ['Electrical Storage', 'Power Draw Rate']
)
charge_power_act = Model.add_ems_actuator(
name: 'battery_charge_power_act',
model_object: ev_elcd,
comp_type_and_control: ['Electrical Storage', 'Power Charge Rate']
)

model.getElectricLoadCenterStorageLiIonNMCBatterys.each do |elcs|
next unless elcs.name.to_s.include? vehicle.id

ev_elcd = model.getElectricLoadCenterDistributions.find { |elcd| elcd.name.to_s.include?(vehicle.id) }
eff_charge_power = ev_elcd.designStorageControlChargePower
min_soc = ev_elcd.minimumStorageStateofChargeFraction
discharging_schedule = ev_elcd.storageDischargePowerFractionSchedule.get
charging_schedule = ev_elcd.storageChargePowerFractionSchedule.get

discharge_power_act = Model.add_ems_actuator(
name: 'battery_discharge_power_act',
model_object: ev_elcd,
comp_type_and_control: ['Electrical Storage', 'Power Draw Rate']
)
charge_power_act = Model.add_ems_actuator(
name: 'battery_charge_power_act',
model_object: ev_elcd,
comp_type_and_control: ['Electrical Storage', 'Power Charge Rate']
)
temp_sensor = Model.add_ems_sensor(
model,
name: 'site_temp',
output_var_or_meter_name: 'Site Outdoor Air Drybulb Temperature',
key_name: 'Environment'
)
discharge_sch_sensor = Model.add_ems_sensor(
model,
name: 'discharge_sch_sensor',
output_var_or_meter_name: 'Schedule Value',
key_name: discharging_schedule.name.to_s
)
charge_sch_sensor = Model.add_ems_sensor(
model,
name: 'charge_sch_sensor',
output_var_or_meter_name: 'Schedule Value',
key_name: charging_schedule.name.to_s
)
soc_sensor = Model.add_ems_sensor(
model,
name: 'soc_sensor',
output_var_or_meter_name: 'Electric Storage Charge Fraction',
key_name: elcs.name.to_s
)

ev_discharge_program = Model.add_ems_program(
model,
name: 'ev_discharge_program'
)
ev_discharge_program.additionalProperties.setFeature('ObjectType', Constants::ObjectTypeBEVDischargeProgram)

unmet_hr_var = Model.add_ems_output_variable(
model,
name: 'unmet_driving_hours',
ems_variable_name: 'unmet_driving_hours',
type_of_data: 'Summed',
update_frequency: 'SystemTimestep',
ems_program_or_subroutine: ev_discharge_program,
units: 'hr'
)

# Power adjustment vs ambient temperature curve; derived from most recent data in Figure 9 of https://www.nrel.gov/docs/fy23osti/83916.pdf
# This adjustment scales power demand based on ambient temperature, and encompasses losses due to battery and space conditioning (i.e., discharging losses), as well as charging losses.
coefs = [1.412768, -3.910397E-02, 9.408235E-04, 8.971560E-06, -7.699244E-07, 1.265614E-08]
power_curve = ''
coefs.each_with_index do |coef, i|
power_curve += "+(#{coef}*(site_temp_adj^#{i}))"
end
power_curve = power_curve[1..]
ev_discharge_program.addLine(" Set power_mult = #{power_curve}")
ev_discharge_program.addLine(" Set site_temp_adj = #{temp_sensor.name}")
ev_discharge_program.addLine(" If #{temp_sensor.name} < #{UnitConversions.convert(0, 'F', 'C').round(3)}")
ev_discharge_program.addLine(" Set site_temp_adj = #{UnitConversions.convert(0, 'F', 'C').round(3)}")
ev_discharge_program.addLine(" ElseIf #{temp_sensor.name} > #{UnitConversions.convert(100, 'F', 'C').round(3)}")
ev_discharge_program.addLine(" Set site_temp_adj = #{UnitConversions.convert(100, 'F', 'C').round(3)}")
ev_discharge_program.addLine(' EndIf')
ev_discharge_program.addLine(" If #{discharge_sch_sensor.name} > 0.0")
ev_discharge_program.addLine(" Set #{discharge_power_act.name} = #{eff_discharge_power} * #{vehicle.fraction_charged_home} * power_mult * #{discharge_sch_sensor.name}")
ev_discharge_program.addLine(" Set #{discharge_power_act.name} = #{vehicle.additional_properties.eff_discharge_power} * #{vehicle.fraction_charged_home} * power_mult * #{discharge_sch_sensor.name}")
ev_discharge_program.addLine(" Set #{charge_power_act.name} = #{eff_charge_power} * #{charge_sch_sensor.name}")
ev_discharge_program.addLine(" If #{soc_sensor.name} <= #{min_soc}")
ev_discharge_program.addLine(" Set #{unmet_hr_var.name} = #{discharge_sch_sensor.name}")
ev_discharge_program.addLine(' Else')
ev_discharge_program.addLine(" Set #{unmet_hr_var.name} = 0")
ev_discharge_program.addLine(' EndIf')
ev_discharge_program.addLine(' Else')
ev_discharge_program.addLine(" Set #{charge_power_act.name} = #{eff_charge_power} * #{charge_sch_sensor.name}")
ev_discharge_program.addLine(" Set #{discharge_power_act.name} = 0")
ev_discharge_program.addLine(" Set #{unmet_hr_var.name} = 0")
ev_discharge_program.addLine(' EndIf')

Model.add_ems_program_calling_manager(
model,
name: 'ev_discharge_pcm',
calling_point: 'BeginTimestepBeforePredictor',
ems_programs: [ev_discharge_program]
)
end

Model.add_ems_program_calling_manager(
model,
name: 'ev_discharge_pcm',
calling_point: 'BeginTimestepBeforePredictor',
ems_programs: [ev_discharge_program]
)
end
end
2 changes: 1 addition & 1 deletion ReportSimulationOutput/measure.rb
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ def modelOutputRequests(model, runner, user_arguments)
total_loads_program = model.getEnergyManagementSystemPrograms.find { |p| p.additionalProperties.getFeatureAsString('ObjectType').to_s == Constants::ObjectTypeTotalLoadsProgram }
comp_loads_program = model.getEnergyManagementSystemPrograms.find { |p| p.additionalProperties.getFeatureAsString('ObjectType').to_s == Constants::ObjectTypeComponentLoadsProgram }
total_airflows_program = model.getEnergyManagementSystemPrograms.find { |p| p.additionalProperties.getFeatureAsString('ObjectType').to_s == Constants::ObjectTypeTotalAirflowsProgram }
unmet_driving_hrs_program = model.getEnergyManagementSystemPrograms.find { |p| p.additionalProperties.getFeatureAsString('ObjectType').to_s == Constants::ObjectTypeBEVDischargeProgram }
unmet_driving_hrs_program = model.getEnergyManagementSystemPrograms.find { |p| p.additionalProperties.getFeatureAsString('ObjectType').to_s == Constants::ObjectTypeVehicleUnmetHoursProgram }
heated_zones = eval(model.getBuilding.additionalProperties.getFeatureAsString('heated_zones').get)
cooled_zones = eval(model.getBuilding.additionalProperties.getFeatureAsString('cooled_zones').get)

Expand Down
6 changes: 3 additions & 3 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>a47e5d33-f72f-463f-9f61-c2da806a5b25</version_id>
<version_modified>2025-09-15T15:40:07Z</version_modified>
<version_id>6184533b-43b4-41b5-8a04-a638297bad6c</version_id>
<version_modified>2025-12-09T20:57:26Z</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,7 +1991,7 @@
<filename>measure.rb</filename>
<filetype>rb</filetype>
<usage_type>script</usage_type>
<checksum>41343519</checksum>
<checksum>1A026522</checksum>
</file>
<file>
<filename>test_report_sim_output.rb</filename>
Expand Down
2 changes: 1 addition & 1 deletion docs/source/workflow_inputs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,7 @@ For these simulations:
Notes/caveats about this approach:

- Some inputs (e.g., EPW location or ground conductivity) cannot vary across ``Building`` elements.
- :ref:`hpxml_batteries` and :ref:`hpxml_vehicles` are not currently supported.
- :ref:`hpxml_batteries` is not currently supported.
- :ref:`hpxml_utility_bill_scenarios` using *detailed* :ref:`electricity_rates` are not supported.

.. _building_site:
Expand Down
1 change: 1 addition & 0 deletions docs/source/workflow_outputs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ Annual Unmet Hours
~~~~~~~~~~~~~~~~~~

Annual unmet hours are listed below.
If running :ref:`bldg_type_whole_mf_buildings`, values will reflect hours in which *any* dwelling unit has an unmet condition (i.e., it is not the sum of unmet hours for each dwelling unit).

============================ =====
Type Notes
Expand Down
Loading