Skip to content

Commit

Permalink
Merge pull request #1918 from NREL/output_meters
Browse files Browse the repository at this point in the history
Timeseries EnergyPlus output meters
  • Loading branch information
shorowit authored Feb 3, 2025
2 parents 96621f2 + deb66cc commit 4959249
Show file tree
Hide file tree
Showing 13 changed files with 226 additions and 74 deletions.
1 change: 1 addition & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## OpenStudio-HPXML v1.10.0

__New Features__
- Allows requesting timeseries EnergyPlus output meters (e.g., `--hourly "MainsWater:Facility"`), similar to requesting EnergyPlus output variables.

__Bugfixes__
- Fixes zero occupants specified for one unit in a whole MF building from being treated like zero occupants for every unit.
Expand Down
11 changes: 11 additions & 0 deletions ReportSimulationOutput/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,17 @@ Optionally generates timeseries EnergyPlus output variables. If multiple output

<br/>

**Generate Timeseries Output: EnergyPlus Output Meters**

Optionally generates timeseries EnergyPlus output meters. If multiple output meters are desired, use a comma-separated list. Example: "Electricity:Facility, HeatingCoils:EnergyTransfer"

- **Name:** ``user_output_meters``
- **Type:** ``String``

- **Required:** ``false``

<br/>

**Annual Output File Name**

If not provided, defaults to 'results_annual.csv' (or 'results_annual.json' or 'results_annual.msgpack').
Expand Down
150 changes: 115 additions & 35 deletions ReportSimulationOutput/measure.rb
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,11 @@ def arguments(model) # rubocop:disable Lint/UnusedMethodArgument
arg.setDescription('Optionally generates timeseries EnergyPlus output variables. If multiple output variables are desired, use a comma-separated list. Do not include key values; by default all key values will be requested. Example: "Zone People Occupant Count, Zone People Total Heating Energy"')
args << arg

arg = OpenStudio::Measure::OSArgument::makeStringArgument('user_output_meters', false)
arg.setDisplayName('Generate Timeseries Output: EnergyPlus Output Meters')
arg.setDescription('Optionally generates timeseries EnergyPlus output meters. If multiple output meters are desired, use a comma-separated list. Example: "Electricity:Facility, HeatingCoils:EnergyTransfer"')
args << arg

arg = OpenStudio::Measure::OSArgument::makeStringArgument('annual_output_file_name', false)
arg.setDisplayName('Annual Output File Name')
arg.setDescription("If not provided, defaults to 'results_annual.csv' (or 'results_annual.json' or 'results_annual.msgpack').")
Expand Down Expand Up @@ -361,7 +366,7 @@ def energyPlusOutputRequests(runner, user_arguments)

args = get_arguments(runner, arguments(model), user_arguments)

setup_outputs(false, args[:user_output_variables])
setup_outputs(false, args)
args = setup_timeseries_includes(@emissions, args)

has_electricity_production = false
Expand Down Expand Up @@ -531,11 +536,16 @@ def energyPlusOutputRequests(runner, user_arguments)
end
end

# Optional output variables (timeseries only)
@output_variables_requests.each do |output_variable_name, _output_variable|
# Output variables (timeseries only)
@output_variables_requests.each do |output_variable_name|
result << OpenStudio::IdfObject.load("Output:Variable,*,#{output_variable_name},#{args[:timeseries_frequency]};").get
end

# Output meters (timeseries only)
@output_meters_requests.each do |output_meter_name|
result << OpenStudio::IdfObject.load("Output:Meter,#{output_meter_name},#{args[:timeseries_frequency]};").get
end

return result.uniq
end

Expand Down Expand Up @@ -576,7 +586,7 @@ def run(runner, user_arguments)
@hpxml_header = hpxml.header
@hpxml_bldgs = hpxml.buildings

setup_outputs(false, args[:user_output_variables])
setup_outputs(false, args)

if not File.exist? File.join(output_dir, 'eplusout.msgpack')
runner.registerError('Cannot find eplusout.msgpack.')
Expand Down Expand Up @@ -608,7 +618,7 @@ def run(runner, user_arguments)
end

if args[:timeseries_frequency] != 'none'
@timestamps, timestamps_dst, timestamps_utc = get_timestamps(@msgpackDataTimeseries, @hpxml_header, @hpxml_bldgs, args)
@timestamps, timestamps_dst, timestamps_utc = get_timestamps(@msgpackDataTimeseries, @msgpackData, @hpxml_header, @hpxml_bldgs, args)
end

# Retrieve outputs
Expand All @@ -627,15 +637,24 @@ def run(runner, user_arguments)

