Skip to content

Commit ea238af

Browse files
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>
1 parent 9037906 commit ea238af

20 files changed

+946
-70
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/model_description.hpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,13 @@ enum variable_variability
5959
continuous
6060
};
6161

62+
/// A list of simulator capabilities
63+
struct simulator_capabilities
64+
{
65+
bool can_save_state = false;
66+
bool can_export_state = false;
67+
};
68+
6269

6370
/// Returns a textual representation of `v`.
6471
constexpr const char* to_text(variable_type v)
@@ -195,6 +202,9 @@ struct model_description
195202

196203
/// Variable descriptions.
197204
std::vector<variable_description> variables;
205+
206+
/// Simulator capabilities
207+
simulator_capabilities capabilities;
198208
};
199209

200210
/// Getter for returning a variable description.

include/cosim/observer/file_observer.hpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ namespace cosim
2323

2424
class file_observer;
2525

26+
2627
/**
2728
* Configuration options for file_observer.
2829
*/
@@ -88,9 +89,22 @@ class file_observer_config
8889
*/
8990
static file_observer_config parse(const filesystem::path& configPath);
9091

92+
/**
93+
* Sets fixed precision value for floating point numbers
94+
*
95+
* \param precision the number of digits after the decimal point. The observer uses the default precision
96+
* value of 6 if the argument is less than zero.
97+
*/
98+
void fixed_precision(const int precision)
99+
{
100+
precision_ = precision;
101+
}
102+
91103
private:
92104
bool timeStampedFileNames_{true};
93105
size_t defaultDecimationFactor_{1};
106+
int precision_{-1};
107+
94108
std::unordered_map<std::string, std::pair<size_t, std::vector<std::string>>> variablesToLog_;
95109

96110
[[nodiscard]] bool should_log_simulator(const std::string& name) const

include/cosim/serialization.hpp

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,30 @@ struct node_data_translator
9898
node_data put_value(const T& value) { return node_data(value); }
9999
};
100100

101-
}} // namespace cosim::serialization
101+
namespace format
102+
{
103+
const auto format_xalloc = std::ios_base::xalloc();
104+
const int FORMAT_CBOR = 1;
105+
const int FORMAT_PRETTY_PRINT = 2;
106+
107+
std::ios_base& cbor(std::ios_base& os);
108+
std::ios_base& pretty_print(std::ios_base& os);
109+
110+
} // namespace format
102111

112+
} // namespace serialization
113+
} // namespace cosim
114+
115+
/**
116+
* Serializes `data` into a format specified via cosim::serialization::format and writes it to the output stream `out`.
117+
*/
118+
std::ostream& operator<<(std::ostream& out, const cosim::serialization::node& data);
119+
120+
/**
121+
* Reads a stream from `in` and converts it to `data` format specified via cosim::serialization::format.
122+
*/
123+
std::istream& operator>>(std::istream& in, cosim::serialization::node& data);
103124

104-
// Ordinarily, the following function would be in the `cosim::serialization`
105-
// namespace and found by the compiler via ADL. This doesn't work here,
106-
// because `cosim::serialization::node` is just an alias for a type in the
107-
// `boost::property_tree` namespace.
108125
/**
109126
* Writes the contents of `data` to the output stream `out` in a human-readable
110127
* format.
@@ -113,7 +130,7 @@ struct node_data_translator
113130
* corresponding "read" function, nor is the output format designed to support
114131
* round-trip information or type preservation.
115132
*/
116-
std::ostream& operator<<(std::ostream& out, const cosim::serialization::node& data);
133+
void print_ptree(std::ostream& out, const cosim::serialization::node& data);
117134

118135

119136
// Make node_translator the default translator for property trees whose data

src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ target_link_libraries(cosim
180180
libzip::zip
181181
XercesC::XercesC
182182
yaml-cpp
183+
libcbor::libcbor
183184
)
184185

185186
if(LIBCOSIM_WITH_PROXYFMU)

