FhSim  3.1.0
Marine systems simulation
Loading...
Searching...
No Matches
0057 — The finite difference of a non-analytical object's own block differs between the fill passes
ID 0057
Class BUG
Severity 2
Status blocked

Models: —

Found: 2026-10-20 F3 review of the fill paths (a406e955), re-located at df9569e8

Source plan: plans/arch-2026-10-20/f3-jacobian-assembler-split.md, finding 2 (workspace fhsim_workspace)

Decision needed: owner to rule (F3-Q2: file and leave). Unifying the paths moves results, so it is not part of the F3 refactor.

Evidence

The block of a stateful object without an analytical Jacobian is differenced in five places, with two different ODE callbacks (src/engine/model/):

  • whole-model m_runtime.odeFcn: the dense pass (JacobianAssembler.cpp:585, :543), the CSR value pass (:669, :680) and block-tridiagonal path B (:800, :809);
  • the object's own simObject->OdeFcn: block-tridiagonal path A (JacobianAssembler.cpp:870, :880) and SimObjectJacobianBlock::EvaluateLocalJacobian (SimObjectJacobianBlock.cpp:109, :115), which ComponentSolver and the JacobianChecker call.
simObject->OdeFcn(time, stateForFd, m_fdPerturbedDerivatives.data()); // path A, :870
m_runtime.odeFcn(time, stateForFd, m_fdPerturbedDerivatives.data()); // path B, :800

The whole-model ODE re-evaluates the ports before each object, so a perturbed state reaches the object's own input ports. The object's own OdeFcn reads the input port values left by the last whole-model evaluation and does not move them.

Effect

For an object that feeds itself through ports (A to S to A, here a stateless gain), the whole-model difference includes the loop and the own-OdeFcn difference leaves it out. In the test fixture the block is [[-0.04, 1.06], [0.96, 0.56]] through the dense, CSR and path B passes and [[-0.54, 0.06], [-0.04, -1.44]] through path A and SimObjectJacobianBlock. The own-OdeFcn result also depends on whatever the ports held from the previous evaluation. Which one a solver gets depends on the linear solver chosen: ComponentSolver takes the own-OdeFcn block.

Possible fix

Owner to rule (F3-Q2: file and leave). Options: make every copy call the whole-model ODE (the block then includes the loop, and SimObjectJacobianBlock needs a whole-model callback it does not have today); or make every copy use the object's own OdeFcn and add the loop through the self-coupling link (a non-analytical self link is a zero matrix today by the settled decision, so the loop would be lost in the dense pass). Either choice moves results of hybrid models with a self-feeding object.

Test that would prove it

JacobianFillPathDifferences.NonAnalyticalSelfFeedback_EachPathPinnedToItsOwnArithmetic pins what each path returns today (green). JacobianFillPathDifferences.DISABLED_NonAnalyticalSelfFeedback_PathsAgreeOnTheObjectsBlock asserts path A and SimObjectJacobianBlock equal CSR and is red; enable it with the fix. Both in tests/model/JacobianFillPathDifferences_test.cpp.

Risk

Moves the Jacobian, so Newton iteration counts and step sequences, of every implicit run that has a non-analytical object coupled to itself through ports. Needs a baseline ruling (doc/agents/baselines.md). The F3 refactor keeps each path's own callback as an argument so that it does not move them.