# TODO
#
# @param msgpackDataTimeseries [TODO] TODO
# @param msgpackData [TODO] TODO
# @param hpxml_header [TODO] TODO
# @param hpxml_bldgs [TODO] TODO
# @param args [Hash] Map of :argument_name => value
# @return [TODO] TODO
def get_timestamps(msgpackData, hpxml_header, hpxml_bldgs, args)
return if msgpackData.nil?
def get_timestamps(msgpackDataTimeseries, msgpackData, hpxml_header, hpxml_bldgs, args)
if not msgpackDataTimeseries.nil?
ep_timestamps = msgpackDataTimeseries['Rows'].map { |r| r.keys[0] }
elsif not msgpackData.nil?
msgpack_timeseries_name = get_msgpack_timeseries_name(args[:timeseries_frequency])
timeseries_data = msgpackData['MeterData'][msgpack_timeseries_name]
if not timeseries_data.nil?
ep_timestamps = timeseries_data['Rows'].map { |r| r.keys[0] }
end
end

ep_timestamps = msgpackData['Rows'].map { |r| r.keys[0] }
return if ep_timestamps.nil?

if args[:add_timeseries_dst_column] || args[:use_dview_format]
dst_start_ts = Time.utc(hpxml_header.sim_calendar_year, hpxml_bldgs[0].dst_begin_month, hpxml_bldgs[0].dst_begin_day, 2)
Expand Down Expand Up @@ -1190,10 +1209,15 @@ def sanitize_name(name)
end
end

# Output Variables
@output_variables = {}
@output_variables_requests.each do |output_variable_name, _output_variable|
@output_variables_requests.each do |output_variable_name|
key_values, units = get_report_variable_data_timeseries_key_values_and_units(output_variable_name)
runner.registerWarning("Request for output variable '#{output_variable_name}' returned no key values.") if key_values.empty?
if key_values.empty?
runner.registerWarning("Request for output variable '#{output_variable_name}' returned no results.")
next
end

key_values.each do |key_value|
@output_variables[[output_variable_name, key_value]] = OutputVariable.new
@output_variables[[output_variable_name, key_value]].name = "#{output_variable_name}: #{key_value.split.map(&:capitalize).join(' ')}"
Expand All @@ -1202,6 +1226,21 @@ def sanitize_name(name)
end
end

# Output Meters
@output_meters = {}
@output_meters_requests.each do |output_meter_name|
units = get_report_meter_data_timeseries_units(output_meter_name, args[:timeseries_frequency])
if units.nil?
runner.registerWarning("Request for output meter '#{output_meter_name}' returned no results.")
next
end

@output_meters[output_meter_name] = OutputMeter.new
@output_meters[output_meter_name].name = output_meter_name
@output_meters[output_meter_name].timeseries_units = units
@output_meters[output_meter_name].timeseries_output = get_report_meter_data_timeseries([output_meter_name], 1, 0, args[:timeseries_frequency])
end

# Emissions
if not @emissions.empty?
kwh_to_mwh = UnitConversions.convert(1.0, 'kWh', 'MWh')
Expand Down Expand Up @@ -1806,17 +1845,24 @@ def report_timeseries_output_results(runner, outputs, timeseries_output_path, ar
output_variables_data = []
end

# EnergyPlus output meters
if not @output_meters.empty?
output_meters_data = @output_meters.values.map { |x| [x.name, x.timeseries_units] + x.timeseries_output }
else
output_meters_data = []
end

return if (total_energy_data.size + fuel_data.size + end_use_data.size + system_use_data.size + emissions_data.size + emission_fuel_data.size +
emission_end_use_data.size + hot_water_use_data.size + total_loads_data.size + comp_loads_data.size + unmet_hours_data.size +
zone_temps_data.size + airflows_data.size + weather_data.size + resilience_data.size + output_variables_data.size) == 0
zone_temps_data.size + airflows_data.size + weather_data.size + resilience_data.size + output_variables_data.size + output_meters_data.size) == 0

fail 'Unable to obtain timestamps.' if @timestamps.empty?

if ['csv'].include? args[:output_format]
# Assemble data
data = data.zip(*timestamps2, *timestamps3, *total_energy_data, *fuel_data, *end_use_data, *system_use_data, *emissions_data,
*emission_fuel_data, *emission_end_use_data, *hot_water_use_data, *total_loads_data, *comp_loads_data,
*unmet_hours_data, *zone_temps_data, *airflows_data, *weather_data, *resilience_data, *output_variables_data)
*emission_fuel_data, *emission_end_use_data, *hot_water_use_data, *total_loads_data, *comp_loads_data, *unmet_hours_data,
*zone_temps_data, *airflows_data, *weather_data, *resilience_data, *output_variables_data, *output_meters_data)

