Skip to content
Closed
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,5 @@ buildstockbatch_crash_details.log
# Postprocessing build outputs
/postprocessing/build/
/postprocessing/resstockpostproc.egg-info/

.history/
218 changes: 218 additions & 0 deletions measures/OCHRE/measure.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
# frozen_string_literal: true

# see the URL below for information on how to write OpenStudio measures
# http://nrel.github.io/OpenStudio-user-documentation/reference/measure_writing_guide/

require 'openstudio'
require 'json'
require 'csv'

# Load HPXML resources
resources_path = File.absolute_path(File.join(File.dirname(__FILE__), '../../resources/hpxml-measures/HPXMLtoOpenStudio/resources'))
Dir["#{resources_path}/*.rb"].each do |resource_file|
next if resource_file.include? 'minitest_helper.rb'

require resource_file
end

# start the measure
class OCHRE < OpenStudio::Measure::ModelMeasure
# human readable name
def name
return 'OCHRE Simulator'
end

# human readable description
def description
return 'Runs OCHRE (Object-oriented Controllable High-resolution Residential Energy Model) simulation as an alternative to HPXMLtoOpenStudio/EnergyPlus.'
end

# human readable description of modeling approach
def modeler_description
return 'This measure replaces the HPXMLtoOpenStudio + EnergyPlus workflow with OCHRE simulation. It takes an HPXML file as input, runs OCHRE via command line, and converts the outputs to match the expected format for downstream reporting measures.'
end

# define the arguments that the user will input
def arguments(model) # rubocop:disable Lint/UnusedMethodArgument
args = OpenStudio::Measure::OSArgumentVector.new

arg = OpenStudio::Measure::OSArgument.makeStringArgument('hpxml_path', true)
arg.setDisplayName('HPXML File Path')
arg.setDescription('Absolute/relative path of the HPXML file.')
args << arg

arg = OpenStudio::Measure::OSArgument.makeStringArgument('output_dir', true)
arg.setDisplayName('Output Directory')
arg.setDescription('Absolute/relative path for outputs.')
args << arg

arg = OpenStudio::Measure::OSArgument.makeIntegerArgument('time_res_minutes', false)
arg.setDisplayName('Time Resolution (minutes)')
arg.setDescription('Time resolution for OCHRE simulation in minutes.')
arg.setDefaultValue(60)
args << arg

arg = OpenStudio::Measure::OSArgument.makeIntegerArgument('duration_days', false)
arg.setDisplayName('Duration (days)')
arg.setDescription('Simulation duration in days.')
arg.setDefaultValue(365)
args << arg

arg = OpenStudio::Measure::OSArgument.makeBoolArgument('debug', false)
arg.setDisplayName('Debug Mode')
arg.setDescription('If true, generates additional debug output.')
arg.setDefaultValue(false)
args << arg

return args
end

# define what happens when the measure is run
def run(model, runner, user_arguments)
super(model, runner, user_arguments)

# use the built-in error checking
if !runner.validateUserArguments(arguments(model), user_arguments)
return false
end

# assign the user inputs to variables
args = runner.getArgumentValues(arguments(model), user_arguments)
args = Hash[args.map { |k, v| [k.to_sym, v] }]

# Validate required inputs
hpxml_path = args[:hpxml_path]
if hpxml_path.nil? || hpxml_path.empty?
runner.registerError('HPXML file path is required.')
return false
end

output_dir = args[:output_dir]
if output_dir.nil? || output_dir.empty?
output_dir = '.'
end

# Create output directory if it doesn't exist
unless Dir.exist?(output_dir)
Dir.mkdir(output_dir)
end

# Get absolute paths
hpxml_path = File.absolute_path(hpxml_path) if File.exist?(hpxml_path)
output_dir = File.absolute_path(output_dir)

# Check if HPXML file exists
unless File.exist?(hpxml_path)
runner.registerError("HPXML file not found: #{hpxml_path}")
return false
end

runner.registerInfo("HPXML file: #{hpxml_path}")
runner.registerInfo("Output directory: #{output_dir}")

# Load HPXML to extract weather and schedule files
begin
hpxml = HPXML.new(hpxml_path: hpxml_path)
rescue => e
runner.registerError("Failed to load HPXML file: #{e.message}")
return false
end

# Extract weather file
weather_file = nil
if hpxml.buildings.size > 0
weather_file = hpxml.buildings[0].climate_and_risk_zones.weather_station_epw_filepath
end

if weather_file.nil? || weather_file.empty?
runner.registerWarning('Weather file path not found in HPXML.')
else
weather_file = File.expand_path(weather_file, File.dirname(hpxml_path))
runner.registerInfo("Found weather file in HPXML: #{weather_file}")
end

# Extract schedule file
schedule_file = nil
if hpxml.buildings.size > 0 && !hpxml.buildings[0].header.schedules_filepaths.nil? && !hpxml.buildings[0].header.schedules_filepaths.empty?
schedule_file = hpxml.buildings[0].header.schedules_filepaths[0]
runner.registerInfo("Found schedule file in HPXML: #{schedule_file}")
else
runner.registerWarning('Schedule file path not found in HPXML.')
end

