VAV-0005 — VAV damper hunting or oscillation
| Status | verified — engine e2ff2f8, cxf:fnv1a128:5f4289e7553426911e4a689b1e3c83a6, 2026-08-17 |
| Severity | 3 |
| Method | rule |
| Phase | 2 |
| Category | COMFORT_ENERGY |
| Confidence | LOW |
| Estimation | QUALITATIVE_ONLY |
| G36 | — |
| Clusters | — |
| Suppresses | — |
| Suppressed by | — |
| Related | AHU-0022, VAV-0004 |
| Playbooks | — |
| Source | HVAC FDD Reference v1.0 §10, VAV-0005; Torabi et al. 2022; Gunay et al. 2020 |
| Operating states | all (fan running) |
Preconditions (host-enforced): Supply fan running — a damper in a dead air stream does not oscillate, and a box parked at a fixed position during unoccupied hours produces no crossings either way. Damper position feedback (or, failing that, the position command) available as a live analog signal; a point that only updates on change-of-value with a wide deadband will hide the oscillation entirely. The host must report NO_EVAL for the first eval_window (30 min) after engine start: while the three moving averages fill, each divides by elapsed time rather than by the window, so the crossing count reads as an extrapolated pace and the amplitude as a partial average. This gate is load-bearing, not a formality — alarm_delay (15 min) is half of eval_window, so delayOnInit cannot cover the warm-up and a short burst at startup can reach a verdict (
warmup_burst_assertspins it). Host tick interval must lie in [28.6 s, 180 s) with count_scale set to match; 60 s is recommended (see Deviations). When any gate is unmet the verdict is NO_EVAL, not healthy.
Points: zone_dmpr_pos
Outputs:
yFault— True while the damper has crossed its own rolling mean more than max_reversals_per_window times per eval_window, with a mean absolute deviation above min_oscillation_amplitude, for at least alarm_delay
Parameters:
| Name | Default | Unit | CXF path | Description |
|---|---|---|---|---|
eval_window | 1800.0 | s | muS.delta, xRate.delta, mad.delta | Rolling window the crossings are counted over and the amplitude averaged over (30 min). It drives three moving-average stages — the mean, the crossing rate, and the deviation average — and a host must move all three paths together; count_scale must be retuned with it |
max_reversals_per_window | 10.0 | 1 | xHigh.t | Mean crossings per eval_window above which damper motion counts as hunting rather than load following. Each oscillation cycle produces two crossings, so the default is five cycles per half hour |
min_oscillation_amplitude | 5.0 | % | madHigh.t | Rolling mean absolute deviation of damper position above which the swing is large enough to matter. MAD units, not peak-to-peak: the reference’s 10% swing is ±5% about the mean, which is a MAD of 5.0 for a square oscillation and 3.2 for a sinusoidal one (see Deviations) |
count_scale | 30.0 | 1 | xCount.k | Rescales the moving average of the one-tick crossing pulse train back into a crossing count: k = eval_window / host tick interval in seconds. The default 30.0 is 1800/60, correct only at a 60 s tick; a host on a different tick MUST retune this or every count is wrong by the ratio of the two intervals |
alarm_delay | 900.0 | s | persist.delayTime | Continuous fault persistence required before the alarm asserts (15 min) |
Description
The damper never stops moving. A VAV box’s flow loop is supposed to find a blade position that delivers the setpoint and stay there until the setpoint moves; a hunting box overshoots, corrects, overshoots the other way, and repeats every few minutes all day. The zone usually feels fine — the swings average out — so nobody reports it, and the box quietly strokes its actuator tens of thousands of extra times a year. That wear is the real cost; the energy penalty is small and diffuse (the reference puts it at 1–3% of zone energy), but a floating actuator driven to death takes the zone with it when it fails. The usual cause is a proportional gain too high for the box’s damper authority, though a noisy airflow signal or a duct-static loop fighting a zone-demand loop produce the same picture with nothing mistuned. Prevalence roughly 5%.
Detection Logic
muS = MovingAverage(zone_dmpr_pos, eval_window) rolling mean position
above = zone_dmpr_pos > muS which side of the mean
pulse = (above ≠ previous tick's above) one tick wide, one per crossing
count = MovingAverage(pulse, eval_window) × count_scale crossings in the trailing 30 min
mad = MovingAverage(|zone_dmpr_pos − muS|, eval_window) amplitude, in MAD units
yFault = (count > max_reversals_per_window)
AND (mad > min_oscillation_amplitude)
sustained continuously for alarm_delay
Block graph (rule.cxf.jsonld):
muS is the spine: one rolling mean feeds both tests — the crossing count on one
branch, the mean absolute deviation on the other — so the rule asks how often the
position ends up on the other side of its own half-hour average and how far from
that average it typically sits. Frequent crossings with a large deviation is
hunting; frequent crossings with a small deviation is dither on a feedback
signal; a large deviation with few crossings is a box following a load.
Reals.MovingAverage is a continuous-time integral mean, so a one-tick pulse of
height 1.0 encloses one tick interval of area and count_scale = eval_window/dt
converts the pulse average back into a crossing count. That couples this rule to
the host’s tick harder than any other in the library: read the Nyquist band and
count_scale deviations before deploying, and honour the warm-up NO_EVAL
precondition. Both comparisons are strict — exactly ten crossings reads clear,
eleven alarms — and persist requires 15 continuous minutes of both conditions,
three or four more cycles at the reference’s severe rate. delayOnInit = true
holds that window across a restart.
Possible Diagnoses
- PID loop tuning too aggressive — proportional gain too high for the damper’s authority, or integral time too short for the box’s response
- Airflow sensor noise causing erratic setpoint tracking: the loop is chasing measurement noise, and no tuning change fixes a bad velocity pickup
- Conflicting control signals — the AHU’s duct static pressure loop and the zone demand loop acting on the same damper with overlapping response times
- Damper actuator mechanical backlash: the blade lags the command, the loop over-drives to compensate, and the limit cycle is mechanical, not computational
- Zone load disturbance near the thermostat — a copier, a projector, direct sun through a blind that opens and closes — a real load the box is following
Energy Impact
COMFORT_ENERGY, LOW confidence, QUALITATIVE_ONLY. There is no waste term to compute from this rule’s inputs: it sees a damper position and cannot price a stroke. The reference puts the loss at 1–3% of zone energy from inefficient control and names actuator wear as the affected subsystem, which is the honest ordering. Size the opportunity host-side per Energy Impact Reference §4.4 (hunting hours × zone fan and reheat energy); this rule contributes the hours. LOW confidence for AHU-0022’s reason — no controlled study isolates the losses of a hunting loop from the tuning change that fixes it, and no PNNL measure covers loop stability. Climate-neutral. Per zone the number is small enough to ignore, which is why it goes unfixed; several hundred boxes with 5% hunting is a dozen actuators being consumed years early against a fix that costs a gain setting.
Emissions Impact
Scope 2, QUALITATIVE_EMISSIONS, LOW confidence. Minimal in absolute terms: the direct emissions are the marginal fan and reheat energy above, and the reference records the concern as actuator wear rather than emissions. The larger indirect term is embodied carbon in actuators replaced early. Avoided-emissions basis: N/A.
Deviations
- Direction reversals → mean crossings, the substitution this rule is built
on. The reference counts direction changes, which need the previous sample
(
CDL.Discrete.UnitDelay, whosesamplePeriodis one more silent coupling to the host tick) and have no noise floor — a position quantized to 1% reverses on essentially every tick it is not moving. The two counts agree on anything periodic (a cycle contains exactly two reversals and two mean crossings, and all three reference vectors reproduce) and diverge only on drift with small ripple, where the crossing count under-reads. That error is one-directional and the cases it misses are small-amplitude ripplemin_oscillation_amplitudewould have rejected anyway. min_oscillation_amplitudeis restated in MAD units. The reference’s default 10% swing reads as peak-to-peak; the engine has no peak detector and no rolling standard deviation, so the available statistic is AHU-0022’s rolling mean absolute deviation. For a square oscillation of ±A about the mean MAD = A, so 10% peak-to-peak becomes 5.0 MAD; for a sinusoid MAD = 2A/π = 0.637 A, so a 10% sine has a MAD of 3.2 and needs 15.7% to trip. The default matches the reference for the square-ish limit cycles a floating actuator produces and is conservative for smooth ones; a site wanting sine-equivalence sets 3.2.- Nyquist: this rule cannot run at the library’s usual 300 s tick.
abovechanges at most once per tick, so the observable ceiling on the count iseval_window/dt— 6 at a 300 s tick, below the threshold of 10, and the rule could never fire (RTU-0001’s failure mode). A legal deployment needsdt ≥ eval_window/63≈ 28.6 s (the moving average’s 64-checkpoint ring holdseval_window/dt + 1entries) anddt < eval_window/max_reversals= 180 s for the threshold to be reachable. Detection before the ceiling wants headroom, so 60 s is recommended and is the only tick these vectors exercise. count_scaleis coupled to the host’s tick interval, and the failure is silent.k = eval_window / dt, so a host ticking every 120 s must setcount_scaleto 15.0; left at 30.0 it reports double the true count and alarms on six crossings. Worse here than in AHU-0004 or RTU-0001 because tick,count_scaleandeval_windowmust move together with the Nyquist band above.eval_windowbinds three CXF parameter paths —muS.delta,xRate.delta,mad.delta— and a host must set all three together and recomputecount_scalewith them. Splitting them changes what the statistic means: a mean over one window compared against deviations averaged over another is not a MAD of anything. AHU-0022 bound two paths per window for the same reason.- Rolling count built from a moving average, because the block set has no
windowed counter.
Integers.OnCountercounts monotonically from a reset, so a trailing-window count would need a host-driven reset — a tumbling count whose verdict depends on where the boundary fell. AHU-0004’s trade, including its half-open window: a crossing exactlyeval_windowold has just left the count. Reals.MovingAverageis a continuous-time integral mean, not a sample mean. The engine accumulatesu·dtforward-Euler and divides by the window, so hand-computed sample statistics do not match it and every assertion edge invectors.jsonwas derived by replaying the graph at the pinned engine rev.- Warm-up NO_EVAL is load-bearing, and a warm-up transient can assert. While
t < eval_windowall three averages divide by elapsed time, so eight crossings in the first ten minutes read as a count of 24.alarm_delay(900 s) is half ofeval_window, sodelayOnInitcannot cover the gap as it does in AHU-0004 and a startup burst can reach a verdict (warmup_burst_assertspins it). The host precondition is not optional here (RTU-0001 precedent). - Startup artifact: a spurious first-tick pulse, which costs nothing. On the
first tick
muSoutputs 0.0, so any positive damper position makesabovetrue andLogical.Changeemits a pulse at t = 0. It encloses no area (dtis zero on the first tick) and never reaches the count.flip.pre_u_startis written explicitly asfalseand not exposed as a card parameter. - A perfectly steady damper produces exactly one crossing. While the window
fills,
muSruns a hair below a constant input (its divisor is elapsed time plus a 1 ms guard) soaboveis true; when the window fills the mean equals the input exactly andflipemits one pulse. Harmless, but it is why a quiet box reads a count of 1 rather than 0 for its first half hour. - Neither statistic drops when the hunting stops; both decay across the
trailing window. The count falls only as old crossings age out and
madonly as the flat stretch dilutes the average, so the alarm outlives the fix by design, the release time is a property of the window rather than the repair, and whichever statistic crosses back first releases it. What the rule reports is oscillation sustained above both thresholds, not every excursion through them. - The reference’s three test vectors are statistical summaries (reversal counts and swing percentages), not tick traces, so each is re-expressed as a damper trajectory producing the stated statistics, following AHU-0022’s treatment of the same problem; the remaining scenarios are authored here.
playbooks: []. Nothing inplaybooks/covers control-loop tuning — the gap AHU-0022 recorded when it landed. A loop-tuning playbook can adopt both faults when it is written.relatedadds VAV-0004. The reference lists AHU-0022 only, which this card keeps as the same instability one level up. VAV-0004 is the box-mechanics pair: FC-053 asks whether the flow loop reaches its setpoint, this one whether it can stay there.- Method stays
ruleper the reference, even though sibling AHU-0022 isstatisticaland this rule computes two rolling statistics: the crossing count is a deterministic count of events, and nothing here estimates a parameter or compares against a learned baseline. Severity 3 and phase 2 are the chapter 10 card’s; its §5.8.2 index carries no severity column. - Operating states are declared, not gated: the reference marks the fault applicable in every state with the fan running, and the graph has nothing to exclude.
persist.delayOnInit = true(Modelica/CDL default isfalse), the library’s standing choice: an oscillation already in progress at load waits out the full 15 minutes instead of alarming on the first tick after a restart.
Notes
Bind position feedback where the box has it. A rule bound to the damper command still catches diagnoses 1, 2, 3 and 5, since the loop is what oscillates; what it cannot see is diagnosis 4, where backlash makes the blade lag and overshoot while the command looks calmer than the motion. A command-bound instance is a legitimate deployment with one diagnosis out of reach.
Check the airflow signal before touching the gains: diagnosis 2 produces textbook hunting statistics behind a perfectly tuned loop, and retuning to compensate for a bad measurement leaves the noise where it was. Trend measured flow next to position — control hunting is smooth and roughly periodic, a noisy velocity pickup is neither. Diagnosis 5 is the case where nothing is broken, and the 30-minute window helps: a genuine load disturbance gives a handful of large excursions, not ten crossings of the running mean, so a marginal count with a large amplitude means look at the zone. For diagnosis 3 the cross-check is the number of zones — a dozen boxes on one trunk hunting together is the duct static loop, and no zone-level retuning will settle it.
Test Vectors
13 scenarios, clock step 60 s over 10800 s.
| Scenario | Description |
|---|---|
stable_position | A damper parked at 50% for the whole run — a zone whose load has not moved. The only crossing in the entire trace is an artifact of the statistic: while the window fills, muS runs a hair under the constant input, so above is true; at t=1800 s the window is full, muS reads exactly 50.0, the strict comparison goes false and Logical.Change emits one pulse. The count reads 1.0 against a threshold of 10 for one window and ages back to zero at t=3600 s; mad stays at zero throughout. This is the baseline every other scenario departs from. |
stable_control | Reference vector 1 (3 reversals per 30 min, 5% amplitude). A 20-minute square wave of +/-2.5% about 50% — a damper following a slow load swing, which is what control is supposed to look like. Two crossings per cycle gives a steady count of exactly 3.0 and mad settles at 2.42; both tests fail and nothing is reported. |
mild_hunting | Reference vector 2 (8 reversals per 30 min, 8% amplitude). An 8-minute square wave of +/-4% about 50%. The window holds 3.75 cycles, so the rolling count alternates between 7.0 and 8.0 — the reference’s 8 reversals, as close as a symmetric wave can be placed on a 60 s grid — and mad settles at 4.06. Both tests fail, and either one alone would be enough. |
severe_hunting | Reference vector 3 (15 reversals per 30 min, 20% amplitude). A 4-minute square wave of +/-10% about 50% — a proportional gain far too high for the box’s damper authority, stroking the actuator every two ticks. The count settles at exactly 15.0 and mad at 9.67 (the 0.33 shortfall from 10 is the mean wobbling by +/-0.67 across the window’s 7.5 cycles). Both conditions latch at t=240 s and yFault asserts one alarm_delay later at t=1140 s. Note this is inside the host’s warm-up NO_EVAL window, but unlike warmup_burst_asserts the verdict survives the window filling: the fault stays asserted for the rest of the horizon. |
fast_but_shallow | Count without amplitude: the damper alternates +/-2% every tick, 30 crossings per window — the fastest this rule can observe at a 60 s tick and three times the threshold. mad is 2.0, under min_oscillation_amplitude, so nothing is reported. A hair of dither on a position feedback is not hunting, and this vector pins that the And is load-bearing. |
slow_and_deep | Amplitude without count, the mirror of fast_but_shallow: a 20-minute +/-15% swing gives mad 14.5 — three times the amplitude threshold — on a count of exactly 3.0. A damper making large slow moves is tracking a load, not hunting, and the count test blocks the fault. |
amplitude_exactly_at_threshold | Amplitude edge, low side. A tick-rate square wave of +/-5% about 50%: the window holds 15 whole cycles, so muS is exactly 50.0, every deviation is exactly 5.0, and mad reads exactly 5.000000 with no rounding. madHigh is strict, so scatter sitting exactly on min_oscillation_amplitude reads clear even with the count at its 30-crossing ceiling. |
amplitude_just_over_threshold | Amplitude edge, high side: the same wave at +/-5.5%, half a percent past the previous scenario. mad climbs past 5.0 at t=1500 s — still inside the warm-up, where both averaging stages are working on partial windows — and yFault asserts one alarm_delay later at t=2400 s; the settled reading is exactly 5.5 once the deviation average has flushed the warm-up at t=3600 s. The pair brackets min_oscillation_amplitude from both sides. |
count_exactly_at_threshold | Count edge, low side. A 6-minute square wave of +/-15% about 50%: five whole cycles per window, so every rolling window holds exactly 10 crossings and xCount lands on exactly 10.000000 (the pulse integral is 600 s of area against an 1800 s window, times a count_scale of 30, all exact in IEEE-754). xHigh is strict, so ten crossings reads clear. mad is 14.7, far above its threshold, so the count test is the only thing blocking the fault. |
count_just_over_threshold | Count edge, high side. A 5-minute asymmetric cycle — one tick at 90%, four at 45% — puts 12 crossings in every window at mad 14.4. A symmetric wave cannot place 11 or 12 crossings on a 60 s grid (the available periods jump from 10 crossings at 360 s to 15 at 240 s), so the duty cycle is skewed instead; the mean sits at 54%, above the low value and below the high one, and each cycle still crosses it twice. yFault asserts at t=2760 s. |
hunting_stops_before_delay | Transient: 22 minutes of hard hunting (+/-10%, 4-minute period) starting at t=1800 s, after the windows have filled, then the damper settles. The eleventh crossing lands at t=3120 s and lifts the count over the threshold just as the oscillation ends; the count falls back to exactly 10.0 at t=3720 s as the first crossing ages out of the trailing window. both holds for 540 s against a 900 s alarm_delay, so one disturbance rejection does not report. |
recovery_clears | Recovery: the same hunting starts at t=1800 s and runs for 90 minutes before a technician retunes the loop at t=7200 s. yFault asserts at t=4020 s and clears at t=7800 s — 600 s after the last swing, the time five of the fifteen crossings in the window need to age out and bring the count back to exactly 10.0. Neither statistic drops when the hunting stops; both decay across the trailing window, and here the count reaches its threshold first (mad does not fall under 5.0 until t=8040 s). |
warmup_burst_asserts | The warm-up artifact, and why the host NO_EVAL precondition is load-bearing. Ten minutes of tick-rate +/-20% swinging at engine start produces 8 crossings, and 8 crossings per 30 minutes is the reference’s own NO_FAULT vector. But while the window is filling, xRate divides by elapsed time rather than by eval_window, so those 8 crossings read as a count of 24 at t=600 s and stay above 10 until t=1440 s. mad is inflated the same way. Both hold long enough for yFault to assert at t=1140 s; it clears at t=1440 s as the divisor grows, and the settled count after the window fills is 8. alarm_delay (900 s) is half of eval_window (1800 s) and cannot cover this — only the host gate can. |
vectors.json
{
"schema": "cxf-library/vectors/v1",
"clock": {
"step_s": 60,
"horizon_s": 10800
},
"scenarios": [
{
"name": "stable_position",
"description": "A damper parked at 50% for the whole run \u2014 a zone whose load has not moved. The only crossing in the entire trace is an artifact of the statistic: while the window fills, muS runs a hair under the constant input, so `above` is true; at t=1800 s the window is full, muS reads exactly 50.0, the strict comparison goes false and Logical.Change emits one pulse. The count reads 1.0 against a threshold of 10 for one window and ages back to zero at t=3600 s; mad stays at zero throughout. This is the baseline every other scenario departs from.",
"inputs": {
"zone_dmpr_pos": 50.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 10800,
"equals": false
}
]
},
{
"name": "stable_control",
"description": "Reference vector 1 (3 reversals per 30 min, 5% amplitude). A 20-minute square wave of +/-2.5% about 50% \u2014 a damper following a slow load swing, which is what control is supposed to look like. Two crossings per cycle gives a steady count of exactly 3.0 and mad settles at 2.42; both tests fail and nothing is reported.",
"inputs": {
"zone_dmpr_pos": [
{
"t": 0,
"value": 52.5
},
{
"t": 600,
"value": 47.5
},
{
"t": 1200,
"value": 52.5
},
{
"t": 1800,
"value": 47.5
},
{
"t": 2400,
"value": 52.5
},
{
"t": 3000,
"value": 47.5
},
{
"t": 3600,
"value": 52.5
},
{
"t": 4200,
"value": 47.5
},
{
"t": 4800,
"value": 52.5
},
{
"t": 5400,
"value": 47.5
},
{
"t": 6000,
"value": 52.5
},
{
"t": 6600,
"value": 47.5
},
{
"t": 7200,
"value": 52.5
},
{
"t": 7800,
"value": 47.5
},
{
"t": 8400,
"value": 52.5
},
{
"t": 9000,
"value": 47.5
},
{
"t": 9600,
"value": 52.5
},
{
"t": 10200,
"value": 47.5
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 10800,
"equals": false
}
]
},
{
"name": "mild_hunting",
"description": "Reference vector 2 (8 reversals per 30 min, 8% amplitude). An 8-minute square wave of +/-4% about 50%. The window holds 3.75 cycles, so the rolling count alternates between 7.0 and 8.0 \u2014 the reference's 8 reversals, as close as a symmetric wave can be placed on a 60 s grid \u2014 and mad settles at 4.06. Both tests fail, and either one alone would be enough.",
"inputs": {
"zone_dmpr_pos": [
{
"t": 0,
"value": 54.0
},
{
"t": 240,
"value": 46.0
},
{
"t": 480,
"value": 54.0
},
{
"t": 720,
"value": 46.0
},
{
"t": 960,
"value": 54.0
},
{
"t": 1200,
"value": 46.0
},
{
"t": 1440,
"value": 54.0
},
{
"t": 1680,
"value": 46.0
},
{
"t": 1920,
"value": 54.0
},
{
"t": 2160,
"value": 46.0
},
{
"t": 2400,
"value": 54.0
},
{
"t": 2640,
"value": 46.0
},
{
"t": 2880,
"value": 54.0
},
{
"t": 3120,
"value": 46.0
},
{
"t": 3360,
"value": 54.0
},
{
"t": 3600,
"value": 46.0
},
{
"t": 3840,
"value": 54.0
},
{
"t": 4080,
"value": 46.0
},
{
"t": 4320,
"value": 54.0
},
{
"t": 4560,
"value": 46.0
},
{
"t": 4800,
"value": 54.0
},
{
"t": 5040,
"value": 46.0
},
{
"t": 5280,
"value": 54.0
},
{
"t": 5520,
"value": 46.0
},
{
"t": 5760,
"value": 54.0
},
{
"t": 6000,
"value": 46.0
},
{
"t": 6240,
"value": 54.0
},
{
"t": 6480,
"value": 46.0
},
{
"t": 6720,
"value": 54.0
},
{
"t": 6960,
"value": 46.0
},
{
"t": 7200,
"value": 54.0
},
{
"t": 7440,
"value": 46.0
},
{
"t": 7680,
"value": 54.0
},
{
"t": 7920,
"value": 46.0
},
{
"t": 8160,
"value": 54.0
},
{
"t": 8400,
"value": 46.0
},
{
"t": 8640,
"value": 54.0
},
{
"t": 8880,
"value": 46.0
},
{
"t": 9120,
"value": 54.0
},
{
"t": 9360,
"value": 46.0
},
{
"t": 9600,
"value": 54.0
},
{
"t": 9840,
"value": 46.0
},
{
"t": 10080,
"value": 54.0
},
{
"t": 10320,
"value": 46.0
},
{
"t": 10560,
"value": 54.0
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 10800,
"equals": false
}
]
},
{
"name": "severe_hunting",
"description": "Reference vector 3 (15 reversals per 30 min, 20% amplitude). A 4-minute square wave of +/-10% about 50% \u2014 a proportional gain far too high for the box's damper authority, stroking the actuator every two ticks. The count settles at exactly 15.0 and mad at 9.67 (the 0.33 shortfall from 10 is the mean wobbling by +/-0.67 across the window's 7.5 cycles). Both conditions latch at t=240 s and yFault asserts one alarm_delay later at t=1140 s. Note this is inside the host's warm-up NO_EVAL window, but unlike warmup_burst_asserts the verdict survives the window filling: the fault stays asserted for the rest of the horizon.",
"inputs": {
"zone_dmpr_pos": [
{
"t": 0,
"value": 60.0
},
{
"t": 120,
"value": 40.0
},
{
"t": 240,
"value": 60.0
},
{
"t": 360,
"value": 40.0
},
{
"t": 480,
"value": 60.0
},
{
"t": 600,
"value": 40.0
},
{
"t": 720,
"value": 60.0
},
{
"t": 840,
"value": 40.0
},
{
"t": 960,
"value": 60.0
},
{
"t": 1080,
"value": 40.0
},
{
"t": 1200,
"value": 60.0
},
{
"t": 1320,
"value": 40.0
},
{
"t": 1440,
"value": 60.0
},
{
"t": 1560,
"value": 40.0
},
{
"t": 1680,
"value": 60.0
},
{
"t": 1800,
"value": 40.0
},
{
"t": 1920,
"value": 60.0
},
{
"t": 2040,
"value": 40.0
},
{
"t": 2160,
"value": 60.0
},
{
"t": 2280,
"value": 40.0
},
{
"t": 2400,
"value": 60.0
},
{
"t": 2520,
"value": 40.0
},
{
"t": 2640,
"value": 60.0
},
{
"t": 2760,
"value": 40.0
},
{
"t": 2880,
"value": 60.0
},
{
"t": 3000,
"value": 40.0
},
{
"t": 3120,
"value": 60.0
},
{
"t": 3240,
"value": 40.0
},
{
"t": 3360,
"value": 60.0
},
{
"t": 3480,
"value": 40.0
},
{
"t": 3600,
"value": 60.0
},
{
"t": 3720,
"value": 40.0
},
{
"t": 3840,
"value": 60.0
},
{
"t": 3960,
"value": 40.0
},
{
"t": 4080,
"value": 60.0
},
{
"t": 4200,
"value": 40.0
},
{
"t": 4320,
"value": 60.0
},
{
"t": 4440,
"value": 40.0
},
{
"t": 4560,
"value": 60.0
},
{
"t": 4680,
"value": 40.0
},
{
"t": 4800,
"value": 60.0
},
{
"t": 4920,
"value": 40.0
},
{
"t": 5040,
"value": 60.0
},
{
"t": 5160,
"value": 40.0
},
{
"t": 5280,
"value": 60.0
},
{
"t": 5400,
"value": 40.0
},
{
"t": 5520,
"value": 60.0
},
{
"t": 5640,
"value": 40.0
},
{
"t": 5760,
"value": 60.0
},
{
"t": 5880,
"value": 40.0
},
{
"t": 6000,
"value": 60.0
},
{
"t": 6120,
"value": 40.0
},
{
"t": 6240,
"value": 60.0
},
{
"t": 6360,
"value": 40.0
},
{
"t": 6480,
"value": 60.0
},
{
"t": 6600,
"value": 40.0
},
{
"t": 6720,
"value": 60.0
},
{
"t": 6840,
"value": 40.0
},
{
"t": 6960,
"value": 60.0
},
{
"t": 7080,
"value": 40.0
},
{
"t": 7200,
"value": 60.0
},
{
"t": 7320,
"value": 40.0
},
{
"t": 7440,
"value": 60.0
},
{
"t": 7560,
"value": 40.0
},
{
"t": 7680,
"value": 60.0
},
{
"t": 7800,
"value": 40.0
},
{
"t": 7920,
"value": 60.0
},
{
"t": 8040,
"value": 40.0
},
{
"t": 8160,
"value": 60.0
},
{
"t": 8280,
"value": 40.0
},
{
"t": 8400,
"value": 60.0
},
{
"t": 8520,
"value": 40.0
},
{
"t": 8640,
"value": 60.0
},
{
"t": 8760,
"value": 40.0
},
{
"t": 8880,
"value": 60.0
},
{
"t": 9000,
"value": 40.0
},
{
"t": 9120,
"value": 60.0
},
{
"t": 9240,
"value": 40.0
},
{
"t": 9360,
"value": 60.0
},
{
"t": 9480,
"value": 40.0
},
{
"t": 9600,
"value": 60.0
},
{
"t": 9720,
"value": 40.0
},
{
"t": 9840,
"value": 60.0
},
{
"t": 9960,
"value": 40.0
},
{
"t": 10080,
"value": 60.0
},
{
"t": 10200,
"value": 40.0
},
{
"t": 10320,
"value": 60.0
},
{
"t": 10440,
"value": 40.0
},
{
"t": 10560,
"value": 60.0
},
{
"t": 10680,
"value": 40.0
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 1020,
"equals": false
},
{
"output": "yFault",
"from_s": 1260,
"to_s": 10800,
"equals": true
}
]
},
{
"name": "fast_but_shallow",
"description": "Count without amplitude: the damper alternates +/-2% every tick, 30 crossings per window \u2014 the fastest this rule can observe at a 60 s tick and three times the threshold. mad is 2.0, under min_oscillation_amplitude, so nothing is reported. A hair of dither on a position feedback is not hunting, and this vector pins that the `And` is load-bearing.",
"inputs": {
"zone_dmpr_pos": [
{
"t": 0,
"value": 52.0
},
{
"t": 60,
"value": 48.0
},
{
"t": 120,
"value": 52.0
},
{
"t": 180,
"value": 48.0
},
{
"t": 240,
"value": 52.0
},
{
"t": 300,
"value": 48.0
},
{
"t": 360,
"value": 52.0
},
{
"t": 420,
"value": 48.0
},
{
"t": 480,
"value": 52.0
},
{
"t": 540,
"value": 48.0
},
{
"t": 600,
"value": 52.0
},
{
"t": 660,
"value": 48.0
},
{
"t": 720,
"value": 52.0
},
{
"t": 780,
"value": 48.0
},
{
"t": 840,
"value": 52.0
},
{
"t": 900,
"value": 48.0
},
{
"t": 960,
"value": 52.0
},
{
"t": 1020,
"value": 48.0
},
{
"t": 1080,
"value": 52.0
},
{
"t": 1140,
"value": 48.0
},
{
"t": 1200,
"value": 52.0
},
{
"t": 1260,
"value": 48.0
},
{
"t": 1320,
"value": 52.0
},
{
"t": 1380,
"value": 48.0
},
{
"t": 1440,
"value": 52.0
},
{
"t": 1500,
"value": 48.0
},
{
"t": 1560,
"value": 52.0
},
{
"t": 1620,
"value": 48.0
},
{
"t": 1680,
"value": 52.0
},
{
"t": 1740,
"value": 48.0
},
{
"t": 1800,
"value": 52.0
},
{
"t": 1860,
"value": 48.0
},
{
"t": 1920,
"value": 52.0
},
{
"t": 1980,
"value": 48.0
},
{
"t": 2040,
"value": 52.0
},
{
"t": 2100,
"value": 48.0
},
{
"t": 2160,
"value": 52.0
},
{
"t": 2220,
"value": 48.0
},
{
"t": 2280,
"value": 52.0
},
{
"t": 2340,
"value": 48.0
},
{
"t": 2400,
"value": 52.0
},
{
"t": 2460,
"value": 48.0
},
{
"t": 2520,
"value": 52.0
},
{
"t": 2580,
"value": 48.0
},
{
"t": 2640,
"value": 52.0
},
{
"t": 2700,
"value": 48.0
},
{
"t": 2760,
"value": 52.0
},
{
"t": 2820,
"value": 48.0
},
{
"t": 2880,
"value": 52.0
},
{
"t": 2940,
"value": 48.0
},
{
"t": 3000,
"value": 52.0
},
{
"t": 3060,
"value": 48.0
},
{
"t": 3120,
"value": 52.0
},
{
"t": 3180,
"value": 48.0
},
{
"t": 3240,
"value": 52.0
},
{
"t": 3300,
"value": 48.0
},
{
"t": 3360,
"value": 52.0
},
{
"t": 3420,
"value": 48.0
},
{
"t": 3480,
"value": 52.0
},
{
"t": 3540,
"value": 48.0
},
{
"t": 3600,
"value": 52.0
},
{
"t": 3660,
"value": 48.0
},
{
"t": 3720,
"value": 52.0
},
{
"t": 3780,
"value": 48.0
},
{
"t": 3840,
"value": 52.0
},
{
"t": 3900,
"value": 48.0
},
{
"t": 3960,
"value": 52.0
},
{
"t": 4020,
"value": 48.0
},
{
"t": 4080,
"value": 52.0
},
{
"t": 4140,
"value": 48.0
},
{
"t": 4200,
"value": 52.0
},
{
"t": 4260,
"value": 48.0
},
{
"t": 4320,
"value": 52.0
},
{
"t": 4380,
"value": 48.0
},
{
"t": 4440,
"value": 52.0
},
{
"t": 4500,
"value": 48.0
},
{
"t": 4560,
"value": 52.0
},
{
"t": 4620,
"value": 48.0
},
{
"t": 4680,
"value": 52.0
},
{
"t": 4740,
"value": 48.0
},
{
"t": 4800,
"value": 52.0
},
{
"t": 4860,
"value": 48.0
},
{
"t": 4920,
"value": 52.0
},
{
"t": 4980,
"value": 48.0
},
{
"t": 5040,
"value": 52.0
},
{
"t": 5100,
"value": 48.0
},
{
"t": 5160,
"value": 52.0
},
{
"t": 5220,
"value": 48.0
},
{
"t": 5280,
"value": 52.0
},
{
"t": 5340,
"value": 48.0
},
{
"t": 5400,
"value": 52.0
},
{
"t": 5460,
"value": 48.0
},
{
"t": 5520,
"value": 52.0
},
{
"t": 5580,
"value": 48.0
},
{
"t": 5640,
"value": 52.0
},
{
"t": 5700,
"value": 48.0
},
{
"t": 5760,
"value": 52.0
},
{
"t": 5820,
"value": 48.0
},
{
"t": 5880,
"value": 52.0
},
{
"t": 5940,
"value": 48.0
},
{
"t": 6000,
"value": 52.0
},
{
"t": 6060,
"value": 48.0
},
{
"t": 6120,
"value": 52.0
},
{
"t": 6180,
"value": 48.0
},
{
"t": 6240,
"value": 52.0
},
{
"t": 6300,
"value": 48.0
},
{
"t": 6360,
"value": 52.0
},
{
"t": 6420,
"value": 48.0
},
{
"t": 6480,
"value": 52.0
},
{
"t": 6540,
"value": 48.0
},
{
"t": 6600,
"value": 52.0
},
{
"t": 6660,
"value": 48.0
},
{
"t": 6720,
"value": 52.0
},
{
"t": 6780,
"value": 48.0
},
{
"t": 6840,
"value": 52.0
},
{
"t": 6900,
"value": 48.0
},
{
"t": 6960,
"value": 52.0
},
{
"t": 7020,
"value": 48.0
},
{
"t": 7080,
"value": 52.0
},
{
"t": 7140,
"value": 48.0
},
{
"t": 7200,
"value": 52.0
},
{
"t": 7260,
"value": 48.0
},
{
"t": 7320,
"value": 52.0
},
{
"t": 7380,
"value": 48.0
},
{
"t": 7440,
"value": 52.0
},
{
"t": 7500,
"value": 48.0
},
{
"t": 7560,
"value": 52.0
},
{
"t": 7620,
"value": 48.0
},
{
"t": 7680,
"value": 52.0
},
{
"t": 7740,
"value": 48.0
},
{
"t": 7800,
"value": 52.0
},
{
"t": 7860,
"value": 48.0
},
{
"t": 7920,
"value": 52.0
},
{
"t": 7980,
"value": 48.0
},
{
"t": 8040,
"value": 52.0
},
{
"t": 8100,
"value": 48.0
},
{
"t": 8160,
"value": 52.0
},
{
"t": 8220,
"value": 48.0
},
{
"t": 8280,
"value": 52.0
},
{
"t": 8340,
"value": 48.0
},
{
"t": 8400,
"value": 52.0
},
{
"t": 8460,
"value": 48.0
},
{
"t": 8520,
"value": 52.0
},
{
"t": 8580,
"value": 48.0
},
{
"t": 8640,
"value": 52.0
},
{
"t": 8700,
"value": 48.0
},
{
"t": 8760,
"value": 52.0
},
{
"t": 8820,
"value": 48.0
},
{
"t": 8880,
"value": 52.0
},
{
"t": 8940,
"value": 48.0
},
{
"t": 9000,
"value": 52.0
},
{
"t": 9060,
"value": 48.0
},
{
"t": 9120,
"value": 52.0
},
{
"t": 9180,
"value": 48.0
},
{
"t": 9240,
"value": 52.0
},
{
"t": 9300,
"value": 48.0
},
{
"t": 9360,
"value": 52.0
},
{
"t": 9420,
"value": 48.0
},
{
"t": 9480,
"value": 52.0
},
{
"t": 9540,
"value": 48.0
},
{
"t": 9600,
"value": 52.0
},
{
"t": 9660,
"value": 48.0
},
{
"t": 9720,
"value": 52.0
},
{
"t": 9780,
"value": 48.0
},
{
"t": 9840,
"value": 52.0
},
{
"t": 9900,
"value": 48.0
},
{
"t": 9960,
"value": 52.0
},
{
"t": 10020,
"value": 48.0
},
{
"t": 10080,
"value": 52.0
},
{
"t": 10140,
"value": 48.0
},
{
"t": 10200,
"value": 52.0
},
{
"t": 10260,
"value": 48.0
},
{
"t": 10320,
"value": 52.0
},
{
"t": 10380,
"value": 48.0
},
{
"t": 10440,
"value": 52.0
},
{
"t": 10500,
"value": 48.0
},
{
"t": 10560,
"value": 52.0
},
{
"t": 10620,
"value": 48.0
},
{
"t": 10680,
"value": 52.0
},
{
"t": 10740,
"value": 48.0
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 10800,
"equals": false
}
]
},
{
"name": "slow_and_deep",
"description": "Amplitude without count, the mirror of fast_but_shallow: a 20-minute +/-15% swing gives mad 14.5 \u2014 three times the amplitude threshold \u2014 on a count of exactly 3.0. A damper making large slow moves is tracking a load, not hunting, and the count test blocks the fault.",
"inputs": {
"zone_dmpr_pos": [
{
"t": 0,
"value": 65.0
},
{
"t": 600,
"value": 35.0
},
{
"t": 1200,
"value": 65.0
},
{
"t": 1800,
"value": 35.0
},
{
"t": 2400,
"value": 65.0
},
{
"t": 3000,
"value": 35.0
},
{
"t": 3600,
"value": 65.0
},
{
"t": 4200,
"value": 35.0
},
{
"t": 4800,
"value": 65.0
},
{
"t": 5400,
"value": 35.0
},
{
"t": 6000,
"value": 65.0
},
{
"t": 6600,
"value": 35.0
},
{
"t": 7200,
"value": 65.0
},
{
"t": 7800,
"value": 35.0
},
{
"t": 8400,
"value": 65.0
},
{
"t": 9000,
"value": 35.0
},
{
"t": 9600,
"value": 65.0
},
{
"t": 10200,
"value": 35.0
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 10800,
"equals": false
}
]
},
{
"name": "amplitude_exactly_at_threshold",
"description": "Amplitude edge, low side. A tick-rate square wave of +/-5% about 50%: the window holds 15 whole cycles, so muS is exactly 50.0, every deviation is exactly 5.0, and mad reads exactly 5.000000 with no rounding. madHigh is strict, so scatter sitting exactly on min_oscillation_amplitude reads clear even with the count at its 30-crossing ceiling.",
"inputs": {
"zone_dmpr_pos": [
{
"t": 0,
"value": 55.0
},
{
"t": 60,
"value": 45.0
},
{
"t": 120,
"value": 55.0
},
{
"t": 180,
"value": 45.0
},
{
"t": 240,
"value": 55.0
},
{
"t": 300,
"value": 45.0
},
{
"t": 360,
"value": 55.0
},
{
"t": 420,
"value": 45.0
},
{
"t": 480,
"value": 55.0
},
{
"t": 540,
"value": 45.0
},
{
"t": 600,
"value": 55.0
},
{
"t": 660,
"value": 45.0
},
{
"t": 720,
"value": 55.0
},
{
"t": 780,
"value": 45.0
},
{
"t": 840,
"value": 55.0
},
{
"t": 900,
"value": 45.0
},
{
"t": 960,
"value": 55.0
},
{
"t": 1020,
"value": 45.0
},
{
"t": 1080,
"value": 55.0
},
{
"t": 1140,
"value": 45.0
},
{
"t": 1200,
"value": 55.0
},
{
"t": 1260,
"value": 45.0
},
{
"t": 1320,
"value": 55.0
},
{
"t": 1380,
"value": 45.0
},
{
"t": 1440,
"value": 55.0
},
{
"t": 1500,
"value": 45.0
},
{
"t": 1560,
"value": 55.0
},
{
"t": 1620,
"value": 45.0
},
{
"t": 1680,
"value": 55.0
},
{
"t": 1740,
"value": 45.0
},
{
"t": 1800,
"value": 55.0
},
{
"t": 1860,
"value": 45.0
},
{
"t": 1920,
"value": 55.0
},
{
"t": 1980,
"value": 45.0
},
{
"t": 2040,
"value": 55.0
},
{
"t": 2100,
"value": 45.0
},
{
"t": 2160,
"value": 55.0
},
{
"t": 2220,
"value": 45.0
},
{
"t": 2280,
"value": 55.0
},
{
"t": 2340,
"value": 45.0
},
{
"t": 2400,
"value": 55.0
},
{
"t": 2460,
"value": 45.0
},
{
"t": 2520,
"value": 55.0
},
{
"t": 2580,
"value": 45.0
},
{
"t": 2640,
"value": 55.0
},
{
"t": 2700,
"value": 45.0
},
{
"t": 2760,
"value": 55.0
},
{
"t": 2820,
"value": 45.0
},
{
"t": 2880,
"value": 55.0
},
{
"t": 2940,
"value": 45.0
},
{
"t": 3000,
"value": 55.0
},
{
"t": 3060,
"value": 45.0
},
{
"t": 3120,
"value": 55.0
},
{
"t": 3180,
"value": 45.0
},
{
"t": 3240,
"value": 55.0
},
{
"t": 3300,
"value": 45.0
},
{
"t": 3360,
"value": 55.0
},
{
"t": 3420,
"value": 45.0
},
{
"t": 3480,
"value": 55.0
},
{
"t": 3540,
"value": 45.0
},
{
"t": 3600,
"value": 55.0
},
{
"t": 3660,
"value": 45.0
},
{
"t": 3720,
"value": 55.0
},
{
"t": 3780,
"value": 45.0
},
{
"t": 3840,
"value": 55.0
},
{
"t": 3900,
"value": 45.0
},
{
"t": 3960,
"value": 55.0
},
{
"t": 4020,
"value": 45.0
},
{
"t": 4080,
"value": 55.0
},
{
"t": 4140,
"value": 45.0
},
{
"t": 4200,
"value": 55.0
},
{
"t": 4260,
"value": 45.0
},
{
"t": 4320,
"value": 55.0
},
{
"t": 4380,
"value": 45.0
},
{
"t": 4440,
"value": 55.0
},
{
"t": 4500,
"value": 45.0
},
{
"t": 4560,
"value": 55.0
},
{
"t": 4620,
"value": 45.0
},
{
"t": 4680,
"value": 55.0
},
{
"t": 4740,
"value": 45.0
},
{
"t": 4800,
"value": 55.0
},
{
"t": 4860,
"value": 45.0
},
{
"t": 4920,
"value": 55.0
},
{
"t": 4980,
"value": 45.0
},
{
"t": 5040,
"value": 55.0
},
{
"t": 5100,
"value": 45.0
},
{
"t": 5160,
"value": 55.0
},
{
"t": 5220,
"value": 45.0
},
{
"t": 5280,
"value": 55.0
},
{
"t": 5340,
"value": 45.0
},
{
"t": 5400,
"value": 55.0
},
{
"t": 5460,
"value": 45.0
},
{
"t": 5520,
"value": 55.0
},
{
"t": 5580,
"value": 45.0
},
{
"t": 5640,
"value": 55.0
},
{
"t": 5700,
"value": 45.0
},
{
"t": 5760,
"value": 55.0
},
{
"t": 5820,
"value": 45.0
},
{
"t": 5880,
"value": 55.0
},
{
"t": 5940,
"value": 45.0
},
{
"t": 6000,
"value": 55.0
},
{
"t": 6060,
"value": 45.0
},
{
"t": 6120,
"value": 55.0
},
{
"t": 6180,
"value": 45.0
},
{
"t": 6240,
"value": 55.0
},
{
"t": 6300,
"value": 45.0
},
{
"t": 6360,
"value": 55.0
},
{
"t": 6420,
"value": 45.0
},
{
"t": 6480,
"value": 55.0
},
{
"t": 6540,
"value": 45.0
},
{
"t": 6600,
"value": 55.0
},
{
"t": 6660,
"value": 45.0
},
{
"t": 6720,
"value": 55.0
},
{
"t": 6780,
"value": 45.0
},
{
"t": 6840,
"value": 55.0
},
{
"t": 6900,
"value": 45.0
},
{
"t": 6960,
"value": 55.0
},
{
"t": 7020,
"value": 45.0
},
{
"t": 7080,
"value": 55.0
},
{
"t": 7140,
"value": 45.0
},
{
"t": 7200,
"value": 55.0
},
{
"t": 7260,
"value": 45.0
},
{
"t": 7320,
"value": 55.0
},
{
"t": 7380,
"value": 45.0
},
{
"t": 7440,
"value": 55.0
},
{
"t": 7500,
"value": 45.0
},
{
"t": 7560,
"value": 55.0
},
{
"t": 7620,
"value": 45.0
},
{
"t": 7680,
"value": 55.0
},
{
"t": 7740,
"value": 45.0
},
{
"t": 7800,
"value": 55.0
},
{
"t": 7860,
"value": 45.0
},
{
"t": 7920,
"value": 55.0
},
{
"t": 7980,
"value": 45.0
},
{
"t": 8040,
"value": 55.0
},
{
"t": 8100,
"value": 45.0
},
{
"t": 8160,
"value": 55.0
},
{
"t": 8220,
"value": 45.0
},
{
"t": 8280,
"value": 55.0
},
{
"t": 8340,
"value": 45.0
},
{
"t": 8400,
"value": 55.0
},
{
"t": 8460,
"value": 45.0
},
{
"t": 8520,
"value": 55.0
},
{
"t": 8580,
"value": 45.0
},
{
"t": 8640,
"value": 55.0
},
{
"t": 8700,
"value": 45.0
},
{
"t": 8760,
"value": 55.0
},
{
"t": 8820,
"value": 45.0
},
{
"t": 8880,
"value": 55.0
},
{
"t": 8940,
"value": 45.0
},
{
"t": 9000,
"value": 55.0
},
{
"t": 9060,
"value": 45.0
},
{
"t": 9120,
"value": 55.0
},
{
"t": 9180,
"value": 45.0
},
{
"t": 9240,
"value": 55.0
},
{
"t": 9300,
"value": 45.0
},
{
"t": 9360,
"value": 55.0
},
{
"t": 9420,
"value": 45.0
},
{
"t": 9480,
"value": 55.0
},
{
"t": 9540,
"value": 45.0
},
{
"t": 9600,
"value": 55.0
},
{
"t": 9660,
"value": 45.0
},
{
"t": 9720,
"value": 55.0
},
{
"t": 9780,
"value": 45.0
},
{
"t": 9840,
"value": 55.0
},
{
"t": 9900,
"value": 45.0
},
{
"t": 9960,
"value": 55.0
},
{
"t": 10020,
"value": 45.0
},
{
"t": 10080,
"value": 55.0
},
{
"t": 10140,
"value": 45.0
},
{
"t": 10200,
"value": 55.0
},
{
"t": 10260,
"value": 45.0
},
{
"t": 10320,
"value": 55.0
},
{
"t": 10380,
"value": 45.0
},
{
"t": 10440,
"value": 55.0
},
{
"t": 10500,
"value": 45.0
},
{
"t": 10560,
"value": 55.0
},
{
"t": 10620,
"value": 45.0
},
{
"t": 10680,
"value": 55.0
},
{
"t": 10740,
"value": 45.0
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 10800,
"equals": false
}
]
},
{
"name": "amplitude_just_over_threshold",
"description": "Amplitude edge, high side: the same wave at +/-5.5%, half a percent past the previous scenario. mad climbs past 5.0 at t=1500 s \u2014 still inside the warm-up, where both averaging stages are working on partial windows \u2014 and yFault asserts one alarm_delay later at t=2400 s; the settled reading is exactly 5.5 once the deviation average has flushed the warm-up at t=3600 s. The pair brackets min_oscillation_amplitude from both sides.",
"inputs": {
"zone_dmpr_pos": [
{
"t": 0,
"value": 55.5
},
{
"t": 60,
"value": 44.5
},
{
"t": 120,
"value": 55.5
},
{
"t": 180,
"value": 44.5
},
{
"t": 240,
"value": 55.5
},
{
"t": 300,
"value": 44.5
},
{
"t": 360,
"value": 55.5
},
{
"t": 420,
"value": 44.5
},
{
"t": 480,
"value": 55.5
},
{
"t": 540,
"value": 44.5
},
{
"t": 600,
"value": 55.5
},
{
"t": 660,
"value": 44.5
},
{
"t": 720,
"value": 55.5
},
{
"t": 780,
"value": 44.5
},
{
"t": 840,
"value": 55.5
},
{
"t": 900,
"value": 44.5
},
{
"t": 960,
"value": 55.5
},
{
"t": 1020,
"value": 44.5
},
{
"t": 1080,
"value": 55.5
},
{
"t": 1140,
"value": 44.5
},
{
"t": 1200,
"value": 55.5
},
{
"t": 1260,
"value": 44.5
},
{
"t": 1320,
"value": 55.5
},
{
"t": 1380,
"value": 44.5
},
{
"t": 1440,
"value": 55.5
},
{
"t": 1500,
"value": 44.5
},
{
"t": 1560,
"value": 55.5
},
{
"t": 1620,
"value": 44.5
},
{
"t": 1680,
"value": 55.5
},
{
"t": 1740,
"value": 44.5
},
{
"t": 1800,
"value": 55.5
},
{
"t": 1860,
"value": 44.5
},
{
"t": 1920,
"value": 55.5
},
{
"t": 1980,
"value": 44.5
},
{
"t": 2040,
"value": 55.5
},
{
"t": 2100,
"value": 44.5
},
{
"t": 2160,
"value": 55.5
},
{
"t": 2220,
"value": 44.5
},
{
"t": 2280,
"value": 55.5
},
{
"t": 2340,
"value": 44.5
},
{
"t": 2400,
"value": 55.5
},
{
"t": 2460,
"value": 44.5
},
{
"t": 2520,
"value": 55.5
},
{
"t": 2580,
"value": 44.5
},
{
"t": 2640,
"value": 55.5
},
{
"t": 2700,
"value": 44.5
},
{
"t": 2760,
"value": 55.5
},
{
"t": 2820,
"value": 44.5
},
{
"t": 2880,
"value": 55.5
},
{
"t": 2940,
"value": 44.5
},
{
"t": 3000,
"value": 55.5
},
{
"t": 3060,
"value": 44.5
},
{
"t": 3120,
"value": 55.5
},
{
"t": 3180,
"value": 44.5
},
{
"t": 3240,
"value": 55.5
},
{
"t": 3300,
"value": 44.5
},
{
"t": 3360,
"value": 55.5
},
{
"t": 3420,
"value": 44.5
},
{
"t": 3480,
"value": 55.5
},
{
"t": 3540,
"value": 44.5
},
{
"t": 3600,
"value": 55.5
},
{
"t": 3660,
"value": 44.5
},
{
"t": 3720,
"value": 55.5
},
{
"t": 3780,
"value": 44.5
},
{
"t": 3840,
"value": 55.5
},
{
"t": 3900,
"value": 44.5
},
{
"t": 3960,
"value": 55.5
},
{
"t": 4020,
"value": 44.5
},
{
"t": 4080,
"value": 55.5
},
{
"t": 4140,
"value": 44.5
},
{
"t": 4200,
"value": 55.5
},
{
"t": 4260,
"value": 44.5
},
{
"t": 4320,
"value": 55.5
},
{
"t": 4380,
"value": 44.5
},
{
"t": 4440,
"value": 55.5
},
{
"t": 4500,
"value": 44.5
},
{
"t": 4560,
"value": 55.5
},
{
"t": 4620,
"value": 44.5
},
{
"t": 4680,
"value": 55.5
},
{
"t": 4740,
"value": 44.5
},
{
"t": 4800,
"value": 55.5
},
{
"t": 4860,
"value": 44.5
},
{
"t": 4920,
"value": 55.5
},
{
"t": 4980,
"value": 44.5
},
{
"t": 5040,
"value": 55.5
},
{
"t": 5100,
"value": 44.5
},
{
"t": 5160,
"value": 55.5
},
{
"t": 5220,
"value": 44.5
},
{
"t": 5280,
"value": 55.5
},
{
"t": 5340,
"value": 44.5
},
{
"t": 5400,
"value": 55.5
},
{
"t": 5460,
"value": 44.5
},
{
"t": 5520,
"value": 55.5
},
{
"t": 5580,
"value": 44.5
},
{
"t": 5640,
"value": 55.5
},
{
"t": 5700,
"value": 44.5
},
{
"t": 5760,
"value": 55.5
},
{
"t": 5820,
"value": 44.5
},
{
"t": 5880,
"value": 55.5
},
{
"t": 5940,
"value": 44.5
},
{
"t": 6000,
"value": 55.5
},
{
"t": 6060,
"value": 44.5
},
{
"t": 6120,
"value": 55.5
},
{
"t": 6180,
"value": 44.5
},
{
"t": 6240,
"value": 55.5
},
{
"t": 6300,
"value": 44.5
},
{
"t": 6360,
"value": 55.5
},
{
"t": 6420,
"value": 44.5
},
{
"t": 6480,
"value": 55.5
},
{
"t": 6540,
"value": 44.5
},
{
"t": 6600,
"value": 55.5
},
{
"t": 6660,
"value": 44.5
},
{
"t": 6720,
"value": 55.5
},
{
"t": 6780,
"value": 44.5
},
{
"t": 6840,
"value": 55.5
},
{
"t": 6900,
"value": 44.5
},
{
"t": 6960,
"value": 55.5
},
{
"t": 7020,
"value": 44.5
},
{
"t": 7080,
"value": 55.5
},
{
"t": 7140,
"value": 44.5
},
{
"t": 7200,
"value": 55.5
},
{
"t": 7260,
"value": 44.5
},
{
"t": 7320,
"value": 55.5
},
{
"t": 7380,
"value": 44.5
},
{
"t": 7440,
"value": 55.5
},
{
"t": 7500,
"value": 44.5
},
{
"t": 7560,
"value": 55.5
},
{
"t": 7620,
"value": 44.5
},
{
"t": 7680,
"value": 55.5
},
{
"t": 7740,
"value": 44.5
},
{
"t": 7800,
"value": 55.5
},
{
"t": 7860,
"value": 44.5
},
{
"t": 7920,
"value": 55.5
},
{
"t": 7980,
"value": 44.5
},
{
"t": 8040,
"value": 55.5
},
{
"t": 8100,
"value": 44.5
},
{
"t": 8160,
"value": 55.5
},
{
"t": 8220,
"value": 44.5
},
{
"t": 8280,
"value": 55.5
},
{
"t": 8340,
"value": 44.5
},
{
"t": 8400,
"value": 55.5
},
{
"t": 8460,
"value": 44.5
},
{
"t": 8520,
"value": 55.5
},
{
"t": 8580,
"value": 44.5
},
{
"t": 8640,
"value": 55.5
},
{
"t": 8700,
"value": 44.5
},
{
"t": 8760,
"value": 55.5
},
{
"t": 8820,
"value": 44.5
},
{
"t": 8880,
"value": 55.5
},
{
"t": 8940,
"value": 44.5
},
{
"t": 9000,
"value": 55.5
},
{
"t": 9060,
"value": 44.5
},
{
"t": 9120,
"value": 55.5
},
{
"t": 9180,
"value": 44.5
},
{
"t": 9240,
"value": 55.5
},
{
"t": 9300,
"value": 44.5
},
{
"t": 9360,
"value": 55.5
},
{
"t": 9420,
"value": 44.5
},
{
"t": 9480,
"value": 55.5
},
{
"t": 9540,
"value": 44.5
},
{
"t": 9600,
"value": 55.5
},
{
"t": 9660,
"value": 44.5
},
{
"t": 9720,
"value": 55.5
},
{
"t": 9780,
"value": 44.5
},
{
"t": 9840,
"value": 55.5
},
{
"t": 9900,
"value": 44.5
},
{
"t": 9960,
"value": 55.5
},
{
"t": 10020,
"value": 44.5
},
{
"t": 10080,
"value": 55.5
},
{
"t": 10140,
"value": 44.5
},
{
"t": 10200,
"value": 55.5
},
{
"t": 10260,
"value": 44.5
},
{
"t": 10320,
"value": 55.5
},
{
"t": 10380,
"value": 44.5
},
{
"t": 10440,
"value": 55.5
},
{
"t": 10500,
"value": 44.5
},
{
"t": 10560,
"value": 55.5
},
{
"t": 10620,
"value": 44.5
},
{
"t": 10680,
"value": 55.5
},
{
"t": 10740,
"value": 44.5
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 2280,
"equals": false
},
{
"output": "yFault",
"from_s": 2520,
"to_s": 10800,
"equals": true
}
]
},
{
"name": "count_exactly_at_threshold",
"description": "Count edge, low side. A 6-minute square wave of +/-15% about 50%: five whole cycles per window, so every rolling window holds exactly 10 crossings and xCount lands on exactly 10.000000 (the pulse integral is 600 s of area against an 1800 s window, times a count_scale of 30, all exact in IEEE-754). xHigh is strict, so ten crossings reads clear. mad is 14.7, far above its threshold, so the count test is the only thing blocking the fault.",
"inputs": {
"zone_dmpr_pos": [
{
"t": 0,
"value": 65.0
},
{
"t": 180,
"value": 35.0
},
{
"t": 360,
"value": 65.0
},
{
"t": 540,
"value": 35.0
},
{
"t": 720,
"value": 65.0
},
{
"t": 900,
"value": 35.0
},
{
"t": 1080,
"value": 65.0
},
{
"t": 1260,
"value": 35.0
},
{
"t": 1440,
"value": 65.0
},
{
"t": 1620,
"value": 35.0
},
{
"t": 1800,
"value": 65.0
},
{
"t": 1980,
"value": 35.0
},
{
"t": 2160,
"value": 65.0
},
{
"t": 2340,
"value": 35.0
},
{
"t": 2520,
"value": 65.0
},
{
"t": 2700,
"value": 35.0
},
{
"t": 2880,
"value": 65.0
},
{
"t": 3060,
"value": 35.0
},
{
"t": 3240,
"value": 65.0
},
{
"t": 3420,
"value": 35.0
},
{
"t": 3600,
"value": 65.0
},
{
"t": 3780,
"value": 35.0
},
{
"t": 3960,
"value": 65.0
},
{
"t": 4140,
"value": 35.0
},
{
"t": 4320,
"value": 65.0
},
{
"t": 4500,
"value": 35.0
},
{
"t": 4680,
"value": 65.0
},
{
"t": 4860,
"value": 35.0
},
{
"t": 5040,
"value": 65.0
},
{
"t": 5220,
"value": 35.0
},
{
"t": 5400,
"value": 65.0
},
{
"t": 5580,
"value": 35.0
},
{
"t": 5760,
"value": 65.0
},
{
"t": 5940,
"value": 35.0
},
{
"t": 6120,
"value": 65.0
},
{
"t": 6300,
"value": 35.0
},
{
"t": 6480,
"value": 65.0
},
{
"t": 6660,
"value": 35.0
},
{
"t": 6840,
"value": 65.0
},
{
"t": 7020,
"value": 35.0
},
{
"t": 7200,
"value": 65.0
},
{
"t": 7380,
"value": 35.0
},
{
"t": 7560,
"value": 65.0
},
{
"t": 7740,
"value": 35.0
},
{
"t": 7920,
"value": 65.0
},
{
"t": 8100,
"value": 35.0
},
{
"t": 8280,
"value": 65.0
},
{
"t": 8460,
"value": 35.0
},
{
"t": 8640,
"value": 65.0
},
{
"t": 8820,
"value": 35.0
},
{
"t": 9000,
"value": 65.0
},
{
"t": 9180,
"value": 35.0
},
{
"t": 9360,
"value": 65.0
},
{
"t": 9540,
"value": 35.0
},
{
"t": 9720,
"value": 65.0
},
{
"t": 9900,
"value": 35.0
},
{
"t": 10080,
"value": 65.0
},
{
"t": 10260,
"value": 35.0
},
{
"t": 10440,
"value": 65.0
},
{
"t": 10620,
"value": 35.0
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 10800,
"equals": false
}
]
},
{
"name": "count_just_over_threshold",
"description": "Count edge, high side. A 5-minute asymmetric cycle \u2014 one tick at 90%, four at 45% \u2014 puts 12 crossings in every window at mad 14.4. A symmetric wave cannot place 11 or 12 crossings on a 60 s grid (the available periods jump from 10 crossings at 360 s to 15 at 240 s), so the duty cycle is skewed instead; the mean sits at 54%, above the low value and below the high one, and each cycle still crosses it twice. yFault asserts at t=2760 s.",
"inputs": {
"zone_dmpr_pos": [
{
"t": 0,
"value": 90.0
},
{
"t": 60,
"value": 45.0
},
{
"t": 300,
"value": 90.0
},
{
"t": 360,
"value": 45.0
},
{
"t": 600,
"value": 90.0
},
{
"t": 660,
"value": 45.0
},
{
"t": 900,
"value": 90.0
},
{
"t": 960,
"value": 45.0
},
{
"t": 1200,
"value": 90.0
},
{
"t": 1260,
"value": 45.0
},
{
"t": 1500,
"value": 90.0
},
{
"t": 1560,
"value": 45.0
},
{
"t": 1800,
"value": 90.0
},
{
"t": 1860,
"value": 45.0
},
{
"t": 2100,
"value": 90.0
},
{
"t": 2160,
"value": 45.0
},
{
"t": 2400,
"value": 90.0
},
{
"t": 2460,
"value": 45.0
},
{
"t": 2700,
"value": 90.0
},
{
"t": 2760,
"value": 45.0
},
{
"t": 3000,
"value": 90.0
},
{
"t": 3060,
"value": 45.0
},
{
"t": 3300,
"value": 90.0
},
{
"t": 3360,
"value": 45.0
},
{
"t": 3600,
"value": 90.0
},
{
"t": 3660,
"value": 45.0
},
{
"t": 3900,
"value": 90.0
},
{
"t": 3960,
"value": 45.0
},
{
"t": 4200,
"value": 90.0
},
{
"t": 4260,
"value": 45.0
},
{
"t": 4500,
"value": 90.0
},
{
"t": 4560,
"value": 45.0
},
{
"t": 4800,
"value": 90.0
},
{
"t": 4860,
"value": 45.0
},
{
"t": 5100,
"value": 90.0
},
{
"t": 5160,
"value": 45.0
},
{
"t": 5400,
"value": 90.0
},
{
"t": 5460,
"value": 45.0
},
{
"t": 5700,
"value": 90.0
},
{
"t": 5760,
"value": 45.0
},
{
"t": 6000,
"value": 90.0
},
{
"t": 6060,
"value": 45.0
},
{
"t": 6300,
"value": 90.0
},
{
"t": 6360,
"value": 45.0
},
{
"t": 6600,
"value": 90.0
},
{
"t": 6660,
"value": 45.0
},
{
"t": 6900,
"value": 90.0
},
{
"t": 6960,
"value": 45.0
},
{
"t": 7200,
"value": 90.0
},
{
"t": 7260,
"value": 45.0
},
{
"t": 7500,
"value": 90.0
},
{
"t": 7560,
"value": 45.0
},
{
"t": 7800,
"value": 90.0
},
{
"t": 7860,
"value": 45.0
},
{
"t": 8100,
"value": 90.0
},
{
"t": 8160,
"value": 45.0
},
{
"t": 8400,
"value": 90.0
},
{
"t": 8460,
"value": 45.0
},
{
"t": 8700,
"value": 90.0
},
{
"t": 8760,
"value": 45.0
},
{
"t": 9000,
"value": 90.0
},
{
"t": 9060,
"value": 45.0
},
{
"t": 9300,
"value": 90.0
},
{
"t": 9360,
"value": 45.0
},
{
"t": 9600,
"value": 90.0
},
{
"t": 9660,
"value": 45.0
},
{
"t": 9900,
"value": 90.0
},
{
"t": 9960,
"value": 45.0
},
{
"t": 10200,
"value": 90.0
},
{
"t": 10260,
"value": 45.0
},
{
"t": 10500,
"value": 90.0
},
{
"t": 10560,
"value": 45.0
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 2640,
"equals": false
},
{
"output": "yFault",
"from_s": 2880,
"to_s": 10800,
"equals": true
}
]
},
{
"name": "hunting_stops_before_delay",
"description": "Transient: 22 minutes of hard hunting (+/-10%, 4-minute period) starting at t=1800 s, after the windows have filled, then the damper settles. The eleventh crossing lands at t=3120 s and lifts the count over the threshold just as the oscillation ends; the count falls back to exactly 10.0 at t=3720 s as the first crossing ages out of the trailing window. `both` holds for 540 s against a 900 s alarm_delay, so one disturbance rejection does not report.",
"inputs": {
"zone_dmpr_pos": [
{
"t": 0,
"value": 50.0
},
{
"t": 1800,
"value": 60.0
},
{
"t": 1920,
"value": 40.0
},
{
"t": 2040,
"value": 60.0
},
{
"t": 2160,
"value": 40.0
},
{
"t": 2280,
"value": 60.0
},
{
"t": 2400,
"value": 40.0
},
{
"t": 2520,
"value": 60.0
},
{
"t": 2640,
"value": 40.0
},
{
"t": 2760,
"value": 60.0
},
{
"t": 2880,
"value": 40.0
},
{
"t": 3000,
"value": 60.0
},
{
"t": 3120,
"value": 50.0
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 10800,
"equals": false
}
]
},
{
"name": "recovery_clears",
"description": "Recovery: the same hunting starts at t=1800 s and runs for 90 minutes before a technician retunes the loop at t=7200 s. yFault asserts at t=4020 s and clears at t=7800 s \u2014 600 s after the last swing, the time five of the fifteen crossings in the window need to age out and bring the count back to exactly 10.0. Neither statistic drops when the hunting stops; both decay across the trailing window, and here the count reaches its threshold first (mad does not fall under 5.0 until t=8040 s).",
"inputs": {
"zone_dmpr_pos": [
{
"t": 0,
"value": 50.0
},
{
"t": 1800,
"value": 60.0
},
{
"t": 1920,
"value": 40.0
},
{
"t": 2040,
"value": 60.0
},
{
"t": 2160,
"value": 40.0
},
{
"t": 2280,
"value": 60.0
},
{
"t": 2400,
"value": 40.0
},
{
"t": 2520,
"value": 60.0
},
{
"t": 2640,
"value": 40.0
},
{
"t": 2760,
"value": 60.0
},
{
"t": 2880,
"value": 40.0
},
{
"t": 3000,
"value": 60.0
},
{
"t": 3120,
"value": 40.0
},
{
"t": 3240,
"value": 60.0
},
{
"t": 3360,
"value": 40.0
},
{
"t": 3480,
"value": 60.0
},
{
"t": 3600,
"value": 40.0
},
{
"t": 3720,
"value": 60.0
},
{
"t": 3840,
"value": 40.0
},
{
"t": 3960,
"value": 60.0
},
{
"t": 4080,
"value": 40.0
},
{
"t": 4200,
"value": 60.0
},
{
"t": 4320,
"value": 40.0
},
{
"t": 4440,
"value": 60.0
},
{
"t": 4560,
"value": 40.0
},
{
"t": 4680,
"value": 60.0
},
{
"t": 4800,
"value": 40.0
},
{
"t": 4920,
"value": 60.0
},
{
"t": 5040,
"value": 40.0
},
{
"t": 5160,
"value": 60.0
},
{
"t": 5280,
"value": 40.0
},
{
"t": 5400,
"value": 60.0
},
{
"t": 5520,
"value": 40.0
},
{
"t": 5640,
"value": 60.0
},
{
"t": 5760,
"value": 40.0
},
{
"t": 5880,
"value": 60.0
},
{
"t": 6000,
"value": 40.0
},
{
"t": 6120,
"value": 60.0
},
{
"t": 6240,
"value": 40.0
},
{
"t": 6360,
"value": 60.0
},
{
"t": 6480,
"value": 40.0
},
{
"t": 6600,
"value": 60.0
},
{
"t": 6720,
"value": 40.0
},
{
"t": 6840,
"value": 60.0
},
{
"t": 6960,
"value": 40.0
},
{
"t": 7080,
"value": 60.0
},
{
"t": 7200,
"value": 50.0
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 3900,
"equals": false
},
{
"output": "yFault",
"from_s": 4140,
"to_s": 7620,
"equals": true
},
{
"output": "yFault",
"from_s": 7860,
"to_s": 10800,
"equals": false
}
]
},
{
"name": "warmup_burst_asserts",
"description": "The warm-up artifact, and why the host NO_EVAL precondition is load-bearing. Ten minutes of tick-rate +/-20% swinging at engine start produces 8 crossings, and 8 crossings per 30 minutes is the reference's own NO_FAULT vector. But while the window is filling, xRate divides by elapsed time rather than by eval_window, so those 8 crossings read as a count of 24 at t=600 s and stay above 10 until t=1440 s. mad is inflated the same way. Both hold long enough for yFault to assert at t=1140 s; it clears at t=1440 s as the divisor grows, and the settled count after the window fills is 8. alarm_delay (900 s) is half of eval_window (1800 s) and cannot cover this \u2014 only the host gate can.",
"inputs": {
"zone_dmpr_pos": [
{
"t": 0,
"value": 70.0
},
{
"t": 60,
"value": 30.0
},
{
"t": 120,
"value": 70.0
},
{
"t": 180,
"value": 30.0
},
{
"t": 240,
"value": 70.0
},
{
"t": 300,
"value": 30.0
},
{
"t": 360,
"value": 70.0
},
{
"t": 420,
"value": 30.0
},
{
"t": 480,
"value": 70.0
},
{
"t": 540,
"value": 30.0
},
{
"t": 600,
"value": 50.0
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 1020,
"equals": false
},
{
"output": "yFault",
"from_s": 1200,
"to_s": 1320,
"equals": true
},
{
"output": "yFault",
"from_s": 1500,
"to_s": 10800,
"equals": false
}
]
}
]
}