FhSim  3.1.0
Marine systems simulation
Loading...
Searching...
No Matches
Implicit Integration with Analytical Jacobians

Overview

FhSim supports implicit integration methods for simulating stiff systems that are challenging or impossible to solve efficiently with explicit methods. Implicit methods require solving nonlinear equations at each time step, which in turn requires Jacobian matrix information. FhSim supports multiple Jacobian computation strategies:

  1. Analytical Jacobians — provided by SimObjects that implement the OdeJacobian() method
  2. Hybrid Jacobians — analytical where available, numerical finite-difference for the rest
  3. Numerical Jacobians — approximated entirely by finite differences (fallback)

When all SimObjects with states provide analytical Jacobians, FhSim automatically uses them with implicit solvers, resulting in better performance and numerical stability.

Note
Backwards compatibility: Existing SimObjects require zero changes. The default implementation of HasJacobians() returns false, causing the system to fall back to numerical Jacobian approximation. Your existing simulations will continue to work unchanged.

When to Use Implicit Integration

Implicit methods are particularly beneficial for stiff systems — systems where some states evolve much faster than others. Common examples in marine simulation include:

  • Mooring systems with high stiffness cables
  • Hydraulic systems with fast pressure dynamics
  • Structural models with high-frequency modes
  • Systems with widely separated time scales

Signs that your system may benefit from implicit integration:

  • Explicit solvers require extremely small time steps for stability
  • Simulation fails with "step size too small" errors
  • Long simulation times despite seemingly simple dynamics

Available Implicit Methods

Note
This page still contains some legacy XML snippets for historical context. For new models, use v3 XML: <SIMULATION> + <Integrator Method="...">.

Engine Framework

Method Description Best For
BackwardEuler_i Implicit Backward Euler with Newton iteration Simple stiff systems, fixed-step requirements

StableSolver_i, despite its name, is a stability-enhanced explicit fixed-step scheme: it runs no Newton iteration and uses no linear solver, so nothing in this chapter applies to it.

Backward Euler uses the formula:

\[ x_{n+1} = x_n + \Delta t \cdot f(t_{n+1}, x_{n+1}) \]

The implicit equation is solved with Newton iteration using analytical, hybrid, or numerical Jacobian information depending on model capabilities and settings.

Use in the input file (v3):

<SIMULATION>
<Timing TStart="0" TEnd="10"/>
<Integrator Method="BackwardEuler_i">
<StepControl Step="0.01"/>
<LinearSolver Type="dense"/>
<Jacobian Type="dense"/>
</Integrator>
</SIMULATION>

SUNDIALS Framework

Method Description Best For
BDF Backward Differentiation Formula (CVODES, order 1-5) General stiff systems, adaptive stepping
DIRK Diagonally implicit Runge-Kutta (ARKODE) Stiff systems requiring high accuracy

Use in the input file (v3):

<SIMULATION>
<Timing TStart="0" TEnd="10"/>
<Integrator Method="BDF">
<StepControl AbsTol="1e-6" RelTol="1e-6"/>
<LinearSolver Type="SPGMR" NonlinearSolver="newton"/>
<Jacobian Type="dense"/>
</Integrator>
</SIMULATION>

Should you write an analytical Jacobian at all?

Deriving OdeJacobian() for a non-trivial model is days of work, so decide deliberately.

Leave <Jacobian Policy> on auto unless one of these holds. auto already uses every analytical Jacobian your model provides and fills the rest numerically, so there is nothing to gain by naming a policy:

  • Set Policy="analytical" when you must guarantee no numerical entry is used — for example when a regression suite has to fail loudly if someone adds a SimObject without a Jacobian. It is an assertion, not an optimisation: auto already picks the analytical path when the model qualifies.
  • Set Policy="numeric" to turn every analytical Jacobian off, for A/B comparison while debugging a suspect derivative.
  • Set Policy="hybrid" only to prevent the fully-analytical path from being chosen while still using the analytical blocks that exist.
Warning
<Jacobian Policy> is honoured by the SUNDIALS backend only. The Engine backend ignores it and logs a warning; Euler1imp decides purely on whether the model reports analytical Jacobians. Setting a policy on an Engine method changes nothing.

Write the analytical Jacobian when the model is stiff and the run is long enough that the finite-difference cost matters — each numerical column costs one extra model evaluation per Newton step, which is why the payoff grows with the number of states and with how often the solver refactorises. It is not worth it for a model that already runs happily under an explicit method.

Note
The repository contains no measured analytical-versus-numerical timings, on the Van der Pol example or anything else. Treat any speed-up claim you read as unquantified, and measure your own model before committing to the work.

<JacobianVerify> is not optional