# Error-check
n_elements = []
Expand Down Expand Up @@ -1882,7 +1928,7 @@ def report_timeseries_output_results(runner, outputs, timeseries_output_path, ar

[total_energy_data, fuel_data, end_use_data, system_use_data, emissions_data, emission_fuel_data,
emission_end_use_data, hot_water_use_data, total_loads_data, comp_loads_data, unmet_hours_data,
zone_temps_data, airflows_data, weather_data, resilience_data, output_variables_data].each do |d|
zone_temps_data, airflows_data, weather_data, resilience_data, output_variables_data, output_meters_data].each do |d|
d.each do |o|
grp, name = o[0].split(':', 2)
h[grp] = {} if h[grp].nil?
Expand Down Expand Up @@ -2011,10 +2057,7 @@ def get_resilience_timeseries(init_time_step, batt_kwh, batt_kw, batt_soc_kwh, c
def get_report_meter_data_timeseries(meter_names, unit_conv, unit_adder, timeseries_frequency)
return [0.0] * @timestamps.size if meter_names.empty?

msgpack_timeseries_name = { 'timestep' => 'TimeStep',
'hourly' => 'Hourly',
'daily' => 'Daily',
'monthly' => 'Monthly' }[timeseries_frequency]
msgpack_timeseries_name = get_msgpack_timeseries_name(timeseries_frequency)
timeseries_data = @msgpackData['MeterData'][msgpack_timeseries_name]
cols = timeseries_data['Cols']
rows = timeseries_data['Rows']
Expand Down Expand Up @@ -2107,23 +2150,55 @@ def apply_ems_shift(timeseries_frequency)

# TODO
#
# @param var [TODO] TODO
# @param var_name [TODO] TODO
# @return [TODO] TODO
def get_report_variable_data_timeseries_key_values_and_units(var)
def get_report_variable_data_timeseries_key_values_and_units(var_name)
keys = []
units = ''
if not @msgpackDataTimeseries.nil?
@msgpackDataTimeseries['Cols'].each do |col|
next unless col['Variable'].end_with? ":#{var}"
return keys, units if @msgpackDataTimeseries.nil?

keys << col['Variable'].split(':')[0..-2].join(':')
units = col['Units']
end
@msgpackDataTimeseries['Cols'].each do |col|
next unless col['Variable'].end_with? ":#{var_name}"

keys << col['Variable'].split(':')[0..-2].join(':')
units = col['Units']
end

return keys, units
end

# TODO
#
# @param meter_name [TODO] TODO
# @param timeseries_frequency [TODO] TODO
# @return [TODO] TODO
def get_report_meter_data_timeseries_units(meter_name, timeseries_frequency)
return if @msgpackData.nil?

msgpack_timeseries_name = get_msgpack_timeseries_name(timeseries_frequency)
timeseries_data = @msgpackData['MeterData'][msgpack_timeseries_name]
return if timeseries_data.nil?

timeseries_data['Cols'].each do |col|
next unless col['Variable'] == meter_name

return col['Units']
end

return
end

# TODO
#
# @param timeseries_frequency [TODO] TODO
# @return [TODO] TODO
def get_msgpack_timeseries_name(timeseries_frequency)
return { 'timestep' => 'TimeStep',
'hourly' => 'Hourly',
'daily' => 'Daily',
'monthly' => 'Monthly' }[timeseries_frequency]
end

# TODO
#
# @param report_name [TODO] TODO
Expand Down Expand Up @@ -2425,12 +2500,20 @@ def initialize
attr_accessor()
end

# TODO
class OutputMeter < BaseOutput
def initialize
super()
end
attr_accessor()
end

# TODO
#
# @param called_from_outputs_method [TODO] TODO
# @param user_output_variables [TODO] TODO
# @param args [TODO] TODO
# @return [TODO] TODO
def setup_outputs(called_from_outputs_method, user_output_variables = nil)
def setup_outputs(called_from_outputs_method, args = {})
# TODO
#
# @param fuel_type [TODO] TODO
Expand Down Expand Up @@ -2760,13 +2843,10 @@ def get_timeseries_units_from_fuel_type(fuel_type)
end

# Output Variables
@output_variables_requests = {}
if not user_output_variables.nil?
output_variables = user_output_variables.split(',').map(&:strip)
output_variables.each do |output_variable|
@output_variables_requests[output_variable] = OutputVariable.new
end
end
@output_variables_requests = args[:user_output_variables].to_s.split(',').map(&:strip)

# Output Meters
@output_meters_requests = args[:user_output_meters].to_s.split(',').map(&:strip)
end

# TODO
Expand Down
18 changes: 13 additions & 5 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>5e609cb1-8e83-479b-afda-c1de10cd62ea</version_id>
<version_modified>2024-11-27T02:33:41Z</version_modified>
<version_id>ca741975-505b-4e74-a17b-b96671720b39</version_id>
<version_modified>2025-01-25T08:36:10Z</version_modified>
<xml_checksum>9BF1E6AC</xml_checksum>
<class_name>ReportSimulationOutput</class_name>
<display_name>HPXML Simulation Output Report</display_name>
Expand Down Expand Up @@ -712,6 +712,14 @@
<required>false</required>
<model_dependent>false</model_dependent>
</argument>
<argument>
<name>user_output_meters</name>
<display_name>Generate Timeseries Output: EnergyPlus Output Meters</display_name>
<description>Optionally generates timeseries EnergyPlus output meters. If multiple output meters are desired, use a comma-separated list. Example: "Electricity:Facility, HeatingCoils:EnergyTransfer"</description>
<type>String</type>
<required>false</required>
<model_dependent>false</model_dependent>
</argument>
<argument>
<name>annual_output_file_name</name>
<display_name>Annual Output File Name</display_name>
Expand Down Expand Up @@ -1912,7 +1920,7 @@
<filename>README.md</filename>
<filetype>md</filetype>
<usage_type>readme</usage_type>
<checksum>CDB2D617</checksum>
<checksum>E1D3B16D</checksum>
</file>
<file>
<filename>README.md.erb</filename>
Expand All @@ -1929,13 +1937,13 @@
<filename>measure.rb</filename>
<filetype>rb</filetype>
<usage_type>script</usage_type>
<checksum>4C7478A8</checksum>
<checksum>2D3EC7C0</checksum>
</file>
<file>
<filename>test_report_sim_output.rb</filename>
<filetype>rb</filetype>
<usage_type>test</usage_type>
<checksum>8552F493</checksum>
<checksum>F630E4A7</checksum>
</file>
</files>
</measure>
25 changes: 25 additions & 0 deletions ReportSimulationOutput/tests/test_report_sim_output.rb
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,11 @@ def teardown
'Surface Construction Index: Window4'
]

