FhSim  3.1.0
Marine systems simulation
Loading...
Searching...
No Matches
Integrate FhSim in own applications

This page explains how to integrate an FhSim simulation into your own C++ application.

Model specification

You can initialize from:

  • an XML input file, or
  • an XML string (often preferred when embedding).

To exchange data with your host application, include at least one System/ExternalLinkStandard SimObject in your model.

Example model (v3 schema):

<Contents>
<OBJECTS>
<Lib LibName="fhsim_base" SimObject="System/ExternalLinkStandard"
Name="ExtLink"
outputPortNames="out1" Initial_out1="0,0,1"
inputPortNames="in1" Size_in1="3" Initial_in1="2,2,2"/>
<Lib LibName="fhsim_base" SimObject="Body/Mass" Name="M"
Scale="1" Mass="1"/>
</OBJECTS>
<INTERCONNECTIONS>
<Connection
ExtLink.in1="M.Pos"
M.Force="ExtLink.out1"/>
</INTERCONNECTIONS>
<INITIALIZATION>
<InitialCondition
M.Pos="1,1,1"
M.Vel="0,0,0"/>
</INITIALIZATION>
<SIMULATION>
<Timing TStart="0" TEnd="30"/>
<Integrator Method="RK45_i" NumCores="1">
<StepControl AbsTol="1e-3" RelTol="1e-3" StepMax="0.002"/>
</Integrator>
</SIMULATION>
<OBSERVERS>
<FileOutput outputFile="results.csv" Select="objects:all"/>
</OBSERVERS>
</Contents>

Two layers: the wrapper class and the C ABI

The engine is loaded as a shared library (fhSimDll, or fhVisDll when you want the renderer). It exposes a flat C ABI, listed once in fhsim/dll/FhsimDllApi.h. On top of that sits a thin C++ convenience class, FhSimDll (fhsim/dll/DllWrapper.h), which loads the library, resolves every export, owns the opaque context and takes std::string arguments.

The wrapper covers the calls a typical host needs, but not all of them. Where a section below is marked C ABI only, the call has no wrapper method: resolve the exported symbol yourself, or use the function-pointer typedefs the table generates (see fhsim/dll/DllEntry.h). Every export takes the opaque void* context as its first argument.

Lifecycle

Call Layer Purpose
InitFromFile wrapper + C ABI Build a simulation from an XML model file.
InitFromString wrapper + C ABI Build a simulation from an XML string.
StopSim wrapper + C ABI Stop the run and release the engine's resources.
FreeFhSimContext C ABI only Free the context itself.

FhSimDll runs StopSim and then FreeFhSimContext from its destructor, so a host that uses the wrapper never calls FreeFhSimContext by hand. A host on the raw C ABI must call both, in that order: StopSim releases the engine, FreeFhSimContext releases the context that still holds the error message.

Warning
StopSim() returns true when an error was recorded at any point in the run. This is the inverse of Simulate() and PerformSingleStep(), which return true on success. A true from StopSim() does not mean the stop failed; read the message with GetErrorString().

Calling FhSim from C++

Use the DLL wrapper API (fhsim/dll/DllWrapper.h):

#include <fhsim/dll/DllWrapper.h>
#include <stdexcept>
FhSimDll simulation("path/to/runtime/bin-or-lib");
const std::string modelStringXml = "...";
if (!simulation.InitFromString(modelStringXml, "out/results.csv", "out/log.txt", 2, 2)) {
std::string err;
simulation.GetErrorString(&err);
throw std::runtime_error("InitFromString failed: " + err);
}
double deltaSimTime = 0.1;
double maxDeltaRealTime = 0.1;
for (int i = 0; i < 1000; ++i) {
const double input1[3] = {1.0, 2.0, 3.0};
simulation.SetInputCopy("ExtLink", "in1", input1);
if (!simulation.Simulate(deltaSimTime, maxDeltaRealTime)) {
std::string err;
simulation.GetErrorString(&err);
throw std::runtime_error("Simulation failed at step " + std::to_string(i) + ": " + err);
}
const double* output1 = simulation.GetOutput("ExtLink", "out1");
// Consume output1...
}
simulation.StopSim();
Note
SetInputCopy copies values into internal buffers. Use it when input data lifetime is short or stack-allocated.