An analytical Jacobian that is subtly wrong does not fail loudly. It makes the Newton iteration converge slowly, or to the wrong answer, and the symptom looks like a stiffness problem. Run the built-in verification the first time you implement OdeJacobian(), and again after every change to the derivative expressions. It compares the assembled matrix against a forward-difference Jacobian and names the SimObject and port link behind each mismatch. Treat a clean verification run as part of "done", not as an extra.

Jacobian Policy Options

The SUNDIALS integrator supports a Jacobian policy that controls how Jacobians are computed. Set it with the Policy attribute of <Jacobian>:

<SIMULATION>
<Timing TStart="0" TEnd="10"/>
<Integrator Method="BDF">
<Jacobian Type="dense" Policy="auto"/>
</Integrator>
</SIMULATION>
Policy Behavior When to Use
auto (default) Uses analytical Jacobians if all SimObjects provide them (HasJacobians() returns true for all). Falls back to hybrid if some provide them (HasHybridJacobians() returns true). Falls back to SUNDIALS built-in numerical approximation if none provide them. Recommended for most users. Automatically selects the best available strategy.
analytical Requires all SimObjects with states to provide analytical Jacobians. Fails with an error if any SimObject lacks OdeJacobian(). When you need to guarantee that no numerical Jacobian approximation is used.
hybrid Uses the hybrid Jacobian assembly if at least one SimObject provides analytical Jacobians. Falls back to SUNDIALS built-in if none do. When mixing legacy models (no Jacobians) with newer analytical models. See Hybrid Jacobian Mode below.
numeric Always uses SUNDIALS built-in numerical Jacobian approximation, ignoring any analytical Jacobians. For debugging or comparing analytical vs numerical results.

SimObject Interface Summary

All virtual methods on SimObject that participate in implicit integration are listed below. Every method has a safe default — existing SimObjects require no changes. Methods are grouped by concern; implement them in roughly the order shown to build up capability incrementally.

Method Default When to implement Impl. difficulty Performance impact
`HasJacobians()` false Whenever you implement OdeJacobian() Trivial — 1 line Gate: must return true before any analytical Jacobian is used
`OdeJacobian()` No-op Any stateful SO that benefits from analytical J = dF/dX Medium — derive all partial derivatives High — eliminates per-SO finite-difference cost; enables all structured solvers
`GetJacobianSparsity()` Dense (-1) SOs with many states and sparse internal coupling Low–Medium — enumerate non-zeros in CSR High for large SOs — unlocks sparse, band, block_tridiagonal, near_tridiagonal solvers and graph-coloring optimisation
`HasPortJacobians()` false Whenever you implement OutputPortJacobian / InputPortJacobian Trivial — 1 line Gate: must return true before off-diagonal port blocks are computed analytically
`OutputPortJacobian()` No-op Stateful SO whose output ports are consumed by another stateful SO Low–Medium — derive d(port)/dX High for coupled systems — fills off-diagonal Jacobian block analytically; avoids O(states) extra finite-difference perturbations per Newton step
`InputPortJacobian()` No-op Stateful SO that receives input from another stateful SO Low–Medium — derive dF/d(input) (same as OutputPortJacobian)
`InputOutputJacobian()` No-op Stateless SOs only — intermediaries that transform a signal without holding state Low — single matrix of gains Moderate — enables analytical chain-rule through stateless intermediaries (A→S→B); without it the link falls back to numerical perturbation
`GetPortDependencyPolicy()` All When you know an output port depends on only a subset of local states Trivial — 1 line
`GetPortStateDependency()` {} Only when GetPortDependencyPolicy() returns UserSpecified Low — return an index list Moderate for large SOs — tighter sparsity pattern reduces graph-coloring perturbations
Note
HasHybridJacobians() is a system-level query on ModelStructure, not a SimObject method. It returns true when at least one SO provides analytical Jacobians (enabling the hybrid assembly path described in Hybrid Jacobian Mode).
There is no HasInputOutputJacobians() method on SimObject. The assembler detects stateless-chain capability by checking HasPortJacobians() on the intermediate object and calling InputOutputJacobian() directly. Simply override InputOutputJacobian() and return true from HasPortJacobians().

Implementing Analytical Jacobians

To enable analytical Jacobian support in your SimObject, implement three methods:

1. HasJacobians()

Return true to indicate that OdeJacobian() is implemented:

bool MySimObject::HasJacobians() const override
{
return true;
}

2. OdeJacobian()

Compute the Jacobian matrix J = dF/dX, where F is defined by your OdeFcn():

void MySimObject::OdeJacobian(double T, const double* X, double* J, int nStates) override
{
// For a 2-state system:
// dX[0]/dt = -2*X[0] + X[1]
// dX[1]/dt = X[0] - 3*X[1]
//
// Jacobian:
// J = [ dF0/dX0 dF0/dX1 ] [ -2 1 ]
// [ dF1/dX0 dF1/dX1 ] = [ 1 -3 ]
J[0] = -2.0; // dF[0]/dX[0]
J[1] = 1.0; // dF[0]/dX[1]
J[2] = 1.0; // dF[1]/dX[0]
J[3] = -3.0; // dF[1]/dX[1]
}

