Application: available
Catch what the stack O2 analyser cannot see.
Burner degradation and register imbalance both push CO up while the O2 reading stays perfectly normal. Trim air on that reading and you drive the worst burner richer. This is the failure mode worth catching, and it is the one a threshold alarm misses.
The pattern
Two fault classes, both invisible to the instrument an operator trusts most.
burner_degradation
CO climbs while the stack O2 analyser still reads normal
56 labelled hours in the history
register_imbalance
CO climbs while the stack O2 analyser still reads normal
46 labelled hours in the history
Why we do not use a classifier
We trained one. Its honest score is 0.26, and the reason is instructive.
A multi-class fault model on this history scores 0.95 balanced accuracy on a stratified split and 0.26 out of time. Each fault class occupies a single contiguous window, so there is no second occurrence to hold out and generalisation to an unseen instance of a class is untestable. The gap between those two numbers is that fact, not a deficiency we could train away.
Shipping the 0.95 would be dishonest. Shipping the 0.26 as a safety mechanism would be reckless. So the interlock rests on something else entirely.
What we do instead.
We compare the CO analyser against what a heater in good condition would produce at the same duty and air rate. A gap means something is burning badly that the air rate does not explain: burner tips, a skewed register, a damaged throat.
No fault labels go into building it. The labels are only used afterwards, to score it.
81.5 %
of burner-fault hours flagged, with zero false alarms across 2,499 healthy hours.
Correctly silent on fouling and on air deficiency. Neither is a burner problem, and air deficiency is already caught by the CO constraint itself. A detector that fires on everything is not a detector.
Source: ml_metrics.json, keys interlock_performance and fault_classifier.