HW-0002 — Boiler efficiency degradation
| Status | verified — engine e2ff2f8, cxf:fnv1a128:2b1b4b0297999b62251fc5113d6cacf8, 2026-08-17 |
| Severity | 3 |
| Method | statistical |
| Phase | 2 |
| Category | EFFICIENCY_LOSS |
| Confidence | HIGH |
| Estimation | BASELINE_COMPARISON |
| G36 | — |
| Clusters | — |
| Suppresses | — |
| Suppressed by | — |
| Related | HW-0001, HW-0003, HW-0010, HW-0012, HP-0001 |
| Playbooks | hot-water-plant-faults |
| Source | HVAC FDD Reference v1.0 §14 (ch. ‘Hot Water Plants’, pdf pp. 125-126), HW-0002; Meng et al. 2021; Shohet et al. 2020; PNNL-13890 (O&M best practices) |
| Operating states | boiler firing and settled at its current fire — one rule instance per boiler, each carrying that boiler’s fitted line |
Preconditions (host-enforced): fuel_power and thermal_power must be computed on the same heating-value convention the baseline was fitted on. The point dictionary’s warning on fuel_power is the one that bites hardest here: fuel input derived from flow × HHV and fuel input derived from flow × LHV differ by roughly 10% for natural gas, so an efficiency computed one way against a line fitted the other way is wrong by about five times this rule’s whole threshold. Pick one convention per site, record it, and refit if it ever changes. The host owns the baseline: it runs the learning_period_days (14 d) regression of efficiency against boiler_firing_rate for THIS boiler and writes the result into eff_baseline_slope and eff_baseline_intercept with set_param. Until it has, the rule is comparing against shipped placeholders and means nothing (see Deviations). boiler_firing_rate must also lie inside the range the line was fitted over — the graph extrapolates forever and knows nothing about where the fit stops being physical. thermal_power is almost always a host-computed virtual point (HW flow × ΔT × cp); its provenance is part of the baseline’s validity, not separate from it, and a flow meter that reads 5% high moves measured efficiency by five points on its own. The boiler must be firing and settled: the minutes after a light-off are spent heating the vessel rather than the water, and they read as degraded on physics. Evaluability is signalled in-rule by yFuelOk; when it is false the verdict is NO_EVAL, not healthy.
Points: thermal_power, fuel_power, boiler_firing_rate
Outputs:
yFault— True while the measured efficiency has stayed more than efficiency_threshold below the fitted baseline for the current firing rate, continuously for at least alarm_delayyFuelOk— Evaluability signal — true when fuel_power is above fuel_power_min, the floor below which the efficiency quotient is meaningless. False means NO_EVAL and the host must ignore yFault
Parameters:
| Name | Default | Unit | CXF path | Description |
|---|---|---|---|---|
eff_baseline_slope | 0.0006 | 1/% | fireSlope.k | Slope of the host-fitted efficiency-vs-firing-rate regression, in efficiency fraction per percent of fire. PER-BOILER SITE CONFIGURATION — the reference supplies a learned model, not a number, and the shipped 0.0006 is a placeholder for a conventional non-condensing boiler. Inherently signed: a condensing boiler’s fit is negative, because its efficiency is highest at low fire |
eff_baseline_intercept | 0.78 | 1 | expected.p | Intercept of the same regression — the expected efficiency extrapolated to 0% fire, which is not an operating point but is what a straight line needs. PER-BOILER SITE CONFIGURATION on the same terms as the slope; the pair is only meaningful together |
efficiency_threshold | 0.05 | 1 | effLow.t | Shortfall below the fitted line that counts as degradation, in efficiency FRACTION — 0.05 is the reference’s 5 efficiency points. This is an absolute difference, not a relative one: writing 5 into it (percent) silences the rule permanently |
fuel_power_min | 5.0 | kW | fuelOk.t | Fuel input below which the efficiency quotient is not evaluated. Guards the division — at zero fuel the quotient is NaN, and at a standing pilot it reads as total degradation. PER-BOILER SITE CONFIGURATION: set it above the pilot and purge flow and below the smallest genuine firing input |
alarm_delay | 3600.0 | s | persist.delayTime | Continuous shortfall required before the alarm asserts (60 min). ADOPTED, not transcribed — the reference’s tunables line for this card truncates before reaching it (see Deviations) |
Description
A boiler’s efficiency is not a constant and is not supposed to be. The same burner returns more of its fuel as useful heat at full fire than at minimum fire, where jacket and standby losses are spread across a smaller output — or less, if the boiler condenses, because a cooler flue and a wetter heat exchanger are what low fire produces. Which way the curve runs is a fact about the machine, so no fixed efficiency threshold would either stay quiet all winter or ever alarm. What there is, per boiler, is a line: the host fits efficiency against firing rate over fourteen days and writes the slope and intercept in as parameters, and the graph asks whether today’s fire is within five efficiency points of what that line predicts. Five points is a lot of gas, and none of the causes announce themselves — scale, soot and a rich burner all leave the boiler making its setpoint on more fuel.
Detection Logic
measured_eff = thermal_power / fuel_power
expected_eff = eff_baseline_slope × boiler_firing_rate + eff_baseline_intercept
yFuelOk = fuel_power > fuel_power_min (false ⇒ host reports NO_EVAL)
yFault = (expected_eff − measured_eff) > efficiency_threshold AND yFuelOk,
sustained continuously for alarm_delay
Block graph (rule.cxf.jsonld):
fireSlope and expected are the fitted line and the whole statistical content
of the rule at runtime; shortfall and effLow write the reference’s equation
out unchanged, as an absolute difference in efficiency points rather than a
ratio. At the shipped placeholders the line predicts 0.792 at 20% fire, 0.810 at
50% and 0.840 at full fire, with the alarm five points under each — identical
meter readings are healthy at low fire and faulted at high fire, decided
entirely by boiler_firing_rate.
eff is the only division in the rule and its denominator goes to zero every
time the burner stops. With both meters at zero the quotient is NaN and every
comparison against it is false; with a standing pilot and no useful output it is
a clean, believable 0.0, which is the more dangerous of the two. fuelOk drives
both the boundary output yFuelOk and the second input of gate, so a boiler
below the floor holds yFault down and the host reads the silence as “not
evaluated” rather than “healthy”. persist requires 60 continuous minutes of
shortfall — long enough to ride out a light-off, a firing-rate step or a
return-temperature swing — and carries delayOnInit = true.
Possible Diagnoses
Transcribed from the reference’s HW-0002 card:
- Fouled heat exchanger or water-side scale — a millimetre costs several efficiency points and develops slowly enough for a fitted line to catch it
- Burner misalignment or fouling — soot does the same from the fire side, and a sooted burner is usually a badly adjusted one
- Incorrect fuel/air ratio — excess air carries heat up the stack, too little leaves fuel unburned; only a combustion analyser separates them
- Flue gas recirculation problem — an FGR damper out of position moves the NOx/efficiency trade the burner was commissioned on
- Refractory degradation — cracked refractory lets heat into the jacket instead of the water, usually found only when someone opens the front
Energy Impact
EFFICIENCY_LOSS, HIGH confidence, BASELINE_COMPARISON. The estimator is the
quotient the rule already computes:
waste_kw = fuel_power × (1 − measured_eff / expected_eff) — a boiler burning
1000 kW at 0.74 against an 0.81 baseline wastes about 86 kW of gas. The
reference’s range is 5–15% of fuel; PNNL-13890’s case study puts $730/yr on a
300-hp boiler. HIGH confidence holds because both terms are metered rather than
inferred, with two caveats that do not change it: the baseline is the boiler’s
own recent behaviour, so degradation present when the line was fitted is
invisible, and thermal_power is usually derived, so its accuracy is the flow
meter’s. Heating-dominant by construction.
Emissions Impact
Scope 1, PROXY_EMISSIONS, HIGH confidence; the reference’s typical range is 1,000–10,000 kg CO₂e/yr against a static 0.181 kg CO₂e/kWh natural gas factor. This is combustion at the building, so there is no grid to hedge against: the avoided-emissions basis is the static Scope 1 factor and the saving is the same whatever hour the boiler runs, which makes the emissions arithmetic the energy arithmetic times a constant.
Deviations
- The comparison is an absolute difference in efficiency points, as the reference writes it, and it diverges from the sibling cards on purpose: CHW-0001 tests a relative loss and HP-0001 multiplies its ratio through, both to avoid dividing by a fitted line, and neither move is needed here. The consequence at deployment: an absolute threshold is a larger relative tolerance the lower the baseline sits — five points is 6.1% of an 0.82 baseline and 5.6% of an 0.90 one — so this rule is the more forgiving of the two, and most forgiving where there is least efficiency to spare.
efficiency_thresholdcarries a fraction, not a percentage, and the failure mode is silence. The reference prints “5%”; the graph compares two dimensionless quotients, so the parameter is 0.05. Writing5asks for a 500-point shortfall and the rule goes quiet forever with no error. HP-0001’scop_ratio_thresholdhas the mirror-image trap.eff_baseline_slopeandeff_baseline_interceptship as documented placeholders. The reference specifies a model, not numbers (baseline_model.predict(boiler_firing_rate), fitted overlearning_period_days), and this library’s split puts the fitting in the host and the fitted line in the graph asset_paramtargets. The shipped 0.0006 /% and 0.78 describe a conventional non-condensing boiler and exist so the document is runnable as delivered. They are not site values, and a wrong pair fails silently in both directions — fitted five points high, every hour alarms; fitted low, nothing ever does. Precedent: HP-0001’s COP line, VAV-0001’sventilation_requirement.- The slope may be negative — the documented exception to the library’s no-negative-parameters convention. A regression slope is inherently signed and here the sign is a fact about the boiler type: conventional efficiency rises with fire, condensing falls, and one rule instance must accept either without rewiring. HP-0001 carries the identical exception.
alarm_delayis adopted, not transcribed, because the source line truncates. The reference’s tunables line ends at “efficiency_threshold = 5%, learning_period_days = 14,” and whatever followed did not survive the extract. The 60 minutes comes from the two nearest authorities — CHW-0001’sAlarmDelay = 60 minon the same fitted-baseline shape, and HP-0001 — and is the shortest delay that reliably outlasts a light-off transient on a large boiler.fuel_power_minandyFuelOkare adopted, not transcribed. The reference names no fuel floor and no evaluability gate, but the graph divides by a live signal, and per SCHEMA.md a test computable from the rule’s own inputs belongs in the graph as a boundary output rather than as prose.yFuelOkis not an echo offuel_power— it is the comparison the division needs. The 5.0 kW default sits above a standing pilot on a small commercial boiler and is arbitrary on a 3 MW firetube, where minimum fire alone is hundreds of kW.- The heating-value convention is a precondition the rule cannot check. HHV
and LHV differ by about 10% for natural gas, so an efficiency computed one way
against a line fitted the other is off by roughly eight efficiency points —
more than the whole threshold — and a healthy boiler reads as permanently
degraded. Nothing in three signals reveals which convention produced them, so
it lives in
preconditions. It is the single most likely way to deploy this rule wrongly. - The nominal boundary case is a FAULT, and that is arithmetic rather than a
choice. A five-point shortfall cannot be represented exactly at these
magnitudes:
0.05is an odd multiple of 2⁻⁵⁶ while every double in [0.5, 1) is a multiple of 2⁻⁵³, so no difference of two efficiencies in that range can equal the threshold. 0.760 against 0.810 evaluates to 0.050000000000000044 and trips the strict>; one ulp lower clears. That is as tightly as the boundary can be bracketed, and the card says so rather than implying a precision it does not have. - The fitted line is extrapolated without limit. Nothing in the graph knows
the firing-rate range the regression covered, so a far enough fire drifts the
expected efficiency into values the fit never supported. The block set has no
domain guard, so it is a frontmatter precondition; a host that wants it
enforced can clamp
boiler_firing_ratewithReals.Limiterupstream. HP-0001 documents the same open end. - One regressor, which is the reference’s choice and a real blind spot. For
a condensing boiler the dominant variable is return water temperature — the
same burner at the same fire condenses at 40 °C return and does not at 60 °C,
several efficiency points apart — so a plant whose return temperature tracks
the weather shows scatter this line cannot explain. The point dictionary
carries no HW return temperature and the reference’s Required Points list has
three entries; a second regressor would be a different card. Condensing sites
should widen
efficiency_thresholdor restrict the evaluated operating states. learning_period_days(14 d) stays a host precondition. It gates a fitting run that happens offline, outside any tick, and nothing in the block graph could observe it.method: statisticaldescribes the baseline’s provenance, not the runtime. The graph performs one division, one multiply-add, one subtraction and two comparisons; the classification is honest because the coefficients come from a regression. HP-0001 and RTU-0002 carry the same note.persist.delayOnInit = true(CDL defaultfalse), the library’s standing choice: a boiler already below its line at controller restart waits out the full hour rather than alarming on the first tick.clusters: [].clusters/clusters.jsondefines no cluster containing a hot water plant rule, and this card does not edit the cluster set. CLU-06 is the chilled-water analogue this fault would head on the heating side if such a cluster existed.- No test vectors are transcribed, because the reference publishes none. All
fifteen scenarios in
vectors.jsonare authored from the equation and replayed against the pinned engine rev. - Severity 3, phase 2,
method: statistical,confidence: HIGHand the 5% threshold are the reference’s chapter 14 card.g36: null— research-derived (Meng et al. 2021; Shohet et al. 2020), not a G36 clause.
Notes
Read yFuelOk before yFault. The two outputs are what separate a repair from
a burner that simply stopped: a host treating the falling edge of yFault as a
fix will close this fault every night the boiler shuts down.
Whatever schedules the learning run must refuse to re-fit while this fault is active. The line comes from the boiler’s own history, so re-fitting after degradation has developed bakes it in as the new normal and the rule reports healthy on a machine everyone agrees is wasting gas.
Work the diagnoses in the order a combustion analyser can see them: stack temperature and oxygen at high and low fire separate the fuel/air ratio (3) and FGR (4) from the heat-transfer causes (1, 2, 5) in about twenty minutes. HW-0001 reads the same boiler from the cycling side and is worth checking first — a plant tripping both may have one problem.
Test Vectors
15 scenarios, clock step 300 s over 10800 s.
| Scenario | Description |
|---|---|
on_the_baseline_line | A boiler sitting exactly on its fitted line: 50% fire, 1000 kW of gas in, 810 kW of heat out, measured efficiency 0.810 against an expected 0.810. The shortfall is exactly zero and nothing is reported. |
two_points_below_the_line_is_not_a_fault | Ordinary drift: 0.790 measured against 0.810 expected, a two-point shortfall against a five-point threshold. Boilers wander this much between cleanings and the reference does not call it. |
degraded_at_mid_fire | The fault the card is for: 0.740 measured against 0.810 expected, a seven-point shortfall held continuously. The alarm asserts at t = 3600 s. |
nominal_five_point_shortfall_faults | Threshold edge, and the surprise: 0.760 measured against 0.810 expected is nominally exactly the five-point threshold, but 0.81 - 0.76 evaluates to 0.050000000000000044 in IEEE-754 and the strict > fires. The nominal boundary case is a FAULT, not a pass — see Deviations. |
one_ulp_of_efficiency_clears_the_threshold | Threshold edge from the other side, one unit in the last place away: thermal power of 760.0000000000001 kW puts the measured efficiency on the next double above 0.76, the shortfall at 0.04999999999999993, and the verdict back to clear. The boundary is bracketed as tightly as binary arithmetic allows. |
same_efficiency_is_healthy_at_low_fire | The regressor earning its place: 0.780 measured at 20% fire, where the line expects 0.792. A 1.2-point shortfall, clear. |
same_efficiency_is_a_fault_at_high_fire | Identical meter readings to the scenario above, opposite verdict: at 100% fire the line expects 0.840, so the same 0.780 is a six-point shortfall and alarms at 3600 s. Nothing but boiler_firing_rate changed. |
fuel_power_exactly_at_the_floor | Evaluability edge from below: fuel input sitting exactly on fuel_power_min. The strict > reads that as not evaluable, yFuelOk is false, and yFault stays down even though the efficiency shortfall behind the gate is 8.6 points. False here means NO_EVAL, not healthy. |
fuel_power_just_above_the_floor | Evaluability edge from above: a tenth of a kilowatt more fuel and the same 8.6-point shortfall is evaluated and alarmed at 3600 s. |
boiler_off_divide_by_zero | The boiler is off: no fuel, no heat, and the efficiency quotient is 0/0 = NaN. Every comparison against NaN is false, so the rule is silent — but the gate is what makes that silence legible, and yFuelOk says NO_EVAL rather than leaving the host to infer it. |
transient_dip_never_alarms | A half-hour excursion — a cold-start cycle, a load step the burner has not caught up with — that reaches a seven-point shortfall and recovers at t = 2700 s. The hour of persistence swallows it. |
alarm_clears_after_burner_service | Recovery: the boiler runs seven points down until the burner is cleaned and the fuel/air ratio reset at t = 7200 s. The alarm holds from 3600 s and falls on the tick the efficiency returns — no fall delay in this rule. |
fuel_meter_dropout_forces_no_eval | The scenario a host must not read as a repair. The same degraded boiler shuts down at t = 7200 s, both meters go to zero, and yFault falls exactly as it does in alarm_clears_after_burner_service. Only yFuelOk separates them: it stays true through a real recovery and goes false here. |
degradation_released_on_the_alarm_tick | Delay edge from below: the shortfall is present from t = 0 and disappears at exactly 3600 s, the tick persist would mature on. The input is already clear when the timer comes due, so nothing is reported — a full hour of seven-point degradation this rule declines to call. |
degradation_released_one_tick_later | Delay edge from above: the same shortfall held 300 s longer asserts at exactly 3600 s and clears at 3900 s. One tick of input is one tick of alarm. |
vectors.json
{
"schema": "cxf-library/vectors/v1",
"clock": {
"step_s": 300,
"horizon_s": 10800
},
"scenarios": [
{
"name": "on_the_baseline_line",
"description": "A boiler sitting exactly on its fitted line: 50% fire, 1000 kW of gas in, 810 kW of heat out, measured efficiency 0.810 against an expected 0.810. The shortfall is exactly zero and nothing is reported.",
"inputs": {
"boiler_firing_rate": 50.0,
"fuel_power": 1000.0,
"thermal_power": 810.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 10800,
"equals": false
},
{
"output": "yFuelOk",
"from_s": 0,
"to_s": 10800,
"equals": true
}
]
},
{
"name": "two_points_below_the_line_is_not_a_fault",
"description": "Ordinary drift: 0.790 measured against 0.810 expected, a two-point shortfall against a five-point threshold. Boilers wander this much between cleanings and the reference does not call it.",
"inputs": {
"boiler_firing_rate": 50.0,
"fuel_power": 1000.0,
"thermal_power": 790.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 10800,
"equals": false
}
]
},
{
"name": "degraded_at_mid_fire",
"description": "The fault the card is for: 0.740 measured against 0.810 expected, a seven-point shortfall held continuously. The alarm asserts at t = 3600 s.",
"inputs": {
"boiler_firing_rate": 50.0,
"fuel_power": 1000.0,
"thermal_power": 740.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 3300,
"equals": false
},
{
"output": "yFault",
"from_s": 3900,
"to_s": 10800,
"equals": true
}
]
},
{
"name": "nominal_five_point_shortfall_faults",
"description": "Threshold edge, and the surprise: 0.760 measured against 0.810 expected is nominally exactly the five-point threshold, but 0.81 - 0.76 evaluates to 0.050000000000000044 in IEEE-754 and the strict `>` fires. The nominal boundary case is a FAULT, not a pass \u2014 see Deviations.",
"inputs": {
"boiler_firing_rate": 50.0,
"fuel_power": 1000.0,
"thermal_power": 760.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 3300,
"equals": false
},
{
"output": "yFault",
"from_s": 3900,
"to_s": 10800,
"equals": true
}
]
},
{
"name": "one_ulp_of_efficiency_clears_the_threshold",
"description": "Threshold edge from the other side, one unit in the last place away: thermal power of 760.0000000000001 kW puts the measured efficiency on the next double above 0.76, the shortfall at 0.04999999999999993, and the verdict back to clear. The boundary is bracketed as tightly as binary arithmetic allows.",
"inputs": {
"boiler_firing_rate": 50.0,
"fuel_power": 1000.0,
"thermal_power": 760.0000000000001
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 10800,
"equals": false
}
]
},
{
"name": "same_efficiency_is_healthy_at_low_fire",
"description": "The regressor earning its place: 0.780 measured at 20% fire, where the line expects 0.792. A 1.2-point shortfall, clear.",
"inputs": {
"boiler_firing_rate": 20.0,
"fuel_power": 1000.0,
"thermal_power": 780.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 10800,
"equals": false
}
]
},
{
"name": "same_efficiency_is_a_fault_at_high_fire",
"description": "Identical meter readings to the scenario above, opposite verdict: at 100% fire the line expects 0.840, so the same 0.780 is a six-point shortfall and alarms at 3600 s. Nothing but boiler_firing_rate changed.",
"inputs": {
"boiler_firing_rate": 100.0,
"fuel_power": 1000.0,
"thermal_power": 780.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 3300,
"equals": false
},
{
"output": "yFault",
"from_s": 3900,
"to_s": 10800,
"equals": true
}
]
},
{
"name": "fuel_power_exactly_at_the_floor",
"description": "Evaluability edge from below: fuel input sitting exactly on fuel_power_min. The strict `>` reads that as not evaluable, yFuelOk is false, and yFault stays down even though the efficiency shortfall behind the gate is 8.6 points. False here means NO_EVAL, not healthy.",
"inputs": {
"boiler_firing_rate": 10.0,
"fuel_power": 5.0,
"thermal_power": 3.5
},
"expect": [
{
"output": "yFuelOk",
"from_s": 0,
"to_s": 10800,
"equals": false
},
{
"output": "yFault",
"from_s": 0,
"to_s": 10800,
"equals": false
}
]
},
{
"name": "fuel_power_just_above_the_floor",
"description": "Evaluability edge from above: a tenth of a kilowatt more fuel and the same 8.6-point shortfall is evaluated and alarmed at 3600 s.",
"inputs": {
"boiler_firing_rate": 10.0,
"fuel_power": 5.1,
"thermal_power": 3.57
},
"expect": [
{
"output": "yFuelOk",
"from_s": 0,
"to_s": 10800,
"equals": true
},
{
"output": "yFault",
"from_s": 0,
"to_s": 3300,
"equals": false
},
{
"output": "yFault",
"from_s": 3900,
"to_s": 10800,
"equals": true
}
]
},
{
"name": "boiler_off_divide_by_zero",
"description": "The boiler is off: no fuel, no heat, and the efficiency quotient is 0/0 = NaN. Every comparison against NaN is false, so the rule is silent \u2014 but the gate is what makes that silence legible, and yFuelOk says NO_EVAL rather than leaving the host to infer it.",
"inputs": {
"boiler_firing_rate": 0.0,
"fuel_power": 0.0,
"thermal_power": 0.0
},
"expect": [
{
"output": "yFuelOk",
"from_s": 0,
"to_s": 10800,
"equals": false
},
{
"output": "yFault",
"from_s": 0,
"to_s": 10800,
"equals": false
}
]
},
{
"name": "transient_dip_never_alarms",
"description": "A half-hour excursion \u2014 a cold-start cycle, a load step the burner has not caught up with \u2014 that reaches a seven-point shortfall and recovers at t = 2700 s. The hour of persistence swallows it.",
"inputs": {
"boiler_firing_rate": 50.0,
"fuel_power": 1000.0,
"thermal_power": [
{
"t": 0,
"value": 810.0
},
{
"t": 900,
"value": 740.0
},
{
"t": 2700,
"value": 810.0
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 10800,
"equals": false
}
]
},
{
"name": "alarm_clears_after_burner_service",
"description": "Recovery: the boiler runs seven points down until the burner is cleaned and the fuel/air ratio reset at t = 7200 s. The alarm holds from 3600 s and falls on the tick the efficiency returns \u2014 no fall delay in this rule.",
"inputs": {
"boiler_firing_rate": 50.0,
"fuel_power": 1000.0,
"thermal_power": [
{
"t": 0,
"value": 740.0
},
{
"t": 7200,
"value": 810.0
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 3300,
"equals": false
},
{
"output": "yFault",
"from_s": 3900,
"to_s": 6900,
"equals": true
},
{
"output": "yFault",
"from_s": 7200,
"to_s": 10800,
"equals": false
}
]
},
{
"name": "fuel_meter_dropout_forces_no_eval",
"description": "The scenario a host must not read as a repair. The same degraded boiler shuts down at t = 7200 s, both meters go to zero, and yFault falls exactly as it does in alarm_clears_after_burner_service. Only yFuelOk separates them: it stays true through a real recovery and goes false here.",
"inputs": {
"boiler_firing_rate": [
{
"t": 0,
"value": 50.0
},
{
"t": 7200,
"value": 0.0
}
],
"fuel_power": [
{
"t": 0,
"value": 1000.0
},
{
"t": 7200,
"value": 0.0
}
],
"thermal_power": [
{
"t": 0,
"value": 740.0
},
{
"t": 7200,
"value": 0.0
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 3300,
"equals": false
},
{
"output": "yFault",
"from_s": 3900,
"to_s": 6900,
"equals": true
},
{
"output": "yFuelOk",
"from_s": 0,
"to_s": 6900,
"equals": true
},
{
"output": "yFuelOk",
"from_s": 7200,
"to_s": 10800,
"equals": false
},
{
"output": "yFault",
"from_s": 7200,
"to_s": 10800,
"equals": false
}
]
},
{
"name": "degradation_released_on_the_alarm_tick",
"description": "Delay edge from below: the shortfall is present from t = 0 and disappears at exactly 3600 s, the tick persist would mature on. The input is already clear when the timer comes due, so nothing is reported \u2014 a full hour of seven-point degradation this rule declines to call.",
"inputs": {
"boiler_firing_rate": 50.0,
"fuel_power": 1000.0,
"thermal_power": [
{
"t": 0,
"value": 740.0
},
{
"t": 3600,
"value": 810.0
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 10800,
"equals": false
}
]
},
{
"name": "degradation_released_one_tick_later",
"description": "Delay edge from above: the same shortfall held 300 s longer asserts at exactly 3600 s and clears at 3900 s. One tick of input is one tick of alarm.",
"inputs": {
"boiler_firing_rate": 50.0,
"fuel_power": 1000.0,
"thermal_power": [
{
"t": 0,
"value": 740.0
},
{
"t": 3900,
"value": 810.0
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 3300,
"equals": false
},
{
"output": "yFault",
"from_s": 3600,
"to_s": 3600,
"equals": true
},
{
"output": "yFault",
"from_s": 3900,
"to_s": 10800,
"equals": false
}
]
}
]
}