The matrix is stored in row-major order: element (i, j) is at index i * nStates + j.

3. GetJacobianSparsity() (Optional)

For small systems, return -1 to indicate a dense Jacobian:

int MySimObject::GetJacobianSparsity(int nStates, int* rowPtr, int* colIdx) override
{
return -1; // Dense Jacobian
}

For large systems with sparse structure, return the sparsity pattern in CSR format to reduce memory and computation. See SimObject methods for details.

The pattern is model-wide, not per SimObject. A single SimObject returning -1, or a single port coupling whose objects do not provide port Jacobians, abandons the pattern for the whole system: the Jacobian is then assembled dense and every sparse linear solver (sparse, band, block_tridiagonal, near_tridiagonal) becomes unavailable, however sparse the rest of the model is. A model of 30 states or more that ends up on the dense path logs one warning at startup naming the SimObjects that declared nothing and counting the couplings without port Jacobians; below 30 states the dense solver is chosen regardless, so nothing is lost and nothing is logged.

Example: Van der Pol Oscillator

The Van der Pol oscillator is a classic stiff system for \( \mu \gg 1 \):

\[ \begin{aligned} \dot{x}_0 &= x_1 \\ \dot{x}_1 &= \mu (1 - x_0^2) x_1 - x_0 \end{aligned} \]

SimObject Implementation

class VanDerPolObject : public SimObject
{
public:
VanDerPolObject(const std::string& name, double mu = 1000.0)
: SimObject(name), m_mu(mu) {}
void OdeFcn(double T, const double* X, double* XDot) const override
{
XDot[0] = X[1];
XDot[1] = m_mu * (1.0 - X[0] * X[0]) * X[1] - X[0];
}
bool HasJacobians() const override { return true; }
void OdeJacobian(double T, const double* X, double* J, int nStates) override
{
J[0] = 0.0;
J[1] = 1.0;
J[2] = -2.0 * m_mu * X[0] * X[1] - 1.0;
J[3] = m_mu * (1.0 - X[0] * X[0]);
}
int GetJacobianSparsity(int nStates, int* rowPtr, int* colIdx) override
{
return -1; // Dense (2x2 is small)
}
private:
double m_mu;
};

Input File (SUNDIALS BDF)

<Contents>
<OBJECTS>
<Lib LibName="MyLib" SimObject="VanDerPolObject" Name="vdp"
Mu="1000.0"
/>
</OBJECTS>
<INTERCONNECTIONS>
<Connection />
</INTERCONNECTIONS>
<INITIALIZATION>
<InitialCondition
vdp.X="2.0, 0.0"
/>
</INITIALIZATION>
<SIMULATION>
<Timing TStart="0.0" TEnd="3000.0"/>
<Integrator Method="BDF">
<StepControl AbsTol="1e-8" RelTol="1e-6"/>
<LinearSolver Type="DENSE" NonlinearSolver="newton"/>
<Jacobian Type="dense"/>
</Integrator>
</SIMULATION>
</Contents>

Verifying Your Jacobian

To verify that your analytical Jacobian is correct, compare it against a numerical approximation. A simple finite difference check:

void VerifyJacobian(MySimObject& obj, double T, const double* X, int nStates)
{
double* XDot = new double[nStates];
double* J_analytical = new double[nStates * nStates];
double* J_numerical = new double[nStates * nStates];
// Compute analytical Jacobian
obj.OdeJacobian(T, X, J_analytical, nStates);
// Compute numerical Jacobian by finite differences
const double eps = 1e-8;
double* X_perturbed = new double[nStates];
for (int j = 0; j < nStates; j++) {
memcpy(X_perturbed, X, nStates * sizeof(double));
X_perturbed[j] += eps;
obj.OdeFcn(T, X_perturbed, XDot);
double* XDot_base = new double[nStates];
obj.OdeFcn(T, X, XDot_base);
for (int i = 0; i < nStates; i++) {
J_numerical[i * nStates + j] = (XDot[i] - XDot_base[i]) / eps;
}
delete[] XDot_base;
}
// Compare
double max_error = 0.0;
for (int i = 0; i < nStates * nStates; i++) {
double err = std::abs(J_analytical[i] - J_numerical[i]);
max_error = std::max(max_error, err);
}
std::cout << "Max Jacobian error: " << max_error << std::endl;
delete[] XDot;
delete[] J_analytical;
delete[] J_numerical;
delete[] X_perturbed;
}

A well-implemented Jacobian should have errors on the order of \( 10^{-6} \) or less.

Related pages

The rest of the implicit-integration material is split by audience:

See Also