FhSim  3.1.0
Marine systems simulation
Loading...
Searching...
No Matches
0012 — ConstraintSolver warm-starts a 5-iteration CG from the previous call, so OdeFcn is not idempotent
ID 0012
Class BUG
Severity 3
Status blocked

Models: —

Found: 2026-10-22, feature step-size-controller (fhsim FHSIM-0066 moved every fhsim_marine_elements cable result)

Decision needed: whether the solver may keep its warm start. Starting from zero, or iterating to the tolerance, changes every result of every model that uses the solver (a baseline move for fhsim_marine_elements, fhsim_fishery models that use coribo, and coribo's own regression).

Evidence

ConstraintSolver::ComputeDynamicInternal (src/ConstraintSolver/ConstraintSolver.cpp:116-118):

double tolerance = 1e-6;
int iterations = 5;
m_CG_solver->solve(m_Force,m_SecondDerivatives,tolerance,iterations);

m_Force is the right-hand side. m_SecondDerivatives is a member and holds the answer of the previous call; it is the start guess and is overwritten with the result. The solve stops at 5 iterations or a residual of 1e-6, so the result depends on the start guess. OdeFcn of every SimObject that runs the solver is therefore not a function of (t, x) alone: it depends on how many evaluations came before.

Experiment, 2026-10-22 (fhsim 9f7b2de2, with a scratch variant of FhIntegrator::TryStep_, fhsim_marine_elements tests):

Variant fhsim_marine_elements result files that differ from "no extra call"
one extra OdeFcn before AcceptedStep 60 of 193 (12 are run-to-run noise)
one extra OdeFcn after AcceptedStep 60
two extra OdeFcn before AcceptedStep 60, and 55 of 193 differ from the one-call run

The same variants leave the idempotent fhsim_fishery models unchanged (only the AcceptedStep readers move). fhsim_coribo's own WinchableCable.ScenarioRunsWithFiniteWinchForce regression passes without the extra call and fails with it (WinchForce RMS 8e1 to 1.8e5).

Test: ConstraintSolverIdempotence.DISABLED_SameStateEvaluatedTwice_GivesTheSameDerivatives (tests/ConstraintSolver_Idempotence_Test.cpp, committed DISABLED_ so the suite stays green): a bent six-element LineStructure evaluated twice at the same (T, X). The largest difference of the state derivatives is 4.04e-13, not 0 (run with --gtest_also_run_disabled_tests).

Second evidence, coribo's own WinchableCable scenario (tests/in/WinchableCable/WinchableCable_in.xml, StableSolver_i, step 0.01, 1 s). The same run with the scratch fhsim variants, WinchForce_3 at t=0.1 (N):

Variant WinchForce_3 at t=0.1
no extra OdeFcn -1.246e6
one extra OdeFcn after AcceptedStep -1.048e6
one extra OdeFcn before AcceptedStep -0.994e6
two extra OdeFcn before AcceptedStep -0.994e6

An extra evaluation that nothing reads (the one after AcceptedStep) changes the start-up transient by 16 % (-1.246e6 to -1.048e6). That is history dependence through the warm start, in the stiff first 0.1 s where the 5-iteration CG is far from converged. The model settles to 1 to 6 % differences by t=0.4. The last two rows are equal: once a call has converged, a second call returns at once (in the larger fhsim_marine_elements systems five iterations do not converge, and two calls differ from one). The remaining difference between the "before" and "after" rows (5 %) is the stale winch pose read by WinchableCable::AcceptedStep (src/SimObjects/WinchableCable.cpp:70-72), which fhsim FHSIM-0066 corrects. Priority raised to 3 on 2026-10-22: the answer of a stiff start-up depends on how often the model was evaluated.

Effect

A model that uses the solver gives different results when the integrator evaluates it an extra time (as AcceptedStep fixes now do), when a Jacobian pass calls OdeFcn, or when the method changes its stage count. Stiff or contact-driven cases amplify the 1e-6 residual: TrawlCable_BedRegression moved a contact transient by 0.05 s and a bridle force by 1480 N at its peak. Results are not reproducible from the model state alone, and a result file cannot be reproduced by a test that evaluates the model once.

Possible fix

  1. Iterate to the tolerance (raise iterations, or stop only on the residual). Costs instructions, makes the result independent of the start guess to 1e-6.
  2. Start from zero every call: idempotent, but no warm-start speed-up, and the answer is the 5-iteration answer from zero.
  3. Keep the warm start and save and restore m_SecondDerivatives around evaluations that must not advance it (the integrator's extra call).
  4. Keep as it is and document: OdeFcn of a coribo model depends on the call history.

Test that would prove it

The DISABLED_ test above, enabled. After a fix it must pass with largestDifference == 0.

Risk

Every option moves the stored references of the models that use the solver: the fhsim_marine_elements cable, RundDorg and RigidBarCable families and coribo's WinchableCable scenario. Options 1 and 2 change the answer even where nothing evaluates twice.