Skip to content

Commit 9037906

Browse files
authored
Enable saving and restoring full simulation state (#777)
This closes #768. Some changes may warrant a bit of extra explanation: **`execution` and `algorithm`:** I have taken one step towards prohibiting adding/removing sub-simulators after the co-simulation has begun, as decided in #771. In particular, I've added the `execution::initialize()` function, which marks the point where such changes are no longer allowed. (This is a backwards-compatible change, because it gets automatically called by `execution::step()` if it hasn't been called manually.) **`slave_simulator`**: The changes in `slave_simulator.cpp` really ought to have been included in #769. Unfortunately, I didn't realise they were necessary before it was all put into the context of saving algorithm and execution state. Basically, I thought I could get away with just saving each FMU's internal state, but it turns out that we also need to save the `slave_simulator` "get" and "set" caches. (For those interested in the nitty-gritties, this is due to some subtleties regarding exactly when the cache values are set in the course of a co-simulation, relative to when the values are passed to the FMU. At the end of an "algorithm step", the "set cache" is out of sync with the FMUs input variables, and won't be synced before the next step. Simply performing an additional sync prior to saving the state is not sufficient, because that could have an effect on the FMUs output variables, thus invalidating the "get cache". That could in principle be updated too, but then the `slave_simulator` is in a whole different state from where it was when we started to save the state.)
1 parent efdbc87 commit 9037906

16 files changed

+554
-96
lines changed

include/cosim/algorithm/algorithm.hpp

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include <cosim/function/function.hpp>
1616
#include <cosim/model_description.hpp>
1717
#include <cosim/observer/observer.hpp>
18+
#include <cosim/serialization.hpp>
1819
#include <cosim/time.hpp>
1920

2021
#include <functional>
@@ -157,7 +158,9 @@ class algorithm
157158
* values for some of them.
158159
*
159160
* This function is guaranteed to be called after `setup()` and before
160-
* the first `do_step()` call.
161+
* the first `do_step()` call. Furthermore, no more subsimulators and
162+
* functions will be added or removed after `initialize()` has been called;
163+
* that is, `{add,remove}_{simulator,function}()` will not be called again.
161164
*/
162165
virtual void initialize() = 0;
163166

@@ -180,6 +183,36 @@ class algorithm
180183
*/
181184
virtual std::pair<duration, std::unordered_set<simulator_index>> do_step(time_point currentT) = 0;
182185

186+
/**
187+
* Exports the current state of the algorithm.
188+
*
189+
* Note that system-structural information should not be included in the
190+
* data exported by this function, only internal, algorithm-specific data.
191+
* This is because it will be assumed that the system structure is
192+
* unchanged or has already been restored when the state is imported
193+
* again, as explained in the `import_state()` function documentation.
194+
*/
195+
virtual serialization::node export_current_state() const = 0;
196+
197+
/**
198+
* Imports a previously-exported algorithm state.
199+
*
200+
* When this function is called, it should be assumed that the system
201+
* structure is the same as when the state was exported. That is, either
202+
*
203+
* 1. this is the algorithm instance from which the state was exported,
204+
* and the system structure actually hasn't changed
205+
* 2. this is a new instance, but the original system structure has been
206+
* restored prior to calling this function
207+
*
208+
* By "system structure", we here mean the subsimulator indexes, the
209+
* function indexes, and the variable connections.
210+
*
211+
* It is guaranteed that this function is never called before
212+
* `initialize()`.
213+
*/
214+
virtual void import_state(const serialization::node& exportedState) = 0;
215+
183216
virtual ~algorithm() noexcept = default;
184217
};
185218

include/cosim/algorithm/fixed_step_algorithm.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ class fixed_step_algorithm : public algorithm
5656
void setup(time_point startTime, std::optional<time_point> stopTime) override;
5757
void initialize() override;
5858
std::pair<duration, std::unordered_set<simulator_index>> do_step(time_point currentT) override;
59+
serialization::node export_current_state() const override;
60+
void import_state(const serialization::node& exportedState) override;
5961

