Skip to content

Timeseries EnergyPlus output meters #1918

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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