Skip to content

Commit

Permalink
Enable exporting and importing subsimulator state (#769)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
kyllingstad authored Oct 10, 2024
1 parent 981236a commit efdbc87
Show file tree
Hide file tree
Showing 15 changed files with 519 additions and 15 deletions.
18 changes: 18 additions & 0 deletions include/cosim/algorithm/simulator.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

#include <cosim/manipulator/manipulator.hpp>
#include <cosim/model_description.hpp>
#include <cosim/serialization.hpp>
#include <cosim/time.hpp>

#include <functional>
Expand Down Expand Up @@ -182,6 +183,23 @@ class simulator : public manipulable
* implementation is free to reuse the same `state_index` at a later point.
*/
virtual void release_state(state_index stateIndex) = 0;

/**
* Exports a saved state.
*
* This returns a previously-saved state in a generic format so it can be
* serialized, e.g. to write it to disk and use it in a later simulation.
*/
virtual serialization::node export_state(state_index stateIndex) const = 0;

/**
* Imports an exported state.
*
* The imported state is added to the simulator's internal list of saved
* states. Use `restore_state()` to restore it again. The state must have
* been saved by a simulator of the same or a compatible type.
*/
virtual state_index import_state(const serialization::node& exportedState) = 0;
};

} // namespace cosim
Expand Down
2 changes: 2 additions & 0 deletions include/cosim/fmi/v1/fmu.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@ class slave_instance : public fmi::slave_instance
void save_state(state_index overwriteState) override;
void restore_state(state_index state) override;
void release_state(state_index state) override;
serialization::node export_state(state_index stateIndex) const override;
state_index import_state(const serialization::node& exportedState) override;

// fmi::slave_instance methods
std::shared_ptr<fmi::fmu> fmu() const override
Expand Down
3 changes: 3 additions & 0 deletions include/cosim/fmi/v2/fmu.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ class slave_instance : public fmi::slave_instance
void save_state(state_index stateIndex) override;
void restore_state(state_index stateIndex) override;
void release_state(state_index stateIndex) override;
serialization::node export_state(state_index stateIndex) const override;
state_index import_state(const serialization::node& exportedState) override;

// fmi::slave_instance methods
std::shared_ptr<fmi::fmu> fmu() const override
Expand All @@ -192,6 +194,7 @@ class slave_instance : public fmi::slave_instance
bool simStarted = false;
};
void copy_current_state(saved_state& state);
state_index store_new_state(saved_state state);

std::shared_ptr<v2::fmu> fmu_;
fmi2_import_t* handle_;
Expand Down
132 changes: 132 additions & 0 deletions include/cosim/serialization.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/**
* \file
* Supporting functionality for serialization and persistence of simulation state.
*
* \copyright
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
#ifndef COSIM_SERIALIZATION_HPP
#define COSIM_SERIALIZATION_HPP

#include <boost/property_tree/ptree.hpp>

#include <cstddef>
#include <cstdint>
#include <ostream>
#include <string>
#include <variant>
#include <vector>


namespace cosim
{
/// Supporting functionality for serialization and persistence of simulation state.
namespace serialization
{


/**
* The internal data type for `cosim::serialization::node`.
*
* This is a `std::variant` of the types that can be stored in a node's data
* field. This includes a variety of primitive types, plus the aggregate
* types `std::string` (for convenience) and `std::vector<std::byte>` (for
* convenience *and* memory-efficient storage of binary blobs).
*
* A `nullptr` is used as the "empty" state, i.e., to indicate that the node
* stores no data. This is the default if no other value has been set.
*/
using node_data = std::variant<
std::nullptr_t, // represents "empty"
bool,
std::uint8_t,
std::int8_t,
std::uint16_t,
std::int16_t,
std::uint32_t,
std::int32_t,
std::uint64_t,
std::int64_t,
float,
double,
char,
std::string,
std::byte,
std::vector<std::byte>
>;


/**
* A type for storing data intermediately before serialization.
*
* This is a recursive, dynamic data type that can be used to store virtually
* any data structure in a generic yet type-safe manner.
*
* It is just an alias for `boost::property_tree::basic_ptree<std::string, cosim::serialization::node_data>`.
* For guidance on its use, see the [Boost.PropertyTree](https://www.boost.org/doc/libs/release/doc/html/property_tree.html)
* documentation.
*
* \note
* To make convenience functions like `node::get()` and `node::put()` work
* seamlessly, `cosim::serialization::node_data_translator` has been
* declared as the default translator for
* `cosim::serialization::node_data`. However, we only recommend using
* these functions for primitive types, because they return by value
* rather than by reference. In other words, a copy of the node data is
* made. This could significantly impact performance in some cases if the
* node stores a large string or byte array.
* \code
* // Convenient, but always makes a copy
* auto thisIsACopy = myNode.get<std::vector<std::byte>>("huge_binary_blob");
*
* // Uglier, but always gets a reference
* const auto& thisIsARef = std::get<std::vector<std::byte>>(myNode.get_child("huge_binary_blob").data());
* \endcode
*/
using node = boost::property_tree::basic_ptree<std::string, node_data>;


