FhSim  3.1.0
Marine systems simulation
Loading...
Searching...
No Matches
0005 — OdeFcn writing outside its own state block is unchecked
ID 0005
Class KNOWN-LIMITATION
Severity 2
Status blocked
Models
Found 2026-09-22 review of the state-offset contract
Decision needed Should a state-block check for OdeFcn be added as a test-only facility (a testtools harness step, or a debug-build guard), given that a check on every step is too expensive for the integrator's inner loop?

Evidence

ISimObjectCreator::AddState returns a global index, and SimObject::OdeFcn is handed X and XDot spanning the whole model (include/fhsim/simobject/SimObjectInclude.h:44-66). Nothing verifies that an object honours its offset. A SimObject that indexes from zero compiles, runs, and reads and writes the states of whichever object owns the model's first indices.

The initial-condition side of the same contract is now checked: src/engine/model/ModelAssemblyInitialConditions.cpp snapshots the update buffer around InitialConditionSetup and reports a write outside the running object's own states. The assembler can do this because it calls that method itself, a bounded number of times during assembly. It never calls OdeFcn, and the integrator calls it once or more per step, where an O(numStates) scan per call is not affordable.

A live instance of the pattern is tests/testobjects/DecayingStateEstimator.cpp:30-38, which stores m_stateIndex and then writes XDot[0] and reads X[0]. It is correct only because tests/in/Kalman/Kalman_PlainRun_in.xml and Kalman_Estimator_in.xml each declare exactly one SimObject, so its block starts at index 0. Adding any stateful object ahead of it in those files would silently move the dynamics onto the other object's states.

Effect

A model author who indexes locally gets no diagnostic. The symptom is a wrong trajectory in an unrelated SimObject — or, when the misindexed object happens to be first, no symptom at all until another object is added ahead of it. The mistake survives review precisely because the single-object case works.

Possible fix

  1. A testtools check: run OdeFcn for each object in isolation over a buffer prefilled with a sentinel, and report any element written outside the object's own block. Costs nothing at run time and catches the mistake in a model's own test. JacobianChecker already drives single objects over full-length buffers (src/testtools/JacobianChecker.cpp:276-294), so the seam exists.
  2. The same check inside the engine, compiled in only for debug builds. Catches it in any run of a debug build, but splits the two build configurations' behaviour.

Option 1 is the smaller change and does not touch the integrator.

Test that would prove it

A fixture SimObject with one state that writes XDot[0] rather than XDot[m_stateIndex], placed second in a two-object model. At HEAD it integrates the wrong object's state and no diagnostic is produced. The same shape is already used for the initial-condition check (Test/StrayInitialCondition, ModelAssemblyService.InitialConditionSetup_WriteOutsideOwnStates_IsReported).

Risk

Option 1 adds a facility model tests opt into, so nothing existing changes. Option 2 would change every debug-build run and would need the suite re-run in Debug. Neither is a prerequisite for current work; the initial-condition check already covers the assembly-phase half of the contract.