diff --git a/.github/workflows/ci-cmake.yml b/.github/workflows/ci-cmake.yml index 9e487e24b..32c6cf83d 100644 --- a/.github/workflows/ci-cmake.yml +++ b/.github/workflows/ci-cmake.yml @@ -14,19 +14,19 @@ jobs: name: CMake runs-on: ${{ matrix.os }} env: - CC: gcc-${{ matrix.compiler_version }} + CC: gcc-${{ matrix.compiler_version }} CXX: g++-${{ matrix.compiler_version }} strategy: fail-fast: false matrix: - os: [ubuntu-18.04] + os: [ubuntu-20.04] build_type: [Debug, Release] - compiler_version: [7] + compiler_version: [9] shared_libs: [ON, OFF] timeout-minutes: 35 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Install prerequisites run: | sudo apt-get install -y --no-install-recommends \ @@ -61,7 +61,7 @@ jobs: cmake --build build --target install cmake --build build --target install-doc - name: Upload artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: libcosim-${{ runner.os }}-${{ matrix.build_type }}-gcc${{ matrix.compiler_version }} path: install @@ -79,11 +79,11 @@ jobs: timeout-minutes: 35 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Install prerequisites run: | pip3 install --upgrade setuptools pip - pip3 install conan + pip3 install conan==1.59 choco install doxygen.install conan remote add osp https://osp.jfrog.io/artifactory/api/conan/conan-local --force conan install . -s build_type=${{ matrix.build_type }} -o shared=${{ matrix.shared }} -g deploy @@ -110,7 +110,7 @@ jobs: cmake --build build --config ${{ matrix.build_type }} --target install cmake --build build --config ${{ matrix.build_type }} --target install-doc - name: Upload artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: libcosim-${{ runner.os }}-${{ matrix.build_type }} path: install diff --git a/.github/workflows/ci-conan.yml b/.github/workflows/ci-conan.yml index b86152eef..0d4e568ff 100644 --- a/.github/workflows/ci-conan.yml +++ b/.github/workflows/ci-conan.yml @@ -11,14 +11,14 @@ jobs: fail-fast: false matrix: build_type: [Debug, Release] - compiler_version: [7, 8, 9] + compiler_version: [9] compiler_libcxx: [libstdc++11] option_proxyfmu: ['proxyfmu=True', 'proxyfmu=False'] option_shared: ['shared=True', 'shared=False'] timeout-minutes: 35 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Generate Dockerfile run: | mkdir /tmp/osp-builder-docker @@ -81,11 +81,11 @@ jobs: timeout-minutes: 35 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Install prerequisites run: | pip3 install --upgrade setuptools pip - pip3 install conan + pip3 install conan==1.59 - name: Configure Conan run: conan remote add osp https://osp.jfrog.io/artifactory/api/conan/conan-local --force - name: Conan create diff --git a/CHANGELOG.md b/CHANGELOG.md index b9f59b4a4..a9aa3eb6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,27 @@ All notable changes to libcosim will be documented in this file. This includes n This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) +### [v0.10.2] - 2023-02-08 +##### Fixed +* Update to proxyfmu 0.3.1 due to a disconnection issue related to Thrift. + +### [v0.10.1] - 2022-12-08 +##### Changed +* Update to proxyfmu 0.3.0 due to downstream build issues related to Thrift. + +### [v0.10.0] - 2022-12-02 +##### Changed +* GCC7 and GCC8 artifact builds have been removed, and GCC9 artifact builds added. This is currently the only supported GCC version. +* The file observer is now programmatically configurable, and there is no longer a need for clients to create a separate `LogConfig.xml` file upfront to specify it's configuration. +* The file observer no longer outputs headers as `variable name [reference, type, causality].` Instead only the variable name is output, and the extra data about each variable and the model itself is output in a separate metadata file, in YAML format. This will have the same filename as the CSV file with the simulation data, including the timestamp, with `_metadata` at the end. +* Using `find_variable` no longer throws an exception, but rather returns an optional value, which may be empty if the variable could not be found. +* Update to proxyfmu 0.2.9. +##### Added +* An asynchronous version of `simulate_until` is now available through the `execution` interface. This accepts + an optional end time parameter and launches the execution in a new thread. +* Support has been added for optionally specifying simulation end time (where only start time was supported) in `OspSystemStructure.xml`. If specified, end time will also be parsed and made available in the simulation configuration. +##### Fixed + ### [v0.9.0] – 2022-04-05 ##### Changed * Removed fibers to simplify code and increase simulation performance. Concurrency must now be implemented in the master @@ -160,7 +181,7 @@ algorithm, and `fixed_step_algorithm` has been modified to use a thread pool. ([ * introducing`orchestration` interface for classes that resolve model URIs of one or more specific URI schemes ([PR#233](https://github.com/open-simulation-platform/cse-core/pull/233)) * logging configuration ([PR#247](https://github.com/open-simulation-platform/cse-core/pull/247)) * observers can observe string and boolean variables ([PR#257](https://github.com/open-simulation-platform/cse-core/pull/257)) -* can set arbitraty real time factor ([PR#261](https://github.com/open-simulation-platform/cse-core/pull/261)) +* can set arbitrary real time factor ([PR#261](https://github.com/open-simulation-platform/cse-core/pull/261)) * improved error reporting ##### Fixed @@ -193,3 +214,6 @@ First OSP JIP partner release [v0.8.2]: https://github.com/open-simulation-platform/cse-core/compare/v0.8.1...v0.8.2 [v0.8.3]: https://github.com/open-simulation-platform/cse-core/compare/v0.8.2...v0.8.3 [v0.9.0]: https://github.com/open-simulation-platform/cse-core/compare/v0.8.3...v0.9.0 +[v0.10.0]: https://github.com/open-simulation-platform/cse-core/compare/v0.9.0...v0.10.0 +[v0.10.1]: https://github.com/open-simulation-platform/cse-core/compare/v0.10.0...v0.10.1 +[v0.10.2]: https://github.com/open-simulation-platform/cse-core/compare/v0.10.1...v0.10.2 diff --git a/CMakeLists.txt b/CMakeLists.txt index f73f6aa2a..99da9ff02 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -99,7 +99,6 @@ set(LIBCOSIM_EXPORT_TARGET "${PROJECT_NAME}-targets") # ============================================================================== if(LIBCOSIM_USING_CONAN) - if(NOT LIBCOSIM_USING_CONAN_AUTO_CONFIG OR CONAN_EXPORTED) # Opting for manual invocation of conan install prior to loading CMake # or conan create has been invoked, setting CONAN_EXPORTED. @@ -163,7 +162,6 @@ endif() # ============================================================================== add_subdirectory("src") -add_subdirectory("tools/osp-xsd-embedder") if(LIBCOSIM_BUILD_TESTS) enable_testing() add_subdirectory("tests") diff --git a/README.md b/README.md index 7b1709117..beb5dfcdd 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ in the section below or you can use conan. As libcosim is made available as a conan package on https://osp.jfrog.io, you can include it in your application following these steps: -* Install [Conan] +* Install [Conan] version 1.x * Add the OSP Conan repository as a remote: conan remote add osp https://osp.jfrog.io/artifactory/api/conan/conan-local @@ -38,7 +38,7 @@ How to build * Compilers: [Visual Studio] >= 16.0/2019 (Windows), GCC >= 7 (Linux) * Build tool: [CMake] * API documentation generator (optional): [Doxygen] - * Package manager (optional): [Conan] + * Package manager (optional): [Conan] 1.x Throughout this guide, we will use Conan to manage dependencies. However, it should be possible to use other package managers as well, such as [vcpkg], and diff --git a/conanfile.py b/conanfile.py index a5a21b234..b79504356 100755 --- a/conanfile.py +++ b/conanfile.py @@ -53,7 +53,7 @@ def configure(self): def requirements(self): if self.options.proxyfmu: - self.requires("proxyfmu/0.2.7@osp/stable") + self.requires("proxyfmu/0.3.1@osp/stable") def imports(self): binDir = os.path.join("output", str(self.settings.build_type).lower(), "bin") diff --git a/data/xsd/OspSystemStructure.xsd b/data/xsd/OspSystemStructure.xsd index 01b4ba860..19ff4531d 100644 --- a/data/xsd/OspSystemStructure.xsd +++ b/data/xsd/OspSystemStructure.xsd @@ -10,6 +10,7 @@ + diff --git a/include/cosim/observer/file_observer.hpp b/include/cosim/observer/file_observer.hpp index 550bc2c29..6f9750687 100644 --- a/include/cosim/observer/file_observer.hpp +++ b/include/cosim/observer/file_observer.hpp @@ -13,8 +13,6 @@ #include #include -#include - #include #include #include @@ -23,6 +21,86 @@ namespace cosim { +class file_observer; + +/** + * Configuration options for file_observer. + */ +class file_observer_config +{ +public: + file_observer_config() = default; + + /** + * Specify whether or not generated .csv files should be timestamped + * + * \param flag timestamped if true (default) + * \return self + */ + file_observer_config& set_timestamped_filenames(bool flag) + { + timeStampedFileNames_ = flag; + return *this; + } + + /** + * Specify variables for a simulator to log + * + * \param simulatorName name of simulator to log + * \param variableNames a list of variable names to log (empty list means log all variables) + * \param decimationFactor optional decimation factor, where 1 (default) means log every step + * \return self + */ + file_observer_config& log_simulator_variables( + const std::string& simulatorName, + const std::vector& variableNames, + std::optional decimationFactor = std::nullopt) + { + variablesToLog_[simulatorName].first = decimationFactor.value_or(defaultDecimationFactor_); + for (const auto& variableName : variableNames) { + variablesToLog_[simulatorName].second.emplace_back(variableName); + } + return *this; + } + + /** + * Specify that we want to log all variables for a given simulator + * + * \param simulatorName name of simulator to log + * \param variableNames a list of variable names to log (empty list means log all variables) + * \param decimationFactor optional decimation factor, where 1 (default) means log every step + * \return self + */ + file_observer_config& log_all_simulator_variables( + const std::string& simulatorName, + std::optional decimationFactor = std::nullopt) + { + variablesToLog_[simulatorName].first = decimationFactor.value_or(defaultDecimationFactor_); + variablesToLog_[simulatorName].second = {}; // empty variable means log all + return *this; + } + + /** + * Creates a file_observer_config from an xml configuration + * + * \param configPath the path to an xml file containing the logging configuration. + * \return a file_observer_config + */ + static file_observer_config parse(const filesystem::path& configPath); + +private: + bool timeStampedFileNames_{true}; + size_t defaultDecimationFactor_{1}; + std::unordered_map>> variablesToLog_; + + [[nodiscard]] bool should_log_simulator(const std::string& name) const + { + return variablesToLog_.count(name); + } + + friend class file_observer; +}; + /** * An observer implementation, for saving observed variable values to file in csv format. @@ -36,8 +114,9 @@ class file_observer : public observer * Creates an observer which logs all variable values to file in csv format. * * \param logDir the directory where log files will be created. + * \param config an optional logging configuration. */ - file_observer(const cosim::filesystem::path& logDir); + explicit file_observer(const cosim::filesystem::path& logDir, std::optional config = std::nullopt); /** * Creates an observer which logs selected variable values to file in csv format. @@ -108,14 +187,12 @@ class file_observer : public observer class slave_value_writer; std::unordered_map> valueWriters_; std::unordered_map simulators_; - boost::property_tree::ptree ptree_; - cosim::filesystem::path configPath_; + std::optional config_; cosim::filesystem::path logDir_; - bool logFromConfig_ = false; - size_t defaultDecimationFactor_ = 1; std::atomic recording_ = true; }; } // namespace cosim -#endif // header guard + +#endif // COSIM_OBSERVER_FILE_OBSERVER_HPP diff --git a/include/cosim/osp_config_parser.hpp b/include/cosim/osp_config_parser.hpp index 9392919e4..f5167c56a 100644 --- a/include/cosim/osp_config_parser.hpp +++ b/include/cosim/osp_config_parser.hpp @@ -27,6 +27,9 @@ struct osp_config /// The default start time for a simulation time_point start_time; + /// The optional end time for a simulation + std::optional end_time = std::nullopt; + /// The default/recommended step size for a simulation duration step_size; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 262712302..7ff26a9bc 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -137,16 +137,13 @@ foreach(src IN LISTS generatedSources) list(APPEND generatedFiles "${tgt}") endforeach() -# Generate header file from OSP XSD -set(ospSystemStructureXSD "${CMAKE_SOURCE_DIR}/data/xsd/OspSystemStructure.xsd") -set(ospSystemStructureHeader "${generatedSourcesDir}/osp_system_structure_schema.hpp") -add_custom_command( - OUTPUT "${ospSystemStructureHeader}" - COMMAND osp-xsd-embedder "${ospSystemStructureXSD}" "${ospSystemStructureHeader}" - DEPENDS osp-xsd-embedder "${ospSystemStructureXSD}" -) -list(APPEND generatedFiles "${ospSystemStructureHeader}") +function(make_includable input_file output_file) + file(READ ${input_file} content) + set(content "const char* osp_xsd=R\"(${content})\";") + file(WRITE ${output_file} "${content}") +endfunction(make_includable) +make_includable("${CMAKE_SOURCE_DIR}/data/xsd/OspSystemStructure.xsd" "${generatedSourcesDir}/osp_system_structure_schema.hpp") # ============================================================================== # Target definition @@ -159,7 +156,7 @@ endforeach() add_library(cosim ${publicHeadersFull} ${privateHeaders} ${sources} ${generatedFiles}) -target_compile_definitions(cosim PUBLIC "BOOST_ALL_NO_LIB=0" "BOOST_CONFIG_SUPPRESS_OUTDATED_MESSAGE=1") +target_compile_definitions(cosim PUBLIC "BOOST_ALL_NO_LIB" "BOOST_CONFIG_SUPPRESS_OUTDATED_MESSAGE=1") if(NOT Boost_USE_STATIC_LIBS) target_compile_definitions(cosim PUBLIC "BOOST_ALL_DYN_LINK=1") endif() diff --git a/src/cosim/observer/file_observer.cpp b/src/cosim/observer/file_observer.cpp index d945f0b34..06328d83a 100644 --- a/src/cosim/observer/file_observer.cpp +++ b/src/cosim/observer/file_observer.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include namespace cosim @@ -74,7 +75,8 @@ class file_observer::slave_value_writer std::lock_guard lock(mutex_); if (recording_) { if (!fsw_.is_open()) { - create_log_file(); + auto dataFileName = create_log_file(); + create_metadata_file(dataFileName); } if (timeStep % decimationFactor_ == 0) { @@ -129,6 +131,8 @@ class file_observer::slave_value_writer } private: + int keyWidth_ = 14; + template void write(const std::vector& values, std::stringstream& ss) { @@ -159,7 +163,7 @@ class file_observer::slave_value_writer } } - /** Default constructor initialization, all variables are logged. */ + /** Default constructor initialization, all variables - except those with causality local - are logged. */ void initialize_default() { if (!timeStampedFileNames_) { @@ -187,14 +191,16 @@ class file_observer::slave_value_writer } } - void create_log_file() + std::string create_log_file() { std::string filename; std::stringstream ss; + std::string time_str; + if (!timeStampedFileNames_) { filename = observable_->name().append(".csv"); } else { - auto time_str = format_time(boost::posix_time::microsec_clock::local_time()); + time_str = format_time(boost::posix_time::microsec_clock::local_time()); filename = observable_->name().append("_").append(time_str).append(".csv"); } @@ -203,22 +209,25 @@ class file_observer::slave_value_writer fsw_.open(filePath, std::ios_base::out | std::ios_base::app); if (fsw_.fail()) { - throw std::runtime_error("Failed to open file stream for logging"); + std::stringstream error; + error << "Failed to open log file stream: " << filePath.c_str(); + throw std::runtime_error(error.str()); } ss << "Time,StepCount"; + // Add variable names for (const auto& vd : realVars_) { - ss << "," << vd.name << " [" << vd.reference << " " << vd.type << " " << vd.causality << "]"; + ss << "," << vd.name; } for (const auto& vd : intVars_) { - ss << "," << vd.name << " [" << vd.reference << " " << vd.type << " " << vd.causality << "]"; + ss << "," << vd.name; } for (const auto& vd : boolVars_) { - ss << "," << vd.name << " [" << vd.reference << " " << vd.type << " " << vd.causality << "]"; + ss << "," << vd.name; } for (const auto& vd : stringVars_) { - ss << "," << vd.name << " [" << vd.reference << " " << vd.type << " " << vd.causality << "]"; + ss << "," << vd.name; } ss << std::endl; @@ -226,6 +235,67 @@ class file_observer::slave_value_writer if (fsw_.is_open()) { fsw_ << ss.rdbuf(); } + + return time_str; + } + + void write_variable_metadata(std::stringstream& ss, std::vector& variables) const + { + for (const auto& v : variables) { + ss << " - " << std::setw(keyWidth_) << "name:" << v.name << std::endl + << " " << std::setw(keyWidth_) << "reference:" << v.reference << std::endl + << " " << std::setw(keyWidth_) << "type:" << v.type << std::endl + << " " << std::setw(keyWidth_) << "causality:" << v.causality << std::endl + << " " << std::setw(keyWidth_) << "variability:" << v.variability << std::endl; + + if (v.start.has_value()) { + ss << " " << std::setw(keyWidth_) << "start value:"; + std::visit([&](const auto& val) { ss << val << std::endl; }, v.start.value()); + } + } + } + + void create_metadata_file(const std::string& time_str) + { + std::ofstream metadata_fw; + std::string filename; + std::stringstream ss; + + if (!timeStampedFileNames_) { + filename = observable_->name().append("_metadata.yaml"); + } else { + filename = observable_->name().append("_").append(time_str).append("_metadata.yaml"); + } + + const auto filePath = logDir_ / filename; + metadata_fw.open(filePath, std::ios_base::out | std::ios_base::app); + + if (fsw_.fail()) { + std::stringstream error; + error << "Failed to open log metadata file stream: " << filePath.c_str(); + throw std::runtime_error(error.str()); + } + + auto md = observable_->model_description(); + + ss << std::left + << std::setw(keyWidth_) << "name:" << md.name << std::endl + << std::setw(keyWidth_) << "uuid:" << md.uuid << std::endl + << std::setw(keyWidth_) << "description:" << md.description << std::endl + << std::setw(keyWidth_) << "author:" << md.description << std::endl + << std::setw(keyWidth_) << "version:" << md.version << std::endl; + + ss << "variables:" << std::endl; + + write_variable_metadata(ss, realVars_); + write_variable_metadata(ss, intVars_); + write_variable_metadata(ss, boolVars_); + write_variable_metadata(ss, stringVars_); + + if (metadata_fw.is_open()) { + metadata_fw << ss.rdbuf(); + } + metadata_fw.close(); } void persist() @@ -272,18 +342,15 @@ class file_observer::slave_value_writer bool timeStampedFileNames_ = true; }; -file_observer::file_observer(const cosim::filesystem::path& logDir) - : logDir_(cosim::filesystem::absolute(logDir)) +file_observer::file_observer(const cosim::filesystem::path& logDir, std::optional config) + : config_(std::move(config)) + , logDir_(cosim::filesystem::absolute(logDir)) { } -file_observer::file_observer(const cosim::filesystem::path& logDir, const cosim::filesystem::path& configPath) - : configPath_(configPath) - , logDir_(cosim::filesystem::absolute(logDir)) - , logFromConfig_(true) +file_observer::file_observer(const filesystem::path& logDir, const filesystem::path& configPath) + : file_observer(logDir, file_observer_config::parse(configPath)) { - boost::property_tree::read_xml(configPath_.string(), ptree_, - boost::property_tree::xml_parser::no_comments | boost::property_tree::xml_parser::trim_whitespace); } namespace @@ -296,10 +363,12 @@ T get_attribute(const boost::property_tree::ptree& tree, const std::string& key) } template -T get_attribute(const boost::property_tree::ptree& tree, const std::string& key, T& defaultValue) +std::optional get_optional_attribute(const boost::property_tree::ptree& tree, const std::string& key) { - return tree.get("." + key, defaultValue); + const auto result = tree.get_optional("." + key); + return result ? *result : std::optional(); } + } // namespace void file_observer::simulator_added( @@ -309,15 +378,8 @@ void file_observer::simulator_added( { simulators_[index] = simulator; - if (logFromConfig_) { - // Read all configured model names from the XML. If simulator name is not in the list, do nothing. - std::vector modelNames; - for (const auto& simulatorChild : ptree_.get_child("simulators")) { - if (simulatorChild.first == "simulator") { - modelNames.push_back(get_attribute(simulatorChild.second, "name")); - } - } - if (std::find(modelNames.begin(), modelNames.end(), simulator->name()) != modelNames.end()) { + if (config_) { + if (config_->should_log_simulator(simulator->name())) { auto config = parse_config(simulator->name()); valueWriters_[index] = std::make_unique( @@ -415,65 +477,85 @@ cosim::observable* find_simulator( file_observer::simulator_logging_config file_observer::parse_config(const std::string& simulatorName) { - auto simulators = ptree_.get_child("simulators"); - bool timeStampedFileNames = simulators.get(".timeStampedFileNames", true); - for (const auto& childElement : simulators) { - if (childElement.first == "simulator") { - auto simulatorElement = childElement.second; - auto modelName = get_attribute(simulatorElement, "name"); - if (modelName == simulatorName) { - simulator_logging_config config; - config.timeStampedFileNames = timeStampedFileNames; - config.decimationFactor = get_attribute(simulatorElement, "decimationFactor", defaultDecimationFactor_); - - const auto& simulator = find_simulator(simulators_, modelName); - if (simulatorElement.count("variable") == 0) { - - for (const auto& vd : simulator->model_description().variables) { - switch (vd.type) { - case variable_type::real: - case variable_type::integer: - case variable_type::boolean: - case variable_type::string: - config.variables.push_back(vd); - break; - default: - break; - } + const bool timeStampedFileNames = config_->timeStampedFileNames_; + for (const auto& [modelName, variables] : config_->variablesToLog_) { + + if (modelName == simulatorName) { + simulator_logging_config config; + config.timeStampedFileNames = timeStampedFileNames; + config.decimationFactor = variables.first; + + const auto& simulator = find_simulator(simulators_, modelName); + if (variables.second.empty()) { + + for (const auto& vd : simulator->model_description().variables) { + switch (vd.type) { + case variable_type::real: + case variable_type::integer: + case variable_type::boolean: + case variable_type::string: + config.variables.push_back(vd); + break; + default: + break; } - } else { - for (const auto& [variableElementName, variableElement] : simulatorElement) { - if (variableElementName == "variable") { - const auto name = get_attribute(variableElement, "name"); - const auto variableDescription = - find_variable(simulator->model_description(), name); - - if (!variableDescription) { - throw std::runtime_error("Can't find variable descriptor with name " + name + " for model with name " + simulator->model_description().name); - } - - switch (variableDescription->type) { - case variable_type::real: - case variable_type::integer: - case variable_type::boolean: - case variable_type::string: - config.variables.push_back(*variableDescription); - BOOST_LOG_SEV(log::logger(), log::info) << "Logging variable: " << modelName << ":" << name; - break; - default: - COSIM_PANIC_M("Variable type not supported."); - } - } + } + } else { + for (const auto& name : variables.second) { + + const auto variableDescription = + find_variable(simulator->model_description(), name); + + if (!variableDescription) { + throw std::runtime_error("Can't find variable descriptor with name " + name + " for model with name " + simulator->model_description().name); + } + + switch (variableDescription->type) { + case variable_type::real: + case variable_type::integer: + case variable_type::boolean: + case variable_type::string: + config.variables.push_back(*variableDescription); + BOOST_LOG_SEV(log::logger(), log::info) << "Logging variable: " << modelName << ":" << name; + break; + default: + COSIM_PANIC_M("Variable type not supported."); } } - return config; } + return config; } } + return simulator_logging_config(); } file_observer::~file_observer() = default; +file_observer_config file_observer_config::parse(const filesystem::path& configPath) +{ + boost::property_tree::ptree ptree; + boost::property_tree::read_xml(configPath.string(), ptree, + boost::property_tree::xml_parser::no_comments | boost::property_tree::xml_parser::trim_whitespace); + + file_observer_config config; + for (const auto& simulator : ptree.get_child("simulators")) { + if (simulator.first == "simulator") { + const auto modelName = get_attribute(simulator.second, "name"); + const auto decimationFactor = get_optional_attribute(simulator.second, "decimationFactor"); + std::vector variableNames; + for (const auto& variable : simulator.second) { + if (variable.first == "variable") { + const auto variableName = get_attribute(variable.second, "name"); + variableNames.emplace_back(variableName); + } + } + config.log_simulator_variables(modelName, variableNames, decimationFactor); + } + } + + return config; +} + } // namespace cosim diff --git a/src/cosim/osp_config_parser.cpp b/src/cosim/osp_config_parser.cpp index 06a933139..84020d8d4 100644 --- a/src/cosim/osp_config_parser.cpp +++ b/src/cosim/osp_config_parser.cpp @@ -28,6 +28,8 @@ #include #include #include +#include +#include namespace cosim @@ -121,6 +123,7 @@ class osp_config_parser std::string algorithm; double stepSize = 0.1; double startTime = 0.0; + std::optional endTime; }; const SimulationInformation& get_simulation_information() const; @@ -272,7 +275,7 @@ osp_config_parser::osp_config_parser( error_handler errorHandler; - std::string xsd_str = get_embedded_osp_config_xsd(); + std::string xsd_str = osp_xsd; xercesc::MemBufInputSource mis( reinterpret_cast(xsd_str.c_str()), @@ -433,6 +436,11 @@ osp_config_parser::osp_config_parser( simulationInformation_.startTime = boost::lexical_cast(tc(stNodes->item(0)->getTextContent()).get()); } + auto etNodes = rootElement->getElementsByTagName(tc("EndTime").get()); + if (etNodes->getLength() > 0) { + simulationInformation_.endTime = boost::lexical_cast(tc(etNodes->item(0)->getTextContent()).get()); + } + auto saNodes = rootElement->getElementsByTagName(tc("Algorithm").get()); if (saNodes->getLength() > 0) { simulationInformation_.algorithm = std::string(tc(saNodes->item(0)->getTextContent()).get()); @@ -613,10 +621,18 @@ struct extended_model_description const auto xerces_cleanup = final_action([]() { xercesc::XMLPlatformUtils::Terminate(); }); + const auto domImpl = xercesc::DOMImplementationRegistry::getDOMImplementation(tc("LS").get()); const auto parser = static_cast(domImpl)->createLSParser(xercesc::DOMImplementationLS::MODE_SYNCHRONOUS, tc("http://www.w3.org/2001/XMLSchema").get()); + const auto doc = parser->parseURI(ospModelDescription.string().c_str()); - // TODO: Check return value for null + + if (doc == nullptr) { + std::ostringstream oss; + oss << "Validation of " << ospModelDescription.string() << " failed."; + BOOST_LOG_SEV(log::logger(), log::error) << oss.str(); + throw std::runtime_error(oss.str()); + } const auto rootElement = doc->getDocumentElement(); @@ -921,11 +937,11 @@ osp_config load_osp_config( const auto configFile = cosim::filesystem::is_regular_file(absolutePath) ? absolutePath : absolutePath / "OspSystemStructure.xml"; - const auto baseURI = path_to_file_uri(configFile); + const auto baseURI = path_to_file_uri(configFile); const auto parser = osp_config_parser(configFile); - const auto& simInfo = parser.get_simulation_information(); + if (simInfo.stepSize <= 0.0) { std::ostringstream oss; oss << "Configured base step size [" << simInfo.stepSize << "] must be nonzero and positive"; @@ -933,9 +949,19 @@ osp_config load_osp_config( throw std::invalid_argument(oss.str()); } + if (simInfo.endTime.has_value() && simInfo.startTime > simInfo.endTime.value()) { + std::ostringstream oss; + oss << "Configured start time [" << simInfo.startTime << "] is larger than configured end time [" << simInfo.endTime.value() << "]"; + BOOST_LOG_SEV(log::logger(), log::error) << oss.str(); + throw std::invalid_argument(oss.str()); + } + osp_config config; config.start_time = to_time_point(simInfo.startTime); config.step_size = to_duration(simInfo.stepSize); + if (simInfo.endTime.has_value()) { + config.end_time = to_time_point(simInfo.endTime.value()); + } auto simulators = parser.get_elements(); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 609e606a6..106514941 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -14,6 +14,7 @@ set(tests "trend_buffer_test" "scenario_manager_test" "synchronized_xy_series_test" + "config_end_time_test" ) set(unittests diff --git a/tests/config_end_time_test.cpp b/tests/config_end_time_test.cpp new file mode 100644 index 000000000..5b451424e --- /dev/null +++ b/tests/config_end_time_test.cpp @@ -0,0 +1,58 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#define REQUIRE(test) \ + if (!(test)) throw std::runtime_error("Requirement not satisfied: " #test) + + +void test(const cosim::filesystem::path& configPath) +{ + auto resolver = cosim::default_model_uri_resolver(); + const auto config = cosim::load_osp_config(configPath, *resolver); + auto execution = cosim::execution( + config.start_time, + std::make_shared(config.step_size)); + + const auto entityMaps = cosim::inject_system_structure( + execution, config.system_structure, config.initial_values); + + REQUIRE(entityMaps.simulators.size() == 4); + + auto obs = std::make_shared(); + execution.add_observer(obs); + + if (config.end_time.has_value()) { + REQUIRE(config.end_time.value().time_since_epoch().count() / 1e9 == 0.001); + auto result = execution.simulate_until(config.end_time); + REQUIRE(result); + } else { + auto result = execution.simulate_until(cosim::to_time_point(0.001)); + REQUIRE(result); + } +} + +int main() +{ + try { + cosim::log::setup_simple_console_logging(); + cosim::log::set_global_output_level(cosim::log::info); + + const auto testDataDir = std::getenv("TEST_DATA_DIR"); + REQUIRE(testDataDir); + + test(cosim::filesystem::path(testDataDir) / "msmi" / "OspSystemStructure.xml"); + test(cosim::filesystem::path(testDataDir) / "msmi" / "OspSystemStructure_EndTime.xml"); + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what(); + return 1; + } + return 0; +} \ No newline at end of file diff --git a/tests/data/LogConfig.xml b/tests/data/LogConfig.xml index 4926c4a31..e5bcd976d 100644 --- a/tests/data/LogConfig.xml +++ b/tests/data/LogConfig.xml @@ -5,7 +5,7 @@ - + diff --git a/tests/data/msmi/OspSystemStructure.xml b/tests/data/msmi/OspSystemStructure.xml index beb1c7b76..f80ecb573 100644 --- a/tests/data/msmi/OspSystemStructure.xml +++ b/tests/data/msmi/OspSystemStructure.xml @@ -4,7 +4,7 @@ xsi:schemaLocation="http://opensimulationplatform.com/MSMI/OSPSystemStructure ../../../data/xsd/OspSystemStructure.xsd" xmlns="http://opensimulationplatform.com/MSMI/OSPSystemStructure" version="0.1"> - 0.0 + 0.01 1e-4 fixedStep diff --git a/tests/data/msmi/OspSystemStructure_EndTime.xml b/tests/data/msmi/OspSystemStructure_EndTime.xml new file mode 100644 index 000000000..ea374a705 --- /dev/null +++ b/tests/data/msmi/OspSystemStructure_EndTime.xml @@ -0,0 +1,77 @@ + + + 0.0 + 0.001 + 1e-4 + fixedStep + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/file_observer_dynamic_logging_test.cpp b/tests/file_observer_dynamic_logging_test.cpp index ea948c88f..9de7ada86 100644 --- a/tests/file_observer_dynamic_logging_test.cpp +++ b/tests/file_observer_dynamic_logging_test.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -74,7 +75,7 @@ int main() observer->stop_recording(); REQUIRE(!observer->is_recording()); - REQUIRE(filecount(logPath) == 2); + REQUIRE(filecount(logPath) == 4); remove_directory_contents(logPath); REQUIRE(filecount(logPath) == 0); @@ -91,10 +92,12 @@ int main() execution.stop_simulation(); t.get(); - REQUIRE(filecount(logPath) == 4); + + REQUIRE(filecount(logPath) == 8); // Test that files are released. remove_directory_contents(logPath); + REQUIRE(filecount(logPath) == 0); } catch (const std::exception& e) { diff --git a/tests/file_observer_logging_from_config_test.cpp b/tests/file_observer_logging_from_config_test.cpp index d2b9f6e52..f83785577 100644 --- a/tests/file_observer_logging_from_config_test.cpp +++ b/tests/file_observer_logging_from_config_test.cpp @@ -35,7 +35,8 @@ int main() // Set up the execution and add observer auto execution = cosim::execution(startTime, std::make_unique(stepSize)); - auto csv_observer = std::make_shared(csvPath, configPath); + auto csv_config = cosim::file_observer_config::parse(configPath); + auto csv_observer = std::make_shared(csvPath, csv_config); execution.add_observer(csv_observer); // Add two slaves to the execution and connect variables diff --git a/tests/osp_config_parser_test.cpp b/tests/osp_config_parser_test.cpp index b70af75c6..f9717d983 100644 --- a/tests/osp_config_parser_test.cpp +++ b/tests/osp_config_parser_test.cpp @@ -22,20 +22,21 @@ void test(const cosim::filesystem::path& configPath, size_t expectedNumConnectio const auto entityMaps = cosim::inject_system_structure( execution, config.system_structure, config.initial_values); + REQUIRE(entityMaps.simulators.size() == 4); REQUIRE(boost::size(config.system_structure.connections()) == expectedNumConnections); auto obs = std::make_shared(); execution.add_observer(obs); - auto result = execution.simulate_until(cosim::to_time_point(1e-3)); + auto result = execution.simulate_until(cosim::to_time_point(0.01)); REQUIRE(result); const auto simIndex = entityMaps.simulators.at("CraneController"); - const auto varReference = - config.system_structure.get_variable_description({"CraneController", "cl1_min"}).reference; + const auto varReference1 = config.system_structure.get_variable_description({"CraneController", "cl1_min"}).reference; double realValue = -1.0; - obs->get_real(simIndex, gsl::make_span(&varReference, 1), gsl::make_span(&realValue, 1)); + + obs->get_real(simIndex, gsl::make_span(&varReference1, 1), gsl::make_span(&realValue, 1)); double magicNumberFromConf = 2.2; REQUIRE(std::fabs(realValue - magicNumberFromConf) < 1e-9); diff --git a/tools/osp-xsd-embedder/CMakeLists.txt b/tools/osp-xsd-embedder/CMakeLists.txt deleted file mode 100644 index fc5c96578..000000000 --- a/tools/osp-xsd-embedder/CMakeLists.txt +++ /dev/null @@ -1,6 +0,0 @@ -set(sources - "osp-xsd-embedder.cpp" -) - -add_executable(osp-xsd-embedder ${sources}) -target_compile_features(osp-xsd-embedder PUBLIC "cxx_std_17") diff --git a/tools/osp-xsd-embedder/osp-xsd-embedder.cpp b/tools/osp-xsd-embedder/osp-xsd-embedder.cpp deleted file mode 100644 index 31d4b240b..000000000 --- a/tools/osp-xsd-embedder/osp-xsd-embedder.cpp +++ /dev/null @@ -1,72 +0,0 @@ -#include -#include -#include - -std::string encode(std::string& data) -{ - std::string buffer; - buffer.reserve(data.size()); - for (size_t pos = 0; pos != data.size(); ++pos) { - switch (data[pos]) { - case '&': buffer.append("&"); break; - case '\"': buffer.append("""); break; - case '\'': buffer.append("'"); break; - case '<': buffer.append("<"); break; - case '>': buffer.append(">"); break; - case '\r': break; - default: buffer.append(&data[pos], 1); break; - } - } - return buffer; -} - -int main(int argc, char** argv) -{ - if (argc != 3) { - std::cerr << "Wrong number of arguments!" << std::endl; - std::cout << "ospxsdembedder requires 2 arguments:" << std::endl; - std::cout << " 1: Name of input file" << std::endl; - std::cout << " 2: Name of output file" << std::endl; - return 1; - } - - std::string input = argv[1]; - std::string output = argv[2]; - - std::cout << " Embedding: " << input << " -> " << output << std::endl; - - std::ifstream xsd(input); - std::string xsd_str, line; - - while (std::getline(xsd, line)) { - xsd_str += line; - } - - std::string encoded_xsd_str = encode(xsd_str); - - std::ofstream out_file(output); - if (out_file) { - out_file - << "#include " << std::endl - << "#include " << std::endl - << "#define osp_system_structure_xsd \"" << encoded_xsd_str << "\"" << std::endl - << std::endl - << "namespace cosim" << std::endl - << "{" << std::endl - << "std::string get_embedded_osp_config_xsd() {" << std::endl - << " std::string xsd_str(osp_system_structure_xsd);" << std::endl - << " xsd_str = std::regex_replace(xsd_str, std::regex(\"&\"), \"&\");" << std::endl - << " xsd_str = std::regex_replace(xsd_str, std::regex(\""\"), \"\\\"\");" << std::endl - << " xsd_str = std::regex_replace(xsd_str, std::regex(\"'\"), \"\'\");" << std::endl - << " xsd_str = std::regex_replace(xsd_str, std::regex(\"<\"), \"<\");" << std::endl - << " xsd_str = std::regex_replace(xsd_str, std::regex(\">\"), \">\");" << std::endl - << " return xsd_str;" << std::endl - << "}" << std::endl - << "}" << std::endl; - } else { - std::cerr << "Failure opening " << output << std::endl; - return 1; - } - - return 0; -} diff --git a/version.txt b/version.txt index 78bc1abd1..d9df1bbc0 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.10.0 +0.11.0