{"id":116,"date":"2026-09-02T22:19:28","date_gmt":"2026-09-02T22:19:28","guid":{"rendered":"https:\/\/llama.gs\/blog\/?p=116"},"modified":"2026-09-02T22:19:30","modified_gmt":"2026-09-02T22:19:30","slug":"three-tanks-ten-diagnosers-and-half-an-hour-of-cpu","status":"publish","type":"post","link":"https:\/\/llama.gs\/blog\/index.php\/2026\/09\/02\/three-tanks-ten-diagnosers-and-half-an-hour-of-cpu\/","title":{"rendered":"Three Tanks, Ten Diagnosers, and Half an Hour of CPU"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Stanford researchers reported this May that robot policies scoring close to 90 percent in controlled simulation succeed at roughly 12 percent of real tasks; on a published peg-insertion benchmark one method managed 98.75 percent in simulation and 12.5 percent on hardware. The robots failing is not the interesting part. The interesting part is that nothing in the simulated score announced they would \u2014 the number was confident, precise, and silent about its own irrelevance. <strong>A method that cannot signal its own failure is worse than a mediocre one that can.<\/strong> Remember that; one of the ten methods below fails spectacularly, and the manner of its failure is the most reassuring thing in this post.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Psycho Killer<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Today I am releasing <a href=\"https:\/\/gitlab.llama.gs\/diagnostics\/psycho-killer\">psycho-killer<\/a>, a toolkit for model-based diagnosis of dynamical systems. The name is a Talking Heads reference.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It is not a diagnosis algorithm and ships no opinion about which method you should use. It is a framework for <strong>diagnostic analytics<\/strong>: one system model, one shared set of fault scenarios, every applicable method run against them \u2014 filter-based and data-driven alike \u2014 scored on the same metrics. The output is not a diagnostic algorithm, it is a table you use to <em>choose<\/em> a diagnostic method, which is different and more useful.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It also ships a graphical IDE, and the GUI is the point. An engineer who has never opened a Python interpreter can load a model, inject a fault, watch the tanks respond, and get the comparison table out the other end \u2014 precisely the audience that ought to be doing model-based diagnosis and is currently being sold a chatbot instead.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The System<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Three tanks in series, as in <a href=\"https:\/\/llama.gs\/blog\/index.php\/2026\/05\/25\/a-visit-to-europe-the-rise-and-fall-of-big-chips-and-lydia-ng\/\">my May post on Lydia-NG<\/a>. Water flows from tank 1 to 2 to 3 and out through three valves. Each valve has a commanded opening \\(u_i\\) and an actual condition \\(f_i\\), where 1.0 means it delivers what it was asked for. Three noisy pressure sensors are the only window in; nothing observes a valve directly.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Writing \\(h_i\\) for the level in tank \\(i\\), \\(A_i\\) for its cross-section, and<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">$$ \\phi(\\Delta h) \\;=\\; \\frac{\\Delta h}{\\left(\\Delta h^{2} + \\varepsilon^{2}\\right)^{1\/4}} $$<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">for the regularised signed square root of a head difference \u2014 Torricelli, with \\(\\varepsilon\\) smoothing the kink at zero so the integrator does not trip over it:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">$$ \\dot h_1 = \\frac{1}{A_1}\\Big(q_0 &#8211; f_1 u_1 k_1\\,\\phi(h_1-h_2)\\Big) $$<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">$$ \\dot h_2 = \\frac{1}{A_2}\\Big(f_1 u_1 k_1\\,\\phi(h_1-h_2) &#8211; f_2 u_2 k_2\\,\\phi(h_2-h_3)\\Big) $$<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">$$ \\dot h_3 = \\frac{1}{A_3}\\Big(f_2 u_2 k_2\\,\\phi(h_2-h_3) &#8211; f_3 u_3 k_3\\,\\phi(h_3)\\Big) $$<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">with sensors reading \\(p_i = h_i\\,g\\). Infer \\(f_1, f_2, f_3\\) from \\(p_1, p_2, p_3\\). It is a fair test because a closing valve does not merely dim the sensor nearest it \u2014 it backs pressure up behind and starves the tanks ahead, so the signature appears everywhere, differently shaped in each place.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Note \\(\\phi\\). Its derivative runs from \\(\\varepsilon^{-1\/2}\\) near zero head difference down to roughly \\(\\frac{1}{2}|\\Delta h|^{-1\/2}\\) when the tanks are far apart \u2014 orders of magnitude across the ordinary operating range. One of our ten methods is going to die on that.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Run<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Thirteen scenarios per split, 39 in total: one nominal, plus a ramp fault in each valve at each of four magnitudes. Ramps, not steps, because real valves do not fail instantaneously and a benchmark built on step changes flatters methods that detect step changes.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"832\" src=\"https:\/\/llama.gs\/blog\/wp-content\/uploads\/2026\/09\/image-1024x832.png\" alt=\"\" class=\"wp-image-136\" srcset=\"https:\/\/llama.gs\/blog\/wp-content\/uploads\/2026\/09\/image-1024x832.png 1024w, https:\/\/llama.gs\/blog\/wp-content\/uploads\/2026\/09\/image-300x244.png 300w, https:\/\/llama.gs\/blog\/wp-content\/uploads\/2026\/09\/image-768x624.png 768w, https:\/\/llama.gs\/blog\/wp-content\/uploads\/2026\/09\/image.png 1200w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Valve 1 loses half its capacity for a few minutes, then recovers. That is all any method gets to see.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The filters consume raw readings online. The regressors cannot, so a rolling window over each sensor yields ten features apiece \u2014 min, max, mean and standard deviation in the time domain, plus six statistics over the window\u2019s power spectrum.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I ran ten methods on one instance. <strong>Data-driven<\/strong>, trained once on the training scenarios: Decision Tree, Random Forest, Gradient Boosting, Linear and Ridge Regression, SVR, Multi-Layer Perceptron. <strong>Filter-based<\/strong>, with no training step at all, each carrying the ODEs forward in time and correcting against the sensors: Extended Kalman Filter, which linearises at each step; Unscented Kalman Filter, which propagates sample points through the exact nonlinear model instead; and a Particle Filter, deliberately under-provisioned at 50 particles (to save CPU time).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Table<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Method<\/th><th>Family<\/th><th>Isolation accuracy<\/th><th>MSE<\/th><th>CPU\/scenario<\/th><\/tr><\/thead><tbody><tr><td>Random Forest<\/td><td>Data-driven<\/td><td><strong>0.975<\/strong><\/td><td>0.003<\/td><td>0.037 s<\/td><\/tr><tr><td>Decision Tree<\/td><td>Data-driven<\/td><td>0.968<\/td><td>0.0054<\/td><td>0.005 s<\/td><\/tr><tr><td>Ridge Regression<\/td><td>Data-driven<\/td><td>0.844<\/td><td>0.0078<\/td><td>0.006 s<\/td><\/tr><tr><td>Linear Regression<\/td><td>Data-driven<\/td><td>0.835<\/td><td>0.0074<\/td><td>0.006 s<\/td><\/tr><tr><td>Multi-Layer Perceptron<\/td><td>Data-driven<\/td><td>0.762<\/td><td>0.0042<\/td><td>0.026 s<\/td><\/tr><tr><td>Gradient Boosting<\/td><td>Data-driven<\/td><td>0.708<\/td><td>0.0031<\/td><td>0.006 s<\/td><\/tr><tr><td>SVR<\/td><td>Data-driven<\/td><td>0.016<\/td><td>0.083<\/td><td>0.036 s<\/td><\/tr><tr><td>Unscented Kalman Filter<\/td><td>Filter-based<\/td><td>0.486<\/td><td>0.0203<\/td><td>25.5 s<\/td><\/tr><tr><td>Extended Kalman Filter<\/td><td>Filter-based<\/td><td>0.053<\/td><td>26.1<\/td><td>5.9 s<\/td><\/tr><tr><td>Particle Filter (50)<\/td><td>Filter-based<\/td><td>0.017<\/td><td>155.4<\/td><td>134.4 s<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"751\" src=\"https:\/\/llama.gs\/blog\/wp-content\/uploads\/2026\/09\/image-1-1024x751.png\" alt=\"\" class=\"wp-image-137\" srcset=\"https:\/\/llama.gs\/blog\/wp-content\/uploads\/2026\/09\/image-1-1024x751.png 1024w, https:\/\/llama.gs\/blog\/wp-content\/uploads\/2026\/09\/image-1-300x220.png 300w, https:\/\/llama.gs\/blog\/wp-content\/uploads\/2026\/09\/image-1-768x563.png 768w, https:\/\/llama.gs\/blog\/wp-content\/uploads\/2026\/09\/image-1.png 1125w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Two clusters, no overlap. The regressors pay for the model once, offline, in training; the filters pay every second of every scenario, because there is no training step to front-load the cost into. The whole study came to about half an hour of CPU on one machine \u2014 81 percent of it the starved particle filter, while all seven data-driven methods together consumed under two seconds.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Three failures worth naming. <strong>The EKF diverges outright<\/strong> \u2014 that is \\(\\phi\\) collecting its debt, the textbook failure of linearisation against strong nonlinearity, and exactly what the UKF exists to fix: same model, same scenarios, MSE some 1,300 times smaller. <strong>The particle filter was starved and behaved accordingly<\/strong>: 50 particles in a six-dimensional state space gives classic depletion, and it was still the most expensive thing on the board. <strong>SVR with <code class=\"\" data-line=\"\">sklearn<\/code> defaults<\/strong> is no better than the starved filter. \u201cData-driven\u201d is not a synonym for \u201caccurate.\u201d<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Look closely at the Decision Tree, which wins the table. It holds flat at 1.0 through the entire descending ramp before jumping to the correct plateau, spikes to 0.2 when nothing is happening, and glitches once at \\(t = 1090\\,\\text{s}\\) in a fully recovered system. It is not tracking the dynamics. It is recognising a steady-state signature and binning it, extremely well. The UKF, at half the accuracy score, is estimating a continuous state and getting the shape roughly right. Different objects \u2014 which is why the toolkit reports eight metrics and not one.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Where the Labels Came From<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The trees won, by a factor of two on accuracy and seven hundred on cost. This will surprise nobody who has worked in diagnosis. The interesting question is where the training data came from.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Those training scenarios do not exist in nature. Nobody has a record of a valve degrading to exactly 30 percent capacity with ground truth recorded alongside \u2014 as the maintenance vendors themselves concede, <a href=\"https:\/\/maintenanceonline.org\/ai-powered-predictive-maintenance-implementation-guide-2026\/\">if pumps run ten years before failing you have almost no failure data<\/a>. The regressors here were trained on scenarios <strong>simulated from the physical model<\/strong>, the same ODEs the filters integrate online. The model is load-bearing in both columns. The only thing that varies is when you pay for it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Which makes the Random Forest a compiled, cached, lossy artefact <em>of<\/em> model-based diagnosis, fast for the reason a lookup table is fast. Strip the model away and there is no training set, and without a training set there is no forest. Yes, this is synthetic training data, and no, it is not the same activity as a frontier lab training on its own output: this generator is six equations, it conserves mass, its parameters were measured with a ruler and a stopwatch, and when it is wrong you can catch it against a physical system sitting right there. One of these arrangements can be audited by a second-year student with a bucket.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">And the caveats have teeth. Every test fault came from the same generator as the training faults. Show that forest a stuck sensor, a leaking tank wall, or two simultaneous valve faults and it has nothing to generalize from, while the filters \u2014 never trained on anything \u2014 would at least attempt the right calculation. The forest cannot tell you <em>why<\/em>, cannot produce a certificate, and cannot report that it is out of its depth. It will simply answer.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Which returns us to those simulation scores. The EKF failed worse than almost anything here \u2014 and it failed <em>legibly<\/em>. The estimate climbed off the top of the chart and kept going. Nobody glancing at that plot on the way to lunch would mistake it for a diagnosis. It announced its own incompetence in the only language a diagnostic engine has, which is the shape of its output. A method that diverges is a method you can put an alarm on.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Next<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">psycho-killer is open source and the repository is live. Coming shortly: a video of the GUI doing all of the above, and guidelines for building it and pointing it at a system of your own.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">After that, the direction of travel. <strong>Component-based hierarchical models<\/strong>, in the manner of Modelica \u2014 a plant assembled from reusable, nested components rather than hand-written as one flat set of equations, which is the only way this scales past three tanks to a refinery. <strong>A distributed web version<\/strong>, which will be paid, because the compute is not free and neither am I. And <strong>code generation<\/strong>: once the table tells you which method wins on your system, you get a deployable implementation of it rather than a recommendation. That is the whole arc \u2014 measure honestly, choose on evidence, ship the winner.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Ten methods. One model. Eight metrics. Half an hour of CPU, no gigawatts, and a table somebody else can check.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Ceterum censeo slopem esse delendam.<\/em><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">(Cato the Elder ended every speech in the Roman Senate with \u201cCarthage must be destroyed\u201d \u2014 regardless of the topic. This is that, but for AI slop.)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Repository:<\/strong> <a href=\"https:\/\/gitlab.llama.gs\/diagnostics\/psycho-killer\">psycho-killer<\/a><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">What those two studies share is not \u201cAI is bad.\u201d It is that <strong>a diagnostic method which cannot signal its own failure is worse than a mediocre one that can<\/strong>. Remember that; one of the ten methods below fails spectacularly, and the manner of its failure is the most reassuring thing in this post.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Stanford researchers reported this May that robot policies scoring close to 90 percent in controlled simulation succeed at roughly 12 percent of real tasks; on a published peg-insertion benchmark one method managed 98.75 percent in simulation and 12.5 percent on hardware. The robots failing is not the interesting part. The interesting part is that nothing [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-116","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/llama.gs\/blog\/index.php\/wp-json\/wp\/v2\/posts\/116","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/llama.gs\/blog\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/llama.gs\/blog\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/llama.gs\/blog\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/llama.gs\/blog\/index.php\/wp-json\/wp\/v2\/comments?post=116"}],"version-history":[{"count":19,"href":"https:\/\/llama.gs\/blog\/index.php\/wp-json\/wp\/v2\/posts\/116\/revisions"}],"predecessor-version":[{"id":138,"href":"https:\/\/llama.gs\/blog\/index.php\/wp-json\/wp\/v2\/posts\/116\/revisions\/138"}],"wp:attachment":[{"href":"https:\/\/llama.gs\/blog\/index.php\/wp-json\/wp\/v2\/media?parent=116"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/llama.gs\/blog\/index.php\/wp-json\/wp\/v2\/categories?post=116"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/llama.gs\/blog\/index.php\/wp-json\/wp\/v2\/tags?post=116"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}