/// An implementation of Boost.PropertyTree's _Translator_ concept.
template<typename T>
struct node_data_translator
{
using internal_type = node_data;
using external_type = T;
T get_value(const node_data& value) { return std::get<T>(value); }
node_data put_value(const T& value) { return node_data(value); }
};

}} // namespace cosim::serialization


// Ordinarily, the following function would be in the `cosim::serialization`
// namespace and found by the compiler via ADL. This doesn't work here,
// because `cosim::serialization::node` is just an alias for a type in the
// `boost::property_tree` namespace.
/**
* Writes the contents of `data` to the output stream `out` in a human-readable
* format.
*
* This is meant for debugging purposes, not for serialization. There is no
* corresponding "read" function, nor is the output format designed to support
* round-trip information or type preservation.
*/
std::ostream& operator<<(std::ostream& out, const cosim::serialization::node& data);


// Make node_translator the default translator for property trees whose data
// type is node_data.
namespace boost
{
namespace property_tree
{
template<typename T>
struct translator_between<cosim::serialization::node_data, T>
{
using type = cosim::serialization::node_data_translator<T>;
};
}} // namespace boost::property_tree

#endif // COSIM_SERIALIZATION_HPP
22 changes: 18 additions & 4 deletions include/cosim/slave.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#define COSIM_SLAVE_HPP

#include <cosim/model_description.hpp>
#include <cosim/serialization.hpp>
#include <cosim/time.hpp>

#include <boost/container/vector.hpp>
Expand Down Expand Up @@ -290,8 +291,6 @@ class slave
* `state_index`. The index is only valid for this particular slave.
*
* The function may be called at any point after `setup()` has been called.
*
* \pre `this->model_description().can_save_state`
*/
virtual state_index save_state() = 0;

Expand All @@ -301,8 +300,6 @@ class slave
* This function does the same as `save_state()`, except that it
* overwrites a state which has previously been stored by that function.
* The old index thereafter refers to the newly-saved state.
*
* \pre `this->model_description().can_save_state`
*/
virtual void save_state(state_index stateIndex) = 0;

Expand Down Expand Up @@ -330,6 +327,23 @@ class slave
* implementation is free to reuse the same `state_index` at a later point.
*/
virtual void release_state(state_index stateIndex) = 0;

/**
* Exports a saved state.
*
* This returns a previously-saved state in a generic format so it can be
* serialized, e.g. to write it to disk and use it in a later simulation.
*/
virtual serialization::node export_state(state_index stateIndex) const = 0;

/**
* Imports an exported state.
*
* The imported state is added to the slave's internal list of saved
* states. Use `restore_state()` to restore it again. The state must have
* been saved by a slave of the same or a compatible type.
*/
virtual state_index import_state(const serialization::node& exportedState) = 0;
};


Expand Down
2 changes: 2 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ set(publicHeaders
"cosim/osp_config_parser.hpp"
"cosim/scenario.hpp"
"cosim/scenario_parser.hpp"
"cosim/serialization.hpp"
"cosim/slave.hpp"
"cosim/ssp/ssp_loader.hpp"
"cosim/system_structure.hpp"
Expand Down Expand Up @@ -88,6 +89,7 @@ set(sources
"cosim/orchestration.cpp"
"cosim/osp_config_parser.cpp"
"cosim/scenario_parser.cpp"
"cosim/serialization.cpp"
"cosim/slave_simulator.cpp"
"cosim/ssp/ssp_loader.cpp"
"cosim/ssp/ssp_parser.cpp"
Expand Down
16 changes: 16 additions & 0 deletions src/cosim/fmi/v1/fmu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,22 @@ void slave_instance::release_state(state_index)
}


serialization::node slave_instance::export_state(state_index) const
{
throw error(
make_error_code(errc::unsupported_feature),
"Serializing and deserializing state not supported in FMI 1.0");
}


slave::state_index slave_instance::import_state(const serialization::node&)
{
throw error(
make_error_code(errc::unsupported_feature),
"Serializing and deserializing state not supported in FMI 1.0");
}


std::shared_ptr<v1::fmu> slave_instance::v1_fmu() const
{
return fmu_;
Expand Down
Loading

0 comments on commit efdbc87

Please sign in to comment.