For full API details, see headers in:

  • include/fhsim/dll/DllWrapper.h
  • include/fhsim/visual/dll/FhVisDllWrapper.h

Stepping

Call Layer Purpose
Simulate(deltaSimTime, maxDeltaRealTime) wrapper + C ABI Advance by deltaSimTime of model time, spending at most maxDeltaRealTime wall-clock seconds.
PerformSingleStep() C ABI only Take exactly one integration step, whatever its size.
GetDesiredSimTime() / SetDesiredSimTime(t) C ABI only Read or set the end time Simulate() is working towards.
GetTCurr() C ABI only The current simulation time, without publishing the state vector.

Simulate() does not run a fixed number of steps. It adds deltaSimTime to an accumulated desired simulation time and then steps until the model reaches that time or the wall-clock budget runs out, whichever comes first. So a Simulate() call that hits the budget leaves the model short of the requested time, and the shortfall carries over: the next call adds to the same accumulator instead of resetting it. GetDesiredSimTime() reports that accumulator and SetDesiredSimTime() overwrites it, which is how a host resynchronises after a step that ran out of budget.

Use PerformSingleStep() when the host wants step-level control — a co-simulation master, or a controller that must see every integrator step — and Simulate() when the host wants to advance a fixed slice of model time per control period.

Ports and outputs

Every port call addresses a System/ExternalLinkStandard object by name, or by a pair of indices on the fast path.

Call Layer Purpose
GetOutput(object, port) wrapper + C ABI Values of a named output port, nullptr if unknown.
GetOutputSize(object, port) wrapper + C ABI Number of doubles in a named output port.
SetInput(object, port, values) wrapper + C ABI Point an input port at caller-owned values.
SetInputCopy(object, port, values) wrapper + C ABI Copy values into the port's own storage.
GetInputSize(object, port) wrapper + C ABI Number of doubles in a named input port.
GetSimObjectIndex(object) wrapper + C ABI Index of an ExternalLink object, -1 if unknown.
GetSimObjectPortIndex(object, port) wrapper + C ABI Index of a port on that object, -1 if unknown.
GetOutputWithIndex(objectIndex, portIndex) wrapper + C ABI Output values by index (fast path).
GetOutputSizeWithIndex(objectIndex, portIndex) wrapper + C ABI Output size by index.
EvaluateOutputs() C ABI only Refresh the output ports without advancing time.

Query the sizes when port dimensions come from the XML and are not known at compile time:

int inSize = simulation.GetInputSize("ExtLink", "in1"); // e.g., 3
int outSize = simulation.GetOutputSize("ExtLink", "out1"); // e.g., 3

Resolve the indices once, outside the loop, when the same ports are read every control period:

const int linkIndex = simulation.GetSimObjectIndex("ExtLink");
const int outIndex = simulation.GetSimObjectPortIndex("ExtLink", "out1");
for (int i = 0; i < 1000; ++i) {
simulation.Simulate(deltaSimTime, maxDeltaRealTime);
const double* output1 = simulation.GetOutputWithIndex(linkIndex, outIndex);
// Consume output1...
}

EvaluateOutputs() re-runs the ExternalLink outputs and drops the port caches at the current (T, X) without taking a step. Use it after SetStates(), or after SetInput() when the host needs to see the model's response to the new inputs before it commits to a step.

Reading and writing the state vector

Call Layer Purpose
GetStates(&stateVector) wrapper + C ABI Publish the state vector; returns the current time.
SetStates(stateVector, tNew) C ABI only Overwrite the state vector and the current time.
GetNumStates() wrapper + C ABI Number of state variables.
GetStateObjects(&objectNames) wrapper + C ABI Owning object name of every state variable.
GetStateTags(&tagNames) wrapper + C ABI Tag name of every state variable.

For diagnostics or custom logging, you can access the full simulation state. The state array is read-only, so the receiving pointer must be const double*:

const double* stateVector = nullptr;
double currentTime = simulation.GetStates(&stateVector);
int numStates = simulation.GetNumStates();
// stateVector[0..numStates-1] contains all ODE states

To understand which state belongs to which object:

char** objectNames = nullptr;
char** tagNames = nullptr;
simulation.GetStateObjects(&objectNames);
simulation.GetStateTags(&tagNames);
for (int i = 0; i < numStates; ++i) {
std::cout << objectNames[i] << "." << tagNames[i]
<< " = " << stateVector[i] << "\n";
}
Note
The returned pointers point into internal DLL memory. Do not free them. They remain valid until the next call to Simulate() or StopSim().

