diff --git a/include/cosim/algorithm/algorithm.hpp b/include/cosim/algorithm/algorithm.hpp index 3b99caa3..efd0db4e 100644 --- a/include/cosim/algorithm/algorithm.hpp +++ b/include/cosim/algorithm/algorithm.hpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -157,7 +158,9 @@ class algorithm * values for some of them. * * This function is guaranteed to be called after `setup()` and before - * the first `do_step()` call. + * the first `do_step()` call. Furthermore, no more subsimulators and + * functions will be added or removed after `initialize()` has been called; + * that is, `{add,remove}_{simulator,function}()` will not be called again. */ virtual void initialize() = 0; @@ -180,6 +183,36 @@ class algorithm */ virtual std::pair> do_step(time_point currentT) = 0; + /** + * Exports the current state of the algorithm. + * + * Note that system-structural information should not be included in the + * data exported by this function, only internal, algorithm-specific data. + * This is because it will be assumed that the system structure is + * unchanged or has already been restored when the state is imported + * again, as explained in the `import_state()` function documentation. + */ + virtual serialization::node export_current_state() const = 0; + + /** + * Imports a previously-exported algorithm state. + * + * When this function is called, it should be assumed that the system + * structure is the same as when the state was exported. That is, either + * + * 1. this is the algorithm instance from which the state was exported, + * and the system structure actually hasn't changed + * 2. this is a new instance, but the original system structure has been + * restored prior to calling this function + * + * By "system structure", we here mean the subsimulator indexes, the + * function indexes, and the variable connections. + * + * It is guaranteed that this function is never called before + * `initialize()`. + */ + virtual void import_state(const serialization::node& exportedState) = 0; + virtual ~algorithm() noexcept = default; }; diff --git a/include/cosim/algorithm/fixed_step_algorithm.hpp b/include/cosim/algorithm/fixed_step_algorithm.hpp index 26bc02d4..4319d54f 100644 --- a/include/cosim/algorithm/fixed_step_algorithm.hpp +++ b/include/cosim/algorithm/fixed_step_algorithm.hpp @@ -56,6 +56,8 @@ class fixed_step_algorithm : public algorithm void setup(time_point startTime, std::optional stopTime) override; void initialize() override; std::pair> do_step(time_point currentT) override; + serialization::node export_current_state() const override; + void import_state(const serialization::node& exportedState) override; /** * Sets step size decimation factor for a simulator. diff --git a/include/cosim/execution.hpp b/include/cosim/execution.hpp index 31016a84..d15a34d2 100644 --- a/include/cosim/execution.hpp +++ b/include/cosim/execution.hpp @@ -19,6 +19,7 @@ #include +#include #include #include #include @@ -36,7 +37,7 @@ using simulator_index = int; using function_index = int; /// An number which identifies a specific time step in an execution. -using step_number = long long; +using step_number = std::int64_t; /// An object which uniquely identifies a simulator variable in a simulation. struct variable_id @@ -145,6 +146,13 @@ class simulator; * The `execution` class manages all the entities involved in an execution * and provides a high-level API for driving the co-simulation algorithm * forward. + * + * \warning + * In general, the member functions of this class are not exception safe. + * This means that if any of them throw an exception, one must assume that + * the `execution` object is in an invalid state and can no longer be used. + * The same holds for its associated algorithm and any simulators or + * functions that are part of the execution. */ class execution { @@ -179,13 +187,19 @@ class execution * The recommended co-simulation step size for this slave. * Whether and how this is taken into account is algorithm dependent. * If zero, the algorithm will attempt to choose a sensible default. + * + * \pre `initialize()` has not been called. */ simulator_index add_slave( std::shared_ptr slave, std::string_view name, duration stepSizeHint = duration::zero()); - /// Adds a function to the execution. + /** + * Adds a function to the execution. + * + * \pre `initialize()` has not been called. + */ function_index add_function(std::shared_ptr fun); /// Adds an observer to the execution. @@ -242,6 +256,14 @@ class execution /// Returns the current logical time. time_point current_time() const noexcept; + /** + * Initialize the co-simulation (in an algorithm-dependent manner). + * + * After this function is called, it is no longer possible to add more + * subsimulators or functions. + */ + void initialize(); + /** * Advance the co-simulation forward to the given logical time (blocks the current thread). * @@ -255,6 +277,12 @@ class execution * `true` if the co-simulation was advanced to the given time, * or `false` if it was stopped before this. In the latter case, * `current_time()` may be called to determine the actual end time. + * + * \note + * For backwards compatibility, this function automatically calls + * `initialize()` if this hasn't already been done. However, new code + * should always call `initialize()` before any of the + * simulation/stepping functions. */ bool simulate_until(std::optional targetTime); @@ -271,6 +299,12 @@ class execution * `true` if the co-simulation was advanced to the given time, * or `false` if it was stopped before this. In the latter case, * `current_time()` may be called to determine the actual end time. + * + * \note + * For backwards compatibility, this function automatically calls + * `initialize()` if this hasn't already been done. However, new code + * should always call `initialize()` before any of the + * simulation/stepping functions. */ std::future simulate_until_async(std::optional targetTime); @@ -281,6 +315,12 @@ class execution * The actual duration of the step. * `current_time()` may be called to determine the actual time after * the step completed. + * + * \note + * For backwards compatibility, this function automatically calls + * `initialize()` if this hasn't already been done. However, new code + * should always call `initialize()` before any of the + * simulation/stepping functions. */ duration step(); @@ -314,6 +354,28 @@ class execution /// Set initial value for a variable of type string. Must be called before simulation is started. void set_string_initial_value(simulator_index sim, value_reference var, const std::string& value); + /** + * Exports the current state of the co-simulation. + * + * \pre `initialize()` has been called. + * \pre `!is_running()` + */ + serialization::node export_current_state() const; + + /** + * Imports a previously-exported co-simulation state. + * + * Note that the data returned by `export_current_state()` only describe + * the *state* of the system, not its structure. This means that it's the + * caller's responsibility to either + * + * 1. not modify the system structure between state export and state import + * 2. restore the pre-export system structure prior to state import + * + * \pre `initialize()` has been called. + * \pre `!is_running()` + */ + void import_state(const serialization::node& exportedState); private: class impl; diff --git a/include/cosim/observer/file_observer.hpp b/include/cosim/observer/file_observer.hpp index 6f975068..499853dd 100644 --- a/include/cosim/observer/file_observer.hpp +++ b/include/cosim/observer/file_observer.hpp @@ -170,6 +170,8 @@ class file_observer : public observer duration lastStepSize, time_point currentTime) override; + void state_restored(step_number currentStep, time_point currentTime) override; + cosim::filesystem::path get_log_path(); ~file_observer() override; diff --git a/include/cosim/observer/last_value_observer.hpp b/include/cosim/observer/last_value_observer.hpp index f431a803..b235f35b 100644 --- a/include/cosim/observer/last_value_observer.hpp +++ b/include/cosim/observer/last_value_observer.hpp @@ -56,6 +56,8 @@ class last_value_observer : public last_value_provider duration lastStepSize, time_point currentTime) override; + void state_restored(step_number currentStep, time_point currentTime) override; + void get_real( simulator_index sim, gsl::span variables, diff --git a/include/cosim/observer/observer.hpp b/include/cosim/observer/observer.hpp index 3ca9179a..2eecd980 100644 --- a/include/cosim/observer/observer.hpp +++ b/include/cosim/observer/observer.hpp @@ -115,6 +115,9 @@ class observer duration lastStepSize, time_point currentTime) = 0; + /// The simulation was restored to a previously saved state. + virtual void state_restored(step_number currentStep, time_point currentTime) = 0; + virtual ~observer() noexcept { } }; diff --git a/include/cosim/observer/time_series_observer.hpp b/include/cosim/observer/time_series_observer.hpp index 0b508dfa..b4a347ad 100644 --- a/include/cosim/observer/time_series_observer.hpp +++ b/include/cosim/observer/time_series_observer.hpp @@ -65,6 +65,8 @@ class time_series_observer : public time_series_provider duration lastStepSize, time_point currentTime) override; + void state_restored(step_number currentStep, time_point currentTime) override; + /** * Start observing a variable. * diff --git a/src/cosim/algorithm/fixed_step_algorithm.cpp b/src/cosim/algorithm/fixed_step_algorithm.cpp index d59ae4ef..709552af 100644 --- a/src/cosim/algorithm/fixed_step_algorithm.cpp +++ b/src/cosim/algorithm/fixed_step_algorithm.cpp @@ -193,9 +193,9 @@ class fixed_step_algorithm::impl if (stepCounter_ % info.decimationFactor == 0) { pool_.submit([&] { try { - info.stepResult = info.sim->do_step(currentT, baseStepSize_ * info.decimationFactor); + const auto stepResult = info.sim->do_step(currentT, baseStepSize_ * info.decimationFactor); - if (info.stepResult != step_result::complete) { + if (stepResult != step_result::complete) { std::lock_guard lck(m); errMessages << info.sim->name() << ": " @@ -231,6 +231,28 @@ class fixed_step_algorithm::impl return {baseStepSize_, std::move(finished)}; } + serialization::node export_current_state() const + { + auto exportedState = serialization::node(); + exportedState.put("type", std::string("fixed_step_algorithm")); + exportedState.put("step_counter", stepCounter_); + return exportedState; + } + + void import_state(const serialization::node& exportedState) + { + try { + if (exportedState.get("type") != "fixed_step_algorithm") { + throw std::exception(); + } + stepCounter_ = exportedState.get("step_counter"); + } catch (...) { + throw error( + make_error_code(errc::bad_file), + "The serialized algorithm state is invalid or corrupt"); + } + } + void set_stepsize_decimation_factor(cosim::simulator_index i, int factor) { COSIM_INPUT_CHECK(factor > 0); @@ -261,7 +283,6 @@ class fixed_step_algorithm::impl { simulator* sim; int decimationFactor = 1; - step_result stepResult; std::vector outgoingSimConnections; std::vector outgoingFunConnections; }; @@ -421,13 +442,20 @@ class fixed_step_algorithm::impl } } + // Algorithm parameters const duration baseStepSize_; time_point startTime_; std::optional stopTime_; + unsigned int max_threads_ = std::thread::hardware_concurrency() - 1; + + // System structure std::unordered_map simulators_; std::unordered_map functions_; - int64_t stepCounter_ = 0; - unsigned int max_threads_ = std::thread::hardware_concurrency() - 1; + + // Simulation state + std::int64_t stepCounter_ = 0; + + // Other utility::thread_pool pool_; }; @@ -521,6 +549,16 @@ std::pair> fixed_step_algorithm::d return pimpl_->do_step(currentT); } +serialization::node fixed_step_algorithm::export_current_state() const +{ + return pimpl_->export_current_state(); +} + +void fixed_step_algorithm::import_state(const serialization::node& exportedState) +{ + pimpl_->import_state(exportedState); +} + void fixed_step_algorithm::set_stepsize_decimation_factor(cosim::simulator_index simulator, int factor) { pimpl_->set_stepsize_decimation_factor(simulator, factor); diff --git a/src/cosim/execution.cpp b/src/cosim/execution.cpp index 6f6b6bf7..2737a70a 100644 --- a/src/cosim/execution.cpp +++ b/src/cosim/execution.cpp @@ -6,6 +6,7 @@ #include "cosim/execution.hpp" #include "cosim/algorithm.hpp" +#include "cosim/error.hpp" #include "cosim/exception.hpp" #include "cosim/slave_simulator.hpp" #include "cosim/utility/utility.hpp" @@ -48,6 +49,7 @@ class execution::impl std::string_view name, duration stepSizeHint) { + COSIM_PRECONDITION(!initialized_); const auto index = static_cast(simulators_.size()); simulators_.push_back(std::make_unique(slave, name)); algorithm_->add_simulator(index, simulators_.back().get(), stepSizeHint); @@ -63,6 +65,7 @@ class execution::impl function_index add_function(std::shared_ptr fun) { + COSIM_PRECONDITION(!initialized_); const auto index = static_cast(functions_.size()); functions_.push_back(fun); algorithm_->add_function(index, fun.get()); @@ -114,12 +117,7 @@ class execution::impl return currentTime_; } - bool is_running() const noexcept - { - return !stopped_; - } - - duration step() + void initialize() { if (!initialized_) { algorithm_->initialize(); @@ -128,6 +126,17 @@ class execution::impl obs->simulation_initialized(lastStep_, currentTime_); } } + } + + + bool is_running() const noexcept + { + return !stopped_; + } + + duration step() + { + initialize(); // For backwards compatibility. Should be replaced with a precondition check at some point. for (const auto& man : manipulators_) { man->step_commencing(currentTime_); } @@ -258,6 +267,52 @@ class execution::impl simulators_.at(sim)->set_string(var, value); } + serialization::node export_current_state() const + { + COSIM_PRECONDITION(initialized_ && !is_running()); + serialization::node simulatorState; + for (const auto& sim : simulators_) { + const auto savedStateIndex = sim->save_state(); + simulatorState.put_child(sim->name(), sim->export_state(savedStateIndex)); + sim->release_state(savedStateIndex); + } + serialization::node exportedState; + exportedState.put("last_step_number", lastStep_); + exportedState.put("current_time_ticks", currentTime_.time_since_epoch().count()); + exportedState.put_child("algorithm_state", algorithm_->export_current_state()); + exportedState.put_child("simulator_state", simulatorState); + return exportedState; + } + + void import_state(const serialization::node& exportedState) + { + COSIM_PRECONDITION(initialized_ && !is_running()); + try { + lastStep_ = exportedState.get("last_step_number"); + currentTime_ = time_point(duration( + exportedState.get("current_time_ticks"))); + const auto& simulatorState = exportedState.get_child("simulator_state"); + for (const auto& sim : simulators_) { + const auto savedStateIndex = + sim->import_state(simulatorState.get_child(sim->name())); + sim->restore_state(savedStateIndex); + sim->release_state(savedStateIndex); + } + algorithm_->import_state(exportedState.get_child("algorithm_state")); + } catch (const error&) { + throw; + } catch (...) { + throw error( + make_error_code(errc::bad_file), + "The serialized execution state is invalid or corrupt"); + } + + // Update observers + for (const auto& obs : observers_) { + obs->state_restored(lastStep_, currentTime_); + } + } + private: template void connect_variables_impl( @@ -381,6 +436,11 @@ time_point execution::current_time() const noexcept return pimpl_->current_time(); } +void execution::initialize() +{ + pimpl_->initialize(); +} + bool execution::simulate_until(std::optional endTime) { return pimpl_->simulate_until(endTime); @@ -446,6 +506,16 @@ void execution::set_string_initial_value(simulator_index sim, value_reference va pimpl_->set_string_initial_value(sim, var, value); } +serialization::node execution::export_current_state() const +{ + return pimpl_->export_current_state(); +} + +void execution::import_state(const serialization::node& exportedState) +{ + pimpl_->import_state(exportedState); +} + namespace { diff --git a/src/cosim/observer/file_observer.cpp b/src/cosim/observer/file_observer.cpp index bc87b401..72a4011f 100644 --- a/src/cosim/observer/file_observer.cpp +++ b/src/cosim/observer/file_observer.cpp @@ -429,6 +429,15 @@ void file_observer::simulator_step_complete(simulator_index index, step_number l } } +void file_observer::state_restored(step_number currentStep, time_point currentTime) +{ + if (recording_) { + for (const auto& entry : valueWriters_) { + entry.second->observe(currentStep, currentTime); + } + } +} + cosim::filesystem::path file_observer::get_log_path() { return logDir_; diff --git a/src/cosim/observer/last_value_observer.cpp b/src/cosim/observer/last_value_observer.cpp index 53057021..c977f2dd 100644 --- a/src/cosim/observer/last_value_observer.cpp +++ b/src/cosim/observer/last_value_observer.cpp @@ -61,6 +61,15 @@ void last_value_observer::simulator_step_complete( valueProviders_.at(index)->observe(); } +void last_value_observer::state_restored( + step_number /*currentStep*/, + time_point /*currentTime*/) +{ + for (const auto& entry : valueProviders_) { + entry.second->observe(); + } +} + void last_value_observer::get_real( simulator_index sim, gsl::span variables, diff --git a/src/cosim/observer/time_series_observer.cpp b/src/cosim/observer/time_series_observer.cpp index 09d40b8b..44cc55d3 100644 --- a/src/cosim/observer/time_series_observer.cpp +++ b/src/cosim/observer/time_series_observer.cpp @@ -262,6 +262,14 @@ void time_series_observer::simulator_step_complete( slaveObservers_.at(index)->observe(lastStep, currentTime); } +void time_series_observer::state_restored( + step_number currentStep, time_point currentTime) +{ + for (const auto& entry : slaveObservers_) { + entry.second->observe(currentStep, currentTime); + } +} + void time_series_observer::start_observing(variable_id id) { slaveObservers_.at(id.simulator)->start_observing(id.type, id.reference); diff --git a/src/cosim/slave_simulator.cpp b/src/cosim/slave_simulator.cpp index b3bbe5b1..4c39fb05 100644 --- a/src/cosim/slave_simulator.cpp +++ b/src/cosim/slave_simulator.cpp @@ -79,6 +79,38 @@ struct get_variable_cache } } } + + serialization::node export_state() const + { + assert(references.size() == originalValues.size()); + assert(std::all_of(modifiers.begin(), modifiers.end(), [](auto m) { return !m; })); + serialization::node exportedState; + for (std::size_t i = 0; i < references.size(); ++i) { + exportedState.put( + std::to_string(references[i]), + originalValues[i]); + } + return exportedState; + } + + void import_state(const serialization::node& exportedState) + { + std::vector importedReferences; + boost::container::vector importedValues; + std::unordered_map newMapping; + for (const auto& [key, child] : exportedState) { + const value_reference ref = std::stoul(key); + const auto val = child.template get_value(); + importedReferences.push_back(ref); + importedValues.push_back(val); + newMapping[ref] = importedReferences.size() - 1; + } + references.swap(importedReferences); + originalValues.swap(importedValues); + modifiedValues = originalValues; // copy + modifiers.assign(modifiedValues.size(), nullptr); + indexMapping.swap(newMapping); + } }; @@ -102,12 +134,7 @@ class set_variable_cache throw std::out_of_range(oss.str()); } it->second.lastValue = v; - if (it->second.arrayIndex < 0) { - it->second.arrayIndex = references_.size(); - assert(references_.size() == values_.size()); - references_.emplace_back(r); - values_.emplace_back(v); - } else { + if (!make_cache_slot(r, it->second)) { assert(references_[it->second.arrayIndex] == r); values_[it->second.arrayIndex] = v; } @@ -123,13 +150,7 @@ class set_variable_cache << " not found in exposed variables. Variables must be exposed before calling set_modifier()"; throw std::out_of_range(oss.str()); } - if (it->second.arrayIndex < 0) { - // Ensure that the simulator receives an updated value. - it->second.arrayIndex = references_.size(); - assert(references_.size() == values_.size()); - references_.emplace_back(r); - values_.emplace_back(it->second.lastValue); - } + make_cache_slot(r, it->second); if (m) { modifiers_[r] = m; } else { @@ -137,19 +158,15 @@ class set_variable_cache } } - std::pair, gsl::span> modify_and_get(duration deltaT) + std::pair, gsl::span> modify_and_get(duration deltaT) { if (!hasRunModifiers_) { for (const auto& entry : modifiers_) { const auto ref = entry.first; - auto& arrayIndex = exposedVariables_.at(ref).arrayIndex; - if (arrayIndex < 0) { - arrayIndex = references_.size(); - assert(references_.size() == values_.size()); - references_.emplace_back(ref); - values_.emplace_back(exposedVariables_.at(ref).lastValue); - } - values_[arrayIndex] = entry.second(values_[arrayIndex], deltaT); + auto& exposedVariable = exposedVariables_.at(ref); + make_cache_slot(ref, exposedVariable); + values_[exposedVariable.arrayIndex] = + entry.second(values_[exposedVariable.arrayIndex], deltaT); } assert(references_.size() == values_.size()); hasRunModifiers_ = true; @@ -167,6 +184,31 @@ class set_variable_cache hasRunModifiers_ = false; } + serialization::node export_state() const + { + assert(modifiers_.empty()); + serialization::node exportedState; + for (const auto& [ref, var] : exposedVariables_) { + exportedState.put(std::to_string(ref), var.lastValue); + } + return exportedState; + } + + void import_state(const serialization::node& exportedState) + { + assert(modifiers_.empty()); + std::unordered_map variables; + for (const auto& [key, child] : exportedState) { + variables.emplace( + std::stoul(key), + exposed_variable{child.template get_value()}); + } + exposedVariables_.swap(variables); + hasRunModifiers_ = false; + references_.clear(); + values_.clear(); + } + private: struct exposed_variable { @@ -178,6 +220,23 @@ class set_variable_cache std::ptrdiff_t arrayIndex = -1; }; + // If the given `exposed_variable` does not yet have slots in the + // `references_` and `values_` arrays, this function creates them and + // returns `true`. Otherwise, it returns `false` to signify that no new + // slot needed to be created. + bool make_cache_slot(value_reference ref, exposed_variable& var) + { + if (var.arrayIndex < 0) { + var.arrayIndex = references_.size(); + assert(references_.size() == values_.size()); + references_.emplace_back(ref); + values_.emplace_back(var.lastValue); + return true; + } else { + return false; + } + } + std::unordered_map exposedVariables_; // The modifiers associated with certain variables, and a flag that @@ -245,16 +304,16 @@ class slave_simulator::impl { switch (type) { case variable_type::real: - realGetCache_.expose(ref); + state_.realGetCache.expose(ref); break; case variable_type::integer: - integerGetCache_.expose(ref); + state_.integerGetCache.expose(ref); break; case variable_type::boolean: - booleanGetCache_.expose(ref); + state_.booleanGetCache.expose(ref); break; case variable_type::string: - stringGetCache_.expose(ref); + state_.stringGetCache.expose(ref); break; case variable_type::enumeration: COSIM_PANIC(); @@ -263,22 +322,22 @@ class slave_simulator::impl double get_real(value_reference ref) const { - return realGetCache_.get(ref); + return state_.realGetCache.get(ref); } int get_integer(value_reference ref) const { - return integerGetCache_.get(ref); + return state_.integerGetCache.get(ref); } bool get_boolean(value_reference ref) const { - return booleanGetCache_.get(ref); + return state_.booleanGetCache.get(ref); } std::string_view get_string(value_reference ref) const { - return stringGetCache_.get(ref); + return state_.stringGetCache.get(ref); } void expose_for_setting(variable_type type, value_reference ref) @@ -286,16 +345,16 @@ class slave_simulator::impl const auto vd = find_variable_description(ref, type); switch (type) { case variable_type::real: - realSetCache_.expose(ref, get_start_value(vd)); + state_.realSetCache.expose(ref, get_start_value(vd)); break; case variable_type::integer: - integerSetCache_.expose(ref, get_start_value(vd)); + state_.integerSetCache.expose(ref, get_start_value(vd)); break; case variable_type::boolean: - booleanSetCache_.expose(ref, get_start_value(vd)); + state_.booleanSetCache.expose(ref, get_start_value(vd)); break; case variable_type::string: - stringSetCache_.expose(ref, get_start_value(vd)); + state_.stringSetCache.expose(ref, get_start_value(vd)); break; case variable_type::enumeration: COSIM_PANIC(); @@ -304,29 +363,29 @@ class slave_simulator::impl void set_real(value_reference ref, double value) { - realSetCache_.set_value(ref, value); + state_.realSetCache.set_value(ref, value); } void set_integer(value_reference ref, int value) { - integerSetCache_.set_value(ref, value); + state_.integerSetCache.set_value(ref, value); } void set_boolean(value_reference ref, bool value) { - booleanSetCache_.set_value(ref, value); + state_.booleanSetCache.set_value(ref, value); } void set_string(value_reference ref, std::string_view value) { - stringSetCache_.set_value(ref, value); + state_.stringSetCache.set_value(ref, value); } void set_real_input_modifier( value_reference ref, std::function modifier) { - realSetCache_.set_modifier(ref, modifier); + state_.realSetCache.set_modifier(ref, modifier); set_modified_reference(modifiedRealVariables_, ref, modifier ? true : false); } @@ -334,7 +393,7 @@ class slave_simulator::impl value_reference ref, std::function modifier) { - integerSetCache_.set_modifier(ref, modifier); + state_.integerSetCache.set_modifier(ref, modifier); set_modified_reference(modifiedIntegerVariables_, ref, modifier ? true : false); } @@ -342,7 +401,7 @@ class slave_simulator::impl value_reference ref, std::function modifier) { - booleanSetCache_.set_modifier(ref, modifier); + state_.booleanSetCache.set_modifier(ref, modifier); set_modified_reference(modifiedBooleanVariables_, ref, modifier ? true : false); } @@ -350,7 +409,7 @@ class slave_simulator::impl value_reference ref, std::function modifier) { - stringSetCache_.set_modifier(ref, modifier); + state_.stringSetCache.set_modifier(ref, modifier); set_modified_reference(modifiedStringVariables_, ref, modifier ? true : false); } @@ -358,7 +417,7 @@ class slave_simulator::impl value_reference ref, std::function modifier) { - realGetCache_.set_modifier(ref, modifier); + state_.realGetCache.set_modifier(ref, modifier); set_modified_reference(modifiedRealVariables_, ref, modifier ? true : false); } @@ -366,7 +425,7 @@ class slave_simulator::impl value_reference ref, std::function modifier) { - integerGetCache_.set_modifier(ref, modifier); + state_.integerGetCache.set_modifier(ref, modifier); set_modified_reference(modifiedIntegerVariables_, ref, modifier ? true : false); } @@ -374,7 +433,7 @@ class slave_simulator::impl value_reference ref, std::function modifier) { - booleanGetCache_.set_modifier(ref, modifier); + state_.booleanGetCache.set_modifier(ref, modifier); set_modified_reference(modifiedBooleanVariables_, ref, modifier ? true : false); } @@ -382,7 +441,7 @@ class slave_simulator::impl value_reference ref, std::function modifier) { - stringGetCache_.set_modifier(ref, modifier); + state_.stringGetCache.set_modifier(ref, modifier); set_modified_reference(modifiedStringVariables_, ref, modifier ? true : false); } @@ -441,27 +500,29 @@ class slave_simulator::impl simulator::state_index save_state() { check_state_saving_allowed(); - set_variables(duration::zero()); - return slave_->save_state(); + const auto stateIndex = slave_->save_state(); + savedStates_.emplace(stateIndex, state_); + return stateIndex; } void save_state(simulator::state_index stateIndex) { check_state_saving_allowed(); - set_variables(duration::zero()); slave_->save_state(stateIndex); + savedStates_.at(stateIndex) = state_; } void restore_state(simulator::state_index stateIndex) { check_state_saving_allowed(); slave_->restore_state(stateIndex); - get_variables(duration::zero()); + state_ = savedStates_.at(stateIndex); } void release_state(simulator::state_index stateIndex) { slave_->release_state(stateIndex); + savedStates_.erase(stateIndex); } // IMPORTANT: @@ -476,6 +537,15 @@ class slave_simulator::impl serialization::node exportedState; exportedState.put("scheme_version", export_scheme_version); exportedState.put_child("state", slave_->export_state(stateIndex)); + const auto& savedState = savedStates_.at(stateIndex); + exportedState.put_child("real_get_cache", savedState.realGetCache.export_state()); + exportedState.put_child("integer_get_cache", savedState.integerGetCache.export_state()); + exportedState.put_child("boolean_get_cache", savedState.booleanGetCache.export_state()); + exportedState.put_child("string_get_cache", savedState.stringGetCache.export_state()); + exportedState.put_child("real_set_cache", savedState.realSetCache.export_state()); + exportedState.put_child("integer_set_cache", savedState.integerSetCache.export_state()); + exportedState.put_child("boolean_set_cache", savedState.booleanSetCache.export_state()); + exportedState.put_child("string_set_cache", savedState.stringSetCache.export_state()); return exportedState; } @@ -489,7 +559,19 @@ class slave_simulator::impl "The serialized state of subsimulator '" + name_ + "' uses an incompatible scheme"); } - return slave_->import_state(exportedState.get_child("state")); + const auto stateIndex = + slave_->import_state(exportedState.get_child("state")); + assert(savedStates_.count(stateIndex) == 0); + auto& savedState = savedStates_.try_emplace(stateIndex).first->second; + savedState.realGetCache.import_state(exportedState.get_child("real_get_cache")); + savedState.integerGetCache.import_state(exportedState.get_child("integer_get_cache")); + savedState.booleanGetCache.import_state(exportedState.get_child("boolean_get_cache")); + savedState.stringGetCache.import_state(exportedState.get_child("string_get_cache")); + savedState.realSetCache.import_state(exportedState.get_child("real_set_cache")); + savedState.integerSetCache.import_state(exportedState.get_child("integer_set_cache")); + savedState.booleanSetCache.import_state(exportedState.get_child("boolean_set_cache")); + savedState.stringSetCache.import_state(exportedState.get_child("string_set_cache")); + return stateIndex; } catch (const boost::property_tree::ptree_bad_path&) { throw error( make_error_code(errc::bad_file), @@ -506,10 +588,10 @@ class slave_simulator::impl private: void set_variables(duration deltaT) { - const auto [realRefs, realValues] = realSetCache_.modify_and_get(deltaT); - const auto [integerRefs, integerValues] = integerSetCache_.modify_and_get(deltaT); - const auto [booleanRefs, booleanValues] = booleanSetCache_.modify_and_get(deltaT); - const auto [stringRefs, stringValues] = stringSetCache_.modify_and_get(deltaT); + const auto [realRefs, realValues] = state_.realSetCache.modify_and_get(deltaT); + const auto [integerRefs, integerValues] = state_.integerSetCache.modify_and_get(deltaT); + const auto [booleanRefs, booleanValues] = state_.booleanSetCache.modify_and_get(deltaT); + const auto [stringRefs, stringValues] = state_.stringSetCache.modify_and_get(deltaT); slave_->set_variables( gsl::make_span(realRefs), gsl::make_span(realValues), @@ -519,28 +601,28 @@ class slave_simulator::impl gsl::make_span(booleanValues), gsl::make_span(stringRefs), gsl::make_span(stringValues)); - realSetCache_.reset(); - integerSetCache_.reset(); - booleanSetCache_.reset(); - stringSetCache_.reset(); + state_.realSetCache.reset(); + state_.integerSetCache.reset(); + state_.booleanSetCache.reset(); + state_.stringSetCache.reset(); } void get_variables(duration deltaT) { slave_->get_variables( - &variable_values_, - gsl::make_span(realGetCache_.references), - gsl::make_span(integerGetCache_.references), - gsl::make_span(booleanGetCache_.references), - gsl::make_span(stringGetCache_.references)); - copy_contents(variable_values_.real, realGetCache_.originalValues); - copy_contents(variable_values_.integer, integerGetCache_.originalValues); - copy_contents(variable_values_.boolean, booleanGetCache_.originalValues); - copy_contents(variable_values_.string, stringGetCache_.originalValues); - realGetCache_.run_modifiers(deltaT); - integerGetCache_.run_modifiers(deltaT); - booleanGetCache_.run_modifiers(deltaT); - stringGetCache_.run_modifiers(deltaT); + &variableValues_, + gsl::make_span(state_.realGetCache.references), + gsl::make_span(state_.integerGetCache.references), + gsl::make_span(state_.booleanGetCache.references), + gsl::make_span(state_.stringGetCache.references)); + copy_contents(variableValues_.real, state_.realGetCache.originalValues); + copy_contents(variableValues_.integer, state_.integerGetCache.originalValues); + copy_contents(variableValues_.boolean, state_.booleanGetCache.originalValues); + copy_contents(variableValues_.string, state_.stringGetCache.originalValues); + state_.realGetCache.run_modifiers(deltaT); + state_.integerGetCache.run_modifiers(deltaT); + state_.booleanGetCache.run_modifiers(deltaT); + state_.stringGetCache.run_modifiers(deltaT); } variable_description find_variable_description(value_reference ref, variable_type type) @@ -584,22 +666,29 @@ class slave_simulator::impl std::string name_; cosim::model_description modelDescription_; - get_variable_cache realGetCache_; - get_variable_cache integerGetCache_; - get_variable_cache booleanGetCache_; - get_variable_cache stringGetCache_; + struct state + { + get_variable_cache realGetCache; + get_variable_cache integerGetCache; + get_variable_cache booleanGetCache; + get_variable_cache stringGetCache; - set_variable_cache realSetCache_; - set_variable_cache integerSetCache_; - set_variable_cache booleanSetCache_; - set_variable_cache stringSetCache_; + set_variable_cache realSetCache; + set_variable_cache integerSetCache; + set_variable_cache booleanSetCache; + set_variable_cache stringSetCache; + }; + state state_; + std::unordered_map savedStates_; std::unordered_set modifiedRealVariables_; std::unordered_set modifiedIntegerVariables_; std::unordered_set modifiedBooleanVariables_; std::unordered_set modifiedStringVariables_; - cosim::slave::variable_values variable_values_; + // This is a temporary storage which gets reused within certain functions + // to avoid frequent reallocations. + cosim::slave::variable_values variableValues_; }; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1849a9ba..262f955d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -8,6 +8,7 @@ set(tests "multi_fixed_step_algorithm_test" "osp_config_parser_test" "ramp_modifier_test" + "save_state_test" "time_series_observer_test" "trend_buffer_test" "scenario_manager_test" diff --git a/tests/mock_slave.hpp b/tests/mock_slave.hpp index b91d3410..35c85588 100644 --- a/tests/mock_slave.hpp +++ b/tests/mock_slave.hpp @@ -252,7 +252,7 @@ class mock_slave : public cosim::slave es.put("realIn", ss.realIn); es.put("intIn", ss.intIn); es.put("boolIn", ss.boolIn); - es.put("strginIn", ss.stringIn); + es.put("stringIn", ss.stringIn); return es; } diff --git a/tests/save_state_test.cpp b/tests/save_state_test.cpp new file mode 100644 index 00000000..42357d11 --- /dev/null +++ b/tests/save_state_test.cpp @@ -0,0 +1,120 @@ +#include "mock_slave.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + + +// A helper macro to test various assertions +#define REQUIRE(test) \ + if (!(test)) throw std::runtime_error("Requirement not satisfied: " #test) + +// Helper function to get observed values from multiple simulators. +std::vector get_reals( + cosim::last_value_observer& observer, + const std::vector& simulators, + cosim::value_reference valueRef) +{ + auto values = std::vector( + simulators.size(), std::numeric_limits::quiet_NaN()); + for (std::size_t i = 0; i < simulators.size(); ++i) { + observer.get_real( + simulators[i], + gsl::make_span(&valueRef, 1), + gsl::make_span(values.data() + i, 1)); + } + return values; +} + + +int main() +{ + try { + cosim::log::setup_simple_console_logging(); + cosim::log::set_global_output_level(cosim::log::debug); + + constexpr int simulatorCount = 10; + constexpr cosim::time_point time0; + constexpr cosim::time_point time1 = cosim::to_time_point(0.6); + constexpr cosim::time_point time2 = cosim::to_time_point(1.0); + constexpr cosim::duration stepSize = cosim::to_duration(0.05); + + // Set up execution + auto execution = cosim::execution( + time0, + std::make_unique(stepSize, 1)); + + auto observer = std::make_shared(); + execution.add_observer(observer); + + const cosim::value_reference realOutRef = mock_slave::real_out_reference; + const cosim::value_reference realInRef = mock_slave::real_in_reference; + + + // Add and connect subsimulators + std::vector simulators; + simulators.push_back( + execution.add_slave( + std::make_unique([](cosim::time_point t, double) { + return cosim::to_double_time_point(t); + }), + "clock")); + for (int i = 1; i < simulatorCount; ++i) { + simulators.push_back( + execution.add_slave( + std::make_unique([](double x) { + return x + 1.234; + }), + "adder" + std::to_string(i))); + execution.connect_variables( + cosim::variable_id{simulators[i - 1], cosim::variable_type::real, realOutRef}, + cosim::variable_id{simulators[i], cosim::variable_type::real, realInRef}); + } + + // Save initial state + execution.initialize(); + auto state0Values = get_reals(*observer, simulators, realOutRef); + const auto state0 = execution.export_current_state(); + + // Advance to time1 and save state again + execution.simulate_until(time1); + auto state1Values = get_reals(*observer, simulators, realOutRef); + const auto state1 = execution.export_current_state(); + REQUIRE(state1Values > state0Values); + + // Advance to time2 and save state again + execution.simulate_until(time2); + auto state2Values = get_reals(*observer, simulators, realOutRef); + const auto state2 = execution.export_current_state(); + REQUIRE(state2Values > state1Values); + + // Restore state0 and compare values + execution.import_state(state0); + REQUIRE(execution.current_time() == time0); + auto state0ValuesAgain = get_reals(*observer, simulators, realOutRef); + REQUIRE(state0ValuesAgain == state0Values); + + // Advance to time1 again and compare values + execution.simulate_until(time1); + auto state1ValuesAgain = get_reals(*observer, simulators, realOutRef); + REQUIRE(state1ValuesAgain == state1Values); + + // Restore state2 and compare values + execution.import_state(state2); + REQUIRE(execution.current_time() == time2); + auto state2ValuesAgain = get_reals(*observer, simulators, realOutRef); + REQUIRE(state2ValuesAgain == state2Values); + + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; + return 1; + } + return 0; +}