|
FhSim
3.1.0
Marine systems simulation
|
| ID | 0022 |
| Class | ARCHITECTURE |
| Severity | 2 |
| Status | blocked |
| Models | — |
| Found | 2026-09-25, filed from fhsim_fishery FISH-0027 (its remedy 2) on the owner's decision |
| Decision needed | fhsim core owner (Karl-Johan Reite): whether to add a debug-build check on published port samples, which API it needs (port name available at the check, opt-in signaling_NaN seeding), and whether its debug-build cost in output evaluation and input-port reads is acceptable. |
An outport is a member function returning a pointer to storage the SimObject owns (include/fhsim/simobject/PortDefs.h:16-19); the @return line is the whole contract. ISimObjectCreator::AddOutport takes a name, a width and the function pointer (include/fhsim/simobject/ISimObjectCreator.h:223), so core never sees the storage and nothing initialises, poisons or checks it.
Registered states get the opposite treatment: the initial-condition table is filled with quiet_NaN (src/engine/model/ModelAssemblyInitialConditions.cpp:349) and assembly hard-errors on any slot still unset (:504, BuildMissingInitialConditionError). A state without an initial value is a build failure; a port-backing member without one is silence.
Every port read goes through SerialPort::GetPortValueImpl (src/engine/model/SerialPort.cpp:15-21), which caches the pointer the getter returns; consumers' input-port reads and the observer's output evaluation both land there. The observer then copies the samples without inspecting them (src/engine/model/OutputEvaluator.cpp:43-48, reached from EvaluateOutputs at :58). The first observer sample at TStart is taken before any integration step, so a member written only in AcceptedStep (or in OdeFcn under a fixed step or SUNDIALS, or by a peer port) is read uninitialised.
The value a user sees depends on allocation luck. Downstream fhsim_fishery has found four instances (its FISH-0018, FISH-0021, FISH-0024, FISH-0026; FISH-0027 is the architecture item), one of which fed the denormal 5.928788e-323 into a door roll-control loop. Library authors can follow a convention (fishery now documents one), but nothing catches a class the author did not think about. FHSIM-0021 would catch denormal and non-finite values in regression cases only; this item is the general, run-time detector.
A debug-build (!NDEBUG) check, after the getter returns in SerialPort::GetPortValueImpl (covering input-port reads) and in OutputEvaluator::FillOutputBuffer (covering observed outputs), that every one of the port's samples is finite and not subnormal, reported through PrintDuringExec with the SimObject and port name. Two API points need deciding:
SerialPort holds the SimObject and the width but not the port name (src/engine/model/SerialPort.h, constructor SerialPort(PortFunction, SimObject*, int, PrintDuringExec*)), so a named report needs the name passed in or looked up.signaling_NaN, so an unwritten member fails loudly at the first sample instead of reading as a plausible zero.A test-tools SimObject whose outport returns a member written only in AcceptedStep, seeded with a denormal: a debug build must report the object and port at the t=0 sample, and a correctly initialised twin must not.
A change to the shared engine's evaluation path that every downstream library feels: a per-sample check in debug builds (a cost on every port read), and libraries with latent instances will start reporting errors. A signaling_NaN seed would also change what a release build of a broken class does (silent garbage becomes a propagating NaN). Hence blocked on the core API and cost decision.