# Build OCHRE command line
time_res_minutes = args[:time_res_minutes] || 10
duration_days = args[:duration_days] || 365

# Build OCHRE CLI command
ochre_cmd = build_ochre_command(hpxml_path, output_dir,
time_res_minutes, duration_days,
schedule_file, weather_file)

runner.registerInfo("Running OCHRE simulation: #{ochre_cmd}")

begin
output = `#{ochre_cmd} 2>&1`
exit_status = $?.exitstatus

if args[:debug]
runner.registerInfo("OCHRE output:\n#{output}")
end

if exit_status != 0
runner.registerError("OCHRE simulation failed with exit code #{exit_status}")
runner.registerError("Output:\n#{output}")
return false
end

runner.registerInfo('OCHRE simulation completed successfully')
rescue => e
runner.registerError("Failed to run OCHRE: #{e.message}")
return false
end

runner.registerFinalCondition('OCHRE simulation completed successfully')

return true
end

private

# Build OCHRE command to run via CLI
def build_ochre_command(hpxml_path, output_dir, time_res_minutes, duration_days, schedule_file, weather_file)
# Get directory and filename for HPXML
hpxml_dir = File.dirname(hpxml_path)
hpxml_name = File.basename(hpxml_path)

# Escape paths for shell
hpxml_dir_safe = hpxml_dir.gsub("'", "\\\\'")
hpxml_name_safe = hpxml_name.gsub("'", "\\\\'")
output_dir_safe = output_dir.gsub("'", "\\\\'")

# Build the ochre command
# Usage: ochre single [OPTIONS] INPUT_PATH
# INPUT_PATH is the directory containing the HPXML file
cmd = "/Users/radhikar/Documents/buildstock2025/OCHRE/.venv/bin/python /Users/radhikar/Documents/buildstock2025/OCHRE/ochre/cli.py single '#{hpxml_dir_safe}'"
cmd += " --hpxml_file '#{hpxml_name_safe}'"
cmd += " --output_path '#{output_dir_safe}'"
cmd += " --time_res #{time_res_minutes}"
cmd += " --duration #{duration_days}"
cmd += ' --start_year 2018 --start_month 1 --start_day 1'
cmd += ' --verbosity=9'

if schedule_file && !schedule_file.empty?
schedule_file_safe = schedule_file.gsub("'", "\\\\'")
cmd += " --hpxml_schedule_file '#{schedule_file_safe}'"
end

if weather_file && !weather_file.empty?
weather_file_safe = weather_file.gsub("'", "\\\\'")
cmd += " --weather_file_or_path '#{weather_file_safe}'"
end

return cmd
end
end

# register the measure to be used by the application
OCHRE.new.registerWithApplication
92 changes: 92 additions & 0 deletions measures/OCHRE/tests/test_ochre.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# frozen_string_literal: true

require 'openstudio'
require 'openstudio/measure/ShowRunnerOutput'
require 'minitest/autorun'
require_relative '../measure.rb'
require 'fileutils'

class OCHRETest < Minitest::Test
def setup
# Create a temporary directory for tests
@test_dir = File.join(File.dirname(__FILE__), 'output')
FileUtils.mkdir_p(@test_dir) unless File.exist?(@test_dir)
end

def teardown
# Clean up test directory
FileUtils.rm_rf(@test_dir) if File.exist?(@test_dir)
end

def test_number_of_arguments_and_argument_names
# Create an instance of the measure
measure = OCHRE.new

# Make an empty model
model = OpenStudio::Model::Model.new

# Get arguments and test that they are what we are expecting
arguments = measure.arguments(model)
assert_equal(5, arguments.size)

# Check argument names
arg_names = arguments.map(&:name)
assert_includes(arg_names, 'hpxml_path')
assert_includes(arg_names, 'output_dir')
assert_includes(arg_names, 'time_res_minutes')
assert_includes(arg_names, 'duration_days')
assert_includes(arg_names, 'debug')
end

def test_bad_hpxml_path
# Create an instance of the measure
measure = OCHRE.new

# Create runner with empty OSW
runner = OpenStudio::Measure::OSRunner.new(OpenStudio::WorkflowJSON.new)

# Make an empty model
model = OpenStudio::Model::Model.new

# Get arguments
arguments = measure.arguments(model)
argument_map = OpenStudio::Measure.convertOSArgumentVectorToMap(arguments)

# Set argument values
argument_map['hpxml_path'].setValue('nonexistent_file.xml')
argument_map['output_dir'].setValue(@test_dir)

# Run the measure
measure.run(model, runner, argument_map)
result = runner.result

# Assert that it failed (because file doesn't exist)
assert_equal('Fail', result.value.valueName)
end

def test_measure_name
# Create an instance of the measure
measure = OCHRE.new

# Check the name
assert_equal('OCHRE Simulator', measure.name)
end

def test_description
# Create an instance of the measure
measure = OCHRE.new

# Check the description
assert(!measure.description.empty?)
assert(measure.description.include?('OCHRE'))
end

def test_modeler_description
# Create an instance of the measure
measure = OCHRE.new

# Check the modeler description
assert(!measure.modeler_description.empty?)
assert(measure.modeler_description.include?('HPXMLtoOpenStudio'))
end
end
Loading
Loading