SetStates() writes the whole state vector back and sets the current time in one call. It is the entry point for a host that resets, rewinds or re-initialises the model — a co-simulation master rolling back to a saved point, or a filter injecting a corrected estimate. Pass GetNumStates() doubles in the layout GetStateObjects() and GetStateTags() describe; the call does no range checking and does not evaluate outputs, so follow it with EvaluateOutputs() when the host needs the ports to match the new state.

Warning
SetStates() has no wrapper method. Call the exported C symbol directly.

Sharing objects between host and simulation

You can pass pointers between your host application and the simulation via named pointer registration:

// Host registers an object before simulation starts
MyController controller;
simulation.SetObjectPtr("HostController", &controller);
// A SimObject inside the simulation can retrieve it:
// void* ptr = creator->GetSharedResource("HostController");

To retrieve a pointer registered by a SimObject:

void* ptr = simulation.GetObjectPtr("SomeResource");
auto* resource = static_cast<MyResource*>(ptr);

Accessing FhSimMgr directly

For advanced use cases (custom observers, direct object access), you can retrieve the simulation manager:

FhSimMgr* mgr = simulation.GetSimMgr();
// Use mgr for advanced operations — see FhSimMgr.h
Warning
Direct manager access bypasses the stable DLL API. Use it only when the wrapper methods are insufficient.

Sensitivity analysis

The engine can answer directional-derivative questions about the model at its current (T, X): if these input ports move in this direction, how do these output ports move? Both calls are C ABI only — the C++ wrapper has no method for either.

Call Layer Derivative source
GetDirectionalDerivative(...) C ABI only Finite differences over the model.
GetAnalyticalDirectionalDerivative(...) C ABI only The Jacobian machinery.

Both take the outputs as parallel outputObj[] / outputPort[] arrays with a count, the inputs the same way, a seed vector dvKnown[] and a result buffer dvUnknown[]:

void GetDirectionalDerivative(void* context,
const char* outputObj[], const char* outputPort[], int nOutputs,
const char* inputObj[], const char* inputPort[], int nInputs,
const double dvKnown[], double dvUnknown[], int mode);
void GetAnalyticalDirectionalDerivative(void* context,
const char* outputObj[], const char* outputPort[], int nOutputs,
const char* inputObj[], const char* inputPort[], int nInputs,
const double dvKnown[], double dvUnknown[], double dt);

The last argument is what separates them:

  • mode on the finite-difference call selects the direction. Mode 0 is forward: it reads one seed per input and writes one result per output. Mode 1 is adjoint and reverses both. The time differential is the integrator's current step size; the host does not choose it.
  • dt on the analytical call is the time differential the host wants the derivative over, so the host chooses it.
Warning
GetDirectionalDerivative() locates ports through the states of their owning SimObject. A model whose ports depend on no states — a pure feed-through — therefore answers zero. Use GetAnalyticalDirectionalDerivative() for those.

Neither call returns a status. A failure inside the model is recorded on the context, so check GetErrorString() afterwards.

Error handling

All methods that return bool return false on failure. Retrieve the error message with:

std::string errorMsg;
if (simulation.GetErrorString(&errorMsg)) {
std::cerr << "FhSim error: " << errorMsg << "\n";
}

After StopSim() the context is stopped: every query (GetOutput, GetStates, GetNumStates, GetSimMgr, …) returns an empty result — nullptr, 0 or false — and Simulate() returns false. GetErrorString() still reports the error that ended the run, so read it before freeing the context. The index lookups GetSimObjectIndex() and GetSimObjectPortIndex() return -1 for an unknown name and for a stopped or failed context; a valid index is never negative.

The context outlives the engine precisely so the message survives StopSim(). Read it with GetErrorString() before calling FreeFhSimContext() — after that the message is gone. Hosts using the FhSimDll wrapper get the free from the destructor, so the message must be read before the wrapper goes out of scope.

Common failure scenarios:

  • InitFromFile / InitFromString: XML parsing errors, missing libraries, invalid parameters.
  • Simulate: integrator failure, NaN in state vector.
  • SetInput / SetInputCopy: object or port name not found.

See Runtime errors and diagnostics for a complete list of error messages.