diff --git a/resources/Examples/compact_osw/measures/Standard Reports/measure.rb b/resources/Examples/compact_osw/measures/Standard Reports/measure.rb
index b03ff76c0c4..34254c95600 100644
--- a/resources/Examples/compact_osw/measures/Standard Reports/measure.rb
+++ b/resources/Examples/compact_osw/measures/Standard Reports/measure.rb
@@ -16,6 +16,12 @@ def arguments(model = nil)
return args
end #end the arguments method
+ def outputs
+ result = OpenStudio::Measure::OSOutputVector.new
+ result << OpenStudio::Measure::OSOutput.makeDoubleOutput('net_site_energy', false)
+ return result
+ end
+
#define what happens when the measure is run
def run(runner, user_arguments)
super(runner, user_arguments)
@@ -130,6 +136,9 @@ def run(runner, user_arguments)
end
end
+ runner.registerValue("net_site_energy", "Net Site Energy", sqlFile.netSiteEnergy.get, "GJ")
+ runner.registerValue("something!with.invalid_chars_", "Test Sanitizing", 1, "")
+
#closing the sql file
sqlFile.close()
diff --git a/resources/Examples/compact_osw/update_seb_model.rb b/resources/Examples/compact_osw/update_seb_model.rb
new file mode 100644
index 00000000000..a497af28cfc
--- /dev/null
+++ b/resources/Examples/compact_osw/update_seb_model.rb
@@ -0,0 +1,14 @@
+# Just for convenience: having to VT the seb model which is at 1.11.5
+# can be very time consuming, especially on debug builds
+
+require 'openstudio'
+
+model_path = File.join(__dir__, 'files/seb.osm')
+raise "#{model_path} not found" if !File.file?(model_path)
+
+# Starting at 3.7.0, no need to explicitly call vt, but still doing it
+# m = OpenStudio::Model::Model::load(model_path).get()
+vt = OpenStudio::OSVersion::VersionTranslator.new
+OpenStudio::Logger.instance.standardOutLogger.setLogLevel(OpenStudio::Debug)
+m = vt.loadModel(model_path).get()
+m.save(model_path, true)
diff --git a/resources/Examples/with_analysis/analysis.json b/resources/Examples/with_analysis/analysis.json
new file mode 100644
index 00000000000..3276efcd156
--- /dev/null
+++ b/resources/Examples/with_analysis/analysis.json
@@ -0,0 +1,50 @@
+{
+ "analysis": {
+ "display_name": "Test With Analysis JSON",
+ "name": "analysis_json",
+ "output_variables": [
+ {
+ "objective_function": true,
+ "name": "FakeReport.net_site_energy",
+ "objective_function_index": 0,
+ "objective_function_target": 0,
+ "objective_function_group": 1,
+ "scaling_factor": 1.0,
+ "display_name": "Net Site Energy, should be there",
+ "display_name_short": "net_site_energy",
+ "metadata_id": null,
+ "visualize": true,
+ "export": true,
+ "variable_type": "double"
+ },
+ {
+ "objective_function": false,
+ "name": "FakeReport.net_site_energy",
+ "objective_function_index": 1,
+ "objective_function_target": 0,
+ "objective_function_group": 1,
+ "scaling_factor": 1.0,
+ "display_name": "net_site_energy",
+ "display_name_short": "net_site_energy",
+ "metadata_id": null,
+ "visualize": true,
+ "export": true,
+ "variable_type": "double"
+ },
+ {
+ "objective_function": true,
+ "name": "IsNonExisting.NonExisting",
+ "objective_function_index": 2,
+ "objective_function_target": 0,
+ "objective_function_group": 1,
+ "scaling_factor": 1.0,
+ "display_name": "net_site_energy",
+ "display_name_short": "net_site_energy",
+ "metadata_id": null,
+ "visualize": true,
+ "export": true,
+ "variable_type": "double"
+ }
+ ]
+ }
+}
diff --git a/resources/Examples/with_analysis/local/measures/FakeReport/measure.rb b/resources/Examples/with_analysis/local/measures/FakeReport/measure.rb
new file mode 100644
index 00000000000..1cbbede8e44
--- /dev/null
+++ b/resources/Examples/with_analysis/local/measures/FakeReport/measure.rb
@@ -0,0 +1,43 @@
+require 'openstudio'
+
+class FakeReport < OpenStudio::Measure::ReportingMeasure
+
+ def name
+ return "Fake Report"
+ end
+
+ #define the arguments that the user will input
+ def arguments(model = nil)
+ args = OpenStudio::Measure::OSArgumentVector.new
+
+ return args
+ end #end the arguments method
+
+ def outputs
+ result = OpenStudio::Measure::OSOutputVector.new
+ result << OpenStudio::Measure::OSOutput.makeDoubleOutput('net_site_energy', false)
+ return result
+ end
+
+ #define what happens when the measure is run
+ def run(runner, user_arguments)
+ super(runner, user_arguments)
+
+ #use the built-in error checking
+ if not runner.validateUserArguments(arguments(), user_arguments)
+ return false
+ end
+
+ # Register some constant values, so we can only do it during post
+ # processing
+ runner.registerValue("net_site_energy", "Net Site Energy", 167.1, "GJ")
+ runner.registerValue("something!with.invalid_chars_", "Test Sanitizing", 1, "")
+ runner.registerFinalCondition("Goodbye.")
+
+ return true
+
+ end
+
+end
+
+FakeReport.new.registerWithApplication
diff --git a/resources/Examples/with_analysis/local/measures/FakeReport/measure.xml b/resources/Examples/with_analysis/local/measures/FakeReport/measure.xml
new file mode 100644
index 00000000000..4723800902f
--- /dev/null
+++ b/resources/Examples/with_analysis/local/measures/FakeReport/measure.xml
@@ -0,0 +1,57 @@
+
+
+ 3.1
+ fake_report
+ ca6ba362-ea57-4236-b803-17e37b0c0817
+ 32617b1d-91b1-4325-9a3a-0708b9853d29
+ 2023-11-13T17:08:41Z
+ B2AD275E
+ FakeReport
+ Fake Report
+ Change me
+ Change me
+
+
+
+
+
+
+ Reporting.QAQC
+
+
+
+ Measure Type
+ ReportingMeasure
+ string
+
+
+ Measure Language
+ Ruby
+ string
+
+
+ Uses SketchUp API
+ false
+ boolean
+
+
+
+
+
+ OpenStudio
+ 1.1.2
+ 1.1.2
+
+ measure.rb
+ rb
+ script
+ 4CFB5158
+
+
+
diff --git a/resources/Examples/with_analysis/local/with_analysis.osw b/resources/Examples/with_analysis/local/with_analysis.osw
new file mode 100644
index 00000000000..45f7c791883
--- /dev/null
+++ b/resources/Examples/with_analysis/local/with_analysis.osw
@@ -0,0 +1,8 @@
+{
+ "steps": [
+ {
+ "measure_dir_name": "FakeReport",
+ "arguments": {}
+ }
+ ]
+}
diff --git a/src/cli/CMakeLists.txt b/src/cli/CMakeLists.txt
index dced056736a..8f9db4837de 100644
--- a/src/cli/CMakeLists.txt
+++ b/src/cli/CMakeLists.txt
@@ -83,6 +83,12 @@ endif()
if(BUILD_TESTING)
+ # Just for convenience: having to VT the seb model which is at 1.11.5 can be very time consuming, especially on debug builds
+ add_custom_target(update_seb_osm_in_build_dir
+ COMMAND $ execute_ruby_script "${PROJECT_BINARY_DIR}/resources/Examples/compact_osw/update_seb_model.rb"
+ DEPENDS openstudio
+ )
+
add_test(NAME OpenStudioCLI.help
COMMAND $ --help
)
@@ -242,6 +248,12 @@ if(BUILD_TESTING)
add_test(NAME OpenStudioCLI.test_loglevel
COMMAND ${Python_EXECUTABLE} -m pytest --verbose ${Pytest_XDIST_OPTS} --os-cli-path $ "${CMAKE_CURRENT_SOURCE_DIR}/test/test_loglevel.py"
)
+
+ # No Xdist on purpose here
+ add_test(NAME OpenStudioCLI.test_with_analysis
+ COMMAND ${Python_EXECUTABLE} -m pytest --verbose --os-cli-path $ "${CMAKE_CURRENT_SOURCE_DIR}/test/test_with_analysis.py"
+ WORKING_DIRECTORY "${PROJECT_BINARY_DIR}/resources/Examples/with_analysis/local/"
+ )
else()
# TODO: Remove. Fallback on these for now, as I don't know if CI has pytest installed
add_test(NAME OpenStudioCLI.Classic.test_logger_rb
diff --git a/src/cli/test/test_with_analysis.py b/src/cli/test/test_with_analysis.py
new file mode 100644
index 00000000000..a61e1058736
--- /dev/null
+++ b/src/cli/test/test_with_analysis.py
@@ -0,0 +1,84 @@
+import json
+import subprocess
+from pathlib import Path
+
+import pytest
+
+
+@pytest.mark.parametrize(
+ "is_labs",
+ [pytest.param(True, id="labs"), pytest.param(False, id="classic")],
+)
+def test_run_with_analysis(osclipath, is_labs: bool):
+ base_osw_path = Path("with_analysis.osw").resolve()
+ assert base_osw_path.is_file(), f"{base_osw_path=} is not found"
+
+ osw = json.loads(base_osw_path.read_text())
+ suffix = "labs" if is_labs else "classic"
+ osw_path = base_osw_path.parent / f"with_analysis_{suffix}.osw"
+ runDir = base_osw_path.parent / f"run_{suffix}"
+ osw["run_directory"] = str(runDir)
+ runDir.mkdir(exist_ok=True)
+ with open(osw_path, "w") as f:
+ json.dump(osw, fp=f, indent=2, sort_keys=True)
+
+ # Fake having an in.idf or it won't run in the "classic" subcommand, doing it for labs too so that it's less
+ # confusing
+ # if not is_labs:
+ with open(runDir / "in.idf", "w") as f:
+ f.write("Building,;")
+
+ command = [str(osclipath)]
+ if not is_labs:
+ command.append("classic")
+ command += ["run", "--postprocess_only", "-w", str(osw_path)]
+ lines = subprocess.check_output(command, encoding="utf-8").splitlines()
+
+ assert runDir.exists()
+ measure_attributes_path = runDir / "measure_attributes.json"
+ assert measure_attributes_path.is_file()
+ results_path = runDir / "results.json"
+ assert results_path.is_file()
+ objectives_path = runDir / "objectives.json"
+ assert objectives_path.is_file()
+
+ measure_attributes = json.loads(measure_attributes_path.read_text())
+ assert measure_attributes == {
+ "FakeReport": {"applicable": True, "net_site_energy": 167.1, "something_with_invalid_chars": 1}
+ }
+
+ results = json.loads(results_path.read_text())
+ assert results == {"FakeReport": {"applicable": True, "net_site_energy": 167.1, "something_with_invalid_chars": 1}}
+
+ objectives = json.loads(objectives_path.read_text())
+ assert objectives == {
+ "objective_function_1": 167.1,
+ "objective_function_3": 1.7976931348623157e308,
+ "objective_function_group_1": 1.0,
+ "objective_function_group_3": None,
+ "objective_function_target_1": 0.0,
+ "objective_function_target_3": None,
+ "scaling_factor_1": 1.0,
+ "scaling_factor_3": None,
+ }
+
+ data_point_out_path = runDir / "data_point_out.json"
+ assert data_point_out_path.is_file()
+ data_point_out = json.loads(data_point_out_path.read_text())
+ assert data_point_out == {
+ "FakeReport": {"applicable": True, "net_site_energy": 167.1, "something_with_invalid_chars": 1}
+ }
+
+ expected_files_in_run_dir = {
+ "data_point.zip",
+ "finished.job",
+ "in.idf",
+ "measure_attributes.json",
+ "objectives.json",
+ "results.json",
+ "run.log",
+ "started.job",
+ "data_point_out.json",
+ }
+
+ assert set([x.name for x in runDir.glob("*")]) == expected_files_in_run_dir
diff --git a/src/utilities/filetypes/RunOptions.cpp b/src/utilities/filetypes/RunOptions.cpp
index 9c1aabb7cce..affdfcfd006 100644
--- a/src/utilities/filetypes/RunOptions.cpp
+++ b/src/utilities/filetypes/RunOptions.cpp
@@ -50,6 +50,10 @@ namespace detail {
root["skip_energyplus_preprocess"] = m_skipEnergyPlusPreprocess;
}
+ if (!m_is_skipZipResults_defaulted) {
+ root["skip_zip_results"] = m_skipZipResults;
+ }
+
if (!m_is_cleanup_defaulted) {
root["cleanup"] = m_cleanup;
}
@@ -200,6 +204,27 @@ namespace detail {
onUpdate();
}
+ bool RunOptions_Impl::skipZipResults() const {
+ return m_skipZipResults;
+ }
+
+ bool RunOptions_Impl::isSkipZipResultsDefaulted() const {
+ return m_is_skipZipResults_defaulted;
+ }
+
+ bool RunOptions_Impl::setSkipZipResults(bool skipZipResults) {
+ m_skipZipResults = skipZipResults;
+ m_is_skipZipResults_defaulted = false;
+ onUpdate();
+ return true;
+ }
+
+ void RunOptions_Impl::resetSkipZipResults() {
+ m_skipZipResults = DEFAULT_SKIPZIPRESULTS;
+ m_is_skipZipResults_defaulted = true;
+ onUpdate();
+ }
+
bool RunOptions_Impl::cleanup() const {
return m_cleanup;
}
@@ -277,6 +302,10 @@ namespace detail {
setSkipEnergyPlusPreprocess(other.skipEnergyPlusPreprocess());
}
+ if (!other.isSkipZipResultsDefaulted()) {
+ setSkipZipResults(other.skipZipResults());
+ }
+
if (!other.isCleanupDefaulted()) {
setCleanup(other.cleanup());
}
@@ -382,6 +411,10 @@ boost::optional RunOptions::fromString(const std::string& s) {
result.setSkipEnergyPlusPreprocess(value["skip_energyplus_preprocess"].asBool());
}
+ if (value.isMember("skip_zip_results") && value["skip_zip_results"].isBool()) {
+ result.setSkipZipResults(value["skip_zip_results"].asBool());
+ }
+
if (value.isMember("output_adapter")) {
Json::Value outputAdapter = value["output_adapter"];
if (outputAdapter.isMember("custom_file_name") && outputAdapter.isMember("class_name")) {
@@ -504,6 +537,22 @@ void RunOptions::resetSkipEnergyPlusPreprocess() {
getImpl()->resetSkipEnergyPlusPreprocess();
}
+bool RunOptions::skipZipResults() const {
+ return getImpl()->skipZipResults();
+}
+
+bool RunOptions::isSkipZipResultsDefaulted() const {
+ return getImpl()->isSkipZipResultsDefaulted();
+}
+
+bool RunOptions::setSkipZipResults(bool skipZipResults) {
+ return getImpl()->setSkipZipResults(skipZipResults);
+}
+
+void RunOptions::resetSkipZipResults() {
+ getImpl()->resetSkipZipResults();
+}
+
bool RunOptions::cleanup() const {
return getImpl()->cleanup();
}
diff --git a/src/utilities/filetypes/RunOptions.hpp b/src/utilities/filetypes/RunOptions.hpp
index 83623e97af4..1705c5627fe 100644
--- a/src/utilities/filetypes/RunOptions.hpp
+++ b/src/utilities/filetypes/RunOptions.hpp
@@ -62,16 +62,19 @@ class UTILITIES_API RunOptions
/// Serialize to JSON formatted string
std::string string() const;
+ /** Print debugging messages, defaults to false */
bool debug() const;
bool isDebugDefaulted() const;
bool setDebug(bool debug);
void resetDebug();
+ /** Create, export and run using epjson format. Default is false */
bool epjson() const;
bool isEpjsonDefaulted() const;
bool setEpjson(bool epjson);
void resetEpjson();
+ /** Speeds up workflow by skipping steps not needed for running simulations, defaults to false */
bool fast() const;
bool isFastDefaulted() const;
bool setFast(bool fast);
@@ -82,16 +85,27 @@ class UTILITIES_API RunOptions
bool setPreserveRunDir(bool preserveRunDir);
void resetPreserveRunDir();
+ /** Skips the call to the EnergyPlus ExpandObjects program, defaults to false */
bool skipExpandObjects() const;
bool isSkipExpandObjectsDefaulted() const;
bool setSkipExpandObjects(bool skipExpandObjects);
void resetSkipExpandObjects();
+ /** Does not add add default output requests to EnergyPlus input if true.
+ * Requests from reporting measures are added in either case, defaults to false */
bool skipEnergyPlusPreprocess() const;
bool isSkipEnergyPlusPreprocessDefaulted() const;
bool setSkipEnergyPlusPreprocess(bool skipEnergyPlusPreprocess);
void resetSkipEnergyPlusPreprocess();
+ /** Skips creating the data_point.zip file. Setting to `true` can cause issues with workflows expecting .zip files to signal completion
+ * (e.g., OpenStudio Analysis Framework), defaults to false */
+ bool skipZipResults() const;
+ bool isSkipZipResultsDefaulted() const;
+ bool setSkipZipResults(bool skipZipResults);
+ void resetSkipZipResults();
+
+ /** Remove unnecessary files during post processing, defaults to true */
bool cleanup() const;
bool isCleanupDefaulted() const;
bool setCleanup(bool cleanup);
diff --git a/src/utilities/filetypes/RunOptions_Impl.hpp b/src/utilities/filetypes/RunOptions_Impl.hpp
index db961e45999..9c97d07cf7e 100644
--- a/src/utilities/filetypes/RunOptions_Impl.hpp
+++ b/src/utilities/filetypes/RunOptions_Impl.hpp
@@ -66,6 +66,11 @@ namespace detail {
bool setCleanup(bool cleanup);
void resetCleanup();
+ bool skipZipResults() const;
+ bool isSkipZipResultsDefaulted() const;
+ bool setSkipZipResults(bool skipZipResults);
+ void resetSkipZipResults();
+
boost::optional customOutputAdapter() const;
bool setCustomOutputAdapter(const CustomOutputAdapter& adapter);
void resetCustomOutputAdapter();
@@ -94,6 +99,7 @@ namespace detail {
static constexpr bool DEFAULT_SKIPEXPANDOBJECTS = false;
static constexpr bool DEFAULT_SKIPENERGYPLUSPREPROCESS = false;
static constexpr bool DEFAULT_CLEANUP = true;
+ static constexpr bool DEFAULT_SKIPZIPRESULTS = false;
bool m_debug = DEFAULT_DEBUG;
bool m_is_debug_defaulted = true;
@@ -117,6 +123,9 @@ namespace detail {
bool m_cleanup = DEFAULT_CLEANUP;
bool m_is_cleanup_defaulted = true;
+ bool m_skipZipResults = DEFAULT_SKIPZIPRESULTS;
+ bool m_is_skipZipResults_defaulted = true;
+
ForwardTranslatorOptions m_forwardTranslatorOptions;
boost::optional m_customOutputAdapter;
};
diff --git a/src/utilities/filetypes/test/WorkflowJSON_GTest.cpp b/src/utilities/filetypes/test/WorkflowJSON_GTest.cpp
index 5028f4bd578..d88b5b8247a 100644
--- a/src/utilities/filetypes/test/WorkflowJSON_GTest.cpp
+++ b/src/utilities/filetypes/test/WorkflowJSON_GTest.cpp
@@ -1236,6 +1236,19 @@ TEST(Filetypes, RunOptions_GettersSetters) {
ASSERT_TRUE(runOptions.isSkipEnergyPlusPreprocessDefaulted());
ASSERT_TRUE(runOptions.isSkipEnergyPlusPreprocessDefaulted());
+ // Ctor Default
+ ASSERT_FALSE(runOptions.skipZipResults());
+ ASSERT_TRUE(runOptions.isSkipZipResultsDefaulted());
+ // Set to opposite of default
+ ASSERT_TRUE(runOptions.setSkipZipResults(true));
+ ASSERT_TRUE(runOptions.skipZipResults());
+ ASSERT_FALSE(runOptions.isSkipZipResultsDefaulted());
+ // Reset
+ runOptions.resetSkipZipResults();
+ ASSERT_FALSE(runOptions.skipZipResults());
+ ASSERT_TRUE(runOptions.isSkipZipResultsDefaulted());
+ ASSERT_TRUE(runOptions.isSkipZipResultsDefaulted());
+
// Ctor Default
ASSERT_TRUE(runOptions.cleanup());
ASSERT_TRUE(runOptions.isCleanupDefaulted());
@@ -1362,6 +1375,8 @@ TEST(Filetypes, RunOptions_overrideValuesWith) {
ASSERT_TRUE(runOptions.isSkipExpandObjectsDefaulted());
ASSERT_FALSE(runOptions.skipEnergyPlusPreprocess());
ASSERT_TRUE(runOptions.isSkipEnergyPlusPreprocessDefaulted());
+ ASSERT_FALSE(runOptions.skipZipResults());
+ ASSERT_TRUE(runOptions.isSkipZipResultsDefaulted());
ASSERT_TRUE(runOptions.cleanup());
ASSERT_TRUE(runOptions.isCleanupDefaulted());
@@ -1401,6 +1416,8 @@ TEST(Filetypes, RunOptions_overrideValuesWith) {
ASSERT_TRUE(runOptions.isSkipExpandObjectsDefaulted());
ASSERT_FALSE(runOptions.skipEnergyPlusPreprocess());
ASSERT_TRUE(runOptions.isSkipEnergyPlusPreprocessDefaulted());
+ ASSERT_FALSE(runOptions.skipEnergyPlusPreprocess());
+ ASSERT_TRUE(runOptions.isSkipEnergyPlusPreprocessDefaulted());
ASSERT_TRUE(runOptions.cleanup());
ASSERT_TRUE(runOptions.isCleanupDefaulted());
diff --git a/src/workflow/ApplyMeasure.cpp b/src/workflow/ApplyMeasure.cpp
index ecce2a7d008..84968e30c3d 100644
--- a/src/workflow/ApplyMeasure.cpp
+++ b/src/workflow/ApplyMeasure.cpp
@@ -86,6 +86,9 @@ void OSWorkflow::applyMeasures(MeasureType measureType, bool energyplus_output_r
runner.incrementStep();
result.setStepResult(StepResult::Skip);
}
+
+ // Technically here I would need to have gotten className from the measure to match workflow-gem, just to set applicable = false
+ output_attributes[step.name().value_or(measureDirName)]["applicable"] = openstudio::Variant(false);
}
continue;
}
@@ -303,9 +306,7 @@ end
// if doing output requests we are done now
if (!energyplus_output_requests) {
WorkflowStepResult result = runner.result();
- if (auto stepResult_ = result.stepResult()) {
- LOG(Debug, "Step Result: " << stepResult_->valueName());
- }
+
// incrementStep must be called after run
runner.incrementStep();
if (auto errors = result.stepErrors(); !errors.empty()) {
@@ -313,6 +314,21 @@ end
throw std::runtime_error(fmt::format("Measure {} reported an error with [{}]", measureDirName, fmt::join(errors, "\n")));
}
+ const auto measureName = step.name().value_or(className);
+ auto& measureAttributes = output_attributes[measureName];
+ for (const auto& stepValue : result.stepValues()) {
+ measureAttributes[stepValue.name()] = stepValue.valueAsVariant();
+ }
+ auto stepResult_ = result.stepResult();
+ if (!stepResult_.has_value()) {
+ LOG_AND_THROW("Step Result not set for " << scriptPath_->generic_string());
+ }
+
+ // Add an applicability flag to all the measure results
+ const StepResult stepResult = std::move(*stepResult_);
+ LOG(Debug, "Step Result: " << stepResult.valueName());
+ measureAttributes["applicable"] = openstudio::Variant(!((stepResult == StepResult::NA) || (stepResult == StepResult::Skip)));
+
if (measureType == MeasureType::ModelMeasure) {
updateLastWeatherFileFromModel();
}
diff --git a/src/workflow/CMakeLists.txt b/src/workflow/CMakeLists.txt
index ef4ea3a9911..23e406f23ec 100644
--- a/src/workflow/CMakeLists.txt
+++ b/src/workflow/CMakeLists.txt
@@ -26,3 +26,17 @@ add_library(openstudio_workflow
)
target_link_libraries(openstudio_workflow PRIVATE openstudiolib)
+
+if(BUILD_TESTING)
+ set(openstudio_workflow_test_depends
+ openstudio_workflow
+ CONAN_PKG::boost # Maybe at some point replace with openstudiolib more simply
+ CONAN_PKG::fmt
+ )
+
+ set(openstudio_workflow_test_src
+ test/Util_GTest.cpp
+ )
+
+ CREATE_TEST_TARGETS(openstudio_workflow "${openstudio_workflow_test_src}" "${openstudio_workflow_test_depends}")
+endif()
diff --git a/src/workflow/OSWorkflow.cpp b/src/workflow/OSWorkflow.cpp
index 0ed93ab8bed..321506dd3c0 100644
--- a/src/workflow/OSWorkflow.cpp
+++ b/src/workflow/OSWorkflow.cpp
@@ -19,6 +19,7 @@
#include "../utilities/core/Assert.hpp"
#include "../utilities/core/Filesystem.hpp"
#include "../utilities/core/FileLogSink.hpp"
+#include "../utilities/core/Json.hpp"
#include "../utilities/core/Logger.hpp"
#include "../utilities/data/Variant.hpp"
#include "../utilities/filetypes/WorkflowStep.hpp"
@@ -30,9 +31,11 @@
#include
#include
#include
+#include
#include
#include
+#include
#include
#include
@@ -222,6 +225,8 @@ bool OSWorkflow::run() {
hasDeletedRunDir = true;
openstudio::filesystem::remove_all(runDirPath);
}
+ }
+ if (!openstudio::filesystem::is_directory(runDirPath)) {
openstudio::filesystem::create_directory(runDirPath);
}
FileLogSink logFile(runDirPath / "run.log");
@@ -396,13 +401,7 @@ bool OSWorkflow::run() {
}
if (!workflowJSON.runOptions()->fast()) {
- if (m_add_timings) {
- m_timers->newTimer("Zip datapoint");
- }
- openstudio::workflow::util::zipResults(runDirPath);
- if (m_add_timings) {
- m_timers->tockCurrentTimer();
- }
+ communicateResults();
}
if (state == State::Errored) {
@@ -455,4 +454,168 @@ bool OSWorkflow::run() {
}
return (state == State::Finished);
}
+
+Json::Value outputAttributesToJSON(const std::map>& output_attributes,
+ bool sanitize = false) {
+ Json::Value root(Json::objectValue);
+ for (const auto& [oriMeasureName, argMap] : output_attributes) {
+ const std::string measureName = sanitize ? openstudio::workflow::util::sanitizeKey(oriMeasureName) : oriMeasureName;
+ Json::Value measureValues(Json::objectValue);
+ for (const auto& [oriArgName, variantValue] : argMap) {
+ const std::string argName = sanitize ? openstudio::workflow::util::sanitizeKey(oriArgName) : oriArgName;
+ if (variantValue.variantType() == VariantType::String) {
+ measureValues[argName] = variantValue.valueAsString();
+ } else if (variantValue.variantType() == VariantType::Double) {
+ measureValues[argName] = variantValue.valueAsDouble();
+ } else if (variantValue.variantType() == VariantType::Integer) {
+ measureValues[argName] = variantValue.valueAsInteger();
+ } else if (variantValue.variantType() == VariantType::Boolean) {
+ measureValues[argName] = variantValue.valueAsBoolean();
+ }
+ }
+
+ root[measureName] = measureValues;
+ }
+ return root;
+}
+
+void OSWorkflow::communicateMeasureAttributes() const {
+
+ const Json::Value root = outputAttributesToJSON(output_attributes, false);
+ Json::StreamWriterBuilder wbuilder;
+ // mimic the old StyledWriter behavior:
+ wbuilder["indentation"] = " ";
+
+ const std::string result = Json::writeString(wbuilder, root);
+
+ auto jsonPath = workflowJSON.absoluteRunDir() / "measure_attributes.json";
+ openstudio::filesystem::ofstream file(jsonPath);
+ OS_ASSERT(file.is_open());
+ file << result;
+ file.close();
+}
+
+void OSWorkflow::runExtractInputsAndOutputs() const {
+ const Json::Value results = outputAttributesToJSON(output_attributes, true);
+ Json::StreamWriterBuilder wbuilder;
+ wbuilder["indentation"] = " ";
+
+ {
+ const std::string result = Json::writeString(wbuilder, results);
+
+ auto jsonPath = workflowJSON.absoluteRunDir() / "results.json";
+ openstudio::filesystem::ofstream file(jsonPath);
+ OS_ASSERT(file.is_open());
+ file << result;
+ file.close();
+ }
+
+ const auto osa_abs_path = workflowJSON.absoluteRootDir().parent_path() / "analysis.json";
+ if (!openstudio::filesystem::is_regular_file(osa_abs_path)) {
+ return;
+ }
+
+ std::ifstream ifs(openstudio::toSystemFilename(osa_abs_path));
+
+ Json::CharReaderBuilder rbuilder;
+ std::string formattedErrors;
+
+ Json::Value analysis_json;
+ const bool parsingSuccessful = Json::parseFromStream(rbuilder, ifs, &analysis_json, &formattedErrors);
+ if (!parsingSuccessful) {
+ LOG_AND_THROW("OSA Analysis JSON '" << toString(osa_abs_path) << "' cannot be processed, " << formattedErrors);
+ }
+
+ if (!openstudio::checkKeyAndType(analysis_json, "analysis", Json::objectValue)) {
+ return;
+ }
+
+ if (!openstudio::checkKeyAndType(analysis_json["analysis"], "output_variables", Json::arrayValue)) {
+ return;
+ }
+
+ Json::Value objectiveFunctions(Json::objectValue);
+
+ auto& outputVars = analysis_json["analysis"]["output_variables"];
+ for (const auto& variable : outputVars) {
+ if (openstudio::checkKeyAndType(variable, "objective_function", Json::booleanValue) && variable["objective_function"].asBool()) {
+ assertKeyAndType(variable, "name", Json::stringValue);
+ assertKeyAndType(variable, "objective_function_index", Json::intValue);
+ const std::string name = variable["name"].asString();
+ const int idx = variable["objective_function_index"].asInt() + 1;
+
+ LOG(Info, "Looking for objective function " << name);
+
+ // Splitting on a `.` feels very unrealiable
+ const size_t pos = name.find('.');
+ if (pos == std::string::npos) {
+ LOG(Warn, "Objective function name='" << name << "' does not contain a dot (`.`)");
+ continue;
+ }
+ const std::string measureName = name.substr(0, pos);
+ const std::string argName = name.substr(pos + 1);
+ if (results.isMember(measureName) && results[measureName].isMember(argName)) {
+ objectiveFunctions[fmt::format("objective_function_{}", idx)] = results[measureName][argName];
+
+ if (openstudio::checkKeyAndType(variable, "objective_function_target", Json::realValue)) {
+ LOG(Info, "Found objective function target for " << name);
+ objectiveFunctions[fmt::format("objective_function_target_{}", idx)] = variable["objective_function_target"].asDouble();
+ }
+
+ if (openstudio::checkKeyAndType(variable, "scaling_factor", Json::realValue)) {
+ LOG(Info, "Found scaling factor for " << name);
+ objectiveFunctions[fmt::format("scaling_factor_{}", idx)] = variable["scaling_factor"].asDouble();
+ }
+
+ if (openstudio::checkKeyAndType(variable, "objective_function_group", Json::realValue)) {
+ LOG(Info, "Found objective function group for " << name);
+ objectiveFunctions[fmt::format("objective_function_group_{}", idx)] = variable["objective_function_group"].asDouble();
+ }
+
+ } else {
+ LOG(Warn, "No results for objective function " << name);
+ objectiveFunctions[fmt::format("objective_function_{}", idx)] = std::numeric_limits::max();
+ objectiveFunctions[fmt::format("objective_function_target_{}", idx)] = Json::nullValue;
+ objectiveFunctions[fmt::format("scaling_factor_{}", idx)] = Json::nullValue;
+ objectiveFunctions[fmt::format("objective_function_group_{}", idx)] = Json::nullValue;
+ }
+ }
+ }
+
+ {
+ const std::string objectives = Json::writeString(wbuilder, objectiveFunctions);
+
+ auto objectivesJsonPath = workflowJSON.absoluteRunDir() / "objectives.json";
+ openstudio::filesystem::ofstream file(objectivesJsonPath);
+ OS_ASSERT(file.is_open());
+ file << objectives;
+ file.close();
+ }
+}
+
+void OSWorkflow::communicateResults() const {
+ if (!workflowJSON.runOptions()->skipZipResults()) {
+ if (m_add_timings) {
+ m_timers->newTimer("Zip datapoint");
+ }
+ openstudio::workflow::util::zipResults(workflowJSON.absoluteRunDir());
+ if (m_add_timings) {
+ m_timers->tockCurrentTimer();
+ }
+ }
+
+ const Json::Value root = outputAttributesToJSON(output_attributes, true);
+ Json::StreamWriterBuilder wbuilder;
+ // mimic the old StyledWriter behavior:
+ wbuilder["indentation"] = " ";
+
+ const std::string result = Json::writeString(wbuilder, root);
+
+ auto jsonPath = workflowJSON.absoluteRunDir() / "data_point_out.json";
+ openstudio::filesystem::ofstream file(jsonPath);
+ OS_ASSERT(file.is_open());
+ file << result;
+ file.close();
+}
+
} // namespace openstudio
diff --git a/src/workflow/OSWorkflow.hpp b/src/workflow/OSWorkflow.hpp
index e3ec4d86474..0a14f877ebc 100644
--- a/src/workflow/OSWorkflow.hpp
+++ b/src/workflow/OSWorkflow.hpp
@@ -15,6 +15,7 @@
#include "../utilities/filetypes/WorkflowJSON.hpp"
#include
+#include