src/cosim/fmi/v2/fmu.cpp

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ fmu::fmu(
6363
modelDescription_.description = fmi2_import_get_description(handle_);
6464
modelDescription_.author = fmi2_import_get_author(handle_);
6565
modelDescription_.version = fmi2_import_get_model_version(handle_);
66+
modelDescription_.capabilities.can_save_state = !!fmi2_import_get_capability(
67+
handle_,
68+
fmi2_cs_canGetAndSetFMUstate);
69+
modelDescription_.capabilities.can_export_state = !!fmi2_import_get_capability(
70+
handle_,
71+
fmi2_cs_canSerializeFMUstate);
6672
const auto varList = fmi2_import_get_variable_list(handle_, 0);
6773
const auto _ = gsl::finally([&]() {
6874
fmi2_import_free_variable_list(varList);
@@ -624,7 +630,7 @@ serialization::node slave_instance::export_state(state_index stateIndex) const
624630
const auto& savedState = savedStates_.at(stateIndex);
625631

626632
// Check that the FMU supports state serialization
627-
if (!fmi2_import_get_capability(handle_, fmi2_cs_canSerializeFMUstate))
633+
if (!fmu_->model_description()->capabilities.can_export_state)
628634
{
629635
throw error(
630636
make_error_code(errc::unsupported_feature),
@@ -737,7 +743,7 @@ fmi2_import_t* slave_instance::fmilib_handle() const
737743

738744
void slave_instance::copy_current_state(saved_state& state)
739745
{
740-
if (!fmi2_import_get_capability(handle_, fmi2_cs_canGetAndSetFMUstate)) {
746+
if (!fmu_->model_description()->capabilities.can_save_state) {
741747
throw error(
742748
make_error_code(errc::unsupported_feature),
743749
instanceName_ + ": FMU does not support state saving");

src/cosim/observer/file_observer.cpp

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ void clear_file_contents_if_exists(const cosim::filesystem::path& filePath, std:
4646
fsw.close();
4747
}
4848
}
49+
4950
} // namespace
5051

5152

@@ -60,12 +61,18 @@ class file_observer::slave_value_writer
6061
initialize_default();
6162
}
6263

63-
slave_value_writer(observable* observable, cosim::filesystem::path& logDir, size_t decimationFactor,
64-
const std::vector<variable_description>& variables, bool timeStampedFileNames = true)
64+
slave_value_writer(
65+
observable* observable,
66+
cosim::filesystem::path& logDir,
67+
size_t decimationFactor,
68+
const std::vector<variable_description>& variables,
69+
bool timeStampedFileNames = true,
70+
const int precision = -1)
6571
: observable_(observable)
6672
, logDir_(logDir)
6773
, decimationFactor_(decimationFactor)
6874
, timeStampedFileNames_(timeStampedFileNames)
75+
, precision_(precision)
6976
{
7077
initialize_config(variables);
7178
}
@@ -301,10 +308,19 @@ class file_observer::slave_value_writer
301308
void persist()
302309
{
303310
std::stringstream ss;
311+
const auto defaultPrecision = ss.precision();
312+
304313
if (fsw_.is_open()) {
305314

306315
for (const auto& [stepCount, times] : timeSamples_) {
307-
ss << times << "," << stepCount;
316+
if (precision_ > -1) {
317+
ss.precision(defaultPrecision);
318+
ss << times << "," << stepCount;
319+
ss.precision(precision_);
320+
ss << std::fixed;
321+
} else {
322+
ss << times << "," << stepCount;
323+
}
308324

309325
if (realSamples_.count(stepCount)) write<double>(realSamples_[stepCount], ss);
310326
if (intSamples_.count(stepCount)) write<int>(intSamples_[stepCount], ss);
@@ -340,6 +356,7 @@ class file_observer::slave_value_writer
340356
std::atomic<bool> recording_ = true;
341357
std::mutex mutex_;
342358
bool timeStampedFileNames_ = true;
359+
int precision_ = -1;
343360
};
344361

345362
file_observer::file_observer(const cosim::filesystem::path& logDir, std::optional<file_observer_config> config)
@@ -387,7 +404,8 @@ void file_observer::simulator_added(
387404
logDir_,
388405
config.decimationFactor,
389406
config.variables,
390-
config.timeStampedFileNames);
407+
config.timeStampedFileNames,
408+
config_->precision_);
391409
} else {
392410
return;
393411
}

src/cosim/observer/time_series_observer.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,17 @@ class time_series_observer::single_slave_observer
9393
adjustIfFull(timeSamples_, bufSize_);
9494
}
9595

96+
void reset()
97+
{
98+
timeSamples_.clear();
99+
for (auto& [_, step_numbers] : realSamples_) {
100+
step_numbers.clear();
101+
}
102+
for (auto& [_, step_numbers] : intSamples_) {
103+
step_numbers.clear();
104+
}
105+
}
106+
96107
void start_observing(variable_type type, value_reference reference)
97108
{
98109
std::lock_guard<std::mutex> lock(lock_);
@@ -266,6 +277,7 @@ void time_series_observer::state_restored(
266277
step_number currentStep, time_point currentTime)
267278
{
268279
for (const auto& entry : slaveObservers_) {
280+
entry.second->reset();
269281
entry.second->observe(currentStep, currentTime);
270282
}
271283
}

src/cosim/proxy/remote_fmu.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ std::unique_ptr<cosim::model_description> parse_model_description(const proxyfmu
7171
_md->author = md.author;
7272
_md->name = md.modelName;
7373
_md->description = md.description;
74+
_md->capabilities.can_save_state = md.canGetAndSetFMUstate;
75+
_md->capabilities.can_export_state = md.canSerializeFMUstate;
7476

7577
for (auto& var : md.modelVariables) {
7678
cosim::variable_description vd;

src/cosim/proxy/remote_slave.cpp

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include <cosim/error.hpp>
77
#include <cosim/exception.hpp>
88
#include <cosim/proxy/remote_slave.hpp>
9+
#include <proxyfmu/state.hpp>
910

1011
#include <utility>
1112

@@ -204,45 +205,60 @@ void cosim::proxy::remote_slave::set_string_variables(gsl::span<const cosim::val
204205

205206
cosim::slave::state_index cosim::proxy::remote_slave::save_state()
206207
{
207-
throw error(
208-
make_error_code(errc::unsupported_feature),
209-
"Proxyfmu does not support state saving");
208+
return slave_->save_state();
210209
}
211210

212-
void cosim::proxy::remote_slave::save_state(state_index)
211+
void cosim::proxy::remote_slave::save_state(state_index idx)
213212
{
214-
throw error(
215-
make_error_code(errc::unsupported_feature),
216-
"Proxyfmu does not support state saving");
213+
slave_->save_state(idx);
217214
}
218215

219-
void cosim::proxy::remote_slave::restore_state(state_index)
216+
void cosim::proxy::remote_slave::restore_state(state_index idx)
220217
{
221-
throw error(
222-
make_error_code(errc::unsupported_feature),
223-
"Proxyfmu does not support state saving");
218+
slave_->restore_state(idx);
224219
}
225220

226-
void cosim::proxy::remote_slave::release_state(state_index)
221+
void cosim::proxy::remote_slave::release_state(state_index idx)
227222
{
228-
throw error(
229-
make_error_code(errc::unsupported_feature),
230-
"Proxyfmu does not support state saving");
223+
slave_->release_state(idx);
231224
}
232225

233-
cosim::serialization::node cosim::proxy::remote_slave::export_state(state_index) const
226+
cosim::serialization::node cosim::proxy::remote_slave::export_state(state_index idx) const
234227
{
235-
throw error(
236-
make_error_code(errc::unsupported_feature),
237-
"Proxyfmu does not support state serialization");
228+
proxyfmu::state::exported_state state;
229+
slave_->export_state(idx, state);
230+
231+
// Create the exported state
232+
serialization::node exportedState;
233+
exportedState.put("scheme_version", state.schemeVersion);
234+
exportedState.put("fmu_uuid", state.uuid);
235+
236+
std::vector<std::byte> vec;
237+
std::transform(state.fmuState.begin(), state.fmuState.end(), std::back_inserter(vec),
238+
[](unsigned char c) { return std::byte(c); });
239+
240+
exportedState.put("serialized_fmu_state", vec);
241+
exportedState.put("setup_complete", state.setupComplete);
242+
exportedState.put("simulation_started", state.simStarted);
243+
return exportedState;
238244
}
239245

240246
cosim::slave::state_index cosim::proxy::remote_slave::import_state(
241-
const cosim::serialization::node&)
247+
const cosim::serialization::node& node)
242248
{
243-
throw error(
244-
make_error_code(errc::unsupported_feature),
245-
"Proxyfmu does not support state serialization");
249+
proxyfmu::state::exported_state state;
250+
state.schemeVersion = node.get<std::int32_t>("scheme_version");
251+
state.uuid = node.get<std::string>("fmu_uuid");
252+
253+
const auto& serializedFMUState = std::get<std::vector<std::byte>>(
254+
node.get_child("serialized_fmu_state").data());
255+
state.fmuState.reserve(serializedFMUState.size());
256+
std::transform(serializedFMUState.begin(), serializedFMUState.end(), std::back_inserter(state.fmuState),
257+
[](std::byte b) { return static_cast<unsigned char>(b); });
258+
259+
state.setupComplete = node.get<bool>("setup_complete");
260+
state.simStarted = node.get<bool>("simulation_started");
261+
return slave_->import_state(state);
246262
}
247263

248264
cosim::proxy::remote_slave::~remote_slave()

0 commit comments

Comments
 (0)