From 32229d1432ff050446abcda6f7c82d8a8049dd2a Mon Sep 17 00:00:00 2001 From: Julien Marrec Date: Fri, 14 Mar 2025 11:47:52 +0100 Subject: [PATCH 01/11] Add ReportingMeasure::modelOutputRequests --- src/measure/ReportingMeasure.cpp | 7 ++++++- src/measure/ReportingMeasure.hpp | 9 +++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/measure/ReportingMeasure.cpp b/src/measure/ReportingMeasure.cpp index e40ad4f8b5..f1b93ce64e 100644 --- a/src/measure/ReportingMeasure.cpp +++ b/src/measure/ReportingMeasure.cpp @@ -14,7 +14,7 @@ namespace openstudio { namespace measure { - ReportingMeasure::ReportingMeasure() : OSMeasure(MeasureType::ReportingMeasure){}; + ReportingMeasure::ReportingMeasure() : OSMeasure(MeasureType::ReportingMeasure) {}; std::vector ReportingMeasure::arguments(const openstudio::model::Model& /*model*/) const { return {}; @@ -29,6 +29,11 @@ namespace measure { return true; } + bool ReportingMeasure::modelOutputRequests(openstudio::model::Model& /*model*/, OSRunner& /*runner*/, + const std::map& /*user_arguments*/) const { + return true; + } + std::vector ReportingMeasure::energyPlusOutputRequests(OSRunner& /*runner*/, const std::map& /*user_arguments*/) const { return {}; diff --git a/src/measure/ReportingMeasure.hpp b/src/measure/ReportingMeasure.hpp index 0fdc2142d6..64a1293252 100644 --- a/src/measure/ReportingMeasure.hpp +++ b/src/measure/ReportingMeasure.hpp @@ -55,6 +55,15 @@ namespace measure { * super(runner, user_arguments). */ virtual bool run(OSRunner& runner, const std::map& user_arguments) const; + /** This method is called on all reporting measures immediately before the translation to E+ IDF. + * There is an implicit contract that this method should NOT be modifying your model in a way that would produce + * different results, meaning it should only add or modify reporting-related elements (eg: OutputTableSummaryReports, OutputControlFiles, etc) + * but that is not going to be enforced. + * If you mean to modify the model in a significant way, use a ModelMeasure. + */ + virtual bool modelOutputRequests(openstudio::model::Model& model, OSRunner& runner, + const std::map& user_arguments) const; + /** This method is called on all reporting measures immediately before the E+ * simulation. The code that injects these objects into the IDF checks that * only objects of allowed types are added to prevent changes that impact From b507d62a6e85508006c2ae3909f5c67fd68ef872 Mon Sep 17 00:00:00 2001 From: Julien Marrec Date: Fri, 14 Mar 2025 11:48:30 +0100 Subject: [PATCH 02/11] For backward compatibility, we need a way to test if a measure has a method defined before we try to run it --- python/engine/PythonEngine.cpp | 15 +++++++++++++++ python/engine/PythonEngine.hpp | 2 ++ ruby/engine/RubyEngine.cpp | 6 ++++++ ruby/engine/RubyEngine.hpp | 2 ++ src/scriptengine/ScriptEngine.hpp | 4 ++++ 5 files changed, 29 insertions(+) diff --git a/python/engine/PythonEngine.cpp b/python/engine/PythonEngine.cpp index e474a947a5..ce5d9565a4 100644 --- a/python/engine/PythonEngine.cpp +++ b/python/engine/PythonEngine.cpp @@ -413,6 +413,21 @@ int PythonEngine::numberOfArguments(ScriptObject& methodObject, std::string_view return numberOfArguments; } +bool PythonEngine::hasMethod(ScriptObject& methodObject, std::string_view methodName) { + auto val = std::any_cast(methodObject.object); + if (PyObject_HasAttrString(val.obj_, methodName.data()) == 0) { + return false; + } + PyObject* method = PyObject_GetAttrString(val.obj_, methodName.data()); // New reference + bool result = false; + if (PyMethod_Check(method)) { + result = true; + } + + Py_DECREF(method); + return result; +} + } // namespace openstudio extern "C" diff --git a/python/engine/PythonEngine.hpp b/python/engine/PythonEngine.hpp index f05019e0e9..7e01af4723 100644 --- a/python/engine/PythonEngine.hpp +++ b/python/engine/PythonEngine.hpp @@ -38,6 +38,8 @@ class PythonEngine final : public ScriptEngine virtual int numberOfArguments(ScriptObject& methodObject, std::string_view methodName) override; + virtual bool hasMethod(ScriptObject& methodObject, std::string_view methodName) override; + protected: void* getAs_impl(ScriptObject& obj, const std::type_info&) override; diff --git a/ruby/engine/RubyEngine.cpp b/ruby/engine/RubyEngine.cpp index 7db74faec6..e425d119d0 100644 --- a/ruby/engine/RubyEngine.cpp +++ b/ruby/engine/RubyEngine.cpp @@ -237,6 +237,12 @@ int RubyEngine::numberOfArguments(ScriptObject& methodObject, std::string_view m return rb_obj_method_arity(val, method_id); } +bool RubyEngine::hasMethod(ScriptObject& methodObject, std::string_view methodName) { + auto val = std::any_cast(methodObject.object); + ID method_id = rb_intern(methodName.data()); + return rb_respond_to(val, method_id) == 1; +} + } // namespace openstudio extern "C" diff --git a/ruby/engine/RubyEngine.hpp b/ruby/engine/RubyEngine.hpp index 671da26643..2df04ac624 100644 --- a/ruby/engine/RubyEngine.hpp +++ b/ruby/engine/RubyEngine.hpp @@ -39,6 +39,8 @@ class RubyEngine final : public ScriptEngine virtual int numberOfArguments(ScriptObject& methodObject, std::string_view methodName) override; + virtual bool hasMethod(ScriptObject& methodObject, std::string_view methodName) override; + protected: // convert the underlying object to the correct type, then return it as a void * // so the above template function can provide it back to the caller. diff --git a/src/scriptengine/ScriptEngine.hpp b/src/scriptengine/ScriptEngine.hpp index cca628b50a..23754aa8fa 100644 --- a/src/scriptengine/ScriptEngine.hpp +++ b/src/scriptengine/ScriptEngine.hpp @@ -77,8 +77,12 @@ class ScriptEngine // issue for the underlying ScriptObject (and VALUE or PyObject), so just return the ScriptObject virtual ScriptObject loadMeasure(const openstudio::path& measureScriptPath, std::string_view className) = 0; + // Returns number of arguments for methodName of the object methodObject virtual int numberOfArguments(ScriptObject& methodObject, std::string_view methodName) = 0; + // Check if methodObject has a method called methodName + virtual bool hasMethod(ScriptObject& methodObject, std::string_view methodName) = 0; + template T getAs(ScriptObject& obj) { if constexpr (std::is_same_v) { From 1a790d342201bde9aa753756bf4d16c3a1f3a5f3 Mon Sep 17 00:00:00 2001 From: Julien Marrec Date: Fri, 14 Mar 2025 11:50:06 +0100 Subject: [PATCH 03/11] Make the modelOutputRequests run in workflow right after the ModelMeasures --- src/workflow/ApplyMeasure.cpp | 19 ++++++++++++++----- src/workflow/OSWorkflow.hpp | 10 +++++++++- src/workflow/RunEnergyPlusMeasures.cpp | 2 +- src/workflow/RunOpenStudioMeasures.cpp | 6 +++++- src/workflow/RunPreProcess.cpp | 7 +++---- src/workflow/RunReportingMeasures.cpp | 2 +- 6 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/workflow/ApplyMeasure.cpp b/src/workflow/ApplyMeasure.cpp index d2d9a2e122..c522604bd2 100644 --- a/src/workflow/ApplyMeasure.cpp +++ b/src/workflow/ApplyMeasure.cpp @@ -45,7 +45,7 @@ bool isStepMarkedSkip(const std::map& stepArgs return skip_measure; } -void OSWorkflow::applyMeasures(MeasureType measureType, bool energyplus_output_requests) { +void OSWorkflow::applyMeasures(MeasureType measureType, ApplyMeasureType apply_measure_type) { if (m_add_timings && m_detailed_timings) { m_timers->newTimer(fmt::format("{}:apply_measures", measureType.valueName()), 1); @@ -66,7 +66,7 @@ void OSWorkflow::applyMeasures(MeasureType measureType, bool energyplus_output_r auto stepArgs = step.arguments(); const bool skip_measure = isStepMarkedSkip(stepArgs); if (skip_measure || runner.halted()) { - if (!energyplus_output_requests) { + if (apply_measure_type == ApplyMeasureType::Regular) { if (runner.halted()) { LOG(Info, fmt::format("Skipping measure '{}' because simulation halted", measureDirName)); } else { @@ -276,7 +276,16 @@ end } else if (measureType == MeasureType::EnergyPlusMeasure) { static_cast(measurePtr)->run(workspace_.get(), runner, argmap); } else if (measureType == MeasureType::ReportingMeasure) { - if (energyplus_output_requests) { + if (apply_measure_type == ApplyMeasureType::ModelOutputRequests) { + if ((*thisEngine)->hasMethod(measureScriptObject, "modelOutputRequests")) { + auto n = model.numObjects(); + LOG(Debug, "Calling measure.modelOutputRequests for '" << measureDirName << "'"); + static_cast(measurePtr)->modelOutputRequests(model, runner, argmap); + LOG(Debug, "Finished measure.modelOutputRequests for '" << measureDirName << "', " << (model.numObjects() - n) << " objects added"); + } else { + LOG(Debug, "Reporting Measure '" << measureDirName << "' does not have a modelOutputRequests method"); + } + } else if (apply_measure_type == ApplyMeasureType::EnergyPlusOutputRequest) { LOG(Debug, "Calling measure.energyPlusOutputRequests for '" << measureDirName << "'"); std::vector idfObjects; @@ -295,7 +304,7 @@ end } } catch (const std::exception& e) { runner.registerError(e.what()); - if (!energyplus_output_requests) { + if (apply_measure_type == ApplyMeasureType::Regular) { WorkflowStepResult result = runner.result(); // incrementStep must be called after run runner.incrementStep(); @@ -306,7 +315,7 @@ end } // if doing output requests we are done now - if (!energyplus_output_requests) { + if (apply_measure_type == ApplyMeasureType::Regular) { WorkflowStepResult result = runner.result(); // incrementStep must be called after run diff --git a/src/workflow/OSWorkflow.hpp b/src/workflow/OSWorkflow.hpp index 77e3f92bd2..ad558b7a3c 100644 --- a/src/workflow/OSWorkflow.hpp +++ b/src/workflow/OSWorkflow.hpp @@ -124,7 +124,15 @@ class OSWorkflow void initializeWeatherFileFromOSW(); void updateLastWeatherFileFromModel(); - void applyMeasures(MeasureType measureType, bool energyplus_output_requests = false); + + enum class ApplyMeasureType + { + Regular = 0, + ModelOutputRequests, + EnergyPlusOutputRequest + }; + + void applyMeasures(MeasureType measureType, ApplyMeasureType = ApplyMeasureType::Regular); static void applyArguments(measure::OSArgumentMap& argumentMap, const std::string& argumentName, const openstudio::Variant& argumentValue); void saveOSMToRootDirIfDebug(); void saveIDFToRootDirIfDebug(); diff --git a/src/workflow/RunEnergyPlusMeasures.cpp b/src/workflow/RunEnergyPlusMeasures.cpp index 08c2f5a685..596962a257 100644 --- a/src/workflow/RunEnergyPlusMeasures.cpp +++ b/src/workflow/RunEnergyPlusMeasures.cpp @@ -19,7 +19,7 @@ void OSWorkflow::runEnergyPlusMeasures() { // Weather file is handled in runInitialization LOG(Info, "Beginning to execute EnergyPlus Measures."); - applyMeasures(MeasureType::EnergyPlusMeasure, false); + applyMeasures(MeasureType::EnergyPlusMeasure, ApplyMeasureType::Regular); LOG(Info, "Finished applying EnergyPlus Measures."); communicateMeasureAttributes(); diff --git a/src/workflow/RunOpenStudioMeasures.cpp b/src/workflow/RunOpenStudioMeasures.cpp index 50d2961261..bad1ec3314 100644 --- a/src/workflow/RunOpenStudioMeasures.cpp +++ b/src/workflow/RunOpenStudioMeasures.cpp @@ -19,9 +19,13 @@ void OSWorkflow::runOpenStudioMeasures() { // Weather file is handled in runInitialization LOG(Info, "Beginning to execute OpenStudio Measures"); - applyMeasures(MeasureType::ModelMeasure, false); + applyMeasures(MeasureType::ModelMeasure, ApplyMeasureType::Regular); LOG(Info, "Finished applying OpenStudio Measures."); + LOG(Info, "Beginning to execute Reporting Measures's Model Output Requests"); + applyMeasures(MeasureType::ReportingMeasure, ApplyMeasureType::ModelOutputRequests); + LOG(Info, "Finished applying Reporting Measures's Model Output Requests.") + // Save final OSM if (!workflowJSON.runOptions()->fast()) { // Save to run dir diff --git a/src/workflow/RunPreProcess.cpp b/src/workflow/RunPreProcess.cpp index 12ce6c2aea..4d4049dde1 100644 --- a/src/workflow/RunPreProcess.cpp +++ b/src/workflow/RunPreProcess.cpp @@ -43,10 +43,9 @@ void OSWorkflow::runPreProcess() { } // Add any EnergyPlus Output Requests from Reporting Measures - LOG(Info, "Beginning to collect output requests from Reporting measures."); - const bool energyplus_output_requests = true; - applyMeasures(MeasureType::ReportingMeasure, energyplus_output_requests); - LOG(Info, "Finished collecting output requests from Reporting measures."); + LOG(Info, "Beginning to collect EnergyPlus output requests from Reporting measures."); + applyMeasures(MeasureType::ReportingMeasure, ApplyMeasureType::EnergyPlusOutputRequest); + LOG(Info, "Finished collecting EnergyPlus output requests from Reporting measures."); // Skip the pre-processor if halted if (runner.halted()) { diff --git a/src/workflow/RunReportingMeasures.cpp b/src/workflow/RunReportingMeasures.cpp index 365e269fcd..c97563ba30 100644 --- a/src/workflow/RunReportingMeasures.cpp +++ b/src/workflow/RunReportingMeasures.cpp @@ -54,7 +54,7 @@ void OSWorkflow::runReportingMeasures() { } LOG(Info, "Beginning to execute Reporting Measures."); - applyMeasures(MeasureType::ReportingMeasure, false); + applyMeasures(MeasureType::ReportingMeasure, ApplyMeasureType::Regular); LOG(Info, "Finished applying Reporting Measures."); communicateMeasureAttributes(); From 4fa064a4282a54b421bcff22e5692c14d8c05552 Mon Sep 17 00:00:00 2001 From: Julien Marrec Date: Fri, 14 Mar 2025 12:00:25 +0100 Subject: [PATCH 04/11] Update BCL template for ReportingMeasure + tests to have modelOutputRequests --- .../bcl/templates/ReportingMeasure/measure.py | 51 +++++++-- .../bcl/templates/ReportingMeasure/measure.rb | 29 ++++- .../tests/reporting_measure_test.rb | 87 ++++++++++++++- .../tests/test_reporting_measure.py | 104 ++++++++++++++++-- 4 files changed, 245 insertions(+), 26 deletions(-) diff --git a/src/utilities/bcl/templates/ReportingMeasure/measure.py b/src/utilities/bcl/templates/ReportingMeasure/measure.py index 47577a2150..bdb1a13044 100644 --- a/src/utilities/bcl/templates/ReportingMeasure/measure.py +++ b/src/utilities/bcl/templates/ReportingMeasure/measure.py @@ -12,7 +12,7 @@ class ReportingMeasureName(openstudio.measure.ReportingMeasure): """An ReportingMeasure.""" - def name(self): + def name(self) -> str: """Returns the human readable name. Measure name should be the title case of the class name. @@ -24,7 +24,7 @@ def name(self): """ return "NAME_TEXT" - def description(self): + def description(self) -> str: """Human readable description. The measure description is intended for a general audience and should not assume @@ -32,7 +32,7 @@ def description(self): """ return "DESCRIPTION_TEXT" - def modeler_description(self): + def modeler_description(self) -> str: """Human readable description of modeling approach. The modeler description is intended for the energy modeler using the measure. @@ -42,7 +42,7 @@ def modeler_description(self): """ return "MODELER_DESCRIPTION_TEXT" - def arguments(self, model: openstudio.model.Model): + def arguments(self, model: openstudio.model.Model) -> openstudio.measure.OSArgumentVector: """Prepares user arguments for the measure. Measure arguments define which -- if any -- input parameters the user may set before running the measure. @@ -52,12 +52,18 @@ def arguments(self, model: openstudio.model.Model): report_drybulb_temp = openstudio.measure.OSArgument.makeBoolArgument("report_drybulb_temp", True) report_drybulb_temp.setDisplayName("Add output variables for Drybulb Temperature") report_drybulb_temp.setDescription("Will add drybulb temp and report min/max values in html.") - report_drybulb_temp.setValue(True) + report_drybulb_temp.setDefaultValue(True) args.append(report_drybulb_temp) + add_output_json = openstudio.measure.OSArgument.makeBoolArgument("add_output_json", True) + add_output_json.setDisplayName("Request JSON output") + add_output_json.setDescription("Will add Output:JSON with TimeSeriesAndTabular and set Output JSON to true") + add_output_json.setDefaultValue(True) + args.append(add_output_json) + return args - def outputs(self): + def outputs(self) -> openstudio.measure.OSOutputVector: """Define the outputs that the measure will create.""" outs = openstudio.measure.OSOutputVector() @@ -65,10 +71,37 @@ def outputs(self): return outs + def modelOutputRequests( + self, + model: openstudio.model.Model, + runner: openstudio.measure.OSRunner, + user_arguments: openstudio.measure.OSArgumentMap, + ) -> bool: + """This method is called on all reporting measures immediately before the translation to E+ IDF. + + There is an implicit contract that this method should NOT be modifying your model in a way that would produce + different results, meaning it should only add or modify reporting-related elements + (eg: OutputTableSummaryReports, OutputControlFiles, etc) + + If you mean to modify the model in a significant way, use a `ModelMeasure` + NOTE: this method will ONLY be called if you use the C++ CLI, not the `classic` (Ruby) one + """ + if runner.getBoolArgumentValue("add_output_json", user_arguments): + output_json = model.getOutputJSON() + output_json.setOptionType("TimeSeriesAndTabular") + output_json.setOutputJSON(True) + output_json.setOutputCBOR(False) + output_json.setOutputMessagePack(False) + + return True + def energyPlusOutputRequests( self, runner: openstudio.measure.OSRunner, user_arguments: openstudio.measure.OSArgumentMap - ): - """Returns a vector of IdfObject's to request EnergyPlus objects needed by the run method.""" + ) -> openstudio.IdfObjectVector: + """Returns a vector of IdfObject's to request EnergyPlus objects needed by the run method. + + This is done after ForwardTranslation to IDF, and there is a list of accepted objects. + """ super().energyPlusOutputRequests(runner, user_arguments) # Do **NOT** remove this line result = openstudio.IdfObjectVector() @@ -98,7 +131,7 @@ def run( self, runner: openstudio.measure.OSRunner, user_arguments: openstudio.measure.OSArgumentMap, - ): + ) -> bool: """Defines what happens when the measure is run.""" super().run(runner, user_arguments) diff --git a/src/utilities/bcl/templates/ReportingMeasure/measure.rb b/src/utilities/bcl/templates/ReportingMeasure/measure.rb index ece24e3fd1..befcd778eb 100644 --- a/src/utilities/bcl/templates/ReportingMeasure/measure.rb +++ b/src/utilities/bcl/templates/ReportingMeasure/measure.rb @@ -31,9 +31,15 @@ def arguments(model = nil) report_drybulb_temp = OpenStudio::Measure::OSArgument.makeBoolArgument('report_drybulb_temp', true) report_drybulb_temp.setDisplayName('Add output variables for Drybulb Temperature') report_drybulb_temp.setDescription('Will add drybulb temp and report min/mix value in html.') - report_drybulb_temp.setValue(true) + report_drybulb_temp.setDefaultValue(true) args << report_drybulb_temp + add_output_json = OpenStudio::Measure::OSArgument.makeBoolArgument('add_output_json', true) + add_output_json.setDisplayName('Request JSON output') + add_output_json.setDescription('Will add Output:JSON with TimeSeriesAndTabular and set Output JSON to true') + add_output_json.setDefaultValue(true) + args << add_output_json + return args end @@ -46,8 +52,28 @@ def outputs return outs end + # This method is called on all reporting measures immediately before the translation to E+ IDF + # There is an implicit contract that this method should NOT be modifying your model in a way that would produce + # different results, meaning it should only add or modify reporting-related elements + # (eg: OutputTableSummaryReports, OutputControlFiles, etc) + # If you mean to modify the model in a significant way, use a `ModelMeasure` + # NOTE: this method will ONLY be called if you use the C++ CLI, not the `classic` (Ruby) one + def modelOutputRequests(model, runner, user_arguments) + if runner.getBoolArgumentValue('add_output_json', user_arguments) + output_json = model.getOutputJSON + output_json.setOptionType('TimeSeriesAndTabular') + output_json.setOutputJSON(true) + output_json.setOutputCBOR(false) + output_json.setOutputMessagePack(false) + end + + return true + end + # return a vector of IdfObject's to request EnergyPlus objects needed by the run method # Warning: Do not change the name of this method to be snake_case. The method must be lowerCamelCase. + # This is done after ForwardTranslation to IDF, and there is a list of + # accepted objects def energyPlusOutputRequests(runner, user_arguments) super(runner, user_arguments) # Do **NOT** remove this line @@ -67,6 +93,7 @@ def energyPlusOutputRequests(runner, user_arguments) return false end + # NOTE: this should rather be done in modelOutputRequests if runner.getBoolArgumentValue('report_drybulb_temp', user_arguments) request = OpenStudio::IdfObject.load('Output:Variable,,Site Outdoor Air Drybulb Temperature,Hourly;').get result << request diff --git a/src/utilities/bcl/templates/ReportingMeasure/tests/reporting_measure_test.rb b/src/utilities/bcl/templates/ReportingMeasure/tests/reporting_measure_test.rb index 15e35d7e56..21dc3bf426 100644 --- a/src/utilities/bcl/templates/ReportingMeasure/tests/reporting_measure_test.rb +++ b/src/utilities/bcl/templates/ReportingMeasure/tests/reporting_measure_test.rb @@ -84,7 +84,6 @@ def setup_test(test_name, idf_output_requests, model_in_path = model_in_path_def cmd = "\"#{cli_path}\" run -w \"#{osw_path}\"" puts cmd system(cmd) - end def test_number_of_arguments_and_argument_names @@ -96,7 +95,14 @@ def test_number_of_arguments_and_argument_names # get arguments and test that they are what we are expecting arguments = measure.arguments(model) - assert_equal(1, arguments.size) + assert_equal(2, arguments.size) + assert_equal("report_drybulb_temp", arguments[0].name) + assert(arguments[0].hasDefaultValue) + assert(arguments[0].defaultValueAsBool) + + assert_equal("add_output_json", arguments[1].name) + assert(arguments[1].hasDefaultValue) + assert(arguments[1].defaultValueAsBool) end def test_with_drybulb_temp @@ -181,10 +187,19 @@ def test_without_drybulb_temp arguments = measure.arguments(model) argument_map = OpenStudio::Measure.convertOSArgumentVectorToMap(arguments) + # create hash of argument values # set non-default measure argument - report_drybulb_temp = arguments[0].clone - assert(report_drybulb_temp.setValue(false)) - argument_map['report_drybulb_temp'] = report_drybulb_temp + args_hash = {} + args_hash['report_drybulb_temp'] = false + + # populate argument with specified hash value if specified + arguments.each do |arg| + temp_arg_var = arg.clone + if args_hash.key?(arg.name) + assert(temp_arg_var.setValue(args_hash[arg.name])) + end + argument_map[arg.name] = temp_arg_var + end # temp set path so idf_output_requests work runner.setLastOpenStudioModelPath(model_in_path_default) @@ -235,5 +250,67 @@ def test_without_drybulb_temp # make sure the report file exists assert_path_exists(report_path(test_name)) end + + def test_model_output_requests_with_output_json + # create an instance of the measure + measure = ReportingMeasureName.new + + # create runner with empty OSW + osw = OpenStudio::WorkflowJSON.new + runner = OpenStudio::Measure::OSRunner.new(osw) + + # Make an empty model + model = OpenStudio::Model::Model.new + + # get arguments + arguments = measure.arguments(model) + argument_map = OpenStudio::Measure.convertOSArgumentVectorToMap(arguments) + + assert_empty(model.outputJSON) + + assert(measure.modelOutputRequests(model, runner, argument_map)) + + refute_empty(model.outputJSON) + output_json = model.getOutputJSON + assert_equal('TimeSeriesAndTabular', output_json.optionType) + assert(output_json.outputJSON) + refute(output_json.outputCBOR) + refute(output_json.outputMessagePack) + end + + def test_model_output_requests_without_output_json + # create an instance of the measure + measure = ReportingMeasureName.new + + # create runner with empty OSW + osw = OpenStudio::WorkflowJSON.new + runner = OpenStudio::Measure::OSRunner.new(osw) + + # Make an empty model + model = OpenStudio::Model::Model.new + + # get arguments + arguments = measure.arguments(model) + argument_map = OpenStudio::Measure.convertOSArgumentVectorToMap(arguments) + + # create hash of argument values + args_hash = {} + args_hash['add_output_json'] = false + + # populate argument with specified hash value if specified + arguments.each do |arg| + temp_arg_var = arg.clone + if args_hash.key?(arg.name) + assert(temp_arg_var.setValue(args_hash[arg.name])) + end + argument_map[arg.name] = temp_arg_var + end + + assert_empty(model.outputJSON) + + assert(measure.modelOutputRequests(model, runner, argument_map)) + + assert_empty(model.outputJSON) + end end diff --git a/src/utilities/bcl/templates/ReportingMeasure/tests/test_reporting_measure.py b/src/utilities/bcl/templates/ReportingMeasure/tests/test_reporting_measure.py index 7c04eadb71..43a6eabc20 100644 --- a/src/utilities/bcl/templates/ReportingMeasure/tests/test_reporting_measure.py +++ b/src/utilities/bcl/templates/ReportingMeasure/tests/test_reporting_measure.py @@ -12,8 +12,9 @@ CURRENT_DIR_PATH = Path(__file__).parent.absolute() sys.path.insert(0, str(CURRENT_DIR_PATH.parent)) from measure import ReportingMeasureName + sys.path.pop(0) -del sys.modules['measure'] +del sys.modules["measure"] MODEL_IN_PATH_DEFAULT = CURRENT_DIR_PATH / "example_model.osm" @@ -103,8 +104,14 @@ def test_number_of_arguments_and_argument_names(self): # get arguments and test that they are what we are expecting arguments = measure.arguments(model) - assert arguments.size() == 1 + assert arguments.size() == 2 assert arguments[0].name() == "report_drybulb_temp" + assert arguments[0].hasDefaultValue() + assert arguments[0].defaultValueAsBool() + + assert arguments[1].name() == "add_output_json" + assert arguments[1].hasDefaultValue() + assert arguments[1].defaultValueAsBool() def test_with_drybulb_temp(self): """Test running the measure with appropriate arguments, with db temp.""" @@ -124,11 +131,6 @@ def test_with_drybulb_temp(self): arguments = measure.arguments(model) argument_map = openstudio.measure.convertOSArgumentVectorToMap(arguments) - # set argument values to bad value - report_drybulb_temp = arguments[0].clone() - assert report_drybulb_temp.setValue(True) - argument_map["report_drybulb_temp"] = report_drybulb_temp - # temp set path so idf_output_requests work runner.setLastOpenStudioModelPath(MODEL_IN_PATH_DEFAULT) @@ -199,10 +201,18 @@ def test_without_drybulb_temp(self): arguments = measure.arguments(model) argument_map = openstudio.measure.convertOSArgumentVectorToMap(arguments) - # set argument values to bad value - report_drybulb_temp = arguments[0].clone() - assert report_drybulb_temp.setValue(False) - argument_map["report_drybulb_temp"] = report_drybulb_temp + # create hash of argument values. + # If the argument has a default that you want to use, + # you don't need it in the dict + args_dict = {} + args_dict["report_drybulb_temp"] = False + + # populate argument with specified hash value if specified + for arg in arguments: + temp_arg_var = arg.clone() + if arg.name() in args_dict: + assert temp_arg_var.setValue(args_dict[arg.name()]) + argument_map[arg.name()] = temp_arg_var # temp set path so idf_output_requests work runner.setLastOpenStudioModelPath(MODEL_IN_PATH_DEFAULT) @@ -256,10 +266,82 @@ def test_without_drybulb_temp(self): # make sure the report file exists assert report_path.exists() + def test_model_output_requests_with_output_json(self): + """Test running the modelOutputRequests with output_json.""" + # create an instance of the measure + measure = ReportingMeasureName() + + # create runner with empty OSW + osw = openstudio.WorkflowJSON() + runner = openstudio.measure.OSRunner(osw) + + # make an empty model + model = openstudio.model.Model() + + # get arguments + arguments = measure.arguments(model) + argument_map = openstudio.measure.convertOSArgumentVectorToMap(arguments) + + # create hash of argument values. + args_dict = {} + args_dict["add_output_json"] = True + + # populate argument with specified hash value if specified + for arg in arguments: + temp_arg_var = arg.clone() + if arg.name() in args_dict: + assert temp_arg_var.setValue(args_dict[arg.name()]) + argument_map[arg.name()] = temp_arg_var + + assert not model.outputJSON().is_initialized() + + assert measure.modelOutputRequests(model, runner, argument_map) + + assert model.outputJSON().is_initialized() + output_json = model.getOutputJSON() + assert output_json.optionType() == "TimeSeriesAndTabular" + assert output_json.outputJSON() + assert not output_json.outputCBOR() + assert not output_json.outputMessagePack() + + def test_model_output_requests_without_output_json(self): + """Test running the modelOutputRequests without output_json.""" + # create an instance of the measure + measure = ReportingMeasureName() + + # create runner with empty OSW + osw = openstudio.WorkflowJSON() + runner = openstudio.measure.OSRunner(osw) + + # make an empty model + model = openstudio.model.Model() + + # get arguments + arguments = measure.arguments(model) + argument_map = openstudio.measure.convertOSArgumentVectorToMap(arguments) + + # create hash of argument values. + args_dict = {} + args_dict["add_output_json"] = False + + # populate argument with specified hash value if specified + for arg in arguments: + temp_arg_var = arg.clone() + if arg.name() in args_dict: + assert temp_arg_var.setValue(args_dict[arg.name()]) + argument_map[arg.name()] = temp_arg_var + + assert not model.outputJSON().is_initialized() + + assert measure.modelOutputRequests(model, runner, argument_map) + + assert not model.outputJSON().is_initialized() + # This allows running openstudio CLI on this file (`openstudio test_measure.py`, maybe with extra args) if __name__ == "__main__": import sys + if len(sys.argv) > 1: pytest.main([__file__] + sys.argv[1:]) else: From 6d27b3859cfb156ba1ad5554e93df96127cd0fa8 Mon Sep 17 00:00:00 2001 From: Julien Marrec Date: Fri, 14 Mar 2025 14:15:55 +0100 Subject: [PATCH 05/11] Add a failing test for hasMethod --- python/engine/test/PythonEngine_GTest.cpp | 18 ++++++ .../measure.py | 52 +++++++++++++++ .../measure.xml | 44 +++++++++++++ .../measure.py | 44 +++++++++++++ .../measure.xml | 44 +++++++++++++ .../measure.rb | 63 +++++++++++++++++++ .../measure.xml | 44 +++++++++++++ .../measure.rb | 59 +++++++++++++++++ .../measure.xml | 44 +++++++++++++ ruby/engine/test/RubyEngine_GTest.cpp | 18 ++++++ 10 files changed, 430 insertions(+) create mode 100644 python/engine/test/ReportingMeasureWithModelOutputs/measure.py create mode 100644 python/engine/test/ReportingMeasureWithModelOutputs/measure.xml create mode 100644 python/engine/test/ReportingMeasureWithoutModelOutputs/measure.py create mode 100644 python/engine/test/ReportingMeasureWithoutModelOutputs/measure.xml create mode 100644 ruby/engine/test/ReportingMeasureWithModelOutputs/measure.rb create mode 100644 ruby/engine/test/ReportingMeasureWithModelOutputs/measure.xml create mode 100644 ruby/engine/test/ReportingMeasureWithoutModelOutputs/measure.rb create mode 100644 ruby/engine/test/ReportingMeasureWithoutModelOutputs/measure.xml diff --git a/python/engine/test/PythonEngine_GTest.cpp b/python/engine/test/PythonEngine_GTest.cpp index 2fa8d4a0a7..3f5d083a63 100644 --- a/python/engine/test/PythonEngine_GTest.cpp +++ b/python/engine/test/PythonEngine_GTest.cpp @@ -174,3 +174,21 @@ TEST_F(PythonEngineFixture, AlfalfaMeasure) { measurePtr->run(model, runner, arguments); EXPECT_EQ(5, runner.alfalfa().points().size()); } + +TEST_F(PythonEngineFixture, hasMethod) { + { + const std::string classAndDirName = "ReportingMeasureWithoutModelOutputs"; + const auto scriptPath = getScriptPath(classAndDirName); + auto measureScriptObject = (*thisEngine)->loadMeasure(scriptPath, classAndDirName); + EXPECT_FALSE((*thisEngine)->hasMethod(measureScriptObject, "doesNotExists")); + // TODO: this will fail, because the BASE class has it. + EXPECT_FALSE((*thisEngine)->hasMethod(measureScriptObject, "modelOutputRequests")); + } + { + const std::string classAndDirName = "ReportingMeasureWithModelOutputs"; + const auto scriptPath = getScriptPath(classAndDirName); + auto measureScriptObject = (*thisEngine)->loadMeasure(scriptPath, classAndDirName); + EXPECT_FALSE((*thisEngine)->hasMethod(measureScriptObject, "doesNotExists")); + EXPECT_TRUE((*thisEngine)->hasMethod(measureScriptObject, "modelOutputRequests")); + } +} diff --git a/python/engine/test/ReportingMeasureWithModelOutputs/measure.py b/python/engine/test/ReportingMeasureWithModelOutputs/measure.py new file mode 100644 index 0000000000..b637626f48 --- /dev/null +++ b/python/engine/test/ReportingMeasureWithModelOutputs/measure.py @@ -0,0 +1,52 @@ +"""insert your copyright here. + +# see the URL below for information on how to write OpenStudio measures +# http://nrel.github.io/OpenStudio-user-documentation/reference/measure_writing_guide/ +""" + +from pathlib import Path + +import openstudio + + +class ReportingMeasureWithModelOutputs(openstudio.measure.ReportingMeasure): + """An ReportingMeasure.""" + + def name(self): + return "ReportingMeasureWithModelOutputs" + + def description(self): + return "DESCRIPTION_TEXT" + + def modeler_description(self): + return "MODELER_DESCRIPTION_TEXT" + + def arguments(self, model: openstudio.model.Model): + args = openstudio.measure.OSArgumentVector() + return args + + def outputs(self): + outs = openstudio.measure.OSOutputVector() + return outs + + def modelOutputRequests( + self, + model: openstudio.model.Model, + runner: openstudio.measure.OSRunner, + user_arguments: openstudio.measure.OSArgumentMap, + ) -> bool: + return True + + def energyPlusOutputRequests( + self, runner: openstudio.measure.OSRunner, user_arguments: openstudio.measure.OSArgumentMap + ): + result = openstudio.IdfObjectVector() + return result + + def run( + self, + runner: openstudio.measure.OSRunner, + user_arguments: openstudio.measure.OSArgumentMap, + ): + super().run(runner, user_arguments) + return True diff --git a/python/engine/test/ReportingMeasureWithModelOutputs/measure.xml b/python/engine/test/ReportingMeasureWithModelOutputs/measure.xml new file mode 100644 index 0000000000..ec14086ad6 --- /dev/null +++ b/python/engine/test/ReportingMeasureWithModelOutputs/measure.xml @@ -0,0 +1,44 @@ + + + 3.1 + reporting_measure_with_model_outputs + 9971eb7b-3225-4795-9690-9941a7471ce0 + be88754a-3d7f-433e-9912-94a2993c9463 + 2025-03-14T13:15:30Z + 1DA817AF + ReportingMeasureWithModelOutputs + ReportingMeasureWithModelOutputs + DESCRIPTION_TEXT + MODELER_DESCRIPTION_TEXT + + + + + Reporting.QAQC + + + + Measure Type + ReportingMeasure + string + + + Measure Language + Python + string + + + + + + OpenStudio + 3.9.0 + 3.9.0 + + measure.py + py + script + CFB17771 + + + diff --git a/python/engine/test/ReportingMeasureWithoutModelOutputs/measure.py b/python/engine/test/ReportingMeasureWithoutModelOutputs/measure.py new file mode 100644 index 0000000000..642ed33e97 --- /dev/null +++ b/python/engine/test/ReportingMeasureWithoutModelOutputs/measure.py @@ -0,0 +1,44 @@ +"""insert your copyright here. + +# see the URL below for information on how to write OpenStudio measures +# http://nrel.github.io/OpenStudio-user-documentation/reference/measure_writing_guide/ +""" + +from pathlib import Path + +import openstudio + + +class ReportingMeasureWithoutModelOutputs(openstudio.measure.ReportingMeasure): + """An ReportingMeasure.""" + + def name(self): + return "ReportingMeasureWithoutModelOutputs" + + def description(self): + return "DESCRIPTION_TEXT" + + def modeler_description(self): + return "MODELER_DESCRIPTION_TEXT" + + def arguments(self, model: openstudio.model.Model): + args = openstudio.measure.OSArgumentVector() + return args + + def outputs(self): + outs = openstudio.measure.OSOutputVector() + return outs + + def energyPlusOutputRequests( + self, runner: openstudio.measure.OSRunner, user_arguments: openstudio.measure.OSArgumentMap + ): + result = openstudio.IdfObjectVector() + return result + + def run( + self, + runner: openstudio.measure.OSRunner, + user_arguments: openstudio.measure.OSArgumentMap, + ): + super().run(runner, user_arguments) + return True diff --git a/python/engine/test/ReportingMeasureWithoutModelOutputs/measure.xml b/python/engine/test/ReportingMeasureWithoutModelOutputs/measure.xml new file mode 100644 index 0000000000..3c10f0a0b6 --- /dev/null +++ b/python/engine/test/ReportingMeasureWithoutModelOutputs/measure.xml @@ -0,0 +1,44 @@ + + + 3.1 + reporting_measure_without_model_outputs + a0876945-a15b-469d-bc22-90f133720e7c + 49740c2c-1804-4a4e-ace5-c30b6d350c5f + 2025-03-14T13:15:17Z + 1DA817AF + ReportingMeasureWithoutModelOutputs + ReportingMeasureWithoutModelOutputs + DESCRIPTION_TEXT + MODELER_DESCRIPTION_TEXT + + + + + Reporting.QAQC + + + + Measure Type + ReportingMeasure + string + + + Measure Language + Python + string + + + + + + OpenStudio + 3.9.0 + 3.9.0 + + measure.py + py + script + 816BF07D + + + diff --git a/ruby/engine/test/ReportingMeasureWithModelOutputs/measure.rb b/ruby/engine/test/ReportingMeasureWithModelOutputs/measure.rb new file mode 100644 index 0000000000..4f192f1638 --- /dev/null +++ b/ruby/engine/test/ReportingMeasureWithModelOutputs/measure.rb @@ -0,0 +1,63 @@ +# insert your copyright here + +# see the URL below for information on how to write OpenStudio measures +# http://nrel.github.io/OpenStudio-user-documentation/reference/measure_writing_guide/ + +require 'erb' + +# start the measure +class ReportingMeasureWithModelOutputs < OpenStudio::Measure::ReportingMeasure + # human readable name + def name + # Measure name should be the title case of the class name. + return 'ReportingMeasureWithModelOutputs' + end + + # human readable description + def description + return 'DESCRIPTION_TEXT' + end + + # human readable description of modeling approach + def modeler_description + return 'MODELER_DESCRIPTION_TEXT' + end + + # define the arguments that the user will input + def arguments(model = nil) + args = OpenStudio::Measure::OSArgumentVector.new + + return args + end + + # define the outputs that the measure will create + def outputs + outs = OpenStudio::Measure::OSOutputVector.new + + # this measure does not produce machine readable outputs with registerValue, return an empty list + + return outs + end + + def modelOutputRequests(model, runner, user_arguments) + return true + end + + # return a vector of IdfObject's to request EnergyPlus objects needed by the run method + # Warning: Do not change the name of this method to be snake_case. The method must be lowerCamelCase. + def energyPlusOutputRequests(runner, user_arguments) + result = OpenStudio::IdfObjectVector.new + + return result + end + + # define what happens when the measure is run + def run(runner, user_arguments) + super(runner, user_arguments) + + return true + end +end + +# register the measure to be used by the application +ReportingMeasureWithModelOutputs.new.registerWithApplication diff --git a/ruby/engine/test/ReportingMeasureWithModelOutputs/measure.xml b/ruby/engine/test/ReportingMeasureWithModelOutputs/measure.xml new file mode 100644 index 0000000000..ebdf15d302 --- /dev/null +++ b/ruby/engine/test/ReportingMeasureWithModelOutputs/measure.xml @@ -0,0 +1,44 @@ + + + 3.1 + reporting_measure_with_model_outputs + 3bfd478b-0575-4a4e-b347-ea3d82f66c21 + 95038fac-6565-4c3a-be46-fd14ac12bcee + 2025-03-14T13:14:52Z + 58F01CAA + ReportingMeasureWithModelOutputs + ReportingMeasureWithModelOutputs + DESCRIPTION_TEXT + MODELER_DESCRIPTION_TEXT + + + + + Reporting.QAQC + + + + Measure Type + ReportingMeasure + string + + + Measure Language + Ruby + string + + + + + + OpenStudio + 3.9.0 + 3.9.0 + + measure.rb + rb + script + 811D6B05 + + + diff --git a/ruby/engine/test/ReportingMeasureWithoutModelOutputs/measure.rb b/ruby/engine/test/ReportingMeasureWithoutModelOutputs/measure.rb new file mode 100644 index 0000000000..526aecad57 --- /dev/null +++ b/ruby/engine/test/ReportingMeasureWithoutModelOutputs/measure.rb @@ -0,0 +1,59 @@ +# insert your copyright here + +# see the URL below for information on how to write OpenStudio measures +# http://nrel.github.io/OpenStudio-user-documentation/reference/measure_writing_guide/ + +require 'erb' + +# start the measure +class ReportingMeasureWithoutModelOutputs < OpenStudio::Measure::ReportingMeasure + # human readable name + def name + # Measure name should be the title case of the class name. + return 'ReportingMeasureWithoutModelOutputs' + end + + # human readable description + def description + return 'DESCRIPTION_TEXT' + end + + # human readable description of modeling approach + def modeler_description + return 'MODELER_DESCRIPTION_TEXT' + end + + # define the arguments that the user will input + def arguments(model = nil) + args = OpenStudio::Measure::OSArgumentVector.new + + return args + end + + # define the outputs that the measure will create + def outputs + outs = OpenStudio::Measure::OSOutputVector.new + + # this measure does not produce machine readable outputs with registerValue, return an empty list + + return outs + end + + # return a vector of IdfObject's to request EnergyPlus objects needed by the run method + # Warning: Do not change the name of this method to be snake_case. The method must be lowerCamelCase. + def energyPlusOutputRequests(runner, user_arguments) + result = OpenStudio::IdfObjectVector.new + + return result + end + + # define what happens when the measure is run + def run(runner, user_arguments) + super(runner, user_arguments) + + return true + end +end + +# register the measure to be used by the application +ReportingMeasureWithoutModelOutputs.new.registerWithApplication diff --git a/ruby/engine/test/ReportingMeasureWithoutModelOutputs/measure.xml b/ruby/engine/test/ReportingMeasureWithoutModelOutputs/measure.xml new file mode 100644 index 0000000000..0455fdf89d --- /dev/null +++ b/ruby/engine/test/ReportingMeasureWithoutModelOutputs/measure.xml @@ -0,0 +1,44 @@ + + + 3.1 + reporting_measure_without_model_outputs + 0a873e3c-1dba-49c2-8c22-1f4e8661e74b + b7c5702f-b0bd-4b6c-9ff5-53f977e68f3e + 2025-03-14T13:15:10Z + 58F01CAA + ReportingMeasureWithoutModelOutputs + ReportingMeasureWithoutModelOutputs + DESCRIPTION_TEXT + MODELER_DESCRIPTION_TEXT + + + + + Reporting.QAQC + + + + Measure Type + ReportingMeasure + string + + + Measure Language + Ruby + string + + + + + + OpenStudio + 3.9.0 + 3.9.0 + + measure.rb + rb + script + 171A0196 + + + diff --git a/ruby/engine/test/RubyEngine_GTest.cpp b/ruby/engine/test/RubyEngine_GTest.cpp index 5373525171..4896369de5 100644 --- a/ruby/engine/test/RubyEngine_GTest.cpp +++ b/ruby/engine/test/RubyEngine_GTest.cpp @@ -176,3 +176,21 @@ Traceback (most recent call last): }, "stack level too deep"); } + +TEST_F(RubyEngineFixture, hasMethod) { + { + const std::string classAndDirName = "ReportingMeasureWithoutModelOutputs"; + const auto scriptPath = getScriptPath(classAndDirName); + auto measureScriptObject = (*thisEngine)->loadMeasure(scriptPath, classAndDirName); + EXPECT_FALSE((*thisEngine)->hasMethod(measureScriptObject, "doesNotExists")); + // TODO: this will fail, because the BASE class has it. + EXPECT_FALSE((*thisEngine)->hasMethod(measureScriptObject, "modelOutputRequests")); + } + { + const std::string classAndDirName = "ReportingMeasureWithModelOutputs"; + const auto scriptPath = getScriptPath(classAndDirName); + auto measureScriptObject = (*thisEngine)->loadMeasure(scriptPath, classAndDirName); + EXPECT_FALSE((*thisEngine)->hasMethod(measureScriptObject, "doesNotExists")); + EXPECT_TRUE((*thisEngine)->hasMethod(measureScriptObject, "modelOutputRequests")); + } +} From a798f8e98e7692ef33d31d9e6433572b536bff03 Mon Sep 17 00:00:00 2001 From: Julien Marrec Date: Fri, 14 Mar 2025 16:48:17 +0100 Subject: [PATCH 06/11] Ok, implement hasMethod with a overriden_only = true param. --- python/engine/PythonEngine.cpp | 25 ++++++++++++++++++----- python/engine/PythonEngine.hpp | 2 +- python/engine/test/PythonEngine_GTest.cpp | 3 ++- ruby/engine/RubyEngine.cpp | 17 +++++++++++++-- ruby/engine/RubyEngine.hpp | 2 +- ruby/engine/test/RubyEngine_GTest.cpp | 3 ++- src/scriptengine/ScriptEngine.hpp | 4 ++-- 7 files changed, 43 insertions(+), 13 deletions(-) diff --git a/python/engine/PythonEngine.cpp b/python/engine/PythonEngine.cpp index ce5d9565a4..940a27a315 100644 --- a/python/engine/PythonEngine.cpp +++ b/python/engine/PythonEngine.cpp @@ -413,18 +413,33 @@ int PythonEngine::numberOfArguments(ScriptObject& methodObject, std::string_view return numberOfArguments; } -bool PythonEngine::hasMethod(ScriptObject& methodObject, std::string_view methodName) { +bool PythonEngine::hasMethod(ScriptObject& methodObject, std::string_view methodName, bool overriden_only) { auto val = std::any_cast(methodObject.object); if (PyObject_HasAttrString(val.obj_, methodName.data()) == 0) { return false; } PyObject* method = PyObject_GetAttrString(val.obj_, methodName.data()); // New reference - bool result = false; - if (PyMethod_Check(method)) { - result = true; + if (PyMethod_Check(method) == 0) { + // Should never happen with modelOutputRequests since the Base class (C++) has it + return false; } - Py_DECREF(method); + if (!overriden_only) { + return true; + } + + // equivalent to getattr(instance_obj.__class__, method_name) == getattr(instance_obj.__class__.__bases__[0], method_name) + PyTypeObject* class_type = Py_TYPE(val.obj_); // PyObject_Type returns a strong (New) reference, not needed for us + PyObject* class_method = PyObject_GetAttrString((PyObject*)class_type, methodName.data()); // New reference + + assert(class_type->tp_base != nullptr); + auto* base = (PyTypeObject*)class_type->tp_base; + PyObject* base_method = PyObject_GetAttrString((PyObject*)base, methodName.data()); // New reference + + bool result = class_method != base_method; + Py_DECREF(class_method); + Py_DECREF(base_method); + return result; } diff --git a/python/engine/PythonEngine.hpp b/python/engine/PythonEngine.hpp index 7e01af4723..17f4da7e61 100644 --- a/python/engine/PythonEngine.hpp +++ b/python/engine/PythonEngine.hpp @@ -38,7 +38,7 @@ class PythonEngine final : public ScriptEngine virtual int numberOfArguments(ScriptObject& methodObject, std::string_view methodName) override; - virtual bool hasMethod(ScriptObject& methodObject, std::string_view methodName) override; + virtual bool hasMethod(ScriptObject& methodObject, std::string_view methodName, bool overriden_only) override; protected: void* getAs_impl(ScriptObject& obj, const std::type_info&) override; diff --git a/python/engine/test/PythonEngine_GTest.cpp b/python/engine/test/PythonEngine_GTest.cpp index 3f5d083a63..a5fee52a8d 100644 --- a/python/engine/test/PythonEngine_GTest.cpp +++ b/python/engine/test/PythonEngine_GTest.cpp @@ -181,7 +181,7 @@ TEST_F(PythonEngineFixture, hasMethod) { const auto scriptPath = getScriptPath(classAndDirName); auto measureScriptObject = (*thisEngine)->loadMeasure(scriptPath, classAndDirName); EXPECT_FALSE((*thisEngine)->hasMethod(measureScriptObject, "doesNotExists")); - // TODO: this will fail, because the BASE class has it. + EXPECT_TRUE((*thisEngine)->hasMethod(measureScriptObject, "modelOutputRequests", false)); // overriden_only = false EXPECT_FALSE((*thisEngine)->hasMethod(measureScriptObject, "modelOutputRequests")); } { @@ -189,6 +189,7 @@ TEST_F(PythonEngineFixture, hasMethod) { const auto scriptPath = getScriptPath(classAndDirName); auto measureScriptObject = (*thisEngine)->loadMeasure(scriptPath, classAndDirName); EXPECT_FALSE((*thisEngine)->hasMethod(measureScriptObject, "doesNotExists")); + EXPECT_TRUE((*thisEngine)->hasMethod(measureScriptObject, "modelOutputRequests", false)); // overriden_only = false EXPECT_TRUE((*thisEngine)->hasMethod(measureScriptObject, "modelOutputRequests")); } } diff --git a/ruby/engine/RubyEngine.cpp b/ruby/engine/RubyEngine.cpp index e425d119d0..cb594a272e 100644 --- a/ruby/engine/RubyEngine.cpp +++ b/ruby/engine/RubyEngine.cpp @@ -237,10 +237,23 @@ int RubyEngine::numberOfArguments(ScriptObject& methodObject, std::string_view m return rb_obj_method_arity(val, method_id); } -bool RubyEngine::hasMethod(ScriptObject& methodObject, std::string_view methodName) { +bool RubyEngine::hasMethod(ScriptObject& methodObject, std::string_view methodName, bool overriden_only) { auto val = std::any_cast(methodObject.object); ID method_id = rb_intern(methodName.data()); - return rb_respond_to(val, method_id) == 1; + if (rb_respond_to(val, method_id) == 0) { + return false; + } + if (!overriden_only) { + return true; + } + + // I'd have prefered to do the equivalent of `instance_obj.method(:methodName).owner == instance_obj.class` but that is borderline impossible + // Instead, this is equivalent to `instance_obj.class.instance_methods(false).include?(:methodName)` + VALUE klass = rb_obj_class(val); + // include_methods_from_ancestors: false; + VALUE argv[1] = {Qfalse}; + VALUE methods_without_ancestors = rb_class_instance_methods(1, argv, klass); + return rb_ary_includes(methods_without_ancestors, ID2SYM(method_id)) == Qtrue; } } // namespace openstudio diff --git a/ruby/engine/RubyEngine.hpp b/ruby/engine/RubyEngine.hpp index 2df04ac624..90c0bb1153 100644 --- a/ruby/engine/RubyEngine.hpp +++ b/ruby/engine/RubyEngine.hpp @@ -39,7 +39,7 @@ class RubyEngine final : public ScriptEngine virtual int numberOfArguments(ScriptObject& methodObject, std::string_view methodName) override; - virtual bool hasMethod(ScriptObject& methodObject, std::string_view methodName) override; + virtual bool hasMethod(ScriptObject& methodObject, std::string_view methodName, bool overriden_only) override; protected: // convert the underlying object to the correct type, then return it as a void * diff --git a/ruby/engine/test/RubyEngine_GTest.cpp b/ruby/engine/test/RubyEngine_GTest.cpp index 4896369de5..4e89b28a0e 100644 --- a/ruby/engine/test/RubyEngine_GTest.cpp +++ b/ruby/engine/test/RubyEngine_GTest.cpp @@ -183,7 +183,7 @@ TEST_F(RubyEngineFixture, hasMethod) { const auto scriptPath = getScriptPath(classAndDirName); auto measureScriptObject = (*thisEngine)->loadMeasure(scriptPath, classAndDirName); EXPECT_FALSE((*thisEngine)->hasMethod(measureScriptObject, "doesNotExists")); - // TODO: this will fail, because the BASE class has it. + EXPECT_TRUE((*thisEngine)->hasMethod(measureScriptObject, "modelOutputRequests", false)); // overriden_only = false EXPECT_FALSE((*thisEngine)->hasMethod(measureScriptObject, "modelOutputRequests")); } { @@ -191,6 +191,7 @@ TEST_F(RubyEngineFixture, hasMethod) { const auto scriptPath = getScriptPath(classAndDirName); auto measureScriptObject = (*thisEngine)->loadMeasure(scriptPath, classAndDirName); EXPECT_FALSE((*thisEngine)->hasMethod(measureScriptObject, "doesNotExists")); + EXPECT_TRUE((*thisEngine)->hasMethod(measureScriptObject, "modelOutputRequests", false)); // overriden_only = false EXPECT_TRUE((*thisEngine)->hasMethod(measureScriptObject, "modelOutputRequests")); } } diff --git a/src/scriptengine/ScriptEngine.hpp b/src/scriptengine/ScriptEngine.hpp index 23754aa8fa..d33f63208b 100644 --- a/src/scriptengine/ScriptEngine.hpp +++ b/src/scriptengine/ScriptEngine.hpp @@ -80,8 +80,8 @@ class ScriptEngine // Returns number of arguments for methodName of the object methodObject virtual int numberOfArguments(ScriptObject& methodObject, std::string_view methodName) = 0; - // Check if methodObject has a method called methodName - virtual bool hasMethod(ScriptObject& methodObject, std::string_view methodName) = 0; + // Check if methodObject has a method called methodName. If overriden_only, will only look into the current subclass, not the parent ones + virtual bool hasMethod(ScriptObject& methodObject, std::string_view methodName, bool overriden_only = true) = 0; template T getAs(ScriptObject& obj) { From 94603103aa4311d59dee8609fa66f4e2fe1efd49 Mon Sep 17 00:00:00 2001 From: Julien Marrec Date: Fri, 14 Mar 2025 17:20:34 +0100 Subject: [PATCH 07/11] Add a ctest via pytest that will run one reportingMeasure with and one without modelOutputRequests in both classic and labs CLI just so we ensure it runs in either case... --- .../measure.py | 168 ++++++++++++++++++ .../measure.xml | 83 +++++++++ .../measure.py | 135 ++++++++++++++ .../measure.xml | 63 +++++++ .../measure.rb | 145 +++++++++++++++ .../measure.xml | 83 +++++++++ .../measure.rb | 93 ++++++++++ .../measure.xml | 63 +++++++ .../reporting_modeloutputrequests/python.osw | 12 ++ .../reporting_modeloutputrequests/ruby.osw | 12 ++ src/cli/CMakeLists.txt | 5 + .../test_reporting_modeloutputrequests.py | 29 +++ 12 files changed, 891 insertions(+) create mode 100644 resources/workflow/reporting_modeloutputrequests/measures/PythonReportingMeasureWithModelOutputRequests/measure.py create mode 100644 resources/workflow/reporting_modeloutputrequests/measures/PythonReportingMeasureWithModelOutputRequests/measure.xml create mode 100644 resources/workflow/reporting_modeloutputrequests/measures/PythonReportingMeasureWithoutModelOutputRequests/measure.py create mode 100644 resources/workflow/reporting_modeloutputrequests/measures/PythonReportingMeasureWithoutModelOutputRequests/measure.xml create mode 100644 resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithModelOutputRequests/measure.rb create mode 100644 resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithModelOutputRequests/measure.xml create mode 100644 resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithoutModelOutputRequests/measure.rb create mode 100644 resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithoutModelOutputRequests/measure.xml create mode 100644 resources/workflow/reporting_modeloutputrequests/python.osw create mode 100644 resources/workflow/reporting_modeloutputrequests/ruby.osw create mode 100644 src/cli/test/test_reporting_modeloutputrequests.py diff --git a/resources/workflow/reporting_modeloutputrequests/measures/PythonReportingMeasureWithModelOutputRequests/measure.py b/resources/workflow/reporting_modeloutputrequests/measures/PythonReportingMeasureWithModelOutputRequests/measure.py new file mode 100644 index 0000000000..1c13a60d59 --- /dev/null +++ b/resources/workflow/reporting_modeloutputrequests/measures/PythonReportingMeasureWithModelOutputRequests/measure.py @@ -0,0 +1,168 @@ +"""insert your copyright here. + +# see the URL below for information on how to write OpenStudio measures +# http://nrel.github.io/OpenStudio-user-documentation/reference/measure_writing_guide/ +""" + +from pathlib import Path + +import openstudio + + +class PythonReportingMeasureWithModelOutputRequests(openstudio.measure.ReportingMeasure): + """An ReportingMeasure.""" + + def name(self) -> str: + """Returns the human readable name. + + Measure name should be the title case of the class name. + The measure name is the first contact a user has with the measure; + it is also shared throughout the measure workflow, visible in the OpenStudio Application, + PAT, Server Management Consoles, and in output reports. + As such, measure names should clearly describe the measure's function, + while remaining general in nature + """ + return "Reporting Measure with Model Output Requests" + + def description(self) -> str: + """Human readable description. + + The measure description is intended for a general audience and should not assume + that the reader is familiar with the design and construction practices suggested by the measure. + """ + return "A new reporting measure that has a modelOutputRequests" + + def modeler_description(self) -> str: + """Human readable description of modeling approach. + + The modeler description is intended for the energy modeler using the measure. + It should explain the measure's intent, and include any requirements about + how the baseline model must be set up, major assumptions made by the measure, + and relevant citations or references to applicable modeling resources + """ + return "This was added at 3.10.0" + + def arguments(self, model: openstudio.model.Model) -> openstudio.measure.OSArgumentVector: + """Prepares user arguments for the measure. + + Measure arguments define which -- if any -- input parameters the user may set before running the measure. + """ + args = openstudio.measure.OSArgumentVector() + + report_drybulb_temp = openstudio.measure.OSArgument.makeBoolArgument("report_drybulb_temp", True) + report_drybulb_temp.setDisplayName("Add output variables for Drybulb Temperature") + report_drybulb_temp.setDescription("Will add drybulb temp and report min/max values in html.") + report_drybulb_temp.setDefaultValue(True) + args.append(report_drybulb_temp) + + add_output_json = openstudio.measure.OSArgument.makeBoolArgument("add_output_json", True) + add_output_json.setDisplayName("Request JSON output") + add_output_json.setDescription("Will add Output:JSON with TimeSeriesAndTabular and set Output JSON to true") + add_output_json.setDefaultValue(True) + args.append(add_output_json) + + return args + + def outputs(self) -> openstudio.measure.OSOutputVector: + """Define the outputs that the measure will create.""" + outs = openstudio.measure.OSOutputVector() + + # this measure does not produce machine readable outputs with registerValue, return an empty list + + return outs + + def modelOutputRequests( + self, + model: openstudio.model.Model, + runner: openstudio.measure.OSRunner, + user_arguments: openstudio.measure.OSArgumentMap, + ) -> bool: + """This method is called on all reporting measures immediately before the translation to E+ IDF. + + There is an implicit contract that this method should NOT be modifying your model in a way that would produce + different results, meaning it should only add or modify reporting-related elements + (eg: OutputTableSummaryReports, OutputControlFiles, etc) + + If you mean to modify the model in a significant way, use a `ModelMeasure` + NOTE: this method will ONLY be called if you use the C++ CLI, not the `classic` (Ruby) one + """ + if runner.getBoolArgumentValue("add_output_json", user_arguments): + output_json = model.getOutputJSON() + output_json.setOptionType("TimeSeriesAndTabular") + output_json.setOutputJSON(True) + output_json.setOutputCBOR(False) + output_json.setOutputMessagePack(False) + + return True + + def energyPlusOutputRequests( + self, runner: openstudio.measure.OSRunner, user_arguments: openstudio.measure.OSArgumentMap + ) -> openstudio.IdfObjectVector: + """Returns a vector of IdfObject's to request EnergyPlus objects needed by the run method. + + This is done after ForwardTranslation to IDF, and there is a list of accepted objects. + """ + super().energyPlusOutputRequests(runner, user_arguments) # Do **NOT** remove this line + + result = openstudio.IdfObjectVector() + + # To use the built-in error checking we need the model... + # get the last model and sql file + model = runner.lastOpenStudioModel() + if not model.is_initialized(): + runner.registerError("Cannot find last model.") + return False + + model = model.get() + + # use the built-in error checking + if not runner.validateUserArguments(self.arguments(model), user_arguments): + return False + + if runner.getBoolArgumentValue("report_drybulb_temp", user_arguments): + request = openstudio.IdfObject.load( + "Output:Variable, , Site Outdoor Air Drybulb Temperature, Hourly;" + ).get() + result.append(request) + + return result + + def run( + self, + runner: openstudio.measure.OSRunner, + user_arguments: openstudio.measure.OSArgumentMap, + ) -> bool: + """Defines what happens when the measure is run.""" + super().run(runner, user_arguments) + + # get the last model and sql file + model = runner.lastOpenStudioModel() + if not model.is_initialized(): + runner.registerError("Cannot find last model.") + return False + model = model.get() + + # use the built-in error checking (need model) + if not runner.validateUserArguments(self.arguments(model), user_arguments): + return False + + # load sql file + sql_file = runner.lastEnergyPlusSqlFile() + if not sql_file.is_initialized(): + runner.registerError("Cannot find last sql file.") + return False + + sql_file = sql_file.get() + model.setSqlFile(sql_file) + # write html file: any file named 'report*.*' in the current working directory + # will be copied to the ./reports/ folder as 'reports/_.html' + Path("report_python_with.txt").write_text("OK") + + # Close the sql file + sql_file.close() + + return True + + +# register the measure to be used by the application +PythonReportingMeasureWithModelOutputRequests().registerWithApplication() diff --git a/resources/workflow/reporting_modeloutputrequests/measures/PythonReportingMeasureWithModelOutputRequests/measure.xml b/resources/workflow/reporting_modeloutputrequests/measures/PythonReportingMeasureWithModelOutputRequests/measure.xml new file mode 100644 index 0000000000..a795798819 --- /dev/null +++ b/resources/workflow/reporting_modeloutputrequests/measures/PythonReportingMeasureWithModelOutputRequests/measure.xml @@ -0,0 +1,83 @@ + + + 3.1 + python_reporting_measure_with_model_output_requests + 90b5aa6c-a23d-4be3-b24a-a695d27e3848 + 5ddc1769-f3f2-466f-9272-617a0cd080d9 + 2025-03-14T16:35:06Z + 1DA817AF + PythonReportingMeasureWithModelOutputRequests + Reporting Measure with Model Output Requests + A new reporting measure that has a modelOutputRequests + This was added at 3.10.0 + + + report_drybulb_temp + Add output variables for Drybulb Temperature + Will add drybulb temp and report min/max values in html. + Boolean + true + false + true + + + true + true + + + false + false + + + + + add_output_json + Request JSON output + Will add Output:JSON with TimeSeriesAndTabular and set Output JSON to true + Boolean + true + false + true + + + true + true + + + false + false + + + + + + + + Reporting.QAQC + + + + Measure Type + ReportingMeasure + string + + + Measure Language + Python + string + + + + + + OpenStudio + 3.10.0 + 3.10.0 + + measure.py + py + script + C198FABF + + + diff --git a/resources/workflow/reporting_modeloutputrequests/measures/PythonReportingMeasureWithoutModelOutputRequests/measure.py b/resources/workflow/reporting_modeloutputrequests/measures/PythonReportingMeasureWithoutModelOutputRequests/measure.py new file mode 100644 index 0000000000..1d72c8b819 --- /dev/null +++ b/resources/workflow/reporting_modeloutputrequests/measures/PythonReportingMeasureWithoutModelOutputRequests/measure.py @@ -0,0 +1,135 @@ +"""insert your copyright here. + +# see the URL below for information on how to write OpenStudio measures +# http://nrel.github.io/OpenStudio-user-documentation/reference/measure_writing_guide/ +""" + +from pathlib import Path + +import openstudio + + +class PythonReportingMeasureWithoutModelOutputRequests(openstudio.measure.ReportingMeasure): + """An ReportingMeasure.""" + + def name(self): + """Returns the human readable name. + + Measure name should be the title case of the class name. + The measure name is the first contact a user has with the measure; + it is also shared throughout the measure workflow, visible in the OpenStudio Application, + PAT, Server Management Consoles, and in output reports. + As such, measure names should clearly describe the measure's function, + while remaining general in nature + """ + return "Reporting Measure without Model Output Requests" + + def description(self): + """Human readable description. + + The measure description is intended for a general audience and should not assume + that the reader is familiar with the design and construction practices suggested by the measure. + """ + return "An older reporting measure that does not have modelOutputRequests" + + def modeler_description(self): + """Human readable description of modeling approach. + + The modeler description is intended for the energy modeler using the measure. + It should explain the measure's intent, and include any requirements about + how the baseline model must be set up, major assumptions made by the measure, + and relevant citations or references to applicable modeling resources + """ + return "This is a measure that was added before 3.10.0" + + def arguments(self, model: openstudio.model.Model): + """Prepares user arguments for the measure. + + Measure arguments define which -- if any -- input parameters the user may set before running the measure. + """ + args = openstudio.measure.OSArgumentVector() + + report_drybulb_temp = openstudio.measure.OSArgument.makeBoolArgument("report_drybulb_temp", True) + report_drybulb_temp.setDisplayName("Add output variables for Drybulb Temperature") + report_drybulb_temp.setDescription("Will add drybulb temp and report min/max values in html.") + report_drybulb_temp.setValue(True) + args.append(report_drybulb_temp) + + return args + + def outputs(self): + """Define the outputs that the measure will create.""" + outs = openstudio.measure.OSOutputVector() + + # this measure does not produce machine readable outputs with registerValue, return an empty list + + return outs + + def energyPlusOutputRequests( + self, runner: openstudio.measure.OSRunner, user_arguments: openstudio.measure.OSArgumentMap + ): + """Returns a vector of IdfObject's to request EnergyPlus objects needed by the run method.""" + super().energyPlusOutputRequests(runner, user_arguments) # Do **NOT** remove this line + + result = openstudio.IdfObjectVector() + + # To use the built-in error checking we need the model... + # get the last model and sql file + model = runner.lastOpenStudioModel() + if not model.is_initialized(): + runner.registerError("Cannot find last model.") + return False + + model = model.get() + + # use the built-in error checking + if not runner.validateUserArguments(self.arguments(model), user_arguments): + return False + + if runner.getBoolArgumentValue("report_drybulb_temp", user_arguments): + request = openstudio.IdfObject.load( + "Output:Variable, , Site Outdoor Air Drybulb Temperature, Hourly;" + ).get() + result.append(request) + + return result + + def run( + self, + runner: openstudio.measure.OSRunner, + user_arguments: openstudio.measure.OSArgumentMap, + ): + """Defines what happens when the measure is run.""" + super().run(runner, user_arguments) + + # get the last model and sql file + model = runner.lastOpenStudioModel() + if not model.is_initialized(): + runner.registerError("Cannot find last model.") + return False + model = model.get() + + # use the built-in error checking (need model) + if not runner.validateUserArguments(self.arguments(model), user_arguments): + return False + + # load sql file + sql_file = runner.lastEnergyPlusSqlFile() + if not sql_file.is_initialized(): + runner.registerError("Cannot find last sql file.") + return False + + sql_file = sql_file.get() + model.setSqlFile(sql_file) + # write file: any file named 'report*.*' in the current working directory + # will be copied to the ./reports/ folder as 'reports/_.html' + Path("report_python_without.txt").write_text("OK") + + # Close the sql file + sql_file.close() + + return True + + +# register the measure to be used by the application +PythonReportingMeasureWithoutModelOutputRequests().registerWithApplication() diff --git a/resources/workflow/reporting_modeloutputrequests/measures/PythonReportingMeasureWithoutModelOutputRequests/measure.xml b/resources/workflow/reporting_modeloutputrequests/measures/PythonReportingMeasureWithoutModelOutputRequests/measure.xml new file mode 100644 index 0000000000..b9eafa5ed5 --- /dev/null +++ b/resources/workflow/reporting_modeloutputrequests/measures/PythonReportingMeasureWithoutModelOutputRequests/measure.xml @@ -0,0 +1,63 @@ + + + 3.1 + python_reporting_measure_without_model_output_requests + f1cbb7b8-e068-417e-8b2f-505f8441ab14 + 06e75776-b04e-4eef-9f53-33a5c5d54eb5 + 2025-03-14T16:33:26Z + 1DA817AF + PythonReportingMeasureWithoutModelOutputRequests + Reporting Measure without Model Output Requests + An older reporting measure that does not have modelOutputRequests + This is a measure that was added before 3.10.0 + + + report_drybulb_temp + Add output variables for Drybulb Temperature + Will add drybulb temp and report min/max values in html. + Boolean + true + false + + + true + true + + + false + false + + + + + + + + Reporting.QAQC + + + + Measure Type + ReportingMeasure + string + + + Measure Language + Python + string + + + + + + OpenStudio + 3.9.0 + 3.9.0 + + measure.py + py + script + B01FB240 + + + diff --git a/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithModelOutputRequests/measure.rb b/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithModelOutputRequests/measure.rb new file mode 100644 index 0000000000..93b8438ec7 --- /dev/null +++ b/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithModelOutputRequests/measure.rb @@ -0,0 +1,145 @@ +# insert your copyright here + +# see the URL below for information on how to write OpenStudio measures +# http://nrel.github.io/OpenStudio-user-documentation/reference/measure_writing_guide/ + +require 'erb' + +# start the measure +class RubyReportingMeasureWithModelOutputRequests < OpenStudio::Measure::ReportingMeasure + # human readable name + def name + # Measure name should be the title case of the class name. + return 'Reporting Measure with Model Output Requests' + end + + # human readable description + def description + return 'A new reporting measure that has a modelOutputRequests' + end + + # human readable description of modeling approach + def modeler_description + return 'This was added at 3.10.0' + end + + # define the arguments that the user will input + def arguments(model = nil) + args = OpenStudio::Measure::OSArgumentVector.new + + # bool argument to report report_drybulb_temp + report_drybulb_temp = OpenStudio::Measure::OSArgument.makeBoolArgument('report_drybulb_temp', true) + report_drybulb_temp.setDisplayName('Add output variables for Drybulb Temperature') + report_drybulb_temp.setDescription('Will add drybulb temp and report min/mix value in html.') + report_drybulb_temp.setDefaultValue(true) + args << report_drybulb_temp + + add_output_json = OpenStudio::Measure::OSArgument.makeBoolArgument('add_output_json', true) + add_output_json.setDisplayName('Request JSON output') + add_output_json.setDescription('Will add Output:JSON with TimeSeriesAndTabular and set Output JSON to true') + add_output_json.setDefaultValue(true) + args << add_output_json + + return args + end + + # define the outputs that the measure will create + def outputs + outs = OpenStudio::Measure::OSOutputVector.new + + # this measure does not produce machine readable outputs with registerValue, return an empty list + + return outs + end + + # This method is called on all reporting measures immediately before the translation to E+ IDF + # There is an implicit contract that this method should NOT be modifying your model in a way that would produce + # different results, meaning it should only add or modify reporting-related elements + # (eg: OutputTableSummaryReports, OutputControlFiles, etc) + # If you mean to modify the model in a significant way, use a `ModelMeasure` + # NOTE: this method will ONLY be called if you use the C++ CLI, not the `classic` (Ruby) one + def modelOutputRequests(model, runner, user_arguments) + if runner.getBoolArgumentValue('add_output_json', user_arguments) + output_json = model.getOutputJSON + output_json.setOptionType('TimeSeriesAndTabular') + output_json.setOutputJSON(true) + output_json.setOutputCBOR(false) + output_json.setOutputMessagePack(false) + end + + return true + end + + # return a vector of IdfObject's to request EnergyPlus objects needed by the run method + # Warning: Do not change the name of this method to be snake_case. The method must be lowerCamelCase. + # This is done after ForwardTranslation to IDF, and there is a list of + # accepted objects + def energyPlusOutputRequests(runner, user_arguments) + super(runner, user_arguments) # Do **NOT** remove this line + + result = OpenStudio::IdfObjectVector.new + + # To use the built-in error checking we need the model... + # get the last model and sql file + model = runner.lastOpenStudioModel + if model.empty? + runner.registerError('Cannot find last model.') + return false + end + model = model.get + + # use the built-in error checking + if !runner.validateUserArguments(arguments(model), user_arguments) + return false + end + + # NOTE: this should rather be done in modelOutputRequests + if runner.getBoolArgumentValue('report_drybulb_temp', user_arguments) + request = OpenStudio::IdfObject.load('Output:Variable,,Site Outdoor Air Drybulb Temperature,Hourly;').get + result << request + end + + return result + end + + # define what happens when the measure is run + def run(runner, user_arguments) + super(runner, user_arguments) + + # get the last model and sql file + model = runner.lastOpenStudioModel + if model.empty? + runner.registerError('Cannot find last model.') + return false + end + model = model.get + + # use the built-in error checking (need model) + if !runner.validateUserArguments(arguments(model), user_arguments) + return false + end + + # get measure arguments + report_drybulb_temp = runner.getBoolArgumentValue('report_drybulb_temp', user_arguments) + + # load sql file + sql_file = runner.lastEnergyPlusSqlFile + if sql_file.empty? + runner.registerError('Cannot find last sql file.') + return false + end + sql_file = sql_file.get + model.setSqlFile(sql_file) + # write file: any file named 'report*.*' in the current working directory + # will be copied to the ./reports/ folder as 'reports/_.html' + File.write("./report_ruby_with.txt", "OK") + + # close the sql file + sql_file.close + + return true + end +end + +# register the measure to be used by the application +RubyReportingMeasureWithModelOutputRequests.new.registerWithApplication diff --git a/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithModelOutputRequests/measure.xml b/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithModelOutputRequests/measure.xml new file mode 100644 index 0000000000..c16f7f29ad --- /dev/null +++ b/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithModelOutputRequests/measure.xml @@ -0,0 +1,83 @@ + + + 3.1 + ruby_reporting_measure_with_model_output_requests + 6663879f-2582-42e8-967d-b209507f1721 + dd1da652-30c6-4364-b2ff-f0495084c519 + 2025-03-14T16:33:47Z + 58F01CAA + RubyReportingMeasureWithModelOutputRequests + Reporting Measure with Model Output Requests + A new reporting measure that has a modelOutputRequests + This was added at 3.10.0 + + + report_drybulb_temp + Add output variables for Drybulb Temperature + Will add drybulb temp and report min/mix value in html. + Boolean + true + false + true + + + true + true + + + false + false + + + + + add_output_json + Request JSON output + Will add Output:JSON with TimeSeriesAndTabular and set Output JSON to true + Boolean + true + false + true + + + true + true + + + false + false + + + + + + + + Reporting.QAQC + + + + Measure Type + ReportingMeasure + string + + + Measure Language + Ruby + string + + + + + + OpenStudio + 3.10.0 + 3.10.0 + + measure.rb + rb + script + 1D5BDDE8 + + + diff --git a/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithoutModelOutputRequests/measure.rb b/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithoutModelOutputRequests/measure.rb new file mode 100644 index 0000000000..20ebab8f80 --- /dev/null +++ b/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithoutModelOutputRequests/measure.rb @@ -0,0 +1,93 @@ +# insert your copyright here + +# see the URL below for information on how to write OpenStudio measures +# http://nrel.github.io/OpenStudio-user-documentation/reference/measure_writing_guide/ + +require 'erb' + +# start the measure +class RubyReportingMeasureWithoutModelOutputRequests < OpenStudio::Measure::ReportingMeasure + # human readable name + def name + # Measure name should be the title case of the class name. + return 'Reporting Measure without Model Output Requests' + end + + # human readable description + def description + return 'An older reporting measure that does not have modelOutputRequests' + end + + # human readable description of modeling approach + def modeler_description + return 'This is a measure that was added before 3.10.0' + end + + # define the arguments that the user will input + def arguments(model = nil) + args = OpenStudio::Measure::OSArgumentVector.new + + # bool argument to report report_drybulb_temp + report_drybulb_temp = OpenStudio::Measure::OSArgument.makeBoolArgument('report_drybulb_temp', true) + report_drybulb_temp.setDisplayName('Add output variables for Drybulb Temperature') + report_drybulb_temp.setDescription('Will add drybulb temp and report min/mix value in html.') + report_drybulb_temp.setValue(true) + args << report_drybulb_temp + + return args + end + + # define the outputs that the measure will create + def outputs + outs = OpenStudio::Measure::OSOutputVector.new + + # this measure does not produce machine readable outputs with registerValue, return an empty list + + return outs + end + + # return a vector of IdfObject's to request EnergyPlus objects needed by the run method + # Warning: Do not change the name of this method to be snake_case. The method must be lowerCamelCase. + def energyPlusOutputRequests(runner, user_arguments) + super(runner, user_arguments) # Do **NOT** remove this line + + result = OpenStudio::IdfObjectVector.new + + # To use the built-in error checking we need the model... + # get the last model and sql file + model = runner.lastOpenStudioModel + if model.empty? + runner.registerError('Cannot find last model.') + return false + end + model = model.get + + # use the built-in error checking (need model) + if !runner.validateUserArguments(arguments(model), user_arguments) + return false + end + + # get measure arguments + report_drybulb_temp = runner.getBoolArgumentValue('report_drybulb_temp', user_arguments) + + # load sql file + sql_file = runner.lastEnergyPlusSqlFile + if sql_file.empty? + runner.registerError('Cannot find last sql file.') + return false + end + sql_file = sql_file.get + model.setSqlFile(sql_file) + # write file: any file named 'report*.*' in the current working directory + # will be copied to the ./reports/ folder as 'reports/_.html' + File.write("./report_ruby_without.txt", "OK") + + # close the sql file + sql_file.close + + return true + end +end + +# register the measure to be used by the application +RubyReportingMeasureWithoutModelOutputRequests.new.registerWithApplication diff --git a/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithoutModelOutputRequests/measure.xml b/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithoutModelOutputRequests/measure.xml new file mode 100644 index 0000000000..9885040fbd --- /dev/null +++ b/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithoutModelOutputRequests/measure.xml @@ -0,0 +1,63 @@ + + + 3.1 + ruby_reporting_measure_without_model_output_requests + 7fe3d1eb-2118-4cac-91e3-94fd489b97af + 0339fec9-7f58-4e81-8239-d8423927bcdb + 2025-03-14T16:33:21Z + 58F01CAA + RubyReportingMeasureWithoutModelOutputRequests + Reporting Measure without Model Output Requests + An older reporting measure that does not have modelOutputRequests + This is a measure that was added before 3.10.0 + + + report_drybulb_temp + Add output variables for Drybulb Temperature + Will add drybulb temp and report min/mix value in html. + Boolean + true + false + + + true + true + + + false + false + + + + + + + + Reporting.QAQC + + + + Measure Type + ReportingMeasure + string + + + Measure Language + Ruby + string + + + + + + OpenStudio + 3.9.0 + 3.9.0 + + measure.rb + rb + script + B60F2759 + + + diff --git a/resources/workflow/reporting_modeloutputrequests/python.osw b/resources/workflow/reporting_modeloutputrequests/python.osw new file mode 100644 index 0000000000..8a480fd325 --- /dev/null +++ b/resources/workflow/reporting_modeloutputrequests/python.osw @@ -0,0 +1,12 @@ +{ + "weather_file": "../../Examples/compact_osw/files/srrl_2013_amy.epw", + "seed_file": "../example_model.osm", + "steps": [ + { + "measure_dir_name": "PythonReportingMeasureWithModelOutputRequests" + }, + { + "measure_dir_name": "PythonReportingMeasureWithoutModelOutputRequests" + } + ] +} diff --git a/resources/workflow/reporting_modeloutputrequests/ruby.osw b/resources/workflow/reporting_modeloutputrequests/ruby.osw new file mode 100644 index 0000000000..32a008fa2c --- /dev/null +++ b/resources/workflow/reporting_modeloutputrequests/ruby.osw @@ -0,0 +1,12 @@ +{ + "weather_file": "../../Examples/compact_osw/files/srrl_2013_amy.epw", + "seed_file": "../example_model.osm", + "steps": [ + { + "measure_dir_name": "RubyReportingMeasureWithModelOutputRequests" + }, + { + "measure_dir_name": "RubyReportingMeasureWithoutModelOutputRequests" + } + ] +} diff --git a/src/cli/CMakeLists.txt b/src/cli/CMakeLists.txt index 74c557322a..152c06c4e3 100644 --- a/src/cli/CMakeLists.txt +++ b/src/cli/CMakeLists.txt @@ -358,6 +358,11 @@ if(BUILD_TESTING) WORKING_DIRECTORY "${PROJECT_BINARY_DIR}/resources/workflow/output_test/" ) + add_test(NAME OpenStudioCLI.test_reporting_modeloutputrequests + COMMAND ${Python_EXECUTABLE} -m pytest --verbose --os-cli-path $ "${CMAKE_CURRENT_SOURCE_DIR}/test/test_reporting_modeloutputrequests.py" + WORKING_DIRECTORY "${PROJECT_BINARY_DIR}/resources/workflow/reporting_modeloutputrequests/" + ) + file(MAKE_DIRECTORY "${PROJECT_BINARY_DIR}/Testing/") add_test(NAME OpenStudioCLI.test_bcl_measure_templates COMMAND ${Python_EXECUTABLE} -m pytest --verbose --os-cli-path $ "${CMAKE_CURRENT_SOURCE_DIR}/test/test_bcl_measure_templates.py" diff --git a/src/cli/test/test_reporting_modeloutputrequests.py b/src/cli/test/test_reporting_modeloutputrequests.py new file mode 100644 index 0000000000..d593c63381 --- /dev/null +++ b/src/cli/test/test_reporting_modeloutputrequests.py @@ -0,0 +1,29 @@ +import pytest + +from workflow_helpers import run_workflow + +@pytest.mark.parametrize( + "language, is_labs", + [ + ["ruby", False], + ["ruby", True], + # ["python", False], // Not possible + ["python", True], + ], +) + +def test_reportingmeasure_model_output_requests(osclipath, language: str, is_labs: bool): + suffix = "labs" if is_labs else "classic" + base_osw_name = f"{language}.osw" + + runDir, r = run_workflow( + osclipath=osclipath, + base_osw_name=base_osw_name, + suffix=suffix, + is_labs=is_labs, + verbose=False, + debug=True, + post_process_only=False, + ) + r.check_returncode() + assert r.returncode == 0 From 6eac3be4e6a78aa56bcd5cc586af5cb2c324fbe0 Mon Sep 17 00:00:00 2001 From: Julien Marrec Date: Fri, 14 Mar 2025 17:42:11 +0100 Subject: [PATCH 08/11] Do NOT return False in energyPlusOutputRequest when something goes wrong! you should return a IdfObjectVector to match SWIG prototype --- .../measure.py | 4 ++-- .../measure.xml | 6 +++--- .../measure.py | 4 ++-- .../measure.xml | 6 +++--- .../RubyReportingMeasureWithModelOutputRequests/measure.rb | 4 ++-- .../RubyReportingMeasureWithModelOutputRequests/measure.xml | 6 +++--- .../measure.rb | 4 ++-- .../measure.xml | 6 +++--- src/utilities/bcl/templates/ReportingMeasure/measure.py | 4 ++-- src/utilities/bcl/templates/ReportingMeasure/measure.rb | 4 ++-- 10 files changed, 24 insertions(+), 24 deletions(-) diff --git a/resources/workflow/reporting_modeloutputrequests/measures/PythonReportingMeasureWithModelOutputRequests/measure.py b/resources/workflow/reporting_modeloutputrequests/measures/PythonReportingMeasureWithModelOutputRequests/measure.py index 1c13a60d59..c84386501d 100644 --- a/resources/workflow/reporting_modeloutputrequests/measures/PythonReportingMeasureWithModelOutputRequests/measure.py +++ b/resources/workflow/reporting_modeloutputrequests/measures/PythonReportingMeasureWithModelOutputRequests/measure.py @@ -111,13 +111,13 @@ def energyPlusOutputRequests( model = runner.lastOpenStudioModel() if not model.is_initialized(): runner.registerError("Cannot find last model.") - return False + return result model = model.get() # use the built-in error checking if not runner.validateUserArguments(self.arguments(model), user_arguments): - return False + return result if runner.getBoolArgumentValue("report_drybulb_temp", user_arguments): request = openstudio.IdfObject.load( diff --git a/resources/workflow/reporting_modeloutputrequests/measures/PythonReportingMeasureWithModelOutputRequests/measure.xml b/resources/workflow/reporting_modeloutputrequests/measures/PythonReportingMeasureWithModelOutputRequests/measure.xml index a795798819..5000676580 100644 --- a/resources/workflow/reporting_modeloutputrequests/measures/PythonReportingMeasureWithModelOutputRequests/measure.xml +++ b/resources/workflow/reporting_modeloutputrequests/measures/PythonReportingMeasureWithModelOutputRequests/measure.xml @@ -3,8 +3,8 @@ 3.1 python_reporting_measure_with_model_output_requests 90b5aa6c-a23d-4be3-b24a-a695d27e3848 - 5ddc1769-f3f2-466f-9272-617a0cd080d9 - 2025-03-14T16:35:06Z + 735c4950-ce2a-49da-bec9-d08a55e06496 + 2025-03-14T16:42:28Z 1DA817AF PythonReportingMeasureWithModelOutputRequests Reporting Measure with Model Output Requests @@ -77,7 +77,7 @@ measure.py py script - C198FABF + 70ECC2B3 diff --git a/resources/workflow/reporting_modeloutputrequests/measures/PythonReportingMeasureWithoutModelOutputRequests/measure.py b/resources/workflow/reporting_modeloutputrequests/measures/PythonReportingMeasureWithoutModelOutputRequests/measure.py index 1d72c8b819..ceec071ac7 100644 --- a/resources/workflow/reporting_modeloutputrequests/measures/PythonReportingMeasureWithoutModelOutputRequests/measure.py +++ b/resources/workflow/reporting_modeloutputrequests/measures/PythonReportingMeasureWithoutModelOutputRequests/measure.py @@ -78,13 +78,13 @@ def energyPlusOutputRequests( model = runner.lastOpenStudioModel() if not model.is_initialized(): runner.registerError("Cannot find last model.") - return False + return result model = model.get() # use the built-in error checking if not runner.validateUserArguments(self.arguments(model), user_arguments): - return False + return result if runner.getBoolArgumentValue("report_drybulb_temp", user_arguments): request = openstudio.IdfObject.load( diff --git a/resources/workflow/reporting_modeloutputrequests/measures/PythonReportingMeasureWithoutModelOutputRequests/measure.xml b/resources/workflow/reporting_modeloutputrequests/measures/PythonReportingMeasureWithoutModelOutputRequests/measure.xml index b9eafa5ed5..3fe627fb3b 100644 --- a/resources/workflow/reporting_modeloutputrequests/measures/PythonReportingMeasureWithoutModelOutputRequests/measure.xml +++ b/resources/workflow/reporting_modeloutputrequests/measures/PythonReportingMeasureWithoutModelOutputRequests/measure.xml @@ -3,8 +3,8 @@ 3.1 python_reporting_measure_without_model_output_requests f1cbb7b8-e068-417e-8b2f-505f8441ab14 - 06e75776-b04e-4eef-9f53-33a5c5d54eb5 - 2025-03-14T16:33:26Z + 26cc30f8-03b9-4f32-a48c-6106c078e625 + 2025-03-14T16:42:43Z 1DA817AF PythonReportingMeasureWithoutModelOutputRequests Reporting Measure without Model Output Requests @@ -57,7 +57,7 @@ measure.py py script - B01FB240 + 9AA7CFE7 diff --git a/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithModelOutputRequests/measure.rb b/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithModelOutputRequests/measure.rb index 93b8438ec7..1c8a28e3f9 100644 --- a/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithModelOutputRequests/measure.rb +++ b/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithModelOutputRequests/measure.rb @@ -84,13 +84,13 @@ def energyPlusOutputRequests(runner, user_arguments) model = runner.lastOpenStudioModel if model.empty? runner.registerError('Cannot find last model.') - return false + return result end model = model.get # use the built-in error checking if !runner.validateUserArguments(arguments(model), user_arguments) - return false + return result end # NOTE: this should rather be done in modelOutputRequests diff --git a/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithModelOutputRequests/measure.xml b/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithModelOutputRequests/measure.xml index c16f7f29ad..2894e36f80 100644 --- a/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithModelOutputRequests/measure.xml +++ b/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithModelOutputRequests/measure.xml @@ -3,8 +3,8 @@ 3.1 ruby_reporting_measure_with_model_output_requests 6663879f-2582-42e8-967d-b209507f1721 - dd1da652-30c6-4364-b2ff-f0495084c519 - 2025-03-14T16:33:47Z + e19b1fb5-bb69-4e22-aeba-e805359bb686 + 2025-03-14T16:42:34Z 58F01CAA RubyReportingMeasureWithModelOutputRequests Reporting Measure with Model Output Requests @@ -77,7 +77,7 @@ measure.rb rb script - 1D5BDDE8 + 09DAD053 diff --git a/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithoutModelOutputRequests/measure.rb b/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithoutModelOutputRequests/measure.rb index 20ebab8f80..9677cf2b24 100644 --- a/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithoutModelOutputRequests/measure.rb +++ b/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithoutModelOutputRequests/measure.rb @@ -58,13 +58,13 @@ def energyPlusOutputRequests(runner, user_arguments) model = runner.lastOpenStudioModel if model.empty? runner.registerError('Cannot find last model.') - return false + return result end model = model.get # use the built-in error checking (need model) if !runner.validateUserArguments(arguments(model), user_arguments) - return false + return result end # get measure arguments diff --git a/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithoutModelOutputRequests/measure.xml b/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithoutModelOutputRequests/measure.xml index 9885040fbd..522de8baf7 100644 --- a/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithoutModelOutputRequests/measure.xml +++ b/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithoutModelOutputRequests/measure.xml @@ -3,8 +3,8 @@ 3.1 ruby_reporting_measure_without_model_output_requests 7fe3d1eb-2118-4cac-91e3-94fd489b97af - 0339fec9-7f58-4e81-8239-d8423927bcdb - 2025-03-14T16:33:21Z + e1ce6548-a004-4697-9c10-090eee9f413b + 2025-03-14T16:42:49Z 58F01CAA RubyReportingMeasureWithoutModelOutputRequests Reporting Measure without Model Output Requests @@ -57,7 +57,7 @@ measure.rb rb script - B60F2759 + 3605AEE1 diff --git a/src/utilities/bcl/templates/ReportingMeasure/measure.py b/src/utilities/bcl/templates/ReportingMeasure/measure.py index bdb1a13044..abcfe383a6 100644 --- a/src/utilities/bcl/templates/ReportingMeasure/measure.py +++ b/src/utilities/bcl/templates/ReportingMeasure/measure.py @@ -111,13 +111,13 @@ def energyPlusOutputRequests( model = runner.lastOpenStudioModel() if not model.is_initialized(): runner.registerError("Cannot find last model.") - return False + return result model = model.get() # use the built-in error checking if not runner.validateUserArguments(self.arguments(model), user_arguments): - return False + return result if runner.getBoolArgumentValue("report_drybulb_temp", user_arguments): request = openstudio.IdfObject.load( diff --git a/src/utilities/bcl/templates/ReportingMeasure/measure.rb b/src/utilities/bcl/templates/ReportingMeasure/measure.rb index befcd778eb..ab773e0a9a 100644 --- a/src/utilities/bcl/templates/ReportingMeasure/measure.rb +++ b/src/utilities/bcl/templates/ReportingMeasure/measure.rb @@ -84,13 +84,13 @@ def energyPlusOutputRequests(runner, user_arguments) model = runner.lastOpenStudioModel if model.empty? runner.registerError('Cannot find last model.') - return false + return result end model = model.get # use the built-in error checking if !runner.validateUserArguments(arguments(model), user_arguments) - return false + return result end # NOTE: this should rather be done in modelOutputRequests From efe63f29849a9c58eb7420b8e710cf6bc0523d3c Mon Sep 17 00:00:00 2001 From: Julien Marrec Date: Fri, 14 Mar 2025 17:52:04 +0100 Subject: [PATCH 09/11] I botched my measures doing copy paste --- .../measure.py | 2 +- .../measure.xml | 6 ++-- .../measure.rb | 6 ++-- .../measure.xml | 6 ++-- .../measure.rb | 28 +++++++++++++++++++ .../measure.xml | 6 ++-- 6 files changed, 41 insertions(+), 13 deletions(-) diff --git a/resources/workflow/reporting_modeloutputrequests/measures/PythonReportingMeasureWithModelOutputRequests/measure.py b/resources/workflow/reporting_modeloutputrequests/measures/PythonReportingMeasureWithModelOutputRequests/measure.py index c84386501d..c9eed01cdf 100644 --- a/resources/workflow/reporting_modeloutputrequests/measures/PythonReportingMeasureWithModelOutputRequests/measure.py +++ b/resources/workflow/reporting_modeloutputrequests/measures/PythonReportingMeasureWithModelOutputRequests/measure.py @@ -154,7 +154,7 @@ def run( sql_file = sql_file.get() model.setSqlFile(sql_file) - # write html file: any file named 'report*.*' in the current working directory + # write file: any file named 'report*.*' in the current working directory # will be copied to the ./reports/ folder as 'reports/_.html' Path("report_python_with.txt").write_text("OK") diff --git a/resources/workflow/reporting_modeloutputrequests/measures/PythonReportingMeasureWithModelOutputRequests/measure.xml b/resources/workflow/reporting_modeloutputrequests/measures/PythonReportingMeasureWithModelOutputRequests/measure.xml index 5000676580..36bc0f8031 100644 --- a/resources/workflow/reporting_modeloutputrequests/measures/PythonReportingMeasureWithModelOutputRequests/measure.xml +++ b/resources/workflow/reporting_modeloutputrequests/measures/PythonReportingMeasureWithModelOutputRequests/measure.xml @@ -3,8 +3,8 @@ 3.1 python_reporting_measure_with_model_output_requests 90b5aa6c-a23d-4be3-b24a-a695d27e3848 - 735c4950-ce2a-49da-bec9-d08a55e06496 - 2025-03-14T16:42:28Z + 398d6d2e-3240-4390-83b4-2477193285eb + 2025-03-14T16:51:11Z 1DA817AF PythonReportingMeasureWithModelOutputRequests Reporting Measure with Model Output Requests @@ -77,7 +77,7 @@ measure.py py script - 70ECC2B3 + 0B936742 diff --git a/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithModelOutputRequests/measure.rb b/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithModelOutputRequests/measure.rb index 1c8a28e3f9..6ff83d7f65 100644 --- a/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithModelOutputRequests/measure.rb +++ b/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithModelOutputRequests/measure.rb @@ -110,13 +110,13 @@ def run(runner, user_arguments) model = runner.lastOpenStudioModel if model.empty? runner.registerError('Cannot find last model.') - return false + return result end model = model.get # use the built-in error checking (need model) if !runner.validateUserArguments(arguments(model), user_arguments) - return false + return result end # get measure arguments @@ -126,7 +126,7 @@ def run(runner, user_arguments) sql_file = runner.lastEnergyPlusSqlFile if sql_file.empty? runner.registerError('Cannot find last sql file.') - return false + return result end sql_file = sql_file.get model.setSqlFile(sql_file) diff --git a/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithModelOutputRequests/measure.xml b/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithModelOutputRequests/measure.xml index 2894e36f80..4f34e86feb 100644 --- a/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithModelOutputRequests/measure.xml +++ b/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithModelOutputRequests/measure.xml @@ -3,8 +3,8 @@ 3.1 ruby_reporting_measure_with_model_output_requests 6663879f-2582-42e8-967d-b209507f1721 - e19b1fb5-bb69-4e22-aeba-e805359bb686 - 2025-03-14T16:42:34Z + 70a45976-2ac0-43a6-9853-b6fae76f5908 + 2025-03-14T16:51:16Z 58F01CAA RubyReportingMeasureWithModelOutputRequests Reporting Measure with Model Output Requests @@ -77,7 +77,7 @@ measure.rb rb script - 09DAD053 + 1E726765 diff --git a/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithoutModelOutputRequests/measure.rb b/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithoutModelOutputRequests/measure.rb index 9677cf2b24..85dde9808d 100644 --- a/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithoutModelOutputRequests/measure.rb +++ b/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithoutModelOutputRequests/measure.rb @@ -48,6 +48,8 @@ def outputs # return a vector of IdfObject's to request EnergyPlus objects needed by the run method # Warning: Do not change the name of this method to be snake_case. The method must be lowerCamelCase. + # This is done after ForwardTranslation to IDF, and there is a list of + # accepted objects def energyPlusOutputRequests(runner, user_arguments) super(runner, user_arguments) # Do **NOT** remove this line @@ -62,6 +64,32 @@ def energyPlusOutputRequests(runner, user_arguments) end model = model.get + # use the built-in error checking + if !runner.validateUserArguments(arguments(model), user_arguments) + return result + end + + # NOTE: this should rather be done in modelOutputRequests + if runner.getBoolArgumentValue('report_drybulb_temp', user_arguments) + request = OpenStudio::IdfObject.load('Output:Variable,,Site Outdoor Air Drybulb Temperature,Hourly;').get + result << request + end + + return result + end + + # define what happens when the measure is run + def run(runner, user_arguments) + super(runner, user_arguments) + + # get the last model and sql file + model = runner.lastOpenStudioModel + if model.empty? + runner.registerError('Cannot find last model.') + return result + end + model = model.get + # use the built-in error checking (need model) if !runner.validateUserArguments(arguments(model), user_arguments) return result diff --git a/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithoutModelOutputRequests/measure.xml b/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithoutModelOutputRequests/measure.xml index 522de8baf7..47d8bccdcb 100644 --- a/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithoutModelOutputRequests/measure.xml +++ b/resources/workflow/reporting_modeloutputrequests/measures/RubyReportingMeasureWithoutModelOutputRequests/measure.xml @@ -3,8 +3,8 @@ 3.1 ruby_reporting_measure_without_model_output_requests 7fe3d1eb-2118-4cac-91e3-94fd489b97af - e1ce6548-a004-4697-9c10-090eee9f413b - 2025-03-14T16:42:49Z + d07158b5-8a23-44ea-85d0-dd234c9f9cb9 + 2025-03-14T16:51:24Z 58F01CAA RubyReportingMeasureWithoutModelOutputRequests Reporting Measure without Model Output Requests @@ -57,7 +57,7 @@ measure.rb rb script - 3605AEE1 + B3C25E43 From 90c936a4267d28ac37e1abfb64a6e5b9449892cb Mon Sep 17 00:00:00 2001 From: Julien Marrec Date: Fri, 14 Mar 2025 17:56:57 +0100 Subject: [PATCH 10/11] clang format --- src/measure/ReportingMeasure.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/measure/ReportingMeasure.cpp b/src/measure/ReportingMeasure.cpp index f1b93ce64e..710217e640 100644 --- a/src/measure/ReportingMeasure.cpp +++ b/src/measure/ReportingMeasure.cpp @@ -14,7 +14,7 @@ namespace openstudio { namespace measure { - ReportingMeasure::ReportingMeasure() : OSMeasure(MeasureType::ReportingMeasure) {}; + ReportingMeasure::ReportingMeasure() : OSMeasure(MeasureType::ReportingMeasure){}; std::vector ReportingMeasure::arguments(const openstudio::model::Model& /*model*/) const { return {}; From 678ee71ba651e3a9037abc9185537b3479c37563 Mon Sep 17 00:00:00 2001 From: Julien Marrec Date: Fri, 14 Mar 2025 17:59:34 +0100 Subject: [PATCH 11/11] Shush cppcheck --- python/engine/PythonEngine.cpp | 3 +++ ruby/engine/RubyEngine.cpp | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/python/engine/PythonEngine.cpp b/python/engine/PythonEngine.cpp index 940a27a315..ad53b802ce 100644 --- a/python/engine/PythonEngine.cpp +++ b/python/engine/PythonEngine.cpp @@ -430,10 +430,13 @@ bool PythonEngine::hasMethod(ScriptObject& methodObject, std::string_view method // equivalent to getattr(instance_obj.__class__, method_name) == getattr(instance_obj.__class__.__bases__[0], method_name) PyTypeObject* class_type = Py_TYPE(val.obj_); // PyObject_Type returns a strong (New) reference, not needed for us + // cppcheck-suppress cstyleCast PyObject* class_method = PyObject_GetAttrString((PyObject*)class_type, methodName.data()); // New reference assert(class_type->tp_base != nullptr); + // cppcheck-suppress cstyleCast auto* base = (PyTypeObject*)class_type->tp_base; + // cppcheck-suppress cstyleCast PyObject* base_method = PyObject_GetAttrString((PyObject*)base, methodName.data()); // New reference bool result = class_method != base_method; diff --git a/ruby/engine/RubyEngine.cpp b/ruby/engine/RubyEngine.cpp index cb594a272e..26e338efda 100644 --- a/ruby/engine/RubyEngine.cpp +++ b/ruby/engine/RubyEngine.cpp @@ -251,8 +251,8 @@ bool RubyEngine::hasMethod(ScriptObject& methodObject, std::string_view methodNa // Instead, this is equivalent to `instance_obj.class.instance_methods(false).include?(:methodName)` VALUE klass = rb_obj_class(val); // include_methods_from_ancestors: false; - VALUE argv[1] = {Qfalse}; - VALUE methods_without_ancestors = rb_class_instance_methods(1, argv, klass); + VALUE args[1] = {Qfalse}; + VALUE methods_without_ancestors = rb_class_instance_methods(1, args, klass); return rb_ary_includes(methods_without_ancestors, ID2SYM(method_id)) == Qtrue; }