|
FhSim
3.1.0
Marine systems simulation
|
A SimObject can implement a range of methods. Some of these are optional, some are required. Their calling sequence is given in the figure below.
SimObject(const string& simObjectName);
| [in] | simObjectName | -> The name of the simobject |
The constructor performs all initial setup for a SimObject. It is responsible for reading the parameters belonging to the SimObject from the input file and defining the interface of the SimObject, in terms of:
A list of methods used in the constructor is found in SimObject construction methods.
void SharedResourceSetup(ISimObjectCreator* creator);
| [in] | creator | -> Retrieve shared resources |
Retrieves the shared resources this SimObject uses. Called once for every SimObject, after all constructors have run and before any InitialConditionSetup, so an output port may use what it resolves here even when another object reads it while setting up its initial conditions. Optional: the default does nothing. Typed shared resources (set with SetSharedResource<T>) can be read from here on (ADR 0013); FinalSetup remains a valid place to read them for an object that does not need them earlier.
void InitialConditionSetup(const double T, const double* const currentIC, double* const updatedIC, ISimObjectCreator* const creator);
| [in] | T | The time at the beginning of the simulation. Usually but not necessarily zero |
| [in] | currentIC | Array of initial conditions, one entry per state of the whole model (global indices, like X in OdeFcn). If the value is QNAN, the value is not set yet and can be set by the SimObject, otherwise the value is set, and can not be changed. Trying to change an already set value will trigger an error |
| [out] | updatedIC | Array of QNAN with the same layout and global indices. Any initial condition that is ready to be set should be written to this array, at the global index of the state. |
| [in] | creator | Pointer to creator class |
Method for SimObjects that wants to set it's own initial conditions based on the value of it's input ports. SimObjects that wants to use this feature overrides the method, and it will be called if the SimObject has unspecified initial conditions after the values from the SimObject constructor, and the input file are set.
The following service contract applies to SimObjects that wishes to override this method:
virtual void FinalSetup(double T, const double* X, ISimObjectCreator* creator);
| [in] | T | -> Current simulation time |
| [in] | X | -> Current simulation state |
| [in] | creator | -> Retrieve shared resources |
Make final preparations before simulation starts. Mainly for retrieving shared resources registered during construction phase. Note: Called before RenderInit. Simobjects that wish to exchange shared resources through GetSharedResource/SetSharedResource may not retrieve said resources inside the constructor due to unpredictable order of construction.
void RenderInit(Ogre::Root* ogreRoot, ISimObjectCreator* creator);
| [in] | ogreRoot | -> Register visualization resources |
| [in] | creator | -> Retrieve parameters. |
Sets up visualization resources. Optional: the default does nothing, so a SimObject that draws nothing need not override it (visualisation builds only).
void PreOdeFcn(double T, const double* X);
| [in] | T | -> Current simulation time |
| [in] | X | -> Current simulation state (read-only) |
Pre ode-function pass. Called once before every accepted step, never on a rejected try. A SimObject cannot write a state here (IStateUpdater was removed, ADR 0015). A rule on a state belongs in OdeFcn: saturate the derivative (a limit), or add a correction term to the derivative (a normalisation).
void OdeFcn(double T, const double* X, double* XDot) const = 0;
| [in] | T | -> Current simulation time |
| [in] | X | -> Current simulation state |
| [out] | XDot | -> State derivatives |
This method is responsible for calculating the derivative of the states of the SimObject as a function of time, states and input ports.
X and XDot span the whole model, not this SimObject, and are indexed by global state index. A SimObject must therefore offset every access by the index ISimObjectCreator::AddState returned to it in the constructor:
XDot[m_stateIndex + i] = ...; // correct XDot[i] = ...; // wrong unless this object owns the model's first states
Indexing from zero reads and writes some other SimObject's states. It appears to work in a model with a single stateful SimObject, because that object's block starts at index 0.
OdeFcn is const and should contain derivative computations only. Use AcceptedStep() for once-per-accepted-step side effects.
void AcceptedStep(double T, const double* X);
| [in] | T | -> Accepted simulation time |
| [in] | X | -> Accepted simulation state |
Called once after each accepted integration step.
Typical uses:
if (isMajorTimeStep) branchesDefault implementation is empty.
bool HasJacobians() const;
Returns whether this SimObject can provide analytical Jacobian information. The default implementation returns false. Override this method to return true if you have implemented OdeJacobian().
This method is used by implicit integration methods (e.g., SUNDIALS CVODES with BDF, ARKODE with DIRK, or the Engine's Euler1imp method) to determine whether to use analytical or numerically approximated Jacobians. Providing analytical Jacobians improves both performance and numerical stability, especially for stiff systems.
false from HasJacobians(), the simulation will fall back to hybrid or numerical Jacobian approximation depending on the <Jacobian Policy> setting. See Implicit Integration with Analytical Jacobians for details.void OdeJacobian(double T, const double* X, double* J, int nStates);
| [in] | T | -> Current simulation time |
| [in] | X | -> This SimObject's State Slice (local states only, X[0] is the first state returned by AddState) |
| [out] | J | -> Jacobian matrix stored in row-major order (nStates x nStates) |
| [in] | nStates | -> Number of states (matrix dimension) |
Computes the analytical Jacobian matrix J = dF/dX at the given time and state, where F is the right-hand side function defined by OdeFcn().
The Jacobian matrix is stored in row-major order, meaning element J[i][j] (the partial derivative of state derivative i with respect to state j) is stored at index i * nStates + j in the array.
This method is only called if HasJacobians() returns true. The default implementation does nothing.
For a 2-state system with:
The Jacobian is constant:
Implementation:
void InputOutputJacobian(const std::string& outputPortName, const std::string& inputPortName,
double T, const double* X, double* dY_dU,
int outputPortSize, int inputPortSize, int index = -1);
| [in] | outputPortName | -> Name of the output port |
| [in] | inputPortName | -> Name of the input port |
| [in] | T | -> Current simulation time |
| [in] | X | -> Unused; may be nullptr. Do not read it. |
| [out] | dY_dU | -> Jacobian in row-major order (outputPortSize x inputPortSize) |
| [in] | outputPortSize | -> Size of the output port signal |
| [in] | inputPortSize | -> Size of the input port signal |
| [in] | index | -> Port index for indexed ports, -1 for standard ports |
Direct feedthrough Jacobian d(output port)/d(input port) at constant state. Only called on a stateless SimObject that sits between two others in a port coupling (A to S to B). Element (i, j) is at dY_dU[i * inputPortSize + j]. A stateless object's feedthrough can depend only on T and its input ports. The default implementation does nothing, so implement it (zeros for no feedthrough) when you want analytical chain assembly.
int GetJacobianSparsity(int nStates, int* rowPtr, int* colIdx);
| [in] | nStates | -> Number of states |
| [out] | rowPtr | -> Row pointer array (size nStates+1), may be nullptr for query |
| [out] | colIdx | -> Column index array, may be nullptr for query |
Returns the sparsity pattern of the Jacobian matrix in Compressed Sparse Row (CSR) format. This is used by SUNDIALS to construct sparse Jacobian matrices for large systems, reducing memory usage and computation time.
The CSR format consists of two arrays:
rowPtr[i] gives the starting index in colIdx for row icolIdx[p] gives the column index of the p-th non-zero elementReturn -1 to indicate a dense Jacobian (the default). For small systems (typically fewer than ~50 states), dense Jacobians are usually more efficient than sparse ones.
The method may be called with rowPtr = nullptr and colIdx = nullptr to query the number of non-zeros. In this case, only the return value is used.
For a 2x2 dense Jacobian (all 4 elements non-zero):
For a tridiagonal 3x3 Jacobian (7 non-zeros):
-1 (dense) is sufficient. Sparse Jacobians are primarily beneficial for large systems with many states.bool HasPortJacobians() const;
Returns whether this SimObject can provide analytical Jacobian information for its input and output ports. The default implementation returns false. Override this method to return true if you have implemented both OutputPortJacobian() and InputPortJacobian().
This method is used during Jacobian assembly to determine whether off-diagonal blocks (from port connections between stateful SimObjects) can be computed analytically via the chain rule, or must be approximated numerically.
OutputPortJacobian() and InputPortJacobian() must be implemented for this to return true. If only one is implemented, the port connection will fall back to numerical approximation. enum class PortDependencyPolicy { All, UserSpecified };
Policy for determining which local states an output port depends on. Used to build structural sparsity patterns for off-diagonal Jacobian blocks.
| Value | Description |
|---|---|
All | Conservative: assumes the port depends on all local states. Safe but may result in more numerical perturbations than necessary. |
UserSpecified | The SimObject author provides exact dependency information via GetPortStateDependency(). |
The default policy is All. Override GetPortDependencyPolicy() to return UserSpecified and implement GetPortStateDependency() for tighter sparsity.
PortDependencyPolicy GetPortDependencyPolicy() const;
Returns the policy used to determine which local states each output port depends on. The default implementation returns All (conservative). Override to return UserSpecified if you implement GetPortStateDependency().
std::vector<int> GetPortStateDependency(const std::string& portName) const;
| [in] | portName | -> Name of the output port |
Returns the list of local state indices that a specific output port depends on. Only called when GetPortDependencyPolicy() returns UserSpecified.
Return an empty vector to fall back to "all states" for that port.
void OutputPortJacobian(const std::string& portName, double T, const double* X,
double* dPort_dX, int portSize, int nStates, int index = -1);
| [in] | portName | -> Name of the output port |
| [in] | T | -> Current simulation time |
| [in] | X | -> Current simulation state (local to this SimObject) |
| [out] | dPort_dX | -> Jacobian matrix (row-major, portSize x nStates) |
| [in] | portSize | -> Size of the port signal |
| [in] | nStates | -> Number of local states |
| [in] | index | -> Port index for indexed ports, -1 for standard ports |
Computes the Jacobian of an output port with respect to the SimObject's local states: \( \frac{\partial (\text{port\_value})}{\partial X_{local}} \).
The result is stored in row-major order with dimensions [portSize x nStates]. Element (i, j) at index i * nStates + j represents the partial derivative of the i-th port element with respect to the j-th local state.
CommonComputations, you do not call them. The engine has already evaluated OdeFcn() at the same (T, X) before it calls this method, so input ports, common computations and members written by OdeFcn() hold the values for this state.For a 2-state system with a scalar output port force = 3*X[0] + 2*X[1]:
void InputPortJacobian(const std::string& portName, double T, const double* X,
double* dF_dInput, int portSize, int nStates);
| [in] | portName | -> Name of the input port |
| [in] | T | -> Current simulation time |
| [in] | X | -> Current simulation state (local to this SimObject) |
| [out] | dF_dInput | -> Sensitivity matrix (row-major, nStates x portSize) |
| [in] | portSize | -> Size of the port signal |
| [in] | nStates | -> Number of local states |
Computes the sensitivity of the SimObject's ODE derivatives with respect to an input port value: \( \frac{\partial F}{\partial (\text{input\_port})} \).
The result is stored in row-major order with dimensions [nStates x portSize]. Element (i, j) at index i * portSize + j represents the partial derivative of the i-th state derivative with respect to the j-th input port element.
For a 2-state system with a scalar input port u:
void RenderUpdate(double T, const double* X);
| [in] | T | -> Current simulation time |
| [in] | X | -> Current simulation state |
Updates visualization resources to reflect current state. Optional: the default does nothing.
void RenderRelease();
Releases the visualisation resources created in RenderInit (visualisation builds only). Called once, from the visualisation observer, while the Ogre root and scene manager are still alive and before Ogre is shut down. Optional: the default does nothing.
Text a SimObject wants on the 3D view goes on the HUD (fhsim::Hud, <fhsim/visual/renderer/Hud.h>, visualisation builds only). The framework owns the font, the colours, the size and the placement, so lines from different SimObjects never overlap; a SimObject names only a corner, a meaning and a priority. What the viewer sees is described in Text on the 3D view: the HUD. Fetch the HUD in RenderInit and keep the handles it hands out:
HudCorner::TopLeft, TopRight, BottomLeft and BottomRight. Top regions stack downward, bottom regions upward. The first top-left line is always the simulation time. Nothing is placed at the bottom centre, where FhSimUI's progress bar is, and in FhSimUI the regions move clear of the menu bar, the orientation gizmo and the logo.AddTextLine/AddValueLine, default fhsim::Hud::kDefaultPriority (1000). A lower number sits closer to the corner's edge; lines with the same priority keep the order they were added. The lowest int is the framework's.HudStyle::Normal, Highlight, Warning and Error (white, cyan, yellow, red). Change it with SetStyle.SetText does nothing when the text is unchanged. A value line shows "label value unit"; SetValue formats only a changed value, at most ten times a second, and the latest value is always shown in the end, so calling it every frame is cheap. The format must consume exactly one double.fhsim::Hud::kDefaultBackgroundColour). hud->SetBackgroundColour(fhsim::HudColour{0.2f, 0.0f, 0.0f, 0.6f}) changes it for all four regions; there is one colour for the whole HUD, so if several SimObjects set it, the last call wins. Tearing the scene down puts the default back, so set it again in RenderInit in every scene.HudLine is movable and not copyable. Destroying it or calling Remove() removes the line. Lines may be added and removed in RenderUpdate as well as in RenderInit. When the scene is torn down the HUD drops every line, and any handle still alive stops doing anything (IsActive() returns false), so a SimObject destroyed after the renderer needs no teardown hook.TextRenderer::addTextBox, removeTextBox, setText and printf still work but are deprecated in favour of the HUD; define FHSIM_SILENCE_TEXTRENDERER_DEPRECATION before including TextRenderer.h to silence the warning while a library moves over.