中文 | English
This document describes the actual algorithm implementation for online titration endpoint detection in AutoTitrator: the potential channel, the spectral channel, the two-state Kalman fusion, the final refinement, and the reliability diagnostics. The implementation lives in TController/crates/controller-core/src/processing/; the workflow wiring is in src/workflow.rs. The protocol and data formats are covered in companion documents.
During a titration, the sample pump injects sample into the reaction vessel and the titrant pump adds titrant at a fixed rate. The endpoint is the point of sharp change in potential or spectrum near the equivalence point. Online detection must produce a verdict while liquid is still being added, using only samples collected so far (causal).
The system uses two independent channels, each producing an endpoint candidate, then fuses them with a Kalman filter:
flowchart TD
A[Potential channel<br/>volume-domain derivative + reentrant event machine] --> C[Potential candidate<br/>deepest event]
B[Spectral channel<br/>JS speed + permanent-color discrimination] --> D[Spectral candidate<br/>permanent events win]
C --> E[Two-state KF fusion<br/>endpoint volume + spectral delay]
D --> E
E --> F[EndpointResult<br/>volume / method / confidence / reliability]
Both channels consume historical samples only. Either modality's endpoint can be revised later (the spectral candidate can be superseded by a stronger excursion, the potential candidate by AMPD refinement), so the KF reruns from scratch whenever the observation pair changes, rather than gating a revision with stale state.
Adaptive thresholds are the theme of the 2026-08 rework: the trigger thresholds of both channels are no longer pure constants but are estimated online from robust baseline-period statistics (median / MAD); the old constants are demoted to lower-bound floors. Three design constraints hold throughout:
- Causal: statistics come only from samples before the baseline is armed;
- Degeneracy-safe: with zero dispersion (MAD → 0) the estimate falls back to the constant floor, so behavior on flat synthetic data matches the legacy version bit-for-bit (locked by tests);
- Bounded: estimates are clamped above, defending against the baseline window itself being polluted by an anomalous transient.
The potential channel locates the steepest downward excursion in the dE/dV curve. It works in four stages: volume-domain differentiation, smoothing, threshold estimation, and a reentrant event machine.
The derivative's independent variable changed from time to volume:
Motivation from field data: the host timestamps contain micro-bursts (in the 2026-08-26 dataset, 15% of samples have
When the volume step is below POT_DV_FLOOR = 1e-4 mL (repeated frame) or negative (non-monotonic), the derivative holds its previous level — mirroring the spectral channel's anchoring.
The voltage and its derivative each pass through a first-order causal EWMA (
Before the accumulated volume exceeds POT_OBSERVE_VOL = 0.1 mL, the channel only collects derivative samples. At the end of the observation period it computes robust statistics: the median level
Enter/exit thresholds use a relative ladder:
- Floor 0.02 V/mL: with near-zero noise the behavior falls back to the fixed legacy action;
- Cap 5.0 V/mL: defends against a polluted observation window pushing the threshold out of reach;
- The exit offset is a fixed ratio (
POT_EXIT_RATIO = 0.2) of the enter offset.
When MAD is unavailable (degenerate samples), the channel falls back to the mean–standard-deviation formula with the same clipping.
Idle/EndConfirmed→Tracking: the smoothed derivative falls below the enter threshold.Tracking: new lows update the candidate volume; when the derivative rises back above the exit threshold and the volume gained since entry exceedsPOT_CONFIRM_VOL = 0.15mL, the episode is committed as a confirmed event and the state returns toEndConfirmed(reentrant).- The reported endpoint is the deepest event's candidate volume; a later event must be deeper by
POT_SUPERSEDE_RATIO = 1.5×to supersede.
The legacy implementation was a one-shot latch (first confirmation locked, all later dips ignored). On the 2026-08-26 dataset it locked onto a noise-induced shallow dip and completely missed the true inflection at 5.3–6.3 mL — the same failure mode the spectral channel had already fixed, so both channels now share the strongest-event semantics.
The spectral channel turns "shape change" into a scalar speed that drives a reentrant state machine, with permanent-color discrimination applied at event commit. The main measure is the Jensen–Shannon divergence.
The early implementation drove events with cross-entropy, which has a structural flaw: cross_entropy_excess (KL after subtracting the self-bound) remains as the use_jsd = false compatibility path.
The speed is the JS divergence between the current smoothed spectrum and the last advancing anchor frame, divided by the squared volume step:
Anchoring to the advancing frame is deliberate: in production several spectra share one pump volume, so anchoring to the previous frame would inject zero steps and dilute real events. Divergences below the rounding floor JS_FLOOR = 1e-14 are not normalized (platform noise ~2e-12 amplified by
The baseline is established within volume ≤ SPEC_BASELINE_MAX_VOL = 0.30 mL at SPEC_BASELINE_FRAMES = 12 frames.
Adaptive thresholds (independently switchable) are armed from two robust statistics when the baseline is established:
| Threshold | Source statistic | Formula (K=8) | Floor / Cap |
|---|---|---|---|
enter θ_enter |
speeds of advancing frames during baseline | median + K·MAD-σ̂ | 0.05 / 0.5 nats/mL² |
exit θ_exit |
fixed ratio of the enter value | 0.16 × θ_enter | — |
base θ_base |
JS residuals of baseline frames vs the final baseline | median + K·MAD-σ̂ | 3e-7 / 5e-5 |
The caps defend a scenario seen in the field: the baseline window polluted by an early strong transient (in-window median speed already 0.26 nats/mL²) would otherwise push the adaptive threshold out of reach. If statistics are insufficient (< 6 advancing frames) the baseline window extends to 0.45 mL; failing that, fixed constants apply for the whole run.
Idle/EndConfirmed→InChange: smoothed speed ≥ enter threshold and baseline JS ≥ base threshold. A lookback window (8 frames) seeds the peak so the candidate lands on the fastest frame rather than the crossing point.InChange: new speed highs update the peak; speed falling back ≤ exit threshold accumulates recovery frames.InChange→EndConfirmed: recovery frames ≥SPEC_CONFIRM_FRAMES = 10and event span ≥SPEC_MIN_EVENT_VOL = 0.08mL — or the plateau path (below).
EndConfirmed is reentrant: excursions are recorded into an event list. This hysteresis design fixed a real regression where a one-shot latch locked the endpoint onto a transient 0.97 mL before the true endpoint.
Speed thresholds alone cannot separate two chemically different spectral changes:
- Transient excursion: color appears then fades completely back to the pre-event level (recovery ratio ≈ 1×);
- Endpoint form switch: the indicator changes form, partially fades, but settles at a new level clearly above the pre-event one (measured step ratio ~3200×).
The criterion is the self-referenced recovery gap of the baseline distance. Each event snapshots the pre-event quiet-period median level
The ratio is taken against the pre-event level rather than the initial baseline, so slow drifts such as cumulative dilution cancel out. Three mechanisms:
- Commit-time snapshot: the
permanentflag is provisionally evaluated per the formula above; - Retroactive demotion: a transient can be misjudged as permanent because its recovery tail has not settled at commit time; quiet frames later re-evaluate the flag against the current level and demote + re-elect the strongest event if unsupported;
- Plateau confirmation: in high-noise systems where the speed never falls back, a converged baseline-distance plateau within the episode (±35% between window halves) with a clear elevation also commits — the "step-and-hold" shape that once kept the state machine stuck in InChange for an entire run.
The selection rule upgrades accordingly: permanent events unconditionally beat transient events regardless of peak speed; within the same tier the 1.5× supersede hysteresis applies. The switch (permanent_color) is independent of the adaptive-threshold switch and can be combined with fixed thresholds; legacy_fixed disables everything and matches the legacy behavior bit-for-bit.
The fusion layer combines the potential and spectral endpoint candidates. Two-state linear KF: state = [endpoint volume, spectral delay].
The potential observation is directly the endpoint volume; the spectral observation is endpoint + delay, so the delay is estimated.
| Parameter | Value |
|---|---|
| Potential observation noise std | 0.012 |
| Spectral observation noise std | 0.025 |
| Delay std | 0.08 |
| Process noise std | 0.004 |
| Delay prior | 0.02 |
| NIS gate | 6.635 |
The first observation initializes the filter: a first potential observation sets state
Fixed observation variances encode the signal-to-noise ratio assumed at calibration time; in noisy systems fixed R overstates confidence and loosens the NIS gate. Before the first observation of each run, each modality's R is scaled once by the normalized noise ratio
Reference values
Each innovation is a scalar, normalized and compared against the chi-square distribution with 1 degree of freedom (99th percentile 6.635; the legacy 9.21 was a dof mismatch, now fixed). Rejected observations do not update the state and are recorded in the snapshot for diagnostics.
A superseded spectral candidate or an AMPD-refined potential candidate changes the observation pair. On change, endpoint.rs runs kf.reset() before re-observing, so revisions are never gated by stale state.
detect() returns a result based on the two channel candidates and fusion capability:
| Potential | Spectral | KF fusable | method | confidence |
|---|---|---|---|---|
| yes | yes | yes | consensus | high |
| yes | yes | no (KF off, |
consensus | high |
| yes | yes | no (NIS gate failed) | conflict | low |
| yes | no | — | potential_only | medium |
| no | yes | — | spectral_only | medium |
conflict means "both modalities confirmed but the NIS gate rejected fusion; fall back to the potential endpoint" — it still carries potential evidence, so the workflow uses it to control the pump. spectral_only has no electrode evidence and never controls the pump.
Dynamic pump-stop target: after T=1, every decision refreshes the endpoint candidate from the latest potential-evidenced report, and the T=2 criterion (
When the titration reaches
- The input is the twice-EWMA-smoothed derivative: isolated timestamp-glitch spikes in the raw difference directly pollute multiscale detection;
- The search is confined to a ±0.5 mL window around the strongest event's candidate: unrestricted global search fails on non-stationary derivatives — when the reliable scale degenerates to the maximum (σ = L−1), few candidate indices remain and a trivial score wins, which once dragged a correct event to a wrong location (measured 4.20 mL vs the true ~5.37 mL);
- The peak position must lie within
AMPD_MAX_POSITION = 0.9of the record (tail peaks lack scale support).
If the window has no AMPD peak or no confirmed event exists, the original candidate is kept. A successful refinement updates the strongest event in place so the KF re-run stays consistent.
Reliability aggregates the status and reason codes:
| Status | Meaning |
|---|---|
| CONFIRMED | both channels confirmed and KF fused |
| CONFLICT | both channels confirmed but the NIS gate rejected |
| CANDIDATE | single channel confirmed |
| CONFIRMING | either channel tracking |
| UNOBSERVABLE | no data |
| EARLY_WARNING | partial data, no candidate |
Diagnostics also carry data quality (potential/spectral sample counts, valid frames, repeated volume, non-monotonic volume, baseline readiness), the KF snapshot (endpoint_std, NIS, innovation), adaptive threshold snapshots (adaptive.spectral_enter/exit/base, adaptive.potential_enter/exit, omitted in legacy mode), and reason codes (kf_innovation_gate, spectral_endpoint_superseded, baseline_pending, …), pushed to the frontend with the backend://state snapshot.
| Channel | Parameter | Value | Meaning |
|---|---|---|---|
| Potential | POT_V_ALPHA / POT_D_ALPHA | 0.15 / 0.05 | voltage / derivative EWMA |
| Potential | POT_DV_FLOOR | 1e-4 mL | advancing-volume floor (hold below) |
| Potential | POT_MIN_ENTER / POT_MAX_ENTER | 0.02 / 5.0 V/mL | adaptive offset floor / cap |
| Potential | POT_EXIT_RATIO | 0.2 | exit offset = 0.2 × enter offset |
| Potential | POT_SUPERSEDE_RATIO | 1.5 | late-event supersede factor |
| Potential | POT_CONFIRM_VOL | 0.15 mL | volume gain required to confirm |
| Spectral | SPEC_JS_ENTER / EXIT | 0.05 / 0.008 nats/mL² | fixed thresholds (= adaptive floors) |
| Spectral | adaptive K / caps | 8 / 0.5 and 5e-5 | median + 8·MAD, capped |
| Spectral | RECOVERY_GAP_RATIO | 3.0 | permanent-color recovery ratio |
| Spectral | SPEC_SUPERSEDE_RATIO | 1.5 | supersede hysteresis |
| Spectral | JS_FLOOR | 1e-14 | rounding floor |
| KF | DEFAULT_NIS_GATE | 6.635 | chi-square (dof 1) 99th percentile |
| KF | quality scaling | ×(ρ/ρ_ref)² ∈ [1,4] | one-sided amplification |
| AMPD | AMPD_MAX_POSITION / REFINE_WINDOW | 0.9 / ±0.5 mL | peak position cap / search radius |
Calibration rationale and per-dataset validation records live in tools/adaptive_tuning_notes.md (repository root, outside SourceCode).
tests/endpoint_reliability.rs: JS symmetry/boundedness, causal features, repeated-volume level hold, supersede hysteresis, rounding floor, KF reset, AMPD vs dense reference.tests/workflow.rs: the T=1 deadlock regression (conflict verdicts with potential evidence still control the pump; spectral-only never does).tests/adaptive_params.rs: flat-baseline legacy parity (zero dispersion falls back to floors, bit-for-bit), armed thresholds land in [floor, cap], legacy mode emits no adaptive keys, one-sided KF clamping.tests/potential_reentrant.rs: a late deep dip supersedes an early shallow latch, volume-domain differentiation is immune to timestamp jitter (shuffled timestamps give identical results), fixed and adaptive modes agree.tests/permanent_color.rs: transient+permanent picks the permanent event, a pure transient is reported and later demoted, a later stronger transient cannot dethrone a permanent event.examples/replay_csv.rs: replays real titration CSVs through the production path;--dump-statsexports per-frame diagnostics,--fixedruns the fixed-threshold control.tests/tmp_diff_python.rsis a one-shot differential test against out-of-repo generated data; skipped when the file is absent.