BaseHPXMLTimeseriesColsEnergyPlusOutputMeters = [
'MainsWater:Facility',
'HeatingCoils:EnergyTransfer'
]

def all_base_hpxml_timeseries_cols
return (BaseHPXMLTimeseriesColsEnergy +
BaseHPXMLTimeseriesColsFuels +
Expand Down Expand Up @@ -1320,6 +1325,26 @@ def test_timeseries_energyplus_output_variables
assert(File.readlines(run_log).any? { |line| line.include?("Request for output variable 'Foo'") })
end

def test_timeseries_energyplus_output_meters
args_hash = { 'hpxml_path' => File.join(File.dirname(__FILE__), '../../workflow/sample_files/base.xml'),
'skip_validation' => true,
'add_component_loads' => true,
'timeseries_frequency' => 'hourly',
'user_output_meters' => 'MainsWater:Facility, Foo:Meter, HeatingCoils:EnergyTransfer' }
annual_csv, timeseries_csv, run_log = _test_measure(args_hash)
assert(File.exist?(annual_csv))
assert(File.exist?(timeseries_csv))
expected_timeseries_cols = ['Time'] + BaseHPXMLTimeseriesColsEnergyPlusOutputMeters
actual_timeseries_cols = File.readlines(timeseries_csv)[0].strip.split(',')
assert_equal(expected_timeseries_cols.sort, actual_timeseries_cols.sort)
timeseries_rows = CSV.read(timeseries_csv)
assert_equal(8760, timeseries_rows.size - 2)
timeseries_cols = timeseries_rows.transpose
assert_equal(1, _check_for_constant_timeseries_step(timeseries_cols[0]))
_check_for_nonzero_avg_timeseries_value(timeseries_csv, BaseHPXMLTimeseriesColsEnergyPlusOutputMeters)
assert(File.readlines(run_log).any? { |line| line.include?("Request for output meter 'Foo:Meter'") })
end

def test_for_unsuccessful_simulation_infinity
# Create HPXML w/ AFUE=0 to generate Infinity result
hpxml_path = File.join(File.dirname(__FILE__), '../../workflow/sample_files/base.xml')
Expand Down
Loading

0 comments on commit 4959249

Please sign in to comment.