From fea149729e8bb5c59208f431377cf7e6cc320472 Mon Sep 17 00:00:00 2001 From: "Timothy Rule (VM/EMT3)" Date: Fri, 24 Jan 2025 11:48:32 +0100 Subject: [PATCH] FMU variable table mechanism for simplified FMU variable access. Signed-off-by: Timothy Rule (VM/EMT3) --- dse/examples/fmu/counter/counter.c | 22 ++++--- dse/fmu/fmi2fmu.c | 23 ++++++- dse/fmu/fmi3fmu.c | 23 ++++++- dse/fmu/fmu.h | 40 +++++++++-- dse/fmu/signal.c | 102 +++++++++++++++++++++++++++++ tests/cmocka/fmu/test_signals.c | 59 +++++++++++++++++ 6 files changed, 252 insertions(+), 17 deletions(-) diff --git a/dse/examples/fmu/counter/counter.c b/dse/examples/fmu/counter/counter.c index b6363eb..e2b23c3 100644 --- a/dse/examples/fmu/counter/counter.c +++ b/dse/examples/fmu/counter/counter.c @@ -2,34 +2,36 @@ // // SPDX-License-Identifier: Apache-2.0 -#include -#include #include -#define UNUSED(x) ((void)x) -#define VR_COUNTER "1" +typedef struct { + double counter; +} VarTable; int fmu_create(FmuInstanceData* fmu) { - UNUSED(fmu); + VarTable* v = malloc(sizeof(VarTable)); + *v = (VarTable){ + .counter = fmu_register_var(fmu, 1, false, offsetof(VarTable, counter)), + }; + fmu_register_var_table(fmu, v); return 0; } int fmu_init(FmuInstanceData* fmu) { - hashmap_set_double(&(fmu->variables.scalar.output), VR_COUNTER, 0.0); + UNUSED(fmu); return 0; } -int fmu_step( - FmuInstanceData* fmu, double CommunicationPoint, double stepSize) +int fmu_step(FmuInstanceData* fmu, double CommunicationPoint, double stepSize) { UNUSED(CommunicationPoint); UNUSED(stepSize); + VarTable* v = fmu_var_table(fmu); /* Increment the counter. */ - double* counter = hashmap_get(&fmu->variables.scalar.output, VR_COUNTER); - if (counter) *counter += 1; + v->counter += 1; return 0; } diff --git a/dse/fmu/fmi2fmu.c b/dse/fmu/fmi2fmu.c index 2c61825..7926700 100644 --- a/dse/fmu/fmi2fmu.c +++ b/dse/fmu/fmi2fmu.c @@ -162,10 +162,14 @@ fmi2Component fmi2Instantiate(fmi2String instance_name, fmi2Type fmu_type, /* Lazy free list. */ hashlist_init(&fmu->variables.binary.free_list, 1024); - /* Specialised Model. */ + /* Create the FMU. */ if (fmu_create(fmu) != fmi2OK) { fmu_log(fmu, fmi2Error, "Error", "The FMU was not created correctly!"); } + if (fmu->var_table.table) { + fmu_log(fmu, fmi2OK, "Debug", "FMU Var Table is not configured"); + } + /* Return the created instance object. */ return (fmi2Component)fmu; } @@ -473,11 +477,21 @@ fmi2Status fmi2DoStep(fmi2Component c, fmi2Real currentCommunicationPoint, /* Make sure that all binary signals were reset at some point. */ if (fmu->variables.vtable.reset) fmu->variables.vtable.reset(fmu); + /* Marshal Signal Vectors to the VarTable. */ + for (FmuVarTableMarshalItem* mi = fmu->var_table.marshal_list; + mi && mi->variable; mi++) { + *mi->variable = *mi->signal; + } /* Step the model. */ int32_t rc = fmu_step(fmu, currentCommunicationPoint, communicationStepSize); + /* Marshal the VarTable to the Signal Vectors. */ + for (FmuVarTableMarshalItem* mi = fmu->var_table.marshal_list; + mi && mi->variable; mi++) { + *mi->signal = *mi->variable; + } /* Reset the binary signal reset mechanism. */ fmu->variables.signals_reset = false; @@ -508,6 +522,13 @@ void fmi2FreeInstance(fmi2Component c) if (fmu->variables.vtable.remove) fmu->variables.vtable.remove(fmu); + fmu_log(fmu, fmi2OK, "Debug", "Release var table"); + free(fmu->var_table.table); + free(fmu->var_table.marshal_list); + if (fmu->var_table.var_list.hash_map.hash_function) { + hashlist_destroy(&fmu->var_table.var_list); + } + fmu_log(fmu, fmi2OK, "Debug", "Destroy the index"); hashmap_destroy(&fmu->variables.scalar.input); hashmap_destroy(&fmu->variables.scalar.output); diff --git a/dse/fmu/fmi3fmu.c b/dse/fmu/fmi3fmu.c index 9385056..ba81937 100644 --- a/dse/fmu/fmi3fmu.c +++ b/dse/fmu/fmi3fmu.c @@ -182,11 +182,15 @@ fmi3Instance fmi3InstantiateCoSimulation(fmi3String instanceName, /* Lazy free list. */ hashlist_init(&fmu->variables.binary.free_list, 1024); - /* Specialised Model. */ + /* Create the FMU. */ if (fmu_create(fmu) != fmi3OK) { fmu_log(fmu, fmi3Error, "Error", "The FMU was not created correctly!"); } + if (fmu->var_table.table) { + fmu_log(fmu, fmi3OK, "Debug", "FMU Var Table is not configured"); + } + /* Return the created instance object. */ return (fmi3Instance)fmu; } @@ -223,6 +227,13 @@ void fmi3FreeInstance(fmi3Instance instance) if (fmu->variables.vtable.remove) fmu->variables.vtable.remove(fmu); + fmu_log(fmu, fmi3OK, "OK", "Release var table"); + free(fmu->var_table.table); + free(fmu->var_table.marshal_list); + if (fmu->var_table.var_list.hash_map.hash_function) { + hashlist_destroy(&fmu->var_table.var_list); + } + fmu_log(fmu, fmi3OK, "Ok", "Destroy the index"); hashmap_destroy(&fmu->variables.scalar.input); hashmap_destroy(&fmu->variables.scalar.output); @@ -1142,11 +1153,21 @@ fmi3Status fmi3DoStep(fmi3Instance instance, /* Make sure that all binary signals were reset at some point. */ if (fmu->variables.vtable.reset) fmu->variables.vtable.reset(fmu); + /* Marshal Signal Vectors to the VarTable. */ + for (FmuVarTableMarshalItem* mi = fmu->var_table.marshal_list; + mi && mi->variable; mi++) { + *mi->variable = *mi->signal; + } /* Step the model. */ int32_t rc = fmu_step(fmu, currentCommunicationPoint, communicationStepSize); + /* Marshal the VarTable to the Signal Vectors. */ + for (FmuVarTableMarshalItem* mi = fmu->var_table.marshal_list; + mi && mi->variable; mi++) { + *mi->signal = *mi->variable; + } /* Reset the binary signal reset mechanism. */ fmu->variables.signals_reset = false; diff --git a/dse/fmu/fmu.h b/dse/fmu/fmu.h index 58d9eeb..3107c47 100644 --- a/dse/fmu/fmu.h +++ b/dse/fmu/fmu.h @@ -18,6 +18,7 @@ #define DLL_PRIVATE __attribute__((visibility("hidden"))) #endif +#define UNUSED(x) ((void)x) /** FMU API @@ -25,21 +26,31 @@ FMU API The FMU API provides a simplified FMU inteface with an abstracted varaible interface (indexing and storage). The FMU Interface includes the methods: -* `[fmu_create()]({{< ref "#fmu_create" >}})` -* `[fmu_init()]({{< ref "#fmu_init" >}})` -* `[fmu_step()]({{< ref "#fmu_step" >}})` -* `[fmu_destroy()]({{< ref "#fmu_destroy" >}})` +* Implemented by FMU developer: + * `[fmu_create()]({{< ref "#fmu_create" >}})` + * `[fmu_init()]({{< ref "#fmu_init" >}})` + * `[fmu_step()]({{< ref "#fmu_step" >}})` + * `[fmu_destroy()]({{< ref "#fmu_destroy" >}})` +* Additional provided functions: + * `[fmu_log()]({{< ref "#fmu_log" >}})` - logging function +* Supporting Variable Table mechanism: + * `[fmu_register_var()]({{< ref "#fmu_register_var" >}})` + * `[fmu_register_var_table()]({{< ref "#fmu_register_var_table" >}})` + * `[fmu_var_table()]({{< ref "#fmu_var_table" >}})` + An additional FMU Signal Interface is available for more complex integrations: * `[fmu_signals_reset()]({{< ref "#fmu_signals_reset" >}})` * `[fmu_signals_setup()]({{< ref "#fmu_signals_setup" >}})` * `[fmu_signals_remove()]({{< ref "#fmu_signals_remove" >}})` + FMUs imlemented using this simplified FMU API can be built for both FMI 2 and FMI 3 standards by linking to the relevant implementations: * `fmi2fmu.c` for and FMI 2 FMU * `fmi3fmu.c` for and FMI 3 FMU + Binary variables are supported for FMI 3 and FMI 2 standards. In FMUs built to the FMI 2 standard, binary variables are implemented via FMI String Variables and an associated encoding. @@ -170,6 +181,12 @@ typedef struct FmuSignalVectorIndex { } FmuSignalVectorIndex; +typedef struct FmuVarTableMarshalItem { + double* variable; // Pointer to FMU allocated storage. + double* signal; // Pointer to FmuSignalVector storage (i.e. scalar). +} FmuVarTableMarshalItem; + + typedef struct FmuInstanceData { /* FMI Instance Data. */ struct { @@ -207,6 +224,15 @@ typedef struct FmuInstanceData { /* FMU Instance Data (additional). */ void* data; + + /* FMU Variable Table, used for indirect variable access. */ + struct { + void* table; + HashList var_list; + + /* NLT for var/signal mirroring. */ + FmuVarTableMarshalItem* marshal_list; + } var_table; } FmuInstanceData; @@ -215,7 +241,11 @@ DLL_PRIVATE char* ascii85_encode(const char* source, size_t len); DLL_PRIVATE char* ascii85_decode(const char* source, size_t* len); /* signal.c (default implementations for generic FMU) */ -DLL_PRIVATE void fmu_load_signal_handlers(FmuInstanceData* fmu); +DLL_PRIVATE void fmu_load_signal_handlers(FmuInstanceData* fmu); +DLL_PRIVATE double fmu_register_var( + FmuInstanceData* fmu, uint32_t vref, bool input, size_t offset); +DLL_PRIVATE void fmu_register_var_table(FmuInstanceData* fmu, void* table); +DLL_PRIVATE void* fmu_var_table(FmuInstanceData* fmu); /* FMU Interface (example implementation in fmu.c) */ DLL_PRIVATE int32_t fmu_create(FmuInstanceData* fmu); diff --git a/dse/fmu/signal.c b/dse/fmu/signal.c index 87cb389..5fca6cc 100644 --- a/dse/fmu/signal.c +++ b/dse/fmu/signal.c @@ -33,6 +33,7 @@ fmu (FmuInstanceData*) */ extern void fmu_signals_reset(FmuInstanceData* fmu); + /** fmu_signals_setup ================= @@ -306,3 +307,104 @@ void fmu_load_signal_handlers(FmuInstanceData* fmu) dlsym(handle, FMU_SIGNALS_REMOVE_FUNC_NAME); } } + + +/** +fmu_register_var +================ + +Register a variable with the FMU Variable Table mechanism. + +Parameters +---------- +fmu (FmuInstanceData*) +: The FMU Descriptor object representing an instance of the FMU Model. +vref (uint32_t) +: Variable reference of the variable being registered. +input (bool) +: Set `true` for input, and `false` for output variable causality. +offset (size_t) +: Offse of the variable (type double) in the FMU provided variable table. + +Returns +------- +start_value (double) +: The configured FMU Variable start value, or 0. +*/ +double fmu_register_var( + FmuInstanceData* fmu, uint32_t vref, bool input, size_t offset) +{ + double* signal = NULL; + char key[HASHLIST_KEY_LEN]; + + /* Lookup the signal. */ + snprintf(key, HASHLIST_KEY_LEN, "%i", vref); + if (input) { + signal = hashmap_get(&fmu->variables.scalar.input, key); + } else { + signal = hashmap_get(&fmu->variables.scalar.output, key); + } + if (signal == NULL) return 0; + + /* Create the marshal list. */ + FmuVarTableMarshalItem* mi = malloc(sizeof(FmuVarTableMarshalItem)); + *mi = (FmuVarTableMarshalItem){ + .variable = (void*)offset, // Corrected in fmu_register_var_table. + .signal = signal, + }; + if (fmu->var_table.var_list.hash_map.hash_function == NULL) { + hashlist_init(&fmu->var_table.var_list, 128); + } + hashlist_append(&fmu->var_table.var_list, mi); + return 0; +} + + +/** +fmu_register_var_table +====================== + +Register the Variable Table. The previouly registered variables, via calls to +`fmu_register_var`, are configured and the FMU Variable Table mechanism +is enabled. + +Parameters +---------- +fmu (FmuInstanceData*) +: The FMU Descriptor object representing an instance of the FMU Model. +table (void*) +: Pointer to the Variable Table being registered. +*/ +void fmu_register_var_table(FmuInstanceData* fmu, void* table) +{ + fmu->var_table.table = table; + fmu->var_table.marshal_list = hashlist_ntl( + &fmu->var_table.var_list, sizeof(FmuVarTableMarshalItem), true); + for (FmuVarTableMarshalItem* mi = fmu->var_table.marshal_list; + mi && mi->signal; mi++) { + /* Correct the varaible pointer offset, to vt base. */ + mi->variable = (double*)(table + (size_t)mi->variable); + } +} + + +/** +fmu_var_table +============= + +Return a reference to the previously registered Variable Table. + +Parameters +---------- +fmu (FmuInstanceData*) +: The FMU Descriptor object representing an instance of the FMU Model. + +Returns +------- +table (void*) +: Pointer to the Variable Table. +*/ +void* fmu_var_table(FmuInstanceData* fmu) +{ + return fmu->var_table.table; +} diff --git a/tests/cmocka/fmu/test_signals.c b/tests/cmocka/fmu/test_signals.c index ee4914b..50dbb69 100644 --- a/tests/cmocka/fmu/test_signals.c +++ b/tests/cmocka/fmu/test_signals.c @@ -139,6 +139,64 @@ void test_fmu_default_signals_reset(void** state) } +typedef struct { + double var_1; + double var_2; +} VarTable; + +void test_fmu_var_table(void** state) +{ + /* Setup the FMU. */ + FmuInstanceData* fmu = *state; + fmu->variables.vtable.setup(fmu); + assert_non_null(fmu->data); + double* var_1 = hashmap_get(&fmu->variables.scalar.input, "1"); + double* var_2 = hashmap_get(&fmu->variables.scalar.output, "2"); + assert_non_null(var_1); + assert_non_null(var_2); + + /* Configure the Var Table. */ + VarTable* vt = malloc(sizeof(VarTable)); + *vt = (VarTable){ + .var_1 = fmu_register_var(fmu, 1, true, offsetof(VarTable, var_1)), + .var_2 = fmu_register_var(fmu, 2, false, offsetof(VarTable, var_2)), + }; + fmu_register_var_table(fmu, vt); + assert_double_equal(vt->var_1, 0, 0); + assert_double_equal(vt->var_2, 0, 0); + + /* Check the marshalling. */ + VarTable* v = fmu_var_table(fmu); + assert_non_null(v); + assert_ptr_equal(v, vt); + *var_1 = 42; + *var_2 = 24; + for (FmuVarTableMarshalItem* mi = fmu->var_table.marshal_list; + mi && mi->variable; mi++) { + *mi->variable = *mi->signal; + } + assert_double_equal(v->var_1, 42, 0); + assert_double_equal(v->var_2, 24, 0); + assert_double_equal(*var_1, 42, 0); + assert_double_equal(*var_2, 24, 0); + v->var_1 = 24; + v->var_2 = 42; + for (FmuVarTableMarshalItem* mi = fmu->var_table.marshal_list; + mi && mi->variable; mi++) { + *mi->signal = *mi->variable; + } + assert_double_equal(v->var_1, 24, 0); + assert_double_equal(v->var_2, 42, 0); + assert_double_equal(*var_1, 24, 0); + assert_double_equal(*var_2, 42, 0); + + /* Finished. */ + fmu->variables.vtable.remove(fmu); + free(fmu->var_table.table); + free(fmu->var_table.marshal_list); +} + + int run_fmu_default_signal_tests(void) { void* s = test_fmu_default_signal_setup; @@ -147,6 +205,7 @@ int run_fmu_default_signal_tests(void) const struct CMUnitTest tests[] = { cmocka_unit_test_setup_teardown(test_fmu_default_signals, s, t), cmocka_unit_test_setup_teardown(test_fmu_default_signals_reset, s, t), + cmocka_unit_test_setup_teardown(test_fmu_var_table, s, t), }; return cmocka_run_group_tests_name("DEFAULT SIGNALS", tests, NULL, NULL);