6062
/**
6163
* Sets step size decimation factor for a simulator.

include/cosim/execution.hpp

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
#include <boost/functional/hash.hpp>
2121

22+
#include <cstdint>
2223
#include <future>
2324
#include <memory>
2425
#include <optional>
@@ -36,7 +37,7 @@ using simulator_index = int;
3637
using function_index = int;
3738

3839
/// An number which identifies a specific time step in an execution.
39-
using step_number = long long;
40+
using step_number = std::int64_t;
4041

4142
/// An object which uniquely identifies a simulator variable in a simulation.
4243
struct variable_id
@@ -145,6 +146,13 @@ class simulator;
145146
* The `execution` class manages all the entities involved in an execution
146147
* and provides a high-level API for driving the co-simulation algorithm
147148
* forward.
149+
*
150+
* \warning
151+
* In general, the member functions of this class are not exception safe.
152+
* This means that if any of them throw an exception, one must assume that
153+
* the `execution` object is in an invalid state and can no longer be used.
154+
* The same holds for its associated algorithm and any simulators or
155+
* functions that are part of the execution.
148156
*/
149157
class execution
150158
{
@@ -179,13 +187,19 @@ class execution
179187
* The recommended co-simulation step size for this slave.
180188
* Whether and how this is taken into account is algorithm dependent.
181189
* If zero, the algorithm will attempt to choose a sensible default.
190+
*
191+
* \pre `initialize()` has not been called.
182192
*/
183193
simulator_index add_slave(
184194
std::shared_ptr<slave> slave,
185195
std::string_view name,
186196
duration stepSizeHint = duration::zero());
187197

188-
/// Adds a function to the execution.
198+
/**
199+
* Adds a function to the execution.
200+
*
201+
* \pre `initialize()` has not been called.
202+
*/
189203
function_index add_function(std::shared_ptr<function> fun);
190204

191205
/// Adds an observer to the execution.
@@ -242,6 +256,14 @@ class execution
242256
/// Returns the current logical time.
243257
time_point current_time() const noexcept;
244258

259+
/**
260+
* Initialize the co-simulation (in an algorithm-dependent manner).
261+
*
262+
* After this function is called, it is no longer possible to add more
263+
* subsimulators or functions.
264+
*/
265+
void initialize();
266+
245267
/**
246268
* Advance the co-simulation forward to the given logical time (blocks the current thread).
247269
*
@@ -255,6 +277,12 @@ class execution
255277
* `true` if the co-simulation was advanced to the given time,
256278
* or `false` if it was stopped before this. In the latter case,
257279
* `current_time()` may be called to determine the actual end time.
280+
*
281+
* \note
282+
* For backwards compatibility, this function automatically calls
283+
* `initialize()` if this hasn't already been done. However, new code
284+
* should always call `initialize()` before any of the
285+
* simulation/stepping functions.
258286
*/
259287
bool simulate_until(std::optional<time_point> targetTime);
260288

@@ -271,6 +299,12 @@ class execution
271299
* `true` if the co-simulation was advanced to the given time,
272300
* or `false` if it was stopped before this. In the latter case,
273301
* `current_time()` may be called to determine the actual end time.
302+
*
303+
* \note
304+
* For backwards compatibility, this function automatically calls
305+
* `initialize()` if this hasn't already been done. However, new code
306+
* should always call `initialize()` before any of the
307+
* simulation/stepping functions.
274308
*/
275309
std::future<bool> simulate_until_async(std::optional<time_point> targetTime);
276310

@@ -281,6 +315,12 @@ class execution
281315
* The actual duration of the step.
282316
* `current_time()` may be called to determine the actual time after
283317
* the step completed.
318+
*
319+
* \note
320+
* For backwards compatibility, this function automatically calls
321+
* `initialize()` if this hasn't already been done. However, new code
322+
* should always call `initialize()` before any of the
323+
* simulation/stepping functions.
284324
*/
285325
duration step();
286326

@@ -314,6 +354,28 @@ class execution
314354
/// Set initial value for a variable of type string. Must be called before simulation is started.
315355
void set_string_initial_value(simulator_index sim, value_reference var, const std::string& value);
316356

357+
/**
358+
* Exports the current state of the co-simulation.
359+
*
360+
* \pre `initialize()` has been called.
361+
* \pre `!is_running()`
362+
*/
363+
serialization::node export_current_state() const;
364+
365+
/**
366+
* Imports a previously-exported co-simulation state.
367+
*
368+
* Note that the data returned by `export_current_state()` only describe
369+
* the *state* of the system, not its structure. This means that it's the
370+
* caller's responsibility to either
371+
*
372+
* 1. not modify the system structure between state export and state import
373+
* 2. restore the pre-export system structure prior to state import
374+
*
375+
* \pre `initialize()` has been called.
376+
* \pre `!is_running()`
377+
*/
378+
void import_state(const serialization::node& exportedState);
317379

318380
private:
319381
class impl;

include/cosim/observer/file_observer.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,8 @@ class file_observer : public observer
170170
duration lastStepSize,
171171
time_point currentTime) override;
172172

173+
void state_restored(step_number currentStep, time_point currentTime) override;
174+
173175
cosim::filesystem::path get_log_path();
174176

175177
~file_observer() override;

include/cosim/observer/last_value_observer.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ class last_value_observer : public last_value_provider
5656
duration lastStepSize,
5757
time_point currentTime) override;
5858

