From ded0150ef07959da0e8e064f8ca7953acde9970f Mon Sep 17 00:00:00 2001 From: "Lars T. Kyllingstad" Date: Tue, 31 Mar 2020 08:36:07 +0200 Subject: [PATCH] Offline model building: system_structure and related functionality (#542) --- include/cse/exception.hpp | 3 + include/cse/execution.hpp | 29 ++ include/cse/function/utility.hpp | 13 + include/cse/system_structure.hpp | 432 +++++++++++++++++++++++++ src/cpp/CMakeLists.txt | 3 + src/cpp/exception.cpp | 2 + src/cpp/execution.cpp | 122 ++++++- src/cpp/function/utility.cpp | 46 +++ src/cpp/system_structure.cpp | 426 ++++++++++++++++++++++++ test/cpp/CMakeLists.txt | 1 + test/cpp/system_structure_unittest.cpp | 132 ++++++++ 11 files changed, 1206 insertions(+), 3 deletions(-) create mode 100644 include/cse/system_structure.hpp create mode 100644 src/cpp/function/utility.cpp create mode 100644 src/cpp/system_structure.cpp create mode 100644 test/cpp/system_structure_unittest.cpp diff --git a/include/cse/exception.hpp b/include/cse/exception.hpp index fe9bc4549..6808cf438 100644 --- a/include/cse/exception.hpp +++ b/include/cse/exception.hpp @@ -42,6 +42,9 @@ enum class errc /// Simulation error simulation_error, + /// Invalid system structure (e.g. an invalid variable connection) + invalid_system_structure, + /// ZIP file error zip_error }; diff --git a/include/cse/execution.hpp b/include/cse/execution.hpp index 47ee59132..585b0985f 100644 --- a/include/cse/execution.hpp +++ b/include/cse/execution.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -15,6 +16,7 @@ #include #include #include +#include namespace cse @@ -311,5 +313,32 @@ class execution }; +/// Maps entity names to simulator/function indices in an `execution`. +struct entity_index_maps +{ + /// Mapping of simulator names to simulator indices. + std::unordered_map simulators; + + /// Mapping of function names to function indices. + std::unordered_map functions; +}; + + +/** + * Adds simulators and connections to an execution, and sets initial values, + * according to a predefined system structure description. + * + * This function may be called multiple times for the same `execution`, as + * long as there is no conflict between the different `system_structure` + * objects. + * + * \returns + * Mappings between entity names and their indexes in the execution. + */ +entity_index_maps inject_system_structure( + execution& exe, + const system_structure& sys, + const variable_value_map& initialValues); + } // namespace cse #endif // header guard diff --git a/include/cse/function/utility.hpp b/include/cse/function/utility.hpp index 83e8011bc..493960be3 100644 --- a/include/cse/function/utility.hpp +++ b/include/cse/function/utility.hpp @@ -54,5 +54,18 @@ T get_function_parameter( } +/** + * Returns a `function_description` with the same contents as the + * `function_description` part of `functionTypeDescription`, but with + * all placeholders replaced by actual parameter values. + * + * The `parameterValues` map *must* contain values for all placeholders + * in `functionTypeDescription`. Otherwise, an exception is thrown. + */ +function_description substitute_function_parameters( + const function_type_description& functionTypeDescription, + const function_parameter_value_map& parameterValues); + + } // namespace cse #endif // header guard diff --git a/include/cse/system_structure.hpp b/include/cse/system_structure.hpp new file mode 100644 index 000000000..67af5b30b --- /dev/null +++ b/include/cse/system_structure.hpp @@ -0,0 +1,432 @@ +#ifndef CSE_SYSTEM_STRUCTURE_HPP +#define CSE_SYSTEM_STRUCTURE_HPP + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + + +namespace cse +{ + +/** + * The qualified name of a variable, consisting of the entity name and + * the variable name. + * + * The validity of the qualified name can only be determined in the context + * of a specific system structure (see `system_structure`). + */ +struct full_variable_name +{ + /// Constructor for simulator variables. + full_variable_name(std::string simulatorName, std::string variableName) + : entity_name(std::move(simulatorName)) + , variable_name(std::move(variableName)) + {} + + /// Constructor for function variables. + full_variable_name( + std::string functionName, + std::string ioGroupName, + int ioGroupInstance, + std::string ioName, + int ioInstance) + : entity_name(std::move(functionName)) + , variable_group_name(std::move(ioGroupName)) + , variable_group_instance(ioGroupInstance) + , variable_name(std::move(ioName)) + , variable_instance(ioInstance) + {} + + /// The name of an entity. + std::string entity_name; + + /** + * The name of a variable group (ignored for simulators). + * + * \note + * This member is sometimes used to determine whether this + * `full_variable_name` refers to a simulator or function variable. + * It is considered to refer to a simulator if and only if this + * string is empty, and a function otherwise. + */ + std::string variable_group_name; + + /// The index of a variable group instance (ignored for simulators). + int variable_group_instance = 0; + + /// The name of a variable. + std::string variable_name; + + /// The index of a variable instance (ignored for simulators). + int variable_instance = 0; + + /// Convenience function to check `variable_group_name` for emptiness. + bool is_simulator_variable() const noexcept + { + return variable_group_name.empty(); + } +}; + + +/// Equality operator for `full_variable_name`. +inline bool operator==( + const full_variable_name& a, const full_variable_name& b) noexcept +{ + return a.entity_name == b.entity_name && + a.variable_group_name == b.variable_group_name && + a.variable_group_instance == b.variable_group_instance && + a.variable_name == b.variable_name && + a.variable_instance == b.variable_instance; +} + +/// Inequality operator for `full_variable_name`. +inline bool operator!=( + const full_variable_name& a, const full_variable_name& b) noexcept +{ + return !operator==(a, b); +} + +/// Writes a `full_variable_name` to an output stream. +inline std::ostream& operator<<(std::ostream& s, const full_variable_name& v) +{ + s << v.entity_name << ':'; + if (v.is_simulator_variable()) { + s << v.variable_name; + } else { + s << v.variable_group_name << '[' << v.variable_group_instance << "]:" + << v.variable_name << '[' << v.variable_instance << ']'; + } + return s; +} + +/// Returns a string representation of a `full_variable_name`. +std::string to_text(const full_variable_name& v); + +} // namespace cse + +namespace std +{ +template<> +struct hash +{ + std::size_t operator()(const cse::full_variable_name& v) const noexcept + { + std::size_t h = 0; + boost::hash_combine(h, v.entity_name); + boost::hash_combine(h, v.variable_group_name); + boost::hash_combine(h, v.variable_group_instance); + boost::hash_combine(h, v.variable_name); + boost::hash_combine(h, v.variable_instance); + return h; + } +}; +} // namespace std + + +namespace cse +{ + +/** + * A description of the structure of a modelled system. + * + * The system structure description contains the list of entities in the + * system and the connections between them. Validation is performed on + * the fly by the class' mutators, and any attempt to make an invalid change + * will result in an exception of type `cse::error` with error code + * `cse::errc::invalid_system_structure`. + */ +class system_structure +{ +public: + /** + * The type of an entity. + * + * This is a shared pointer to a `cse::model` if the entity is a simulator, + * and to a `cse::function_type` if the entity is a function instance. + */ + using entity_type = std::variant< + std::shared_ptr, + std::shared_ptr>; + + /** + * Information about a simulation entity + * + * An entity may be either a simulator or a function instance; + * this is determined by the `type` field. + */ + struct entity + { + /// The entity name. + std::string name; + + /// The entity type. + entity_type type; + + /// Recommended step size (for simulators; ignored for functions). + duration step_size_hint; + + /// Parameter values (for functions; ignored for simulators). + function_parameter_value_map parameter_values; + }; + + /// Information about a connection. + struct connection + { + /// The source variable. + full_variable_name source; + + /// The target variable. + full_variable_name target; + }; + +private: + using entity_map = std::unordered_map; + using connection_map = + std::unordered_map; + using connection_transform = + connection (*)(const connection_map::value_type&); + +public: + using entity_range = boost::select_second_const_range; + using connection_range = + boost::transformed_range< + connection_transform, const connection_map>; + + /** + * Adds an entity to the system. + * + * `e.name` must be unique in the context of the present system. + */ + void add_entity(const entity& e); + + /// \overload + void add_entity( + std::string_view name, + std::shared_ptr type, + duration stepSizeHint = duration::zero()) + { + add_entity({std::string(name), type, stepSizeHint, {}}); + } + + /// \overload + void add_entity( + std::string_view name, + std::shared_ptr type, + function_parameter_value_map parameters) + { + add_entity({std::string(name), type, {}, std::move(parameters)}); + } + + /** + * Returns a list of the entities in the system. + * + * \returns + * A range of `entity` objects. + */ + entity_range entities() const noexcept; + + /** + * Establishes a connection between two variables. + * + * The same target variable may not be connected several times. + */ + void connect_variables(const connection& c); + + /// \overload + void connect_variables( + const full_variable_name& source, const full_variable_name& target) + { + connect_variables({source, target}); + } + + /** + * Returns a list of the scalar connections in the system. + * + * \returns + * A range of `connection` objects. + */ + connection_range connections() const noexcept; + + /** + * Retrieves the description of a simulator variable, given its + * qualified name. + * + * This is a convenience function that provides fast variable lookup + * (O(1) on average). + */ + const variable_description& get_variable_description( + const full_variable_name& v) const; + + /** + * A description of a function variable, including group and variable + * indices. + * + * This is just a `cse::function_io_description` bundled together with + * the indices of the variable and its group. (These indices are + * implicitly defined by the order of groups and variables in a + * `cse::function_description`, and thus not an explicit part of each + * `cse::function_io_description`.) + */ + struct function_io_description + { + int group_index = 0; + int io_index = 0; + cse::function_io_description description; + }; + + /** + * Retrieves the description of a function variable, given its + * qualified name. + * + * This is a convenience function that provides fast variable lookup + * (O(1) on average). + */ + const function_io_description& get_function_io_description( + const full_variable_name& v) const; + +private: + // Entities, indexed by name. + entity_map entities_; + + // Connections. Target is key, source is value. + connection_map connections_; + + // Cache for fast lookup of model info, indexed by model UUID. + struct model_info + { + std::unordered_map variables; + }; + std::unordered_map modelCache_; + + // Cache for fast lookup of function info. + struct function_info + { + function_description description; + std::unordered_map< + std::string, + std::unordered_map< + std::string, + function_io_description>> + ios; + }; + std::unordered_map functionCache_; +}; + + +/** + * Converts an entity type to a model or function type. + * + * This is a convenience function that simply checks whether `et` contains + * a `std::shared_ptr`, and if so, returns it. + * Otherwise, it returns an empty (null) `std::shared_ptr`. + */ +template +std::shared_ptr entity_type_to(system_structure::entity_type et) noexcept +{ + const auto t = std::get_if>(&et); + return t ? *t : nullptr; +} + + +/** + * Checks whether `name` is a valid entity name. + * + * The rules are the same as for C(++) identifiers: The name may only + * consist of ASCII letters, numbers and underscores, and the first + * character must be a letter or an underscore. + * + * If `reason` is non-null and the function returns `false`, a + * human-readable reason will be stored in the string pointed to by + * `reason`. + */ +bool is_valid_entity_name(std::string_view name, std::string* reason) noexcept; + + +/** + * Checks whether `value` is a valid value for a variable described by + * `variable`. + * + * If it is not, the function will store a human-readable reason for the + * rejection in the string pointed to by `reason`. If the function + * returns `true`, or if `reason` is null, this parameter is ignored. + */ +bool is_valid_variable_value( + const variable_description& variable, + const scalar_value& value, + std::string* reason); + + +/** + * Checks whether a connection between two variables described by + * `source` and `target`, respectively, would be valid. + * + * If it is not, the function will store a human-readable reason for the + * rejection in the string pointed to by `reason`. If the function + * returns `true`, or if `reason` is null, this parameter is ignored. + */ +bool is_valid_connection( + const variable_description& source, + const variable_description& target, + std::string* reason); + +/// Overload of `is_valid_connection()` for simulator-function connections. +bool is_valid_connection( + const variable_description& source, + const function_io_description& target, + std::string* reason); + +/// Overload of `is_valid_connection()` for function-simulator connections. +bool is_valid_connection( + const function_io_description& source, + const variable_description& target, + std::string* reason); + + +/** + * A container that holds a set of variable values. + * + * This is a simple container that associates qualified variable names, + * of type `full_variable_name`, to scalar values of type `scalar_value`. + * Being a plain map (`std::unordered_map`, to be precise) it is not + * linked to a particular `system_structure` instance, and does not + * perform any validation of variable names or values. + * + * Instead, the `add_variable_value()` function may be used as a + * convenient method for populating a `variable_value_map` with verified + * values. + */ +using variable_value_map = std::unordered_map; + + +/** + * Validates a variable value and adds it to a `variable_value_map`. + * + * This is a convenience function which verifies that `variable` refers + * to a variable in `systemStructure` and that `value` is a valid value + * for that variable, and then adds the variable-value pair to + * `variableValues`. + * + * \throws `cse::error` with code `cse::errc::invalid_system_structure` + * if any of the above-mentioned checks fail. + */ +void add_variable_value( + variable_value_map& variableValues, + const system_structure& systemStructure, + const full_variable_name& variable, + scalar_value value); + + +} // namespace cse +#endif // header guard diff --git a/src/cpp/CMakeLists.txt b/src/cpp/CMakeLists.txt index 32a661be0..5bbc21c7a 100644 --- a/src/cpp/CMakeLists.txt +++ b/src/cpp/CMakeLists.txt @@ -41,6 +41,7 @@ set(publicHeaders "scenario_parser.hpp" "slave.hpp" "ssp/ssp_loader.hpp" + "system_structure.hpp" "timer.hpp" "uri.hpp" ) @@ -75,6 +76,7 @@ set(sources "fmi/v2/fmu.cpp" "fmi/windows.cpp" "function/linear_transformation.cpp" + "function/utility.cpp" "function/vector_sum.cpp" "log/logger.cpp" "manipulator/scenario_manager.cpp" @@ -89,6 +91,7 @@ set(sources "slave_simulator.cpp" "ssp/ssp_loader.cpp" "ssp/ssp_parser.cpp" + "system_structure.cpp" "timer.cpp" "uri.cpp" "utility/concurrency.cpp" diff --git a/src/cpp/exception.cpp b/src/cpp/exception.cpp index e7016e364..80f2c8bc9 100644 --- a/src/cpp/exception.cpp +++ b/src/cpp/exception.cpp @@ -35,6 +35,8 @@ class my_error_category : public std::error_category return "Variable value is invalid or out of range"; case errc::simulation_error: return "Simulation error"; + case errc::invalid_system_structure: + return "Invalid system structure"; case errc::zip_error: return "ZIP file error"; default: diff --git a/src/cpp/execution.cpp b/src/cpp/execution.cpp index 31b03f9e0..83a33b8a6 100644 --- a/src/cpp/execution.cpp +++ b/src/cpp/execution.cpp @@ -4,6 +4,7 @@ #include "cse/exception.hpp" #include "cse/slave_simulator.hpp" #include "cse/timer.hpp" +#include "cse/utility/utility.hpp" #include #include @@ -365,17 +366,17 @@ void execution::add_manipulator(std::shared_ptr man) void execution::connect_variables(variable_id output, variable_id input) { - pimpl_->connect_variables(output, input); + pimpl_->connect_variables(output, input); } void execution::connect_variables(variable_id output, function_io_id input) { - pimpl_->connect_variables(output, input); + pimpl_->connect_variables(output, input); } void execution::connect_variables(function_io_id output, variable_id input) { - pimpl_->connect_variables(output, input); + pimpl_->connect_variables(output, input); } time_point execution::current_time() const noexcept @@ -458,4 +459,119 @@ void execution::set_string_initial_value(simulator_index sim, value_reference va pimpl_->set_string_initial_value(sim, var, value); } + +namespace +{ +variable_id make_variable_id( + const system_structure& systemStructure, + const entity_index_maps& indexMaps, + const full_variable_name& variableName) +{ + const auto& variableDescription = + systemStructure.get_variable_description(variableName); + return { + indexMaps.simulators.at(variableName.entity_name), + variableDescription.type, + variableDescription.reference}; +} + +function_io_id make_function_io_id( + const system_structure& systemStructure, + const entity_index_maps& indexMaps, + const full_variable_name& variableName) +{ + const auto& variableDescription = + systemStructure.get_function_io_description(variableName); + return { + indexMaps.functions.at(variableName.entity_name), + std::get(variableDescription.description.type), + function_io_reference{ + variableDescription.group_index, + variableName.variable_group_instance, + variableDescription.io_index, + variableName.variable_instance}}; +} +} // namespace + + +entity_index_maps inject_system_structure( + execution& exe, + const system_structure& sys, + const variable_value_map& initialValues) +{ + // Add simulators and functions + entity_index_maps indexMaps; + for (const auto& entity : sys.entities()) { + if (const auto model = entity_type_to(entity.type)) { + // Entity is a simulator + const auto index = exe.add_slave( + model->instantiate(entity.name), + entity.name, + entity.step_size_hint); + indexMaps.simulators.emplace(std::string(entity.name), index); + } else { + // Entity is a function + const auto functionType = entity_type_to(entity.type); + assert(functionType); + const auto index = exe.add_function( + functionType->instantiate(entity.parameter_values)); + indexMaps.functions.emplace(std::string(entity.name), index); + } + } + + // Connect variables + for (const auto& conn : sys.connections()) { + if (conn.source.is_simulator_variable()) { + if (conn.target.is_simulator_variable()) { + // Source is simulator, target is simulator + exe.connect_variables( + make_variable_id(sys, indexMaps, conn.source), + make_variable_id(sys, indexMaps, conn.target)); + } else { + // Source is simulator, target is function + exe.connect_variables( + make_variable_id(sys, indexMaps, conn.source), + make_function_io_id(sys, indexMaps, conn.target)); + } + } else { + // Source is function, target must be simulator + exe.connect_variables( + make_function_io_id(sys, indexMaps, conn.source), + make_variable_id(sys, indexMaps, conn.target)); + } + } + + // Set initial values + for (const auto& [var, val] : initialValues) { + if (!var.is_simulator_variable()) { + throw error( + make_error_code(errc::invalid_system_structure), + "Cannot set initial value of variable " + + to_text(var) + + " (only supported for simulator variables)"); + } + const auto& varDesc = sys.get_variable_description(var); + if (varDesc.causality != variable_causality::parameter && + varDesc.causality != variable_causality::input) { + throw error( + make_error_code(errc::invalid_system_structure), + "Cannot set initial value of variable " + + to_text(var) + + " (only supported for parameters and inputs)"); + } + const auto simIdx = indexMaps.simulators.at(var.entity_name); + const auto valRef = varDesc.reference; + std::visit( + visitor( + [&](double v) { exe.set_real_initial_value(simIdx, valRef, v); }, + [&](int v) { exe.set_integer_initial_value(simIdx, valRef, v); }, + [&](bool v) { exe.set_boolean_initial_value(simIdx, valRef, v); }, + [&](const std::string& v) { exe.set_string_initial_value(simIdx, valRef, v); }), + val); + } + + return indexMaps; +} + + } // namespace cse diff --git a/src/cpp/function/utility.cpp b/src/cpp/function/utility.cpp new file mode 100644 index 000000000..fc4d7bd67 --- /dev/null +++ b/src/cpp/function/utility.cpp @@ -0,0 +1,46 @@ +#include "cse/function/utility.hpp" + + +namespace cse +{ +namespace +{ +template +class replace_placeholder +{ +public: + replace_placeholder( + const std::unordered_map& parameterValues) + : parameterValues_(parameterValues) + {} + + T operator()(const T& value) { return value; } + + T operator()(const function_parameter_placeholder& placeholder) + { + return std::get(parameterValues_.at(placeholder.parameter_index)); + } + +private: + const std::unordered_map& parameterValues_; +}; +} // namespace + + +function_description substitute_function_parameters( + const function_type_description& functionTypeDescription, + const function_parameter_value_map& parameterValues) +{ + auto functionDescription = function_description(functionTypeDescription); + for (auto& group : functionDescription.io_groups) { + group.count = std::visit(replace_placeholder(parameterValues), group.count); + + for (auto& io : group.ios) { + io.count = std::visit(replace_placeholder(parameterValues), io.count); + io.type = std::visit(replace_placeholder(parameterValues), io.type); + } + } + return functionDescription; +} + +} // namespace cse diff --git a/src/cpp/system_structure.cpp b/src/cpp/system_structure.cpp new file mode 100644 index 000000000..159d96eec --- /dev/null +++ b/src/cpp/system_structure.cpp @@ -0,0 +1,426 @@ +#include "cse/system_structure.hpp" + +#include "cse/exception.hpp" +#include "cse/function/utility.hpp" +#include "cse/utility/utility.hpp" + +#include +#include +#include +#include +#include +#include + + +namespace cse +{ + +std::string to_text(const full_variable_name& v) +{ + std::ostringstream s; + s << v; + return s.str(); +} + + +// ============================================================================= +// system_structure +// ============================================================================= + + +namespace +{ +std::unordered_map +make_variable_lookup_table(const model_description& md) +{ + std::unordered_map table; + for (const auto& var : md.variables) { + table.emplace(var.name, var); + } + return table; +} + +std::unordered_map< + std::string, + std::unordered_map< + std::string, + system_structure::function_io_description>> +make_variable_lookup_table(const function_description& fd) +{ + std::unordered_map< + std::string, + std::unordered_map< + std::string, + system_structure::function_io_description>> + table; + for (std::size_t g = 0; g < fd.io_groups.size(); ++g) { + const auto& group = fd.io_groups[g]; + for (std::size_t i = 0; i < group.ios.size(); ++i) { + const auto& io = group.ios[i]; + table[group.name].emplace( + io.name, + system_structure::function_io_description{ + static_cast(g), + static_cast(i), + io}); + } + } + return table; +} + +error make_connection_error( + const system_structure::connection& c, const std::string& e) +{ + std::ostringstream msg; + msg << "Cannot establish connection between variables " + << c.source << " and " << c.target << ": " << e; + return error(make_error_code(errc::invalid_system_structure), msg.str()); +} +} // namespace + + +void system_structure::add_entity(const entity& e) +{ + if (std::string err; !is_valid_entity_name(e.name, &err)) { + throw error( + make_error_code(errc::invalid_system_structure), + "Illegal entity name '" + e.name + "': " + err); + } + if (entities_.count(e.name)) { + throw error( + make_error_code(errc::invalid_system_structure), + "Duplicate entity name: " + e.name); + } + assert(!functionCache_.count(e.name)); + + if (const auto model = entity_type_to(e.type)) { + if (e.step_size_hint < duration::zero()) { + throw error( + make_error_code(errc::invalid_system_structure), + "Negative step size hint: " + e.name); + } + // Make a model cache entry + if (modelCache_.count(model->description()->uuid) == 0) { + modelCache_[model->description()->uuid] = + {make_variable_lookup_table(*model->description())}; + } + } else { + // Make a function cache entry + const auto functionType = entity_type_to(e.type); + assert(functionType); + function_info info; + try { + info.description = substitute_function_parameters( + functionType->description(), + e.parameter_values); + } catch (const std::exception&) { + throw error( + make_error_code(errc::invalid_system_structure), + "Invalid or incomplete function parameter set: " + e.name); + } + info.ios = make_variable_lookup_table(info.description); + functionCache_.emplace(e.name, std::move(info)); + } + entities_.emplace(e.name, e); +} + + +system_structure::entity_range system_structure::entities() const noexcept +{ + return boost::adaptors::values(entities_); +} + + +void system_structure::connect_variables(const connection& c) +{ + std::string validationError; + bool valid = false; + if (c.source.is_simulator_variable()) { + if (c.target.is_simulator_variable()) { + valid = is_valid_connection( + get_variable_description(c.source), + get_variable_description(c.target), + &validationError); + } else { + valid = is_valid_connection( + get_variable_description(c.source), + get_function_io_description(c.target).description, + &validationError); + } + } else { + valid = is_valid_connection( + get_function_io_description(c.source).description, + get_variable_description(c.target), + &validationError); + } + if (!valid) { + throw make_connection_error(c, validationError); + } + const auto cit = connections_.find(c.target); + if (cit != connections_.end()) { + throw make_connection_error( + c, + "Target variable is already connected to " + + to_text(cit->second)); + } + connections_.emplace(c.target, c.source); +} + + +namespace +{ +system_structure::connection to_connection( + const std::pair& varPair) +{ + return {varPair.second, varPair.first}; +} +} // namespace + + +system_structure::connection_range system_structure::connections() + const noexcept +{ + return boost::adaptors::transform(connections_, to_connection); +} + + +const variable_description& system_structure::get_variable_description( + const full_variable_name& v) const +{ + const auto sit = entities_.find(v.entity_name); + if (sit == entities_.end()) { + throw error( + make_error_code(errc::invalid_system_structure), + "Unknown simulator name: " + v.entity_name); + } + const auto model = entity_type_to(sit->second.type); + if (!model) { + throw std::logic_error("Not a simulator: " + to_text(v)); + } + const auto& modelInfo = modelCache_.at(model->description()->uuid); + const auto vit = modelInfo.variables.find(v.variable_name); + if (vit == modelInfo.variables.end()) { + throw error( + make_error_code(errc::invalid_system_structure), + "No such variable: " + to_text(v)); + } + return vit->second; +} + + +const system_structure::function_io_description& +system_structure::get_function_io_description( + const full_variable_name& v) const +{ + const auto fit = entities_.find(v.entity_name); + if (fit == entities_.end()) { + throw error( + make_error_code(errc::invalid_system_structure), + "Unknown function name: " + v.entity_name); + } + const auto functionType = entity_type_to(fit->second.type); + if (!functionType) { + throw std::logic_error("Not a function: " + to_text(v)); + } + const auto& functionInfo = functionCache_.at(v.entity_name); + const auto git = functionInfo.ios.find(v.variable_group_name); + if (git == functionInfo.ios.end()) { + throw error( + make_error_code(errc::invalid_system_structure), + "No such variable: " + to_text(v)); + } + const auto vit = git->second.find(v.variable_name); + if (vit == git->second.end()) { + throw error( + make_error_code(errc::invalid_system_structure), + "No such variable: " + to_text(v)); + } + return vit->second; +} + + +// ============================================================================= +// Free functions +// ============================================================================= + +namespace +{ +constexpr bool is_printable(char c) noexcept +{ + return c >= 0x20 && c <= 0x7e; +} + +std::string illegal_char_err_msg(char c) +{ + std::stringstream msg; + msg << "Illegal character: "; + if (is_printable(c)) { + msg << c; + } else { + msg << "(unprintable: 0x" + << std::hex << std::setw(2) << std::setfill('0') << std::uppercase + << static_cast(c) << ')'; + } + return msg.str(); +} +} // namespace + +bool is_valid_entity_name(std::string_view name, std::string* reason) noexcept +{ + if (name.empty()) return false; + if (!std::isalpha(static_cast(name[0])) && name[0] != '_') { + if (reason != nullptr) *reason = illegal_char_err_msg(name[0]); + return false; + } + for (std::size_t i = 1; i < name.size(); ++i) { + if (!std::isalnum(static_cast(name[i])) && name[i] != '_') { + if (reason != nullptr) *reason = illegal_char_err_msg(name[i]); + return false; + } + } + return true; +} + + +bool is_valid_variable_value( + const variable_description& variable, + const scalar_value& value, + std::string* reason) +{ + const auto valueType = std::visit( + visitor( + [=](double) { return variable_type::real; }, + [=](int) { return variable_type::integer; }, + [=](const std::string&) { return variable_type::string; }, + [=](bool) { return variable_type::boolean; }), + value); + if (valueType != variable.type) { + if (reason != nullptr) { + std::ostringstream msg; + msg << "Cannot assign a value of type '" << to_text(valueType) + << "' to a variable of type '" << to_text(variable.type) + << "'."; + *reason = msg.str(); + } + return false; + } + // TODO: Check min/max value too. + return true; +} + + +bool is_valid_connection( + const variable_description& source, + const variable_description& target, + std::string* reason) +{ + if (source.type != target.type) { + if (reason != nullptr) *reason = "Variable types differ."; + return false; + } + if (source.causality != variable_causality::calculated_parameter && + source.causality != variable_causality::output) { + if (reason != nullptr) { + *reason = "Only variables with causality 'output' or 'calculated " + "parameter' may be used as source variables in a connection."; + } + return false; + } + if (target.causality != variable_causality::input) { + if (reason != nullptr) { + *reason = "Only variables with causality 'input' may be used as " + "target variables in a connection."; + } + return false; + } + if (target.variability == variable_variability::constant || + target.variability == variable_variability::fixed) { + if (reason != nullptr) { + *reason = "The target variable is not modifiable."; + } + return false; + } + return true; +} + + +bool is_valid_connection( + const variable_description& source, + const function_io_description& target, + std::string* reason) +{ + if (source.type != std::get(target.type)) { + if (reason != nullptr) *reason = "Variable types differ."; + return false; + } + if (source.causality != variable_causality::calculated_parameter && + source.causality != variable_causality::output) { + if (reason != nullptr) { + *reason = "Only variables with causality 'output' or 'calculated " + "parameter' may be used as source variables in a connection."; + } + return false; + } + if (target.causality != variable_causality::input) { + if (reason != nullptr) { + *reason = "Only variables with causality 'input' may be used as " + "target variables in a connection."; + } + return false; + } + return true; +} + + +bool is_valid_connection( + const function_io_description& source, + const variable_description& target, + std::string* reason) +{ + if (std::get(source.type) != target.type) { + if (reason != nullptr) *reason = "Variable types differ."; + return false; + } + if (source.causality != variable_causality::calculated_parameter && + source.causality != variable_causality::output) { + if (reason != nullptr) { + *reason = "Only variables with causality 'output' or 'calculated " + "parameter' may be used as source variables in a connection."; + } + return false; + } + if (target.causality != variable_causality::input) { + if (reason != nullptr) { + *reason = "Only variables with causality 'input' may be used as " + "target variables in a connection."; + } + return false; + } + if (target.variability == variable_variability::constant || + target.variability == variable_variability::fixed) { + if (reason != nullptr) { + *reason = "The target variable is not modifiable."; + } + return false; + } + return true; +} + + +void add_variable_value( + variable_value_map& variableValues, + const system_structure& systemStructure, + const full_variable_name& variable, + scalar_value value) +{ + const auto& varDescription = systemStructure.get_variable_description(variable); + if (std::string e; !is_valid_variable_value(varDescription, value, &e)) { + std::ostringstream msg; + msg << "Invalid value for variable '" << variable << "': " << e; + throw error(make_error_code(errc::invalid_system_structure), msg.str()); + } + variableValues.insert_or_assign(variable, value); +} + + +} // namespace cse diff --git a/test/cpp/CMakeLists.txt b/test/cpp/CMakeLists.txt index 5560df25b..bc6705e9e 100644 --- a/test/cpp/CMakeLists.txt +++ b/test/cpp/CMakeLists.txt @@ -30,6 +30,7 @@ set(unittests "orchestration_unittest" "scenario_parser_unittest" "ssp_loader_unittest" + "system_structure_unittest" "uri_unittest" "utility_concurrency_unittest" "utility_filesystem_unittest" diff --git a/test/cpp/system_structure_unittest.cpp b/test/cpp/system_structure_unittest.cpp new file mode 100644 index 000000000..723d37d87 --- /dev/null +++ b/test/cpp/system_structure_unittest.cpp @@ -0,0 +1,132 @@ +#define BOOST_TEST_MODULE system_structure unittests +#include "mock_slave.hpp" + +#include +#include +#include +#include +#include +#include + +#include + +#include + + +class mock_model : public cse::model +{ +public: + std::shared_ptr description() + const noexcept override + { + if (!nextSlave_) nextSlave_ = std::make_shared(); + return std::make_shared( + nextSlave_->model_description()); + } + + std::shared_ptr instantiate(std::string_view /*name*/) + override + { + if (!nextSlave_) nextSlave_ = std::make_shared(); + return cse::make_pseudo_async(std::move(nextSlave_)); + } + +private: + mutable std::shared_ptr nextSlave_; +}; + + +BOOST_AUTO_TEST_CASE(system_structure_basic_use) +{ + // Some test parameters + constexpr auto startTime = cse::time_point(); + constexpr auto stopTime = cse::time_point(std::chrono::seconds(1)); + constexpr auto timeStep = std::chrono::milliseconds(100); + constexpr auto offset = 2.0; + constexpr auto factor = 3.0; + + // Get a model and function type + const auto model = std::make_shared(); + const auto func = std::make_shared(); + const auto funcParams = cse::function_parameter_value_map{ + std::make_pair(cse::linear_transformation_function_type::offset_parameter_index, offset), + std::make_pair(cse::linear_transformation_function_type::factor_parameter_index, factor)}; + + // Set up a system structure, check basic functionality + cse::system_structure ss; + + ss.add_entity("simA", model); + ss.add_entity("simB", model, timeStep * 2); + ss.add_entity("func", func, funcParams); + ss.add_entity("simC", model); + BOOST_CHECK_THROW(ss.add_entity("simB", model), cse::error); // simB exists + BOOST_CHECK_THROW(ss.add_entity("func", func, funcParams), cse::error); // func exists + BOOST_CHECK_THROW(ss.add_entity("sim\nB", model), cse::error); // invalid name + BOOST_CHECK_THROW(ss.add_entity("simB", model, -timeStep), cse::error); // negative step size + + ss.connect_variables({"simA", "realOut"}, {"simB", "realIn"}); // sim-sim connection + ss.connect_variables({"simA", "realOut"}, {"simA", "realIn"}); // sim self connection + ss.connect_variables({"simB", "realOut"}, {"func", "in", 0, "", 0}); // sim-func connection + ss.connect_variables({"func", "out", 0, "", 0}, {"simC", "realIn"}); // func-sim connection + BOOST_CHECK_THROW( + ss.connect_variables({"simB", "realOut"}, {"simB", "realIn"}), + cse::error); // simB.realIn already connected + BOOST_CHECK_THROW( + ss.connect_variables({"simC", "realOut"}, {"func", "in", 0, "", 0}), + cse::error); // func.in already connected + BOOST_CHECK_THROW( + ss.connect_variables({"simA", "realOut"}, {"simB", "intIn"}), + cse::error); // incompatible variables + + const auto entities = ss.entities(); + BOOST_TEST_REQUIRE(std::distance(entities.begin(), entities.end()) == 4); + const auto conns = ss.connections(); + BOOST_TEST_REQUIRE(std::distance(conns.begin(), conns.end()) == 4); + + // Initial values + constexpr auto initialInput = 11.0; + cse::variable_value_map initialValues; + cse::add_variable_value(initialValues, ss, {"simA", "realIn"}, initialInput); + BOOST_CHECK_THROW( + cse::add_variable_value(initialValues, ss, {"noneSuch", "realIn"}, 1.0), + cse::error); + BOOST_CHECK_THROW( + cse::add_variable_value(initialValues, ss, {"simA", "noneSuch"}, 1.0), + cse::error); + BOOST_CHECK_THROW( + cse::add_variable_value(initialValues, ss, {"simA", "realIn"}, "a string"), + cse::error); + BOOST_CHECK(initialValues.size() == 1); + + // Set up and run an execution to verify that the system structure + // turns out as intended. + auto execution = cse::execution( + startTime, std::make_shared(timeStep)); + const auto obs = std::make_shared(); + execution.add_observer(obs); + + const auto entityIndexes = + cse::inject_system_structure(execution, ss, initialValues); + BOOST_CHECK(entityIndexes.simulators.size() == 3); + BOOST_CHECK(entityIndexes.functions.size() == 1); + + BOOST_CHECK(execution.simulate_until(stopTime).get()); + + const auto realOutRef = mock_slave::real_out_reference; + double realOutA = -1.0, realOutB = -1.0, realOutC = -1.0; + obs->get_real( + entityIndexes.simulators.at("simA"), + gsl::make_span(&realOutRef, 1), + gsl::make_span(&realOutA, 1)); + obs->get_real( + entityIndexes.simulators.at("simB"), + gsl::make_span(&realOutRef, 1), + gsl::make_span(&realOutB, 1)); + obs->get_real( + entityIndexes.simulators.at("simC"), + gsl::make_span(&realOutRef, 1), + gsl::make_span(&realOutC, 1)); + BOOST_CHECK(realOutA == initialInput); + BOOST_CHECK(realOutB == initialInput); + BOOST_CHECK(realOutC == offset + factor * initialInput); +}