Model-based fault detection in a two-tank system
Model-based fault detection and isolation on a two-tank hydraulic plant under closed-loop control. Thirteen component relations are reduced by structural analysis to four analytical redundancy relations, evaluated as residuals on Simulink runs, thresholded on healthy data and matched against a fault-signature matrix. Covers detectability and isolability, the noise cost of differentiating level measurements, the failure of exact signature matching, and the calibration a physical installation would require.
A tank leaks. The PI controller sees the level drop, opens the pump a bit more, and puts the level right back on the setpoint. The dashboard stays green. Water is still going onto the floor.
No level alarm will ever see that. This page is about the thing that does: compile the plant's own physics into tests. Take the equations, kill every variable you can't measure, and whatever is left has to be zero while machine and model agree. Then watch zero. Model-based fault detection and isolation, FDI. Two tanks, thirteen equations, four numbers.
What you get out of it, concretely: how conservation laws and component models become four residual signals; how a structural analysis (an incidence matrix, a graph) tells you which residuals are even computable before you touch any algebra; how the pattern across residuals names the broken component instead of just screaming; and where the whole thing falls apart on real hardware, which is noise, differentiation and thresholds picked from a single run.
Why you should care if you don't own a water tank: this is the skill behind every condition monitoring, predictive maintenance and prognostics posting that never says what the job is. The job is this. Most failures on a factory floor don't announce themselves. Control loops absorb them until a seal lets go, a pump cavitates or a bearing seizes, and by then the maintenance window is gone. Catching that early is a residual problem, not a dashboard problem, and the same four-residual skeleton runs in electrical drives, HVAC, battery packs and chemical reactors. The autoencoder-on-sensor-streams version is what vendors sell. The physics version is what gets past the safety engineer. Learn this one first.
The material is a 2023 university exercise: a Simulink model, a LaTeX report with the matrices, screenshots of the runs. Where the report was wrong (units, and one explanation of a pump fault) I say so below and fix it.
The whole plant: two tanks, one pump, two valves, two controllers and five measured signals. Everything the detector will ever know is on this picture.
Your controller is lying to you
The leak in the opening is not a bug. That is the controller doing its job. A feedback loop exists to hide disturbances from the output, and a leak is a disturbance. Every controller on earth is a fault-hiding machine. The better the tuning, the better it hides.
So asking whether the level is too low is the wrong question, and limit checking (compare a sensor to a fixed number, raise an alarm) is a smoke detector with the battery pulled. The right question is: does the water going in, going out and piling up actually add up? Conservation doesn't care what the controller wants. If the books don't balance, something is wrong, and there are only a handful of suspects.
What this method is, and what it isn't
The grown-up name is model-based fault detection and isolation, FDI. The model says what healthy behaviour must satisfy. Measurements break some of those constraints when a part fails. The disagreement signals are called residuals. That's it. That's the field. Forty years of papers hang off that one sentence.
This particular build sits on the parity-relation side of it: you rearrange the plant equations until the internal states vanish and only measured stuff is left. Structural analysis (the incidence matrix, the graph) tells you which eliminations are even possible before you waste an afternoon on symbolic algebra.
People love to call this old school because the final code is tiny. That's confusing two layers that have nothing to do with each other:
Generating the residuals is model-based. Conservation laws and component equations decide what each residual checks. This part is rigorous.
Evaluating the residuals is a hack here. I picked thresholds from one healthy run, turned the residuals into bits and matched bits. This part is a student with a deadline.
The first layer has serious literature. Parity equations, diagnostic observers, Kalman innovations: three ways to generate a residual, and a classic IFAC synthesis shows parity and observer designs collapse into each other once you line up their objectives. Read Analytical Redundancy Methods in Fault Detection and Isolation and the later parity-space and observer tutorial if you want the receipts. They differ in representation and in how gracefully they eat noise. They do not differ in the idea. The idea is to compare what happened with what should have happened.
So no, this is not machine learning before machine learning. It's the opposite way of injecting knowledge. Instead of learning what normal looks like from ten thousand examples, you write down what normal has to obey. If your detector needs a dataset to find out that water is conserved, you don't have a data problem. You have a thinking problem.
Control and diagnosis ask different questions. A controller asks what input moves the system toward the reference; a diagnoser asks which physical assumption just stopped agreeing with the measurements. Same sensors, same model, completely different job, and confusing the two is how you end up with a plant that looks healthy right up to the moment it isn't.
Two tanks are the perfect microscope for this because nothing can hide. Water has to go somewhere. If the arithmetic doesn't close, you have maybe five suspects and they all live on one diagram.
The plant, in one breath
Tank T1T_1T1 gets filled by pump P1P_1P1. A digital PI controller holds its level at h1c=0.5 mh_{1c}=0.5,\mathrm{m}h1c=0.5m. Water crosses into T2T_2T2 through valve VbV_bVb, driven by a dumb on/off controller that opens whenever h2≤0.09 mh_2\le0.09,\mathrm{m}h2≤0.09m and shuts once the level is back in its band. Valve VoV_oVo is the consumer, sucking water out of tank 2.
The parameters fit on a napkin:
Geometry A1,A2A_1, A_2A1,A21.54×10−2 m21.54\times10^{-2},\mathrm{m^2}1.54×10−2m2Tank cross-section hmaxh_{max}hmax0.6 m0.6,\mathrm{m}0.6mMaximum level Flow laws CvbC_{vb}Cvb1.5938×10−4 m5/2/s1.5938\times10^{-4},\mathrm{m^{5/2}/s}1.5938×10−4m5/2/sInter-tank valve coefficient CvoC_{vo}Cvo1.5964×10−4 m5/2/s1.5964\times10^{-4},\mathrm{m^{5/2}/s}1.5964×10−4m5/2/sOutlet valve coefficient kpumpk_{pump}kpump10−310^{-3}10−3Pump gain QpmaxQ_{pmax}Qpmax0.01 m3/s0.01,\mathrm{m^3/s}0.01m3/sMaximum pump flow Injected faults Qf1,Qf2Q_{f1}, Q_{f2}Qf1,Qf210−4 m3/s10^{-4},\mathrm{m^3/s}10−4m3/sLeak flow
Small confession on the units. The 2023 report (and the first version of this page) wrote the valve coefficients in m3/s\mathrm{m^3/s}m3/s. Wrong. They get multiplied by a square root of a level, which has units m1/2\mathrm{m^{1/2}}m1/2, and the product has to be a volumetric flow. So the coefficient is m5/2/s\mathrm{m^{5/2}/s}m5/2/s. Nobody checked. Three years. Check your units, it costs nothing and it catches exactly this.
The model ignores fluid inertia, valve dynamics and temperature. Read that sentence again, because it's the most important one on the page. A model is not reality written in maths. It's a negotiated list of what you can afford to ignore. Every residual we build below is only as honest as that list.
Conservation is the only law you need
Cylindrical tanks, so Vi=AihiV_i=A_ih_iVi=Aihi. Conservation of volume:
dV1dt=Qp−Q12,dV2dt=Q12−Qo\frac{dV_1}{dt}=Q_p-Q_{12}, \qquad \frac{dV_2}{dt}=Q_{12}-Q_odtdV1=Qp−Q12,dtdV2=Q12−Qo
Substitute the geometry:
A1h˙1=Qp−Q12,A2h˙2=Q12−QoA_1\dot h_1=Q_p-Q_{12}, \qquad A_2\dot h_2=Q_{12}-Q_oA1h˙1=Qp−Q12,A2h˙2=Q12−Qo
This is a ledger. Left side is what piles up. Right side is what came in minus what went out. Money in, money out, except the money is water and the accountant is a differential equation.
The pump is linear until it isn't:
Qp={0,Up<0kpumpUp,0≤Up≤UpmaxQpmax,Up>UpmaxQ_p= \begin{cases} 0,&U_p<0\\ k_{pump}U_p,&0\le U_p\le U_{pmax}\\ Q_{pmax},&U_p>U_{pmax} \end{cases}Qp=⎩⎨⎧0,kpumpUp,Qpmax,Up<00≤Up≤UpmaxUp>Upmax
Flow between the tanks goes with the square root of the head difference. The sign term lets water flow backwards if tank 2 is ever higher than tank 1:
Q12=Cvbsgn(h1−h2)∣h1−h2∣ UbQ_{12}=C_{vb}\operatorname{sgn}(h_1-h_2)\sqrt{|h_1-h_2|}\,U_bQ12=Cvbsgn(h1−h2)∣h1−h2∣Ub
The outlet is the same thing with one tank:
Qo=Cvoh2 UoQ_o=C_{vo}\sqrt{h_2}\,U_oQo=Cvoh2Uo
Those square roots are not decoration. They're where the nonlinearity lives. Double the pressure head and the flow does not double, it grows by 2\sqrt22. Every let's just linearise it instinct you have is a small lie you're choosing to tell around one operating point. The original exercise kept the square roots. Good. Keep them.
Two controllers, one of them dumb on purpose
Tank 1 gets a PI:
Up(t)=Kp(h1c−h1(t))+Ki∫0t(h1c−h1(τ))dτU_p(t)=K_p\bigl(h_{1c}-h_1(t)\bigr)+K_i\int_0^t\bigl(h_{1c}-h_1(\tau)\bigr)d\tauUp(t)=Kp(h1c−h1(t))+Ki∫0t(h1c−h1(τ))dτ
with Kp=10−3K_p=10^{-3}Kp=10−3, Ki=5×10−6K_i=5\times10^{-6}Ki=5×10−6, sampled once a second. The proportional term reacts to the error right now. The integral term holds a grudge about error that has persisted. Ideal A/D on the way in, zero-order hold on the way out.
Tank 2 gets a light switch:
Ub={1,0≤h2≤0.090,0.09<h2≤h2maxU_b= \begin{cases} 1,&0\le h_2\le0.09\\ 0,&0.09<h_2\le h_{2max} \end{cases}Ub={1,0,0≤h2≤0.090.09<h2≤h2max
On, off, on, off. So h2h_2h2 is supposed to chatter inside a band, not settle on a nice number. Notice how much is already going on in this toy: continuous physics, sampled control, saturation, switching. Tiny plant. Not a tiny problem.
The Simulink model. The controllers run the plant; a parallel diagnostic path watches the same five signals and keeps its own opinion.
Thirteen tiny truths
Now the part I didn't get in 2023. Instead of one big state-space equation, the structural model chops the plant into thirteen elementary relations. Uglier. Massively more useful. Faults belong to components, so the model has to keep component boundaries visible or you'll never be able to point at the broken part.
Model Relation Component
M1M_1M1 V˙1=Qp−Q12\dot V_1=Q_p-Q_{12}V˙1=Qp−Q12 Tank 1 balance
M2M_2M2 V1=A1h1V_1=A_1h_1V1=A1h1 Tank 1 geometry
M3M_3M3 V˙2=Q12−Qo\dot V_2=Q_{12}-Q_oV˙2=Q12−Qo Tank 2 balance
M4M_4M4 V2=A2h2V_2=A_2h_2V2=A2h2 Tank 2 geometry
M5M_5M5 Qp=f(Up)Q_p=f(U_p)Qp=f(Up) Pump
M6M_6M6 Q12=f(h1,h2,Ub)Q_{12}=f(h_1,h_2,U_b)Q12=f(h1,h2,Ub) Inter-tank valve
M7M_7M7 Qo=f(h2,Uo)Q_o=f(h_2,U_o)Qo=f(h2,Uo) Outlet valve
M8M_8M8 Up=PI(h1c−h1)U_p=PI(h_{1c}-h_1)Up=PI(h1c−h1) PI controller
M9M_9M9 my1=h1my_1=h_1my1=h1 Tank 1 level sensor
M10M_{10}M10 my2=h2my_2=h_2my2=h2 Tank 2 level sensor
M11M_{11}M11 mQp=QpmQ_p=Q_pmQp=Qp Pump-flow sensor
M12M_{12}M12 mUp=UpmU_p=U_pmUp=Up Pump-command sensor
M13M_{13}M13 mUb=UbmU_b=U_bmUb=Ub Valve-command sensor
Look at M9M_9M9. It says the measured level equals the real level. Obvious? It's the single smartest line in the table. h1h_1h1 is the physical level, which you will never know. my1my_1my1 is what a sensor claims. In healthy operation they're equal. When the sensor dies, that equality is precisely what breaks, and because it's its own equation, the break has its own address. Merge the two and a dead sensor becomes indistinguishable from a leak. Don't merge them.
Structure before numbers
Before solving anything, put a 1 wherever relation MiM_iMi contains variable xjx_jxj. That's the incidence matrix. This is the original one from the report, rebuilt for the web instead of replaced with some generic infographic.
The original LaTeX incidence table. The heavy horizontal line splits physical equations from sensor equations.
This matrix knows nothing. No square roots, no gains, no litres per second. Pure wiring. Row M6M_6M6 touches h1,h2,Ub,Q12h_1,h_2,U_b,Q_{12}h1,h2,Ub,Q12, and that alone is enough to say which equations could compute which variables and which components can reach which residual.
Same information as a bipartite graph, and suddenly the substitution problem is a thing you can look at:
The original TikZ relational graph. Sensor branches stay pale red, exactly as in the LaTeX source.
Structural analysis throws away every number on purpose. That feels like vandalism and it is. The payoff is combinatorial: you can hunt for subsets of equations with one more equation than unknowns. Solve the unknowns with a matching, and the leftover equation has nowhere to go. It becomes a consistency test.
redundancy=#equations−#unknowns\text{redundancy}=\#\text{equations}-\#\text{unknowns}redundancy=#equations−#unknowns
Zero: the subset can just barely determine its own unknowns. One: it can determine them and still check itself. The IFAC paper A method to get analytical redundancy relations for fault diagnosis walks the same road: start overdetermined, reduce to expressions that only contain measured signals and their derivatives.
Now the caveat my 2023 self skipped, and it's not a small one. Counting one extra equation is a candidate for a test, not a proof you have one. Equations can be dependent. Coefficients can vanish. A differential relation can demand a derivative you don't have. Write x=z1x=z_1x=z1 and 2x=z22x=z_22x=z2 and you get a real test, z2−2z1=0z_2-2z_1=0z2−2z1=0. Write x=z1x=z_1x=z1 twice and you get two rows on paper and zero information. The incidence matrix cannot tell those apart. Rank can. Structure gets you the candidates; numerics decides which ones are alive.
Analytical redundancy is a spare sensor made of algebra
Hardware redundancy: bolt on a second level sensor and compare. Analytical redundancy: use the rest of the plant to predict what the first sensor should have said.
If I know the pump inflow, the valve command and the tank 2 level, the tank 1 balance gives me an independent opinion about how tank 1 must be evolving. Nobody added a probe. The spare sensor was sitting inside the equations the whole time, unpaid.
The original assignment then adds causality. P is an algebraic relation, I marks a variable reached by integration, O marks one that needs differentiation. What matters for the detector is which equations each residual ends up using, and the four residuals below do not use disjoint sets: the two tank balances share the valve equation and three of the sensor equations, which is exactly why a valve fault will show up in two residuals at once.
Thirteen equations against four residuals. ARR1 and ARR2 share M6, M9, M10 and M13. The table follows the residuals as written below, not the archived coloured paths.
The same table, interactive: pick any two residuals and see which equations they share.
Residual 1: the tank 1 ledger
Substitute the measured levels and measured pump flow into M1M_1M1, then the valve law for Q12Q_{12}Q12:
r1=A1dmy1dt−mQp+Cvbsgn(my1−my2)∣my1−my2∣ mUbr_1=A_1\frac{d m_{y1}}{dt}-mQ_p +C_{vb}\operatorname{sgn}(m_{y1}-m_{y2}) \sqrt{|m_{y1}-m_{y2}|}\,mU_br1=A1dtdmy1−mQp+Cvbsgn(my1−my2)∣my1−my2∣mUb
Healthy means r1≈0r_1\approx0r1≈0. A tank 1 leak is an outflow the model never heard of. A valve fault changes the real Q12Q_{12}Q12 without changing the commanded mUbmU_bmUb. Both make the ledger fail.
Note what r1r_1r1 uses for the inflow: the measured pump flow, not the pump model. Hold that thought. It matters later, when I explain a mistake I published.
Residual 2: the tank 2 ledger
r2=A2dmy2dt−Cvbsgn(my1−my2)∣my1−my2∣ mUb+Cvomy2 Uor_2=A_2\frac{d m_{y2}}{dt} -C_{vb}\operatorname{sgn}(m_{y1}-m_{y2}) \sqrt{|m_{y1}-m_{y2}|}\,mU_b +C_{vo}\sqrt{m_{y2}}\,U_or2=A2dtdmy2−Cvbsgn(my1−my2)∣my1−my2∣mUb+Cvomy2Uo
Reacts to a tank 2 leak and to the same inter-tank valve fault. The valve is shared by both balances, so it leaves a two-residual footprint. That's not a nuisance, that's a fingerprint.
Residual 3: did the PI actually produce this command?
r3=mUp−Kp(h1c−my1)−Ki∫0t(h1c−my1)dτr_3=mU_p-K_p(h_{1c}-m_{y1}) -K_i\int_0^t(h_{1c}-m_{y1})d\taur3=mUp−Kp(h1c−my1)−Ki∫0t(h1c−my1)dτ
This one checks the controller chain, not the hydraulics. If the measured command disagrees with the PI law evaluated on the measured level, something between the level sensor and the pump command is inconsistent.
One trap. The replay in r3r_3r3 must be the real implementation: same sample clock, same integrator initial state, same saturation, same anti-windup. Copy a textbook continuous PI into a monitor of a discrete, saturating PI and you have built a residual that fires on your own laziness.
Residual 4: did the pump obey?
Away from saturation:
r4=mQp−kpumpmUpr_4=mQ_p-k_{pump}mU_pr4=mQp−kpumpmUp
The smallest ARR and the easiest to explain to a five-year-old. Command says ten, flow sensor says four, pump model says those two cannot both be true.
The piecewise model matters. The compact r4r_4r4 is only valid in the pump's linear region. A real residual generator has to evaluate the same piecewise law as M5M_5M5, zero flow and saturation included. Skip that and every normal saturation event looks exactly like a pump fault. You will be woken up at 3 a.m. by a pump that is fine.
A fault is a four-bit word
A residual tells you some relation is inconsistent. It doesn't name the broken part. Isolation comes from the pattern across all four.
Fault mode r1r_1r1 r2r_2r2 r3r_3r3 r4r_4r4 Signature
Healthy 0 0 0 0 0000
Pump fault 0 0 0 1 0001
Leak in tank 1 1 0 0 0 1000
Leak in tank 2 0 1 0 0 0100
Pump-command sensor fault 0 0 1 1 0011
Inter-tank valve fault 1 1 0 0 1100
Two properties fall out for free:
Detectability. Every fault column has at least one 1. No fault on the list can happen without disturbing something.
Isolability. Every column is different. Detect it, and the ideal signature tells you which one.
So the detector is a tiny pipeline: measurements in, the four ARRs, one threshold per residual, four bits, and a lookup against the signature table.
The clever bit is not the lookup. Anyone can write a lookup. The clever bit is designing residuals whose sensitivities overlap in carefully different ways. r4r_4r4 alone points at the pump. r3r_3r3 and r4r_4r4 together point upstream, at the command measurement. r1r_1r1 and r2r_2r2 together point at the one valve both tanks share.
A fault becomes a four-bit word. Say it out loud, it's kind of beautiful.
What zero looks like when a computer does it
Residual equals zero is theory talk. In a sampled simulation, differentiation, integration, switching and solver tolerances all leave crumbs. The healthy runs show the floor we actually got:
Healthy r1. Valve switching plus numerical differentiation leave visible transients.Healthy r2. Its normal envelope is not even symmetric around zero.
Healthy r3. The controller relation stays extremely close to zero.Healthy r4. In this ideal model the pump relation is exact. Suspiciously exact.
The 2023 calibration took the largest healthy absolute value and called it a threshold:
Residual Healthy minimum Healthy maximum Chosen threshold
r1r_1r1 −1.064×10−4-1.064\times10^{-4}−1.064×10−4 9.6×10−59.6\times10^{-5}9.6×10−5 1.064×10−41.064\times10^{-4}1.064×10−4
r2r_2r2 −6.0×10−5-6.0\times10^{-5}−6.0×10−5 1.37×10−41.37\times10^{-4}1.37×10−4 1.37×10−41.37\times10^{-4}1.37×10−4
r3r_3r3 −9.98×10−8-9.98\times10^{-8}−9.98×10−8 9.98×10−89.98\times10^{-8}9.98×10−8 9.98×10−89.98\times10^{-8}9.98×10−8
r4r_4r4 0 0 0
Zero false alarms on the run used to pick the thresholds. Which guarantees exactly nothing about any other run. That's not calibration, that's memorising the answer key.
The zero threshold on r4r_4r4 is the tell. Real flow sensors have quantisation, noise, bias and latency. Pump gain drifts with supply voltage, wear and operating point. mQpmQ_pmQp and mUpmU_pmUp may not even be sampled at the same instant. On hardware you start with a noise model, estimate the healthy residual distribution across operating regimes, add hysteresis and demand persistence over several samples. 3σ3\sigma3σ is a decent first guess, not a law of nature. Also, look at the healthy r1r_1r1 plot again: there's a startup spike around 5×10−45\times10^{-4}5×10−4, five times the threshold. The report never excluded a startup window. So no false alarms on the healthy run was never even true as stated. It was true after the operator stopped looking.
And there's a typo. The table says 9.98×10−89.98\times10^{-8}9.98×10−8 for r3r_3r3. The code says 9.98×10−109.98\times10^{-10}9.98×10−10. A factor of a hundred. That's not editorial dust, that's two detectors pretending to be one. The code and the document need a single source of truth or the document is fiction.
Don't differentiate noise. Integrate it.
Both ledgers contain m˙y\dot m_{y}m˙y, a derivative of a measured level. Fine on paper. On a real sensor it's a disaster. Take a one-step difference of a level with independent noise σh\sigma_hσh and the noise on the derivative is
σy˙=2 σhΔt.\sigma_{\dot y}=\frac{\sqrt2\,\sigma_h}{\Delta t}.σy˙=Δt2σh.
Divide by the sample time. Sample faster to see the leak sooner and you amplify the noise faster. Then you multiply by the tank area and dump the result straight into a flow residual whose threshold is 10−410^{-4}10−4. This is the most common way I've seen model-based detectors die in practice: not a wrong model, a raw differentiator.
The fix is dull and it works. Integrate the balance over a window TTT instead of differentiating it:
R1(t)=A1[my1(t)−my1(t−T)]−∫t−Tt(mQp−Q^12)dτT.R_1(t)=\frac{A_1\bigl[m_{y1}(t)-m_{y1}(t-T)\bigr]-\int_{t-T}^{t}\bigl(mQ_p-\widehat Q_{12}\bigr)d\tau}{T}.R1(t)=TA1[my1(t)−my1(t−T)]−∫t−Tt(mQp−Q12)dτ.
Still a flow residual. Still −Qf1-Q_{f1}−Qf1 for a constant leak filling the window. But now only two level samples enter, and their noise is divided by TTT instead of Δt\Delta tΔt. In code it's a running flow integral and a ring buffer of old levels. No symbolic algebra at the sample rate. An observer does the same job with more ceremony. Either way: never hand a finite difference of a noisy sensor to a threshold and call it engineering.
Break things on purpose
The leak and valve experiments behave exactly the way the structure said they would:
A tank 1 leak is an outflow the model never modelled. r1 leaves its healthy envelope and stays out.A valve fault corrupts the inter-tank flow r1 assumed.
The second ledger catches its own leak.The same valve fault also breaks the tank 2 balance. That completes 1100.
The sensor and pump experiments exercise the controller-actuator chain:
The measured command no longer matches the PI calculation.The measured flow no longer matches the commanded pump flow.
The same command-measurement fault reaches the pump relation too. Second bit of 0011.
Now the ugly part, and the part I got wrong the first time I wrote this up.
In the recorded comparison table, the pump-fault run doesn't only trip r4r_4r4. It also trips r1r_1r1 and r2r_2r2. The ideal signature says 0001. The simulation, after thresholding, says something like 1101. The first version of this article waved that away as closed-loop propagation: the controller reacts to the fault, the levels move, other residuals get excited. Sounds plausible. It's wrong.
Go back to r1r_1r1. It uses the measured pump flow. If the pump loses capacity and its flow sensor is honest, the tank 1 balance still closes. Less water comes in, the sensor says less water comes in, the level does what less water makes it do. Conservation doesn't care how the controller feels about it. An exact balance equation cannot be made false by a different trajectory. So those extra alarms weren't physics. They were an implementation discrepancy: a timing mismatch between signals, a raw differentiator chewing on a faster transient, a flow reading that isn't what the pump actually delivered. Which one? The archived screenshots can't say. That's what you get for keeping PNGs and throwing away the data.
I'd rather publish the correction than keep a nice story. And the correction is more useful than the story, because it's a rule: when a residual fires that structure says can't fire, suspect your implementation before you suspect your physics.
Five things that would make the decision layer less brittle, in the order I'd do them:
Keep an unknown fault outcome. Forcing every unseen bit pattern into a known label is precisely how a diagnostic system becomes confidently wrong. This one first. Use time. Match the residual that fires first, not just the final binary vector. Rank candidates by weighted residual magnitude instead of demanding an exact bit match. Design decoupled residuals that suppress secondary sensitivities where you can. Calibrate thresholds by operating region, especially around valve switching and pump saturation.
Put it next to the neighbours
I wanted a more honest comparison than physics good, machine learning also good. Every method spends its complexity somewhere. The question is where.
Method What makes the residual What it buys What hurts
Limit checking Raw sensor minus fixed limit Free, easy to certify Weak isolation, blind to coupling, fooled by any controller
These ARRs Eliminated physical constraints Component-level structure, nonlinearities stay visible Derivatives amplify noise; model mismatch leaks everywhere
Parity space Projection orthogonal to healthy input-output behaviour Systematic design over a time window Cleaner for linear plants than switching nonlinear ones
Luenberger or unknown-input observer Measured output minus estimated output State estimates, disturbance decoupling, filtering Observer design and observability assumptions
Kalman filter Innovation from stochastic state estimation Noise covariance is explicit Gaussian and model assumptions can be badly wrong
Parameter estimation Online parameter minus nominal Fault size maps to a physical parameter Slow faults, needs excitation
PCA or PLS Projection outside a learned healthy subspace Handles many correlated variables Linear latent structure; diagnosis is not physical
Autoencoder or sequence model Reconstruction or prediction error Learns nonlinear temporal normality Data hunger, calibration, no guarantees outside training support
First surprise: the categories aren't as separate as their names. PCA learns an empirical residual space; parity methods derive one from equations. Observer and parity generators transform into each other under aligned objectives. A recent survey of stochastic model-based diagnosis still organises the whole field around parity space, observers and parameter estimation, with particle filters bolted onto the observer branch. See Model-based fault diagnosis methods for systems with stochastic process .
Second surprise: where this student implementation is actually weak. Not the ARRs. The jump from four continuous signals to a decision: one fixed threshold per residual, and a flag raised when the mean of that residual over the tail of the run exceeds it. Maximally inspectable, which I like. Also calibrated on one healthy trajectory, collapses every temporal shape into a mean, assumes the operating regime never changes, and carries a zero threshold that only survives because the simulated pump is perfect.
A modern version doesn't throw away the four ARRs. Why would you? They're correct. It improves how they're judged:
Estimate p(r∣healthy,z)p(r\mid\text{healthy},z)p(r∣healthy,z) conditioned on the operating point zzz, then threshold a normalised likelihood instead of a raw magnitude. Feed short residual windows to a lightweight temporal classifier that keeps onset order and oscillation shape. Replace raw differentiation with an observer or Kalman innovation, or the windowed integral above. Learn only the unmodelled correction ΔQ\Delta QΔQ (valve hysteresis, pump drift) and keep conservation of mass as a hard constraint. Reject patterns outside the known fault set instead of forcing a match.
That last hybrid has a fashionable name now, physics-informed machine learning. Most of what ships under that banner is a neural network with a differential equation sprinkled on top for the investor deck. The real version is a decision: which parts do you learn because you honestly don't know them, and which parts stay hard constraints because you do. The survey Physics-Informed Machine Learning: Problems, Methods and Applications has a taxonomy that's actually usable.
For these tanks: keep the mass balances, they're free and they're true. Learn valve hysteresis, pump-gain drift and the residual covariance, because those you don't know. Asking a network to rediscover that water is conserved is spending data to buy an equation you already own. It's not sophisticated. It's expensive stupidity.
What I'd build now
The 2023 version used late-window means and a deterministic lookup. If I moved this onto a physical bench tomorrow:
Timestamp and resample every signal onto one clock before evaluating any ARR. Half the faults I've seen in logs were two sensors on two clocks.
Windowed integrals or an observer for the balances. Never a raw finite difference.
Pump saturation and valve hysteresis implemented explicitly inside the residual generators, not documented somewhere and forgotten.
Residuals normalised by their healthy variance, so r1r_1r1 and r3r_3r3 can be compared like adults.
Persistence counters. One sample over a line is noise, not a fault.
Store the residual vector, the operating point and the diagnosis together, so you can recalibrate later instead of guessing.
Compound faults on the test list. The signature matrix assumes one fault at a time, and reality has never signed that contract.
Detected separated from isolated. Knowing something is wrong is worth a lot even when the label isn't.
The literature calls all of this model-based fault detection and isolation. ARRs are one route to a residual; observers, parity equations and parameter estimation are others. The skeleton is always the same: the plant produces measurements, the model produces expectations, and the difference is evidence. A tidy statement of the ARR framing is in this diagnostic bond-graph paper , which calls the numerical evaluation of an ARR a residual fault indicator. Same thing, bigger words.