model
Tests.HorizonResetTests
The packet epoch is re-anchored by a mid-run reset and never drifts from the buffer it describes
Simulated as a top-level model through
Tests/run-horizon.mos. The property is the one thing a reset
can silently break and nothing else in the suite covers: the packet epoch
is carried, not read off a clock, so the only place it can be anchored to
wall time is the reset itself.
Public declarations
Explicit source declarations; dimensions, modifications and defaults are shown as written, without evaluation.
| Role | Name | Type | Dimensions / attributes / default | Description |
|---|---|---|---|---|
| constant | samplePeriod | Real | = 0.00125 | 800 Hz inertial tick |
| constant | fusionPeriod_s | Real | = 0.01 | 100 Hz fusion release |
| constant | fusionHorizon_s | Real | = 0.05 | Five buffered release windows |
| constant | gravityWorldEnu_m_s2 | Real | [3] = {0.0, 0.0, -9.81} | |
| constant | specificForceBodyFlu_m_s2 | Real | [3] = {0.5, 0.0, 9.81} | |
| constant | resetTime_s | Real | = 0.2 | Mid-run, long after the ring has filled and released. This is the case that used to strand the epoch: the block seeded the packet timestamp at minus one sample period whatever the wall clock said, so after a reset at 0.2 s the epoch it published was 0.2 s behind the state it described, for the rest of the flight. Downstream, aiding aligned by timestamp would have been rejected from here on. |
| constant | refilled_s | Real | = 0.28 | Past the reset plus one horizon plus one release window |
Modelica declarations and implementation
Show parameters, interfaces and source
Parameters, inputs, outputs, defaults and equations from the source file.
model HorizonResetTests
"The packet epoch is re-anchored by a mid-run reset and never drifts from
the buffer it describes"
constant Real samplePeriod = 0.00125 "800 Hz inertial tick";
constant Real fusionPeriod_s = 0.01 "100 Hz fusion release";
constant Real fusionHorizon_s = 0.05 "Five buffered release windows";
constant Real gravityWorldEnu_m_s2[3] = {0.0, 0.0, -9.81};
constant Real specificForceBodyFlu_m_s2[3] = {0.5, 0.0, 9.81};
constant Real resetTime_s = 0.2
"Mid-run, long after the ring has filled and released. This is the case
that used to strand the epoch: the block seeded the packet timestamp at
minus one sample period whatever the wall clock said, so after a reset at
0.2 s the epoch it published was 0.2 s behind the state it described, for
the rest of the flight. Downstream, aiding aligned by timestamp would
have been rejected from here on.";
constant Real refilled_s = 0.28
"Past the reset plus one horizon plus one release window";
Real elapsed_s(start = 0.0, fixed = true)
"Continuous anchor. A model assembled only from clocked blocks has no
continuous equation at all and OpenModelica index reduction refuses to
build one. Not under test.";
Estimation.FusionHorizon.OutputPredictor driven(
samplePeriod=samplePeriod,
fusionPeriod_s=fusionPeriod_s,
fusionHorizon_s=fusionHorizon_s,
gravityWorldEnu_m_s2=gravityWorldEnu_m_s2);
discrete Boolean resetPulse(start = false, fixed = true);
discrete Boolean resetDone(start = false, fixed = true);
discrete Real fusionEpochAge_s(start = 0.0, fixed = true);
discrete Real epochAgainstBuffer_s(start = 0.0, fixed = true);
discrete Real epochAgainstReset_s(start = 0.0, fixed = true);
discrete Boolean readyBeforeReset(start = false, fixed = true);
discrete Boolean readyAfterRefill(start = false, fixed = true);
algorithm
when sample(0.0, samplePeriod) then
// Half a tick of slack, because the sampled instants are k * samplePeriod
// in binary floating point and 0.2 need not be one of them to the last
// bit. One tick of reset and no more.
resetPulse := time >= resetTime_s - 0.5 * samplePeriod
and not pre(resetDone);
resetDone := pre(resetDone) or resetPulse;
end when;
algorithm
when sample(0.0, samplePeriod) then
fusionEpochAge_s := time - driven.horizonPacket.timestamp_s;
epochAgainstBuffer_s := fusionEpochAge_s
- driven.bufferedDeltaCount * samplePeriod;
// On the reset tick the epoch must be re-anchored to THIS tick, one
// sample back, exactly as it is at power-on.
epochAgainstReset_s := if resetPulse
then driven.horizonPacket.timestamp_s - (time - samplePeriod)
else 0.0;
readyBeforeReset := pre(readyBeforeReset)
or (driven.horizonReady and time < resetTime_s - samplePeriod);
readyAfterRefill := pre(readyAfterRefill)
or (driven.horizonReady and time > refilled_s);
end when;
equation
der(elapsed_s) = 1.0;
driven.reset = resetPulse;
driven.angularVelocityMeasuredBodyFlu_rad_s = zeros(3);
driven.specificForceMeasuredBodyFlu_m_s2 = specificForceBodyFlu_m_s2;
driven.horizonStateValid = false;
driven.horizonStateShifted = false;
driven.horizonPositionWorldEnu_m = zeros(3);
driven.horizonVelocityWorldEnu_m_s = zeros(3);
driven.horizonQuaternionWorldBody = {1.0, 0.0, 0.0, 0.0};
driven.horizonGyroscopeBiasBodyFlu_rad_s = zeros(3);
driven.horizonAccelerometerBiasBodyFlu_m_s2 = zeros(3);
// THE INVARIANT, and it holds on every tick of the run including the reset
// and the refill behind it: the age of the epoch the block publishes is
// exactly the number of inertial ticks the buffer is carrying. It is what
// makes the epoch a statement about the buffer rather than a number that
// happens to increase. Before the reset was re-anchored this settled at
// about 3.4 times the bound it is allowed and never came back.
assert(abs(epochAgainstBuffer_s) < 0.5 * samplePeriod,
"The published fusion epoch and the buffer span disagree, so the epoch is
not a statement about the state the buffer carries");
assert(abs(epochAgainstReset_s) < 0.5 * samplePeriod,
"A reset did not re-anchor the packet epoch to the tick it happened on, so
every packet after it is stamped with an epoch that predates the reset");
// A reset drops the buffer, so readiness must drop with it and come back
// only when a packet has been released again.
assert(not (driven.horizonReady and resetPulse),
"A reset left horizonReady standing, so a consumer would keep standing on
a fusion instant the buffer no longer holds");
assert(fusionEpochAge_s <= fusionHorizon_s + fusionPeriod_s
+ 2.0 * samplePeriod or not driven.horizonReady,
"The fused epoch fell more than one release window behind the horizon");
assert(readyBeforeReset or time < resetTime_s,
"The horizon never became ready before the reset, so the reset under test
did not drop anything");
assert(readyAfterRefill or time < refilled_s + samplePeriod,
"The horizon never became ready again after the reset, so the buffer did
not refill");
annotation(experiment(StartTime=0.0, StopTime=0.32,
Tolerance=1.0e-8, Interval=0.001),
Documentation(info="<html>
<p>Simulated as a top-level model through
<code>Tests/run-horizon.mos</code>. The property is the one thing a reset
can silently break and nothing else in the suite covers: the packet epoch
is carried, not read off a clock, so the only place it can be anchored to
wall time is the reset itself.</p>
</html>"));
end HorizonResetTests;