diff --git a/highlevel_tests.rb b/highlevel_tests.rb index 62b3ba78..8c2dd24b 100644 --- a/highlevel_tests.rb +++ b/highlevel_tests.rb @@ -64,7 +64,7 @@ def test_rbs_are_defined_sim_tests def test_pys_are_defined_sim_tests all_python_paths = Dir.glob(File.join($ModelDir, '*.py')) all_python_filenames = all_python_paths.map { |p| File.basename(p) } - all_python_filenames -= ['python_plugin_program.py'] + all_python_filenames -= ['python_plugin_program.py', 'python_plugin_search_paths_script.py'] content = File.read('model_tests.rb') sim_test_re = Regexp.new('def test_.*\n(?:\s*#)*\s+result = sim_test\(\'(?.*\.py)\'\)\n(?:\s*#)*\s+end') diff --git a/model/simulationtests/python_plugin.py b/model/simulationtests/python_plugin.py index b7656de4..158ab57e 100644 --- a/model/simulationtests/python_plugin.py +++ b/model/simulationtests/python_plugin.py @@ -106,7 +106,7 @@ def on_end_of_zone_timestep_before_zone_reporting(self, state) -> int: # Write it to a temporary directory so we don't pollute the current directory # ExternalFile will copy it -pluginPath = Path(tempfile.gettempdir()) / "python_plugin_program.py" +pluginPath = Path(tempfile.gettempdir()) / f"{Path(__file__).stem}_program.py" pluginPath.write_text(python_plugin_file_content) # create the external file object diff --git a/model/simulationtests/python_plugin.rb b/model/simulationtests/python_plugin.rb index 361973f6..4cd4fa40 100644 --- a/model/simulationtests/python_plugin.rb +++ b/model/simulationtests/python_plugin.rb @@ -111,7 +111,8 @@ def on_end_of_zone_timestep_before_zone_reporting(self, state) -> int: # Write it to a temporary directory so we don't pollute the current directory # ExternalFile will copy it -pluginPath = File.join(Dir.tmpdir, 'python_plugin_program.py') +stem = File.basename(__FILE__, File.extname(__FILE__)) +pluginPath = File.join(Dir.tmpdir, "#{stem}_program.py") File.write(pluginPath, python_plugin_file_content) # create the external file object diff --git a/model/simulationtests/python_plugin_program.py b/model/simulationtests/python_plugin_program.py index 719a5a06..0a4b6889 100644 --- a/model/simulationtests/python_plugin_program.py +++ b/model/simulationtests/python_plugin_program.py @@ -1,3 +1,4 @@ +# This file is used by python_plugin.osm from pyenergyplus.plugin import EnergyPlusPlugin diff --git a/model/simulationtests/python_plugin_search_paths.py b/model/simulationtests/python_plugin_search_paths.py new file mode 100644 index 00000000..8fccdb2e --- /dev/null +++ b/model/simulationtests/python_plugin_search_paths.py @@ -0,0 +1,132 @@ +import tempfile +from pathlib import Path + +import openstudio + +from lib.baseline_model import BaselineModel + +model = BaselineModel() + +# make a 1 story, 100m X 50m, 5 zone core/perimeter building +model.add_geometry(length=100, width=50, num_floors=1, floor_to_floor_height=4, plenum_height=0, perimeter_zone_depth=3) + +# assign constructions from a local library to the walls/windows/etc. in the model +model.set_constructions() + +# set whole building space type; simplified 90.1-2004 Large Office Whole Building +model.set_space_type() + +# add design days to the model (Chicago) +model.add_design_days() + +zone_names = sorted([x.nameString() for x in model.getThermalZones()]) + +zone_names_str_list = '["' + '", "'.join(zone_names) + '"]' + +# Add a PythonPlugin:Variable (all OS SDK PythonPluginVariable objects are +# translated to a single E+ PythonPlugin:Variables (extensible object)) +py_var = openstudio.model.PythonPluginVariable(model) +py_var.setName("AverageBuildingTemp") + +# Add a PythonPlugin:OutputVariable for that variable +py_out_var = openstudio.model.PythonPluginOutputVariable(py_var) +py_out_var.setName("Averaged Building Temperature") +py_out_var.setTypeofDatainVariable("Averaged") +py_out_var.setUpdateFrequency("ZoneTimestep") +py_out_var.setUnits("C") + +# Add a regular Output:Variable that references it +out_var = openstudio.model.OutputVariable("PythonPlugin:OutputVariable", model) +out_var.setKeyValue(py_out_var.nameString()) +out_var.setReportingFrequency("Timestep") + +# Add output variables for Zone Mean Air Temperature, so we can compare +outputVariable = openstudio.model.OutputVariable("Zone Mean Air Temperature", model) +outputVariable.setReportingFrequency("Timestep") + +# Trend Variable: while this is a fully functioning object, you're probably +# best just using a storage variable on the Python side (eg: a list) +py_trend_var = openstudio.model.PythonPluginTrendVariable(py_var) +py_trend_var.setName("Running Averaged Building Temperature") +n_timesteps = 24 * model.getTimestep().numberOfTimestepsPerHour() +py_trend_var.setNumberofTimestepstobeLogged(n_timesteps) + +py_var2 = openstudio.model.PythonPluginVariable(model) +py_var2.setName("RunningAverageBuildingTemp") + +py_out_trend_var = openstudio.model.PythonPluginOutputVariable(py_var2) +py_out_trend_var.setName("Running Averaged Building Temperature") +py_out_trend_var.setTypeofDatainVariable("Averaged") +py_out_trend_var.setUpdateFrequency("ZoneTimestep") +py_out_trend_var.setUnits("C") + +out_trend_var = openstudio.model.OutputVariable("PythonPlugin:OutputVariable", model) +out_trend_var.setReportingFrequency("Timestep") + +pluginClassName = "AverageZoneTemps" + +python_plugin_file_content = f"""from pyenergyplus.plugin import EnergyPlusPlugin +# NOTE: This external script must be locatable, so we'll add it to the PythonPluginSearchPaths +import python_plugin_search_paths_script + + +class {pluginClassName}(EnergyPlusPlugin): + + def __init__(self): + super().__init__() + self.do_setup = True + + def on_end_of_zone_timestep_before_zone_reporting(self, state) -> int: + if self.do_setup: + self.data['zone_volumes'] = [] + self.data['zone_temps'] = [] + zone_names = {zone_names_str_list} + for zone_name in zone_names: + handle = self.api.exchange.get_internal_variable_handle(state, 'Zone Air Volume', zone_name) + zone_volume = self.api.exchange.get_internal_variable_value(state, handle) + self.data['zone_volumes'].append(zone_volume) + self.data['zone_temps'].append( + self.api.exchange.get_variable_handle(state, 'Zone Mean Air Temperature', zone_name) + ) + self.data['avg_temp_variable'] = self.api.exchange.get_global_handle(state, '{py_var.nameString()}') + self.data['trend'] = self.api.exchange.get_trend_handle(state, '{py_trend_var.nameString()}') + self.data['running_avg_temp_variable'] = self.api.exchange.get_global_handle(state, '{py_var2.nameString()}') + self.do_setup = False + zone_temps = list() + for t_handle in self.data['zone_temps']: + zone_temps.append(self.api.exchange.get_variable_value(state, t_handle)) + numerator = 0.0 + denominator = 0.0 + for i in range(len(self.data['zone_volumes'])): + numerator += self.data['zone_volumes'][i] * zone_temps[i] + denominator += self.data['zone_volumes'][i] + average_temp = numerator / denominator + self.api.exchange.set_global_value(state, self.data['avg_temp_variable'], average_temp) + + past_daily_avg_temp = self.api.exchange.get_trend_average(state, self.data['trend'], {n_timesteps}) + self.api.exchange.set_global_value(state, self.data['running_avg_temp_variable'], past_daily_avg_temp) + return 0 +""" + +# Write it to a temporary directory so we don't pollute the current directory +# ExternalFile will copy it +pluginPath = Path(tempfile.gettempdir()) / f"{Path(__file__).stem}_program.py" +pluginPath.write_text(python_plugin_file_content) + +# create the external file object +external_file = openstudio.model.ExternalFile.getExternalFile(model, str(pluginPath)) +external_file = external_file.get() + +# create the python plugin instance object +python_plugin_instance = openstudio.model.PythonPluginInstance(external_file, pluginClassName) +python_plugin_instance.setRunDuringWarmupDays(False) + +# create the python plugin search paths object (this test should fail without it) +python_plugin_search_paths = model.getPythonPluginSearchPaths() +python_plugin_search_paths.setAddCurrentWorkingDirectorytoSearchPath(True) +python_plugin_search_paths.setAddInputFileDirectorytoSearchPath(True) +python_plugin_search_paths.setAddepinEnvironmentVariabletoSearchPath(True) +python_plugin_search_paths.addSearchPath(Path(__file__).parent) + +# save the OpenStudio model (.osm) +model.save_openstudio_osm(osm_save_directory=None, osm_name="in.osm") diff --git a/model/simulationtests/python_plugin_search_paths.rb b/model/simulationtests/python_plugin_search_paths.rb new file mode 100644 index 00000000..acdd7bfb --- /dev/null +++ b/model/simulationtests/python_plugin_search_paths.rb @@ -0,0 +1,137 @@ +# frozen_string_literal: true + +require 'openstudio' +require_relative 'lib/baseline_model' +require 'tmpdir' + +model = BaselineModel.new + +# make a 1 story, 100m X 50m, 5 zone core/perimeter building +model.add_geometry({ 'length' => 100, + 'width' => 50, + 'num_floors' => 1, + 'floor_to_floor_height' => 4, + 'plenum_height' => 0, + 'perimeter_zone_depth' => 3 }) + +# assign constructions from a local library to the walls/windows/etc. in the model +model.set_constructions + +# set whole building space type; simplified 90.1-2004 Large Office Whole Building +model.set_space_type + +# add design days to the model (Chicago) +model.add_design_days + +zone_names = model.getThermalZones.map(&:nameString).sort + +zone_names_str_list = '["' + zone_names.join('", "') + '"]' + +# Add a PythonPlugin:Variable (all OS SDK PythonPluginVariable objects are +# translated to a single E+ PythonPlugin:Variables (extensible object)) +py_var = OpenStudio::Model::PythonPluginVariable.new(model) +py_var.setName('AverageBuildingTemp') + +# Add a PythonPlugin:OutputVariable for that variable +py_out_var = OpenStudio::Model::PythonPluginOutputVariable.new(py_var) +py_out_var.setName('Averaged Building Temperature') +py_out_var.setTypeofDatainVariable('Averaged') +py_out_var.setUpdateFrequency('ZoneTimestep') +py_out_var.setUnits('C') + +# Add a regular Output:Variable that references it +out_var = OpenStudio::Model::OutputVariable.new('PythonPlugin:OutputVariable', model) +out_var.setKeyValue(py_out_var.nameString) +out_var.setReportingFrequency('Timestep') + +# Add output variables for Zone Mean Air Temperature, so we can compare +outputVariable = OpenStudio::Model::OutputVariable.new('Zone Mean Air Temperature', model) +outputVariable.setReportingFrequency('Timestep') + +# Trend Variable: while this is a fully functioning object, you're probably +# best just using a storage variable on the Python side (eg: a list) +py_trend_var = OpenStudio::Model::PythonPluginTrendVariable.new(py_var) +py_trend_var.setName('Running Averaged Building Temperature') +n_timesteps = 24 * model.getTimestep.numberOfTimestepsPerHour +py_trend_var.setNumberofTimestepstobeLogged(n_timesteps) + +py_var2 = OpenStudio::Model::PythonPluginVariable.new(model) +py_var2.setName('RunningAverageBuildingTemp') + +py_out_trend_var = OpenStudio::Model::PythonPluginOutputVariable.new(py_var2) +py_out_trend_var.setName('Running Averaged Building Temperature') +py_out_trend_var.setTypeofDatainVariable('Averaged') +py_out_trend_var.setUpdateFrequency('ZoneTimestep') +py_out_trend_var.setUnits('C') + +out_trend_var = OpenStudio::Model::OutputVariable.new('PythonPlugin:OutputVariable', model) +out_trend_var.setReportingFrequency('Timestep') + +pluginClassName = 'AverageZoneTemps' + +python_plugin_file_content = ''"from pyenergyplus.plugin import EnergyPlusPlugin +# NOTE: This external script must be locatable, so we'll add it to the PythonPluginSearchPaths +import python_plugin_search_paths_script + + +class #{pluginClassName}(EnergyPlusPlugin): + + def __init__(self): + super().__init__() + self.do_setup = True + + def on_end_of_zone_timestep_before_zone_reporting(self, state) -> int: + if self.do_setup: + self.data['zone_volumes'] = [] + self.data['zone_temps'] = [] + zone_names = #{zone_names_str_list} + for zone_name in zone_names: + handle = self.api.exchange.get_internal_variable_handle(state, 'Zone Air Volume', zone_name) + zone_volume = self.api.exchange.get_internal_variable_value(state, handle) + self.data['zone_volumes'].append(zone_volume) + self.data['zone_temps'].append( + self.api.exchange.get_variable_handle(state, 'Zone Mean Air Temperature', zone_name) + ) + self.data['avg_temp_variable'] = self.api.exchange.get_global_handle(state, '#{py_var.nameString}') + self.data['trend'] = self.api.exchange.get_trend_handle(state, '#{py_trend_var.nameString}') + self.data['running_avg_temp_variable'] = self.api.exchange.get_global_handle(state, '#{py_var2.nameString}') + self.do_setup = False + zone_temps = list() + for t_handle in self.data['zone_temps']: + zone_temps.append(self.api.exchange.get_variable_value(state, t_handle)) + numerator = 0.0 + denominator = 0.0 + for i in range(len(self.data['zone_volumes'])): + numerator += self.data['zone_volumes'][i] * zone_temps[i] + denominator += self.data['zone_volumes'][i] + average_temp = numerator / denominator + self.api.exchange.set_global_value(state, self.data['avg_temp_variable'], average_temp) + + past_daily_avg_temp = self.api.exchange.get_trend_average(state, self.data['trend'], #{n_timesteps}) + self.api.exchange.set_global_value(state, self.data['running_avg_temp_variable'], past_daily_avg_temp) + return 0 +"'' + +# Write it to a temporary directory so we don't pollute the current directory +# ExternalFile will copy it +stem = File.basename(__FILE__, File.extname(__FILE__)) +pluginPath = File.join(Dir.tmpdir, "#{stem}_program.py") +File.write(pluginPath, python_plugin_file_content) + +# create the external file object +external_file = OpenStudio::Model::ExternalFile.getExternalFile(model, pluginPath) +external_file = external_file.get + +# create the python plugin instance object +python_plugin_instance = OpenStudio::Model::PythonPluginInstance.new(external_file, pluginClassName) +python_plugin_instance.setRunDuringWarmupDays(false) + +# create the python plugin search paths object (this test should fail without it) +python_plugin_search_paths = model.getPythonPluginSearchPaths +python_plugin_search_paths.setAddCurrentWorkingDirectorytoSearchPath(true) +python_plugin_search_paths.setAddInputFileDirectorytoSearchPath(true) +python_plugin_search_paths.setAddepinEnvironmentVariabletoSearchPath(true) +python_plugin_search_paths.addSearchPath(File.dirname(__FILE__)) + +# save the OpenStudio model (.osm) +model.save_openstudio_osm({ 'osm_save_directory' => Dir.pwd, 'osm_name' => 'in.osm' }) diff --git a/model/simulationtests/python_plugin_search_paths_script.py b/model/simulationtests/python_plugin_search_paths_script.py new file mode 100644 index 00000000..2814d386 --- /dev/null +++ b/model/simulationtests/python_plugin_search_paths_script.py @@ -0,0 +1,2 @@ +# This file is used by python_plugin_search_paths +print('hello world') diff --git a/model_tests.rb b/model_tests.rb index b0b08c24..abd81eb4 100644 --- a/model_tests.rb +++ b/model_tests.rb @@ -1537,6 +1537,19 @@ def test_python_plugin_osm result = sim_test('python_plugin.osm') end + # TODO: To be added in the next official release after: 3.9.0 + # def test_python_plugin_search_paths_osm + # result = sim_test('python_plugin_search_paths.osm') + # end + + def test_python_plugin_search_paths_rb + result = sim_test('python_plugin_search_paths.rb') + end + + def test_python_plugin_search_paths_py + result = sim_test('python_plugin_search_paths.py') + end + def test_refrigeration_system_rb result = sim_test('refrigeration_system.rb') end diff --git a/test_helpers.rb b/test_helpers.rb index 43a6537d..b57d0d6b 100644 --- a/test_helpers.rb +++ b/test_helpers.rb @@ -708,11 +708,12 @@ def sim_test(filename, options = {}) cbor_target_path = File.join(files_dir, File.basename(cbor_ori_path)) FileUtils.cp(cbor_ori_path, cbor_target_path) - when 'python_plugin.osm' + when 'python_plugin.osm', 'python_plugin_search_paths.osm' # We need to manually copy the supporting schedule into # the testruns folder for the simulation to be able to find it + program_file_name = "#{File.basename(filename, File.extname(filename))}_program.py" plugin_ori_path = File.join(File.dirname(__FILE__), - 'model/simulationtests/python_plugin_program.py') + 'model/simulationtests', program_file_name) plugin_ori_path = File.realpath(plugin_ori_path) # Have to make the directory first