59+
void state_restored(step_number currentStep, time_point currentTime) override;
60+
5961
void get_real(
6062
simulator_index sim,
6163
gsl::span<const value_reference> variables,

include/cosim/observer/observer.hpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,17 @@ class observer
115115
duration lastStepSize,
116116
time_point currentTime) = 0;
117117

118+
/**
119+
* The simulation was restored to a previously saved state.
120+
*
121+
* Note that observers which support this feature must be able to
122+
* reconstruct their internal state using information which is available
123+
* through the `observable` objects they have been given access to. For
124+
* observers where this is not the case, this function should throw
125+
* `cosim::error` with error code `cosim::errc::unsupported_feature`.
126+
*/
127+
virtual void state_restored(step_number currentStep, time_point currentTime) = 0;
128+
118129
virtual ~observer() noexcept { }
119130
};
120131

include/cosim/observer/time_series_observer.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ class time_series_observer : public time_series_provider
6565
duration lastStepSize,
6666
time_point currentTime) override;
6767

68+
void state_restored(step_number currentStep, time_point currentTime) override;
69+
6870
/**
6971
* Start observing a variable.
7072
*

src/cosim/algorithm/fixed_step_algorithm.cpp

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -193,9 +193,9 @@ class fixed_step_algorithm::impl
193193
if (stepCounter_ % info.decimationFactor == 0) {
194194
pool_.submit([&] {
195195
try {
196-
info.stepResult = info.sim->do_step(currentT, baseStepSize_ * info.decimationFactor);
196+
const auto stepResult = info.sim->do_step(currentT, baseStepSize_ * info.decimationFactor);
197197

198-
if (info.stepResult != step_result::complete) {
198+
if (stepResult != step_result::complete) {
199199
std::lock_guard<std::mutex> lck(m);
200200
errMessages
201201
<< info.sim->name() << ": "
@@ -231,6 +231,28 @@ class fixed_step_algorithm::impl
231231
return {baseStepSize_, std::move(finished)};
232232
}
233233

234+
serialization::node export_current_state() const
235+
{
236+
auto exportedState = serialization::node();
237+
exportedState.put("type", std::string("fixed_step_algorithm"));
238+
exportedState.put("step_counter", stepCounter_);
239+
return exportedState;
240+
}
241+
242+
void import_state(const serialization::node& exportedState)
243+
{
244+
try {
245+
if (exportedState.get<std::string>("type") != "fixed_step_algorithm") {
246+
throw std::exception();
247+
}
248+
stepCounter_ = exportedState.get<std::int64_t>("step_counter");
249+
} catch (...) {
250+
throw error(
251+
make_error_code(errc::bad_file),
252+
"The serialized algorithm state is invalid or corrupt");
253+
}
254+
}
255+
234256
void set_stepsize_decimation_factor(cosim::simulator_index i, int factor)
235257
{
236258
COSIM_INPUT_CHECK(factor > 0);
@@ -261,7 +283,6 @@ class fixed_step_algorithm::impl
261283
{
262284
simulator* sim;
263285
int decimationFactor = 1;
264-
step_result stepResult;
265286
std::vector<connection_ss> outgoingSimConnections;
266287
std::vector<connection_sf> outgoingFunConnections;
267288
};
@@ -421,13 +442,20 @@ class fixed_step_algorithm::impl
421442
}
422443
}
423444

445+
// Algorithm parameters
424446
const duration baseStepSize_;
425447
time_point startTime_;
426448
std::optional<time_point> stopTime_;
449+
unsigned int max_threads_ = std::thread::hardware_concurrency() - 1;
450+
451+
// System structure
427452
std::unordered_map<simulator_index, simulator_info> simulators_;
428453
std::unordered_map<function_index, function_info> functions_;
429-
int64_t stepCounter_ = 0;
430-
unsigned int max_threads_ = std::thread::hardware_concurrency() - 1;
454+
455+
// Simulation state
456+
std::int64_t stepCounter_ = 0;
457+
458+
// Other
431459
utility::thread_pool pool_;
432460
};
433461

@@ -521,6 +549,16 @@ std::pair<duration, std::unordered_set<simulator_index>> fixed_step_algorithm::d
521549
return pimpl_->do_step(currentT);
522550
}
523551

552+
serialization::node fixed_step_algorithm::export_current_state() const
553+
{
554+
return pimpl_->export_current_state();
555+
}
556+
557+
void fixed_step_algorithm::import_state(const serialization::node& exportedState)
558+
{
559+
pimpl_->import_state(exportedState);
560+
}
561+
524562
void fixed_step_algorithm::set_stepsize_decimation_factor(cosim::simulator_index simulator, int factor)
525563
{
526564
pimpl_->set_stepsize_decimation_factor(simulator, factor);

0 commit comments

Comments
 (0)