Skip to content

Commit 1d102d6

Browse files
State saving and serialization (#785)
* Enable saving and restoring subsimulator state (#765) This is the first step towards closing #756. I've added functions corresponding to FMI 2.0's `fmi2{Get,Set,Free}FMUstate()` throughout the various layers of subsimulator interfaces and implementations: * `cosim::slave` and its implementation in `cosim::fmi::v2::slave_instance` * `cosim::simulator` and its implementation in `cosim::slave_simulator` This led me to also remove the `slave_state` and `state_guard` stuff that was in `slave_simulator.{hpp,cpp}`. The overloading of the "state" terminology became confusing, and it seemed like it was a lot of code for very little gain. (It was supposed to be a check of correct API usage, but I can't remember it ever actually catching a bug.) * Enable exporting and importing subsimulator state (#769) This is a follow-up to #765 and the second and final step to close #756. Here, I've implemented functionality to export the internal state of individual subsimulators in a generic, structured form, and to import them again later. This exported form is intended as an intermediate step before serialisation and disk storage. The idea was to create a type that can be inspected and serialised to almost any file format we'd like. The type is defined by `cosim::serialization::node` in `cosim/serialization.hpp`. It is a hierarchical, dynamic data type with support for a variety of primitive scalar types and a few aggregate types: strings, arrays of nodes, dictionaries of nodes, and binary blobs. (Think JSON, only with more types.) It is based on Boost.PropertyTree * 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.) * Feature/779 state serialization (#780) * Enable saving and restoring full simulation state Closes #768 * Added tinycbor based serialization method * Debug test * Replaced tinycbor -> libcbor (ver 0.1) * deserialization using cbor_reader * Separated ctx to own struct * Tag as std::optional in ctx * State serialization test * Removed comments * Addressed Linux build issue * Added BouncingBall reference model from modelica for adding byte vector serialization test * Added BouncingBall license in README * Added comments * Deleted state_serialization_test.cpp * Added libcbor as PRIVATE for `target_link_libraries` * Controlling fixed precision for logging float point values (#775) * Added an option to file_observer_config to set fixed precision value. * Fixed build failure * Reading canGetAndSetFMUstate and canSerializeFMUstate from model description file * Added transitive_libs for libcbor to avoid cmake error * linking libcbor statically. * Resetting time series observer upon state restore * State saving support with proxyfmu * Split proxyfmu from the save_state_test. * Uses upgraded thrift/boost * Fixed boost header issue * Fixed boost header issue * Fixed boost header issue * Update dependency * Updated action * Upgraded libzip * Upgraded libzip * libzip deprecated func fix * Observer fix * Addressing comments 1. Defining exported sourced explicitly 2. Try to use shared libcbor. * made libcbor PUBLIC in cmake (may require the consumer also declare libcbor for linking libcosim) * Addressed comments * Addressed comments * Addressed comments - 02 * Addressed comments - 03 * Updated cosim_capabilities -> simulator_capabilities. Using cosim::serialization::format::cbor to choose which serialization/deserialization to use * update * pretty_print format added * Updated error message * Updated error message * Updated error message * Using fmu::model_description::capabilities in copy_current_state() and export_state() * Updated error message * Don't require importing def_types.h * Using updated proxyfmu that hides thrift related libs * Updated field names for the model description * Updated proxyfmu dependency --------- Co-authored-by: Lars T. Kyllingstad <lars.kyllingstad@sintef.no> * Fixed an issue from the merge --------- Co-authored-by: Lars T. Kyllingstad <lars.kyllingstad@sintef.no>
1 parent 8276615 commit 1d102d6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+2371
-270
lines changed

.github/workflows/ci-conan.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ jobs:
4848
--settings="build_type=${{ matrix.build_type }}" \
4949
--options="${{ matrix.option_proxyfmu }}" \
5050
--options="${{ matrix.option_shared }}" \
51+
--update \
5152
--build=missing \
5253
--user=osp \
5354
--channel="${CHANNEL}" \
@@ -103,6 +104,7 @@ jobs:
103104
--settings="build_type=${{ matrix.build_type }}" \
104105
--options="${{ matrix.option_proxyfmu }}" \
105106
--options="${{ matrix.option_shared }}" \
107+
--update \
106108
--build=missing \
107109
--user=osp \
108110
--channel="${CHANNEL}" \

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ find_package(libzip REQUIRED)
113113
find_package(Microsoft.GSL REQUIRED)
114114
find_package(yaml-cpp REQUIRED)
115115
find_package(XercesC MODULE REQUIRED)
116+
find_package(libcbor REQUIRED)
116117
if(LIBCOSIM_WITH_PROXYFMU)
117118
find_package(PROXYFMU CONFIG REQUIRED)
118119
endif()

conanfile.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,19 +39,21 @@ def set_version(self):
3939
def requirements(self):
4040
self.tool_requires("cmake/[>=3.19]")
4141
self.requires("fmilibrary/[~2.3]")
42-
self.requires("libzip/[>=1.7 <1.10]") # 1.10 deprecates some functions we use
42+
self.requires("libcbor/0.11.0")
43+
self.requires("libzip/[~1.11]")
4344
self.requires("ms-gsl/[>=3 <5]", transitive_headers=True)
45+
self.requires("boost/[~1.85]", transitive_headers=True, transitive_libs=True) # Required by Thrift
4446
if self.options.proxyfmu:
45-
self.requires("proxyfmu/0.3.2@osp/stable")
46-
self.requires("boost/[~1.81]", transitive_headers=True, transitive_libs=True) # Required by Thrift
47-
else:
48-
self.requires("boost/[>=1.71]", transitive_headers=True, transitive_libs=True)
47+
self.requires("proxyfmu/0.3.3@osp/testing",
48+
transitive_headers=True,
49+
transitive_libs=True)
4950
self.requires("yaml-cpp/[~0.8]")
5051
self.requires("xerces-c/[~3.2]")
5152

5253
# Exports
5354
exports = "version.txt"
54-
exports_sources = "*"
55+
exports_sources = ("src/*", "include/*", "cmake/*", "data/*", "docs/*", "tests/*", "CHANGELOG.md", "CMakeLists.txt",
56+
"CONTRIBUTING.md", "LICENSE", "README.md", "version.txt")
5557

5658
# Build steps
5759

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/algorithm/simulator.hpp

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
#include <cosim/manipulator/manipulator.hpp>
1414
#include <cosim/model_description.hpp>
15+
#include <cosim/serialization.hpp>
1516
#include <cosim/time.hpp>
1617

1718
#include <functional>
@@ -134,6 +135,72 @@ class simulator : public manipulable
134135
virtual step_result do_step(
135136
time_point currentT,
136137
duration deltaT) = 0;
138+
139+
/// A type used for references to saved states (see `save_state()`).
140+
using state_index = int;
141+
142+
/**
143+
* Saves the current state.
144+
*
145+
* This will create and store a copy of the simulator's current internal
146+
* state, so that it can be restored at a later time. The copy is stored
147+
* internally in the simulator, and must be referred to by the returned
148+
* `state_index`. The index is only valid for this particular simulator.
149+
*
150+
* The function may be called at any point after `setup()` has been called.
151+
*/
152+
virtual state_index save_state() = 0;
153+
154+
/**
155+
* Saves the current state, overwriting a previously-saved state.
156+
*
157+
* This function does the same as `save_state()`, except that it
158+
* overwrites a state which has previously been stored by that function.
159+
* The old index thereafter refers to the newly-saved state.
160+
*/
161+
virtual void save_state(state_index stateIndex) = 0;
162+
163+
/**
164+
* Restores a previously-saved state.
165+
*
166+
* This restores the simulator to a state which has previously been saved
167+
* using `save_state()`.
168+
*
169+
* Note that the saved state is supposed to be the *complete and exact*
170+
* state of the simulator at the moment `save_state()` was called. For example,
171+
* if the state was saved while the simulator was in initialisation mode
172+
* (between `setup()` and `start_simulation()`), then it will be restored
173+
* in that mode, and `start_simulation()` must be called before the
174+
* simulation can start. Similarly, if it is saved at logical time `t`,
175+
* then the first `do_step()` call after restoration must start at `t`.
176+
*/
177+
virtual void restore_state(state_index stateIndex) = 0;
178+
179+
/**
180+
* Frees all resources (e.g. memory) associated with a saved state.
181+
*
182+
* After this, the state may no longer be restored with `restore_state()`,
183+
* nor may it be overwritten with `save_state(state_index)`. The
184+
* implementation is free to reuse the same `state_index` at a later point.
185+
*/
186+
virtual void release_state(state_index stateIndex) = 0;
187+
188+
/**
189+
* Exports a saved state.
190+
*
191+
* This returns a previously-saved state in a generic format so it can be
192+
* serialized, e.g. to write it to disk and use it in a later simulation.
193+
*/
194+
virtual serialization::node export_state(state_index stateIndex) const = 0;
195+
196+
/**
197+
* Imports an exported state.
198+
*
199+
* The imported state is added to the simulator's internal list of saved
200+
* states. Use `restore_state()` to restore it again. The state must have
201+
* been saved by a simulator of the same or a compatible type.
202+
*/
203+
virtual state_index import_state(const serialization::node& exportedState) = 0;
137204
};
138205

139206
} // namespace cosim

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/fmi/v1/fmu.hpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,13 @@ class slave_instance : public fmi::slave_instance
165165
gsl::span<const value_reference> variables,
166166
gsl::span<const std::string> values) override;
167167

168+
state_index save_state() override;
169+
void save_state(state_index overwriteState) override;
170+
void restore_state(state_index state) override;
171+
void release_state(state_index state) override;
172+
serialization::node export_state(state_index stateIndex) const override;
173+
state_index import_state(const serialization::node& exportedState) override;
174+
168175
// fmi::slave_instance methods
169176
std::shared_ptr<fmi::fmu> fmu() const override
170177
{

include/cosim/fmi/v2/fmu.hpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@
2121
#include <string>
2222
#include <string_view>
2323
#include <vector>
24+
#include <queue>
2425

2526

2627
struct fmi2_import_t;
28+
using fmi2_FMU_state_t = void*;
2729

2830

2931
namespace cosim
@@ -165,6 +167,13 @@ class slave_instance : public fmi::slave_instance
165167
gsl::span<const value_reference> variables,
166168
gsl::span<const std::string> values) override;
167169

170+
state_index save_state() override;
171+
void save_state(state_index stateIndex) override;
172+
void restore_state(state_index stateIndex) override;
173+
void release_state(state_index stateIndex) override;
174+
serialization::node export_state(state_index stateIndex) const override;
175+
state_index import_state(const serialization::node& exportedState) override;
176+
168177
// fmi::slave_instance methods
169178
std::shared_ptr<fmi::fmu> fmu() const override
170179
{
@@ -178,13 +187,25 @@ class slave_instance : public fmi::slave_instance
178187
fmi2_import_t* fmilib_handle() const;
179188

180189
private:
190+
struct saved_state
191+
{
192+
fmi2_FMU_state_t fmuState = nullptr;
193+
bool setupComplete = false;
194+
bool simStarted = false;
195+
};
196+
void copy_current_state(saved_state& state);
197+
state_index store_new_state(saved_state state);
198+
181199
std::shared_ptr<v2::fmu> fmu_;
182200
fmi2_import_t* handle_;
183201

184202
bool setupComplete_ = false;
185203
bool simStarted_ = false;
186204

187205
std::string instanceName_;
206+
207+
std::vector<saved_state> savedStates_;
208+
std::queue<state_index> savedStatesFreelist_;
188209
};
189210

190211

0 commit comments

Comments
 (0)