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, and the pattern their residuals form on Simulink runs names the faulty component. Exact signature matching fails on the pump fault; the piece traces that failure and the noise cost of differentiating level measurements before it states what a physical installation would need.
A tank leaks. The PI controller sees the level drop and opens the pump a bit more, until the level is 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. On two tanks, thirteen equations come down to four numbers.
What you get out of it, concretely, is the way conservation laws and component models become four residual signals. A structural analysis (an incidence matrix, a graph) tells you which residuals are even computable before you touch any algebra, and the pattern across residuals names the broken component instead of just screaming. The last part is where the whole thing falls apart on real hardware, mostly on noise and on 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, and this is the job. Most failures on a factory floor don't announce themselves. Control loops absorb them until a seal lets go, and by then the maintenance window is gone. Catching that early is a residual problem, and the same four-residual skeleton runs in electrical drives and battery packs. 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 Simulink model and the LaTeX report I wrote for it, a university exercise from 2023. Where the report was wrong (units, and one explanation of a pump fault) I say so below and fix it.
The whole plant, with the five signals the detector measures. Everything it will ever know is on this picture.
Your controller is lying to you
The leak in the opening 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) only hears the fault once the controller has run out of room to hide it, and a slow leak can take hours to get there. The right question is whether what piles up in each tank matches what went in minus what went out. 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, and that is the whole field: fifty years of papers hang off that one sentence1.
1Willsky, A survey of design methods for failure detection in dynamic systems, Automatica 12, 1976, which already had a decade of work to survey.
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, which confuses 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, and this part is rigorous. Evaluating the residuals is a hack here: I picked thresholds from one healthy run and matched the bits they produced, and this part is a student with a deadline.
The first layer has serious literature. Parity equations are one way to generate a residual and diagnostic observers another, and a classic IFAC synthesis shows the two designs collapse into each other once you line up their objectives1. They differ in representation and in how gracefully they eat noise, and they share the idea, which is to compare what happened with what should have happened.
1Gertler, Analytical redundancy methods in fault detection and isolation: survey and synthesis, IFAC Proceedings Volumes 24 (SAFEPROCESS), 1991, which shows that once the design objectives are fixed, parity-equation and observer designs lead to equivalent residual generators. Kinnaert, Fault diagnosis based on analytical models for linear and nonlinear systems: a tutorial, IFAC Proceedings Volumes 36 (SAFEPROCESS), 2003, works through both approaches for linear and nonlinear models.
Calling it machine learning before machine learning gets the direction backwards, because it injects knowledge the opposite way. Instead of learning what normal looks like from ten thousand examples, you write down what normal has to obey. A detector that needs a dataset to find out that water is conserved is paying in data for what the physics gives away.
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. The sensors and the model are the same while the job is completely different, 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, which is 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 it for three years. Check your units, it costs nothing and it catches exactly this.
The model ignores fluid inertia. It ignores valve dynamics and temperature too. Those two sentences is the most important one on the page, because a model is 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 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, and so should you.
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 instead of settling on a nice number. Notice how much is already going on in this toy: continuous physics under a sampled controller, which saturates on one side and switches on the other. A tiny plant already makes a real 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 and far 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, which looks obvious and is 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 and no gains, only 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. Lunze's IFAC paper A method to get analytical redundancy relations for fault diagnosis (2017) 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. One extra equation in the count gives a candidate for a test, and the candidate can still be empty. 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 what the pump delivers and what the valve and tank 2 are doing, 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 that r1r_1r1 takes its inflow from the measured pump flow, where it could have used 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, and that footprint works as 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 and leaves the hydraulics alone. 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, run on the same sample clock from the same integrator state, saturation and anti-windup included. Copy a textbook continuous PI into a monitor of a discrete, saturating PI and you have built a residual that fires on your own shortcut.
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. The command says ten and the flow sensor says four; by the pump model, both cannot 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. The first is detectability: every fault row has at least one 1, so no fault on the list can happen without disturbing something. The second is isolability: every fault row is different, so once a fault is detected the ideal signature tells you which one it is.
So the detector is a tiny pipeline. The measurements feed the four ARRs and each residual goes through its own threshold. The four bits that come out are looked up in the signature table.
Anyone can write the 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, and each fault becomes a four-bit word.
What zero looks like when a computer does it
A residual that sits exactly at zero exists only in theory. In a sampled simulation the numerical differentiation leaves crumbs, and so does every valve switch. 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 guarantees exactly nothing about any other run: it is memorising the answer key.
The zero threshold on r4r_4r4 is the tell. Real flow sensors are noisy and late, and pump gain drifts with supply voltage and wear. mQpmQ_pmQp and mUpmU_pmUp may not even be sampled at the same instant. On hardware you start with a noise model and estimate the healthy residual distribution across operating regimes. Hysteresis and persistence over several samples come on top. 3σ3\sigma3σ is a decent first guess and nothing more. 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 is more than editorial dust: it is 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, and the culprit is a raw differentiator far more often than a wrong model.
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 reacting to the fault and moving the levels until other residuals got excited. It sounds plausible, and it is 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, and the sensor says exactly that. 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 came from an implementation discrepancy. Two signals may have been out of step in time, or a raw differentiator may have chewed on a faster transient; the flow reading may not even have been what the pump actually delivered. The archived screenshots can't say which one. 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 as well as 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, and the table compares 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 sorts the whole field into the same few families1.
1Zhao et al., Model-based fault diagnosis methods for systems with stochastic process: a survey, Neurocomputing 513, 2022, which classifies the existing model-based results as parity space, observer-based and parameter estimation approaches.
Second surprise: the ARRs hold up, and this student implementation is weak at the jump from four continuous signals to a decision, which is 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. It is also calibrated on one healthy trajectory and collapses every temporal shape into a mean. It assumes the operating regime never changes, and its zero threshold only survives because the simulated pump is perfect.
A modern version keeps the four ARRs, because they're correct, and 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. Under that banner you'll find careful hybrids next to neural networks 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 by Hao and colleagues, Physics-Informed Machine Learning: A Survey on Problems, Methods and Applications (2022), has a taxonomy that's actually usable.
For these tanks: keep the mass balances, they're free and they're true. Learn the residual covariance and the drifts nobody modelled (valve hysteresis, pump gain), 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, and the bill comes back as a detector that is blind wherever the training data ran out.
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 themselves, where they cannot be documented once 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 until the next samples agree.
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.
A fault reported as detected separately from isolated. Knowing something is wrong is worth a lot even when the label isn't.
ARRs are one route to a residual among several. A tidy statement of the ARR framing is in Borutzky's diagnostic bond-graph paper (Journal of Systems and Control Engineering, 2018), which calls the numerical evaluation of an ARR a residual fault indicator.