Burn-in fleet — validation, baseline & calibration
A single place to carefully qualify the burn-in fleet through the campaign:
dilution calibration → the clean-water record → thermal lag → ToF → baseline/QC.
Every stage reads one signal: tlf_amp, the two-way log-model amplitude
— per sensor, every dark-subtracted cell of the LED×bias sweep enters
log(mon2 − dark) = aLED + bovervoltage + c
and c is the amplitude (support-invariant: censored/railed cells enter as bounds, so which cells
survive screening cannot move the answer). It replaced the pooled quadratic
d/d(bias) slope on 2026-08-13. Because it pools every LED and bias there is no operating point to pick and no
per-sensor band to track, which is why absolute mon2 is no longer used anywhere on this page.
Stage 1Dilution calibration — offset and gain
Known tryptophan ladder dosed into the bath on 2026-08-06: 0, 0.1, 0.5, 5, 10 and 50 ppb. Every step is read as a plateau — the window is checked for drift before it is averaged. The 50 ppb window sits at 15:24–15:33 rather than the logged 15:19–15:29, because that dose was still mixing for its first five minutes (24.6 % rise inside the logged window, against ≤ 2.5 % for every other step). The ladder ran before the DI baseline began, which is why the clean-water stages do not read it.
All four panels are built the same way: the mean at each dosed level, per sensor, with hashes at the 95 % CI of that mean (Student t, since the 50 ppb step is only a handful of sweeps). Markers only — a segment drawn between two dose levels would assert a trajectory that was never measured. Each series carries an OLS regression across all six plotted levels, and the R² in the legend is that line’s.
mon2 at the typical operating point
The single-combo reading the fleet used before the slope:
mon2 at LED 512, SiPM bias nearest 3000, one sample, never averaged across
biases. Raw and uncorrected. This is the comparison baseline for the other three panels, and its weakness is
the point: it depends on that one operating point being the right one for that unit. Where auto-range has
moved a sensor elsewhere, the number stops meaning what it means on its neighbours. That is exactly the
dependency the full-sweep slope removes.
Full-sweep TLF slope (uncorrected)
tlf_amp, the two-way log-model amplitude of a
single sample: every dark-subtracted cell of the sweep
(screen 200 < mon2, cells at the 3250 ADC ceiling enter as
censored bounds, ≥6 cells) estimates the same
c in log(mon2 − dark) = aLED + bov + c.
(Replaced the pooled quadratic d/d(bias) slope, still shown on
tlf-dilutions, on 2026-08-13.) Because it pools the whole sweep, no single operating point can carry the reading, and
nothing time-series or temperature enters the feature itself. No temperature correction,
no gain, no offset: this is the signal itself, on the same axes as the panel beside it.
Same chart, temperature-corrected sTLF
Identical construction to the panel beside it, with one change: every sample’s slope is first divided by its own clean-water quench factor, so the two differ only by that correction. The quench is fit on the clean-water window alone (0 ppb throughout, DI outdoors, SiPM ~22 → 37 °C) as a cubic in temperature on log sTLF. The ladder itself cannot be used to fit it, because its temperature rise is collinear with the dose. This is the slope’s own quench, fit on the slope, not the mon2 quench.
Predicted ppb from the corrected sTLF
An offset and a gain taken
from the panel beside it — per sensor,
s = offset + gain · ppb — then inverted to give
predicted ppb = (s − offset) / gain. No other terms.
Plotted against the level actually dosed, with the dashed line at y = x. Six points
and two parameters leaves 4 degrees of freedom, so departure from the diagonal is residual, not zero by
construction. The legend carries each sensor’s R² for that offset/gain fit.
Campaign & window
The window is fixed in the page, not adjustable:
it opens at 2026-08-07 13:15 local, the moment all 25 units were cleaned and put into fresh
DI water outdoors, and closes with the campaign on 2026-08-20 09:00. Every stage reads that one window, so nothing
on this page depends on a control someone may have left in an odd state. Extending it is a code change, in
BASELINE_START, which scripts/build_dilution_calibration.js mirrors as
COOL_FROM.
The full stored record
The complete record for every unit, start of campaign to its close, straight from data/burnin_bands.csv.gz. Unfiltered —
the retired tap baseline, the ToF handling window, the dilution ladder and the dosed bath are all included, so
nothing the later stages leave out is hidden here.
TLF slope (sTLF) — all units
SiPM temperature — all units
ToF turbidity (signal per SPAD)
Internal absolute humidity (leak signal)
Stage 2Clean-water record — DI, outdoors
From 13:15 on 2026-08-07 all 25 sensors were cleaned and placed in fresh DI water, outdoors. This is now the clean-water reference for the whole page: the baseline block, the quench, the ToF calibration and the end-to-end test all read this window and nothing earlier. Outdoors it sweeps a real diurnal range rather than a bath cooldown, which is the condition a deployed sensor actually meets — and, unlike the retired tap window, it breaks the collinearity between temperature and elapsed time.
There is no analyte in DI water, so everything in the raw trace above is instrument. Three terms account for it, fit per sensor on this window:
ln s = a + cubic(T − 21) + c · dT/dt + d · e−n/τ
— a temperature response, a thermal-lag term in the rate of change (the thermistor is on the electronics, not the water), and a settling term where n is samples since the last clean. The clock is sample count, not wall-clock, so it is independent of calendar and cadence. The only disturbance modelled is cleaning; the one event here is 2026-08-07 13:15 local.
Finding the offset: it is the fit intercept.
Set T = 21, dT/dt = 0 and n = ∞ and every term
but a drops out, so ea is the settled blank at 21 °C —
measured directly in clean water, which is what this window is for. Applying it: subtract it,
then divide by the ladder gain mapped into the same frame. The temperature term is multiplicative on the whole
signal, analyte included, so gain@21 = gainraw ÷ ecubic(Tladder−21)
— no blank enters, so there is no circularity. The mapping factor is 0.984 median.
Fit on train, reported on test. Every coefficient, τ included, is fit on the first two thirds of the samples since the clean (—). The last third is withheld and is shaded on the panels below. The split is temporal rather than random, because neighbouring samples are minutes apart and a random split would leak; and because the settling term is a function of samples since the clean, holding out the tail forces it to extrapolate the decay rather than interpolate inside its own training data.
On the held-out third the full model leaves — against — for temperature alone, so the settling term is doing real work out of sample, on — sensors. In-sample it leaves —, and the gap between those two is the honest cost of having fit the thing. A lifetime-count clock was tested and rejected: fit on the earlier window it predicts this one at 17.1 %, so the decay restarts at the clean rather than tracking device age.
mon2 at the calibrated combo
The single-combo reading — LED 512, SiPM bias nearest 3000, one sample, never averaged across biases. Shown first, for comparison only: the rest of the page runs on the full-sweep slope precisely so there is no operating point to track. Watch what the two channels do side by side under the same outdoor temperature swing, and note that mon2 approaches its 3250 saturation screen where the slope does not.
Full-sweep TLF slope
The same samples read as the full-sweep slope: one number per sweep, pooled across every LED and bias that passed the screen, so no single operating point can carry the reading. Raw here means raw — no temperature term, no offset, no gain. Every wiggle below is instrument, because the water is DI and holds no analyte. This window is the clean-water baseline the rest of the page reads.
Fully corrected sTLF — should be flat
The three terms above divided out of the raw trace: temperature, thermal lag and settling, each fit per sensor on this window. What is left should be a flat line, because clean water has nothing in it that varies. The lines are stacked — each sensor settles at its own level, and that level is exactly the offset the next panel removes. The spread between them is per-unit, not drift.
Baseline subtracted — should sit on zero
The same series with each sensor’s settled blank subtracted, that blank being the fit intercept rather than anything read off this chart. Still in sTLF units, not concentration: dividing by gain comes later. The water is clean, so the truth is zero — every line should lie on it and all 24 should overlay. Departure from zero is the honest residual of the whole correction.
Against temperature, before and after
The same data on a temperature axis rather than a time axis, which
is where a quench is easiest to read. Left: raw
ln sTLF against SiPM temperature, with each sensor’s fitted temperature
term drawn through it. Right: the fully corrected signal — temperature, thermal lag and
settling all removed — which should be a horizontal band for every sensor. Any residual
slope or curve is temperature the model did not capture. Both use the model applied in this stage, not a
separate fit.
Clean water, so the truth on the panel above is zero and every departure from it is instrument. Two numbers matter: how far the corrected signal sits from zero (bias, which a per-deployment blank removes) and how much it moves about (noise, which nothing removes). The noise is the one that sets what a real measurement can resolve.
The ppb figures are the sTLF scatter divided by each sensor’s own gain from the Stage 1 ladder, mapped to 21 °C. They are therefore only as good as that gain: it was measured on one ladder, on a different day, over six dose levels. The percentages below need no such transfer — they are the scatter against that sensor’s own blank, measured entirely inside this window.
Against the published requirement
How good does this have to be? The literature answers that through the TLF concentration that corresponds to a given E. coli level. Sorensen et al. (2018) pooled groundwater and surface drinking-water data from India, Malawi, South Africa and Zambia (n = 564) and placed the WHO risk bands at 1.3, 2.4, 6.9 and 27.1 ppb for ≥1, ≥10, ≥100 and ≥1000 organisms per 100 mL. Independent field studies put the detection threshold in the same place: 1.9 ppb from thermotolerant coliforms and 1.7 ppb from E. coli in Malawi (Ward et al., 2021), 2.2 ppb in Uganda (Sorensen et al., 2021), 1.0 ppb in Kenya (Nowicki et al., 2019).
The recreational thresholds sit inside the published High band, so they are read by interpolating between 6.9 ppb at 100 and 27.1 ppb at 1000 CFU/100 mL: the EPA geometric mean of 126 lands at ≈ 7.9 ppb and the statistical threshold value of 410 at ≈ 16.0 ppb. That interpolation is ours, and it carries a drinking-water relationship into recreational surface water, which Ward et al. explicitly caution against. Treat the recreational rows as indicative.
Which noise applies. A threshold crossing is called at a moment, so the relevant figure is the short-term scatter, taken here as the standard deviation of successive-sample differences (÷ √2), which removes any slow drift. That is a different and much smaller quantity than the multi-day figure in the card above, and it is the one comparable to a published detection limit. Both are reported.
For scale, Sorensen et al. (2018) report field fluorimeter detection limits of 1.99 ± 0.53 ppb for the Cyclops-7 and 1.74 ppb for a modified GGUN–FL30, both of which fail their own 1.3 ppb threshold, against 0.17 ± 0.06 ppb for the photomultiplier-based UviLux; field repeatability in Sorensen et al. (2021) was σ = 0.4 ppb. Bedell et al. (2022) reported 0.05 ppb in deionised water for an earlier generation of this sensor.
Excluded sensors
The point of the DI run is to find sensors needing rework and to correct the rest to a flat zero line. These are the ones that cannot be corrected. Withdrawn units are out of every fit and every fleet chart; rework units still report and still hold data, but are kept out of fleet statistics and hidden on the charts above — click them in any legend to bring them back. Ranking is from a clean window with the full 36 °C range and no handling (08-12 14:00 to 08-13 08:40 MT), so it measures the sensor rather than the disturbance.
The same four views as Stage 2, excluded units only
Identical panels to the stage above, so these sit on the same axes as the fleet they were removed from. A withdrawn unit’s trace ends where it stopped reporting, and units without a usable calibration are absent from the two corrected panels — which is itself the reason several of them are here.
mon2 at the calibrated combo
Full-sweep TLF slope
Fully corrected sTLF — should be flat
Baseline subtracted — should sit on zero
Stage 3Thermal lag — what temperature alone misses
Correcting on temperature alone leaves a hysteresis: at the same measured SiPM temperature the signal does not read the same coming down as it did going up. Across — that gap averages —, and it did not shrink when the post-clean decline stopped, so it is not a settling transient. Warming and cooling are separated per sample from the sign of dT/dt, not from a fixed peak time, and the gap is measured at every whole degree the fleet covered rather than at one chosen temperature. That second point matters: quoting a single bin was hiding the result below.
The likely reason is that the thermistor is on the
electronics and there is no thermometer on the water or the optical window. Outdoors the enclosure
follows air quickly while the water body lags it, so at the same measured temperature the medium the
light passes through is not at the same temperature on the two limbs. A lag shows up as a dependence on the
rate of change, so the test is to add a dT/dt term and see whether the loop closes.
(The UV LED reports its own temperature, but it is the same thermistor —
r = 1.0000 against the SiPM — so it adds nothing.)
dT/dt actually does here is shift the cooling limb up against the warming limb by roughly the
same amount everywhere, so whether it looks like a fix depends entirely on which temperature you read it at:
at the bins where the ratio already sat just under 1.000 it lands near 1.000, and at the bins where it sat
above, it moves further away. That is an offset, not a lag being absorbed. The two panels below therefore
look almost identical, and they should — the binned curves differ by well under a percent on an axis
spanning about thirty. The thermal-lag hypothesis is not supported by this test. It is not
refuted either: dT/dt over a fixed 30-minute window is a crude stand-in for a real thermal time
constant, and settling it properly needs a thermometer in the water, which these units do not have.Fleet loop: temperature term only
Every sensor corrected on temperature alone, then pooled into half-degree bins and split by limb. If measured SiPM temperature told the whole story the two curves would lie on top of each other, because the same temperature is the same temperature whichever way it is heading. They do not: the cooling limb sits below the warming limb across the range.
Same, with a dT/dt term added
The identical construction with one extra regressor: the rate of temperature change over a centred 30-minute window. If the gap were a lag between the thermistor and the water this should absorb it and the limbs should close onto one curve. Read the figure under each panel rather than the picture: the change is well under a percent, which is why the two look the same.
Per sensor, before and after
The fleet curves can hide a term that helps some units and
hurts others, so this is one point per sensor: mean |hysteresis − 1| over every
whole-degree bin that sensor covered, without the term on the x axis and with it on the y. The origin is no
hysteresis. Points below the dashed diagonal were helped, points above were made worse.
SiPM temperature over the same window
The driver behind the other three panels: outdoors the sensors sweep a real diurnal range rather than a bath cooldown. A cooldown is monotonic, so temperature and elapsed time are collinear and cannot be told apart. A day/night cycle breaks that, because the same temperature recurs at different times and on both limbs, which is what makes a hysteresis test possible.
Honest limits: the term is fit and tested on the same cycles, so any variance it removes is in-sample and flattered. It does not buy accuracy either — residual SD goes —, which is nothing. Nothing in this stage feeds the correction applied in Stage 2; it is a diagnostic, and on this evidence the honest reading is that the remaining hysteresis is still unexplained.
Stage 4ToF turbidity calibration
The clean-water period is 0 NTU by construction, so every bit of variation in
signal_per_spad_kcps over it is instrument, not water. That makes it a ground truth: fit a
correction there, and the corrected series must flatline on zero.
spscorr = sps − g · (T − 21),
then ΔSPS = spscorr − sps₀.
The axis is ΔSPS, not NTU. No formazin reference was run on these 25 units, so there is
no scale factor to apply. The pooled k ≈ 1.59 on
turbidity comes from two other sensors whose own slopes were 2.05 and 1.38
— a 1.5× disagreement between just those two — so transferring it here would print a unit we
have not earned. Everything below is still a valid relative turbidity signal; converting it to NTU
needs a formazin ladder on this fleet. g is per sensor, not fleet-wide: measured
r(SPS, T) runs from −0.69 to +0.91 across the 24 units, so it does not agree even in sign
and a single coefficient cannot be justified. The fit uses the central 90 % of each sensor’s
clean-water SPS, because three units (500155, 500230, 500228) show sustained excursions there
— 500155 runs 181 consecutive readings above twice its median — and those are events in the
optical path, not drift to be absorbed into a baseline.
Calibrated ΔSPS over clean water
The corrected ToF signal across the clean-water window, with each sensor’s own SPS zero and its linear-in-temperature coefficient g taken out. The water is 0 NTU throughout, so every line should sit on zero; what is left is the turbidity noise floor, in the instrument’s own units.
Calibrated ΔSPS at each dosed level
Same treatment as the corrected sTLF chart in Stage 1: mean ΔSPS per dosed level, per sensor, 95 % CI hashes, OLS regression with R² in the legend. The tryptophan ladder adds no particles, so every line should be flat at 0 — any slope against concentration is the ToF channel responding to something it should not.
Stage 5Indoor bath control — 50059 & 500214
Test ended 2026-08-17 07:44 local (5.9 days). Data below is the complete record. Headline outcomes: 50059’s 08-14 basin-move step (×2.6 sTLF, −15 % ToF) survived two deliberate shakes — only its ToF reverted — while 500214 in the same water stayed within a few percent throughout: handling artifacts are unit-local, shake-resistant, and sit on the LED/SiPM optical path, not the ToF window.
Two units ran from 2026-08-11 10:00 local in a water bath indoors, tracked separately from the 25-unit outdoor fleet. They are a control on the temperature confound: indoors the SiPM sits at —, against the fleet’s 21–40 °C diurnal swing. Any drift here therefore needs no temperature model to see, and cannot be a temperature artifact.
Three things make them not directly comparable to the fleet on absolute level or rate. They are the ToF firmware-swap pair, so the two may be on different builds from each other. Their cadence is far slower — roughly 4 sweeps an hour against the fleet’s 60 — so per-sweep dose accumulates about 15× more slowly, which matters for any comparison framed in dose rather than calendar time. Both start from the same instant here, so the two records are directly comparable in elapsed time.
sTLF over time
The LED-128 matched-filter amplitude, each unit normalised to its own first readings. No temperature correction is applied and none is needed — the bath holds temperature to a fraction of a degree, so this is the raw drift with the largest confound physically removed rather than modelled away.
ToF signal
The IR time-of-flight return (signal per SPAD, kcps), reflected off
the cap interior. The two units run different firmware builds, so absolute levels are not comparable between
them — what matters is each unit’s own steps. A ToF step at a handling event with
distance_mm unchanged means something entered the optical near field (a bubble, debris), not that
the cap moved.
SiPM temperature
Shown to prove the control holds. If this line is flat, the panel beside it is measuring drift and nothing else. Compare with Stage 3, where the same axis spans about 16 °C and temperature dominates everything.
Read with care while the record is short. So far 50059 has moved — and 500214 —. At roughly four sweeps an hour a day of running is only ~100 points, so these rates are indicative until the record is several days long.
18:37 shake outcome (the bubble kill test): a split verdict. Shaking 50059 in place did not collapse its ×2.6 basin-move step — sTLF ticked up (+9 % first sample, settling +5 %) — but its ToF snapped fully back 41 → 35 → 41, recovering its pre-move value. So the IR obstruction was loose (dislodged by the shake) while the TLF-boosting change is shake-resistant: either a bubble held in a crevice at the window, or the unit’s position/reflection geometry, which an in-place shake does not reset. 500214, shaken the same way, stepped up +2.4 % — consistent with the fleet-wide finding that vigorous shaking adds signal. Next discriminator: lift 50059 out and re-insert slowly at a tilt (the manufacturer-prescribed bubble-shedding move), or wipe its face — a positional cause survives both; a crevice bubble does not.
Basin carry — 2026-08-13, the fleet’s own handling step (±1 h)
The outdoor basin was carried to the DI supply at 11:52 local and the tank refilled from 12:02. During the carry itself — before any water changed — 21 of 24 units stepped UP in raw sTLF (median +3.3 %; 500179 +303 %, 500108 +166 %, 50084 +159 %, 50088 +133 %) while the bath temperature fell smoothly, so the jump is not thermal, and only 1 % of the window’s rows are flagged out-of-water. This is the same signature as the indoor pair’s 08-14 basin move (50059: sTLF ×2.6 up, ToF −15 %), consistent with sloshing depositing bubbles in the optical near field. The refill and the 40 °C ramp that followed then drove the level down — the −35 % headline step is that composite, not the carry itself.
sTLF, normalised to each unit’s pre-carry level
Raw full-sweep amplitude, each unit divided by its own 10:52–11:45 median. Shaded band = the carry (11:52–12:02); after it the refill begins. Out-of-water rows are excluded, as everywhere on this page.
ToF signal, normalised the same way
Signal per SPAD, each unit divided by its own pre-carry median — absolute SPS differs unit to unit, so the step is what is comparable. A ToF step coincident with the sTLF step, with no cap change, is the near-field (bubble) signature.
In-place shake — 2026-08-14 19:12, all 24 units (±1 h)
The companion test: the same 24 units shaken in place at 19:12 local, with no transport, no water change, and the bath at ~3.7 °C after a ~48 h cold hold — strongly undersaturated water, so any up-step here cannot be a supersaturation-grown bubble. With an hour of post-shake data: 20 of 24 units up in sTLF, median +19 % (500103 +278 %, 50091 +209 %), while ToF barely moved (median 0.0 %) — unlike the indoor basin move, the fleet’s TLF steps came without a ToF signature. Four units went down (500179 −45 % with ToF +17 %, 50084 −29 %, 500226 −21 %, 500108 −12 %), so shaking removes material from some faces while adding it — most plausibly entrained surface air — to most. The handling itself delivers the scatterer; gas saturation state is not required.
sTLF, normalised to each unit’s pre-shake level
Each unit divided by its own 18:12–19:10 median. Shaded band = the shake window. Out-of-water rows excluded.
ToF signal, normalised the same way
A coincident ToF step with no cap change is the near-field (bubble) signature, as on the indoor pair.
Stage 6Blackout caps — 4 units, fitted 2026-08-12 12:35
New blackout caps went on 500146, 50095, 500226 and 500124 at 12:35 local. 500124 has since been withdrawn — it was leaking, so this stage shows the remaining three. Its dark channel had collapsed from ~167 counts to 0 against a fleet 171–172 95 minutes before the cap was fitted, so the cap did not cause the failure and the earlier “capped versus control” numbers that included it were carrying a leaking unit. The question is what the cap interior does to sTLF — both whether it shifts a unit’s level, and whether a common cap makes units alike. Every unit’s blank differs, and if the inside of the cap is part of that, fitting the same cap to several units should pull their levels together.
Showing only the four means there is no control group to difference against, so the level figure is a before-versus-after and still contains the bath’s temperature swing. The spread panel is the one that survives this: it asks whether the four agree with each other more closely than they did, which does not need a control. Each unit is normalised on a window 8–4 h before the change, deliberately separate from the 2 h “before” window, so the pre-change spread is free rather than forced to zero.
Level — sTLF against each unit’s own earlier value
Each unit normalised to its own median over the window 8–4 h before the change, so the lines share a scale without being pinned to 1.0 at the moment the caps went on. All four are under test; there is no control series. Temperature moves all four together, so a common step at the dashed line is as likely to be the bath as the caps — what the caps would show, and the bath would not, is the four converging on each other.
Agreement — spread across the four
The point of the test. Each line is the spread of normalised sTLF across the units in that group, so lower means the units agree with each other more closely. If a common cap interior is what makes units alike, the capped line should fall below the control line and stay there.
—
Stage 7Baseline, validation & QC
One row per sensor. The baseline block is the median sTLF and
signal_per_spad_kcps (ToF) over the baseline sub-window, with the window mean temperature —
baselines are temperature-dependent, so it is recorded alongside. These become each sensor's canonical
clean_water offsets; a stable baseline has a low CV, a high CV means the window is not clean or
settled yet. The QC block is readiness over the same window: stability of TLF / ToF /
temperature judged over the last 4 h, the leak signal (internal absolute-humidity trend — rising
means ingress) and battery. Click any header to sort.
blank ÷ gain is the ppb-equivalent of each sensor’s blank, taken from the raw ladder fit. It converts blank instability into the units that matter: a unit at 36 ppb loses 0.4 ppb for every 1 % its blank moves, one at 1790 ppb loses 18. It is the acceptance criterion that decides how stable a given sensor has to be. Green ≤ 60, amber > 300.
Per-sensor baseline & QC
Stage 8Air drift test — all 24 units, 08-18 11:00 to 08-20 09:00
Started 2026-08-18 11:00 local. The whole fleet was pulled from the ~29.3 °C bath and left in air. This is the bubble discriminator: a dry window cannot hold or grow a bubble, and the LED still fires every sweep against a fixed target. If the residual burn-in drift (fleet median −0.66 %/day over the final 46 h stable cold hold in water) continues in air, it is LED/electronics aging and the cycle-count correction stands. If it stops, it was water-side — bubbles or films — and window maintenance, not modelling, is the fix. In between: both, in measurable proportion.
This stage is closed. The air test ran 2026-08-18 11:00 → 2026-08-20 09:00 local, about 46 h, and every series, drift fit, fleet median and table entry here is cut at 09:00 — not merely hidden from the charts, so no in-water reading can leak into an air-phase result. One shared end for all 24 also means every unit’s rate covers the same window. Afterwards the fleet went into water in two groups: five units into the vacuum container for the degassing test at 09:29, and the other 19 into the shared DI tank at 11:40, where they join the FDOM/chlorophyll-a batch — that record lives on the variant page, not here.
Reading rules. The pull itself is a handling event — expect a step and ignore it; windows came out wet and cool by evaporation, so the first 2 h are settling, excluded from the fit. Per-unit rates appear once a unit has ≥12 h of settled air data (the same floor the drift-correction validation set; shorter fits are coin flips). There is no temperature control in air — the fleet rides the building’s ambient diurnal — so the drift fit estimates each unit’s own temperature coefficient jointly with the time slope (ln amp = a + bT(T−25) + slope·days, the same structure as the canonical drift correction). The table shows the temperature-corrected rate, the fitted coefficient, and the raw slope for comparison; until a full diurnal cycle decorrelates temperature from time the two are confounded, and such fits carry a ⚠ instead of a verdict. These charts deliberately include the rows the out-of-water detector excludes from every water stage above.
sTLF in air — uncorrected
Full-sweep amplitude (tlf_amp), each unit normalised
to its own median over the first settled hour in air. Whatever wiggle the building’s temperature puts in,
you see here.
sTLF in air — temperature-corrected
The same series divided by each unit’s own fitted temperature response (ebT(T−25), from the joint fit). What remains is drift and noise. The coefficient is provisional until a unit clears the 12 h floor and firms up as diurnal cycles accumulate — early on this panel can over- or under-correct. Flat lines here = no drift dry.
ToF signal
In air the IR return comes off the cap interior instead of water — the step at the pull is the air signature, and each unit’s level should then hold. A wandering ToF in air means something on the window (droplet drying, debris), not turbidity.
SiPM temperature
Air-phase temperature record. The drop at the pull is evaporative cooling off the wet housing; after that the fleet rides the ambient diurnal.
Battery
Pack voltage. The fleet came off mains at 13:53 local on 08-19 (teal marker), so everything right of that line is a discharge curve. This firmware samples every minute and flattened a pack in about three weeks, so the slope here is effectively the clock on the air test — a unit that dies truncates its own drift record.
Per-unit air drift vs the water reference
Stage 9Degassing test — moved to its own page
The five units in the vacuum container — 500220, 500225, 500226, 500108, 500146 — now have their own page: Degassing test →. It reads a dataset scoped to start at the test itself (2026-08-20 09:29 local), so nothing on it is diluted by this campaign's history.
Stage 10Drift correction — the production model on this campaign
The same correction a deployed sensor would apply, run over the frozen campaign:
ln(amp) = Pd(tn) + (r1+r2tn)·dT/dt
+ slope·days, fitted per sensor on its own sipm_temp_c and
evaluated back to 20 °C with dT/dt = 0. Precomputed by
scripts/build_burnin_drift.py, because this archive is 507,660 rows and the cohort is closed.
Each unit gets the richest model its data supports, falling back cubic + lag → cubic → linear + lag → linear → none when a fit implies an implausible correction (>8×) or leaves more than 8 % residual scatter. The pass rate is the finding, not a footnote.
What the correction leaves behind
Fleet median with interquartile band, normalised to each unit’s own first settled window. Blue removes temperature and leaves drift; green removes drift as well — the complete production correction, which should be flat. Shown for the two windows where the model passes fleet-wide; on the outdoor windows too few units pass to draw a fleet band honestly.
Cold hold
Air phase
In-sample, and it matters here. The drift term is fitted on the same window it is removed from, so a flat green trace shows the fit converged, not that the rate would hold tomorrow. The forward test on findings is the honest one, and there extrapolating a fitted slope was worse than assuming the last baseline holds.
The instrument, and what each correction is for
Part numbers are from
SweetSenseInc/tlf_hardware (v0.1/tlf-bom.csv,
v0.1-led/tlf-led-bom.csv); wavelengths from the TLF method documents. Everything in the
measured column comes from this campaign's own data, and is stated separately from
the datasheet because in one important case the two differ by a factor of two.
Optical chain
| Stage | Part | Ref | Note |
|---|---|---|---|
| Excitation | CUD7GF1B ×4 | D1–D4 | SETi / Seoul Viosys, ~273 nm UV-C |
| LED drive | TPS92390 | — | constant-current, so drive is controlled not assumed |
| Emission | — | — | ~350 nm, tryptophan-like fluorescence |
| Detection | AFBR-S4N33C013 | U11 | Broadcom NUV-HD SiPM, 3×3 mm, 9815 microcells |
| Range / turbidity | VL53L4CD | — | ToF; ranges off the cap interior, not the water |
| Humidity / temp | HDC2080 | U3 | enclosure humidity — the leak signal |
The SiPM's peak sensitivity is 420 nm and its quoted PDE (43–54%) is specified there. Emission at ~350 nm sits on the falling shoulder of that curve, so absolute sensitivity is below spec — though this does not affect the temperature coefficient, which depends on breakdown voltage rather than on PDE magnitude.
Detector: datasheet against measurement
| Quantity | Datasheet | Measured here |
|---|---|---|
| Breakdown voltage Vbd | 26.9 V | 28.82 V (fleet median, from each unit's own bias sweep) |
| Vbd uniformity | 180 mV (3σ) | 0.72 V spread across 20 units — 4× the spec |
| dVbd/dT | 26 mV/K | 2.6 DAC counts/K, recovered by scanning the shape-alignment shift — the same number at ~100 counts/V |
| Overvoltage in service | 3 V “typical” | 1.18 V (bias 3000 counts = 30.00 V) |
| Gain tempco (relative) | 0.69 %/°C at 3 V 2.21 %/°C at 1.18 V |
1.73 %/°C (fixed-bias minus overvoltage-keyed) |
The datasheet's gain tempco is conditional on operating point: it is dVbd/dT ÷ Vov, so it doubles at half the overvoltage. Taking the 3 V figure at face value disagreed with measurement by 2×; using the measured 1.18 V it agrees to ~25%, the remainder being gain non-linearity near breakdown plus smaller PDE and crosstalk terms. Two consequences: running at 1.18 V makes the detector roughly 2.5× more temperature-sensitive than it would be at 3 V — a firmware setting, not a physics limit — and a Vbd spread of 4× the specification is why per-sensor calibration is unavoidable.
The correction chain
- Sweep → amplitude. The reading is a product,
mon2 = dark + G(Vov)·L(LED)·Φ·η, so subtracting the measured dark and taking
logs makes a sweep additive:
y = aLED + bovervoltage + c. Fitting a free offset per LED drive assumes no linearity of the emitter; keying the detector shape to overvoltage rather than raw bias follows the physics and was confirmed by the data (scanning the shift minimises scatter at exactly the datasheet's 2.6 counts/K). Clipped cells enter as censored bounds, not discards. Because every cell independently estimates the samec, the estimate cannot move when one is lost — which is what fixed a sensor that was alternating between two discrete values. - Out-of-water screening. A sensor clear of the water reads a higher ToF signal-per-SPAD. Flagged against the unit's own trailing 45 min median (>1.15×) and against the fleet at the same instant (>1.3×); both are needed, because a trailing baseline cannot see a sustained excursion and a fleet comparison cannot see a brief one. ~1% of rows, dropped before any fit reads them.
- Temperature: a single exponential, ρ ≈ 2 %/°C, not a polynomial. d(ln signal)/dT is constant at −1.87 %/°C from 3 to 39 °C, varying ±3% across the interior — there is no curvature for a cubic to fit. It is also what the physics predicts: LED output and fluorescence quench are both exponential in temperature and they multiply. Held out by temperature (train on the middle, predict the ends, which is what a deployment does): exponential 1.28%, quadratic 1.81%, cubic 2.17%, quintic 15.03%.
- Every temperature counts once, not once per second. The bath holds one temperature for hours and crosses the rest in minutes, so a unit's training samples run 929 in the 5 °C bin against 55 at 20 °C. Ordinary least squares weights samples, which silently hands the slope to whichever temperature the bath sat at longest, and ρ came out steep at 2.51 %/°C. Weighting by inverse temperature density instead — each 1 °C bin carrying equal total weight — gives ρ = 2.29 and cuts the clean-window residual from 3.175% to 1.354% on identical data, model form and training span. It improved 16 of 20 units; three of the four that did not were already under 1%.
- Thermal lag, a dT/dt term, measured under that weighting: 1.730% → 1.356% on a clean window. Under sample weighting the same term bought nothing (3.147% → 3.176%) — it could not show its value while the fit was being dominated by an isothermal hold, which is exactly where a lag term has nothing to say.
- Drift: currently not applied. It is fitted and reported, not emitted. On a clean window a log-time drift term made things worse, and the metric that says otherwise is computed on a window containing a handling step — where a flexible term will always look good. Per-unit step offsets at automatically detected changepoints were also tried, and were worse again (1.611% vs 0.961%): the detector finds 1–5 segments per unit and mostly fits noise. The handling log, not a more flexible model, is what closes this.
What is still open
Removing the detector term leaves 1.85 %/°C for LED output and tryptophan quench combined. Splitting them needs the CUD7GF1B optical-power-versus-temperature curve, which is not in the BOM or the repo — so the fluorescence coefficient, the one that actually belongs in a concentration model, is currently absorbed into ρ rather than measured. Separately, the handling steps of 2026-08-13 are not modelled: they are per-sensor multiplicative changes in optical throughput, invisible to every diagnostic channel, and no temperature or drift correction can remove them.