Dhi Labs, product E4

Causal Predictive Alerting

Alerts that grade their own promises.

Predicts an incident seconds before it happens, then checks whether it was right. This product extrapolates short-horizon kinematic trajectories to warn of an incident before it occurs, ships a counterfactual explanation with every alert (for example, "this would not have fired if the primary track were stationary"), and grades every prediction fulfilled or falsified against what actually happened afterward. Two literatures usually stop short of each other on a live stream: anticipation models predict and stop at a risk score, and causal explanation systems only explain a clip after it already happened. This product fuses both on one observation stream, so the causal claim is either true of an executed replay or it is not made at all.

100%recall across all 210 seeded scenarios: every designed hazard in the battery raised an alert before it happened
8.0%overall false-positive rate on the 75 designed non-events, the honest cost side of that recall
50.0% to 66.7%precision range across alert kinds, disclosed per kind rather than blended into one flattering average
Honesty notes

The problem: alerts that cry wolf get ignored

Picture an operator watching a bank of camera feeds with a predictive alerting layer running underneath. The system's whole value proposition is lead time: instead of telling the operator "someone just entered the restricted zone," it says "at this rate, someone will enter the restricted zone in about six seconds." That extra six seconds is the entire point, it is the difference between a warning and an incident report.

But a system that only ever makes claims and never checks them is not trustworthy, it is just loud. If a "hazard in six seconds" warning turns out wrong three times out of four for one kind of alert and right most of the time for another, and the operator has no way to tell which kind of warning they are looking at, they will do exactly what any person does with an alarm that keeps crying wolf: they tune it out, on every kind, including the one that was actually reliable. At that point the predictive layer has made things worse, not better, because operators now discount real warnings along with the noisy ones.

The fix this product commits to is not a claim that the underlying prediction is perfect. Its kinematic forecaster is a straightforward constant-velocity rollout, and it says so plainly. The fix is a bookkeeping discipline: every alert is filed as a checkable promise the moment it fires, that promise is later graded against what actually happened, and the resulting precision is disclosed per alert kind, including the kinds where the number is bad. An operator (or an evaluator) can then decide how much to trust a zone_entry alert versus a crowd_buildup alert, because the two have different, disclosed track records, instead of one blended "the system is X% accurate" number that hides which parts are actually worth tuning-in for.

How it works

Three mechanisms, in order: a kinematic model raises predictive alerts before the hazard occurs, each alert carries a counterfactual explanation backed by an executed replay (not narration), and a falsification ledger grades every prediction against what actually happened. The diagram below follows one alert through the full loop.

1 Event stream {t, track_id, x, y} 2 Causal predictive model constant-velocity rollout 3 Alert with a falsifiable promise "hazard in N seconds," filed to the ledger 4 Outcome observed real observations keep arriving 5 Ledger scores the promise fulfilled or falsified, no third option 6 Counterfactual explanation ablation-replay, presence or motion 7 Precision disclosed per scenario kind 50.0% to 66.7%, never one blended average

Kinematic anticipation

WorldState (src/precausal/state.py) keeps a bounded, robust-smoothed per-track observation history: the median of pairwise finite differences, so one glitchy detection cannot spike the forecast. Predictor.step (src/precausal/predict.py) rolls each track forward at constant velocity to raise three checkable alert kinds: zone_entry (the forecast crosses into a restricted polygon within the horizon), convergence (two tracks' time-to-closest-approach falls under a distance within the horizon), and crowd_buildup (a zone's occupancy trend crosses its threshold within the horizon, computed only while occupancy is still below threshold: once the threshold is hit, that is reactive alerting's job, not prediction's). Learned forecasters, trajectory nets, relation forecasting over an E3-style scene graph, plug in behind the exact same Predictor.step contract at the GPU phase; the kinematic core stays as the auditable baseline any learned model has to beat.

Ablation-replay counterfactuals

explain() (src/precausal/counterfactual.py) is not narration. For a fired alert, it takes the same retained observation history the alert was computed from (WorldState.snapshot()) and re-runs a fresh, forked predictor over it with one candidate cause surgically removed: either presence ablation (delete a track's observations, "would this fire if they weren't there?") or motion ablation (freeze a track at its first-seen position, displacement removed, everything else kept, "is it their presence or their approach?"). "Would not have fired if X" is claimed only when that ablation's replay actually fails to reproduce the same predicted event. No generative world model, no clip narration: every sentence the engine outputs corresponds to an executed replay you could rerun yourself.

The falsification ledger

An alerting system that never gets graded is just an opinion. FalsificationLedger (src/precausal/grounding.py) is the product's honesty metric. Every PredictedAlert lands in the ledger as a pending prediction with an objective fulfillment condition (the track actually enters the zone; the pair actually closes to convergence distance; the zone actually crosses its crowd threshold) and a deadline (predicted event time plus tolerance). As real observations keep flowing, the ledger resolves each pending prediction to fulfilled (recording the actual, not predicted, lead time) or falsified (deadline passed, it did not happen). stats() rolls this up into per-alert-kind precision and mean actual lead time, the number a config plane should watch when tuning thresholds, and the number that stops a predictive alerting system from degenerating into an unchecked fear generator. Worked numbers for all three mechanisms are in Measured results below.

Kinematic anticipation Ablation-replay counterfactuals Falsification ledger

The interactive demo: replay a real scenario

Every one of the 210 rows below is a real output of experiments/scenario_battery.py --seed 0, embedded verbatim as JSON (trimmed to the fields this viewer draws). Pick a kind, pick a scenario, and press play to watch the predictor's own timeline: when it raised the alert, how far ahead it claimed the incident would occur, and what the ledger decided once time actually passed.

would not fire if
    still fires despite

      Measured results

      Three sets of numbers, all reproducible from the commands shown: how the falsification ledger graded a prior battery of alerts, precision and recall per alert kind on the current 210-scenario battery, and how often counterfactual claims held up on replay.

      Ledger outcomes: the honesty mechanism in numbers

      Over an earlier 200-scenario battery, 150 alerts fired (75% of scenarios), and each was later marked fulfilled or falsified against the actual outcome. The falsified count is published as prominently as the fulfilled count.

      63fulfilled (predicted, and it happened)
      87falsified (predicted, and it did not)
      50no alert fired
      63 fulfilled 87 falsified 50 no-alert 200 scenarios, mean predicted lead time 4.688 s (median 4.5 s)

      Source: evidence/battery_200.jsonl, trailing summary row (n_alerts_fired=150, ledger_verdict_counts), reconciled arithmetically against all 200 individual rows: 87 + 63 = 150, 87 + 63 + 50 = 200.

      Precision and recall per scenario kind (210-scenario battery)

      Recall is 1.0 for every kind in this battery: the system never misses a true incident here. The cost of that is precision below 1.0, and it varies a lot by scenario kind. It is reported per kind rather than averaged into one flattering number.

      kindnTPFPprecisionrecallmean abs lead error (s)
      zone_entry4530150.6671.00.403
      convergence3523120.6571.01.349
      convergence_multi10550.5001.00.430
      crowd_buildup4527180.6001.00.962

      Source: evidence/scenario_battery_summary.json, per_kind_metrics, and BATTERY_RESULTS.md, "Per-alert-kind precision / recall / lead-time error." Produced by PYTHONPATH=src .venv/bin/python experiments/scenario_battery.py --seed 0 --out evidence/scenario_battery_rows.jsonl --summary-out evidence/scenario_battery_summary.json.

      crowd_buildup is honestly still the weakest alert kind (precision 0.600, the lowest of the four kinds), up from a prior 0.356 after a fix to Predictor._crowd() (a minimum occupancy-history time span plus consecutive confirmation ticks before an alert locks in). Of its 18 remaining falsified predictions, all are now the "never crowded" case (occupancy growth capped below threshold for a reason the trend has no way to see in advance), not the previous over-optimistic-lead-time failure mode, which the fix eliminated entirely. This is a real, structural improvement, not a scenario-generation artifact. convergence (0.657) and zone_entry (0.667) still sit above it, consistent with kinematic constant-velocity forecasting over individual tracks being more predictable than trend-slope extrapolation over a bounded, external-cap occupancy process. This is stated as-is in the repo's own BATTERY_RESULTS.md and reproduced, not softened, here.

      False-positive rate on designed non-events

      75 of the 210 scenarios are designed to have no genuine hazard at all: a wandering track with no designed target (no_incident), a track starting just outside a zone and heading directly away (departing), a near-zero-speed track parked near a zone boundary (stationary), and a track genuinely approaching but staying beyond the 10 s horizon for the whole observed window (out_of_horizon). Any alert fired on these is, by construction, a false positive.

      kindnfalse positivesrate
      no_incident2000.000
      departing2000.000
      stationary2040.200
      out_of_horizon1520.133
      overall7560.080

      Source: BATTERY_RESULTS.md, "False-positive rate on the no-alert scenarios," and evidence/scenario_battery_summary.json, false_positive_rate.by_kind.

      The false positives concentrate entirely in stationary and out_of_horizon, both scenarios placed close to a zone boundary. The repo's own root-cause analysis traces all 6 to the same mechanism: at high observation noise relative to a short sample interval (for example noise_std_m=0.4 at dt_s=0.2), WorldState's median-of-consecutive-differences velocity estimate, computed from only the 3 warm-up observations required by min_history, can read an apparent speed several times the track's true speed purely from jitter (observed cases: true speed 0.61 to 0.65 m/s read back as 1.8 to 2.6 m/s). That apparent speed is enough to cross min_speed_ms and produce a genuine, if spurious, zone_entry forecast. This is a legitimate robustness limitation of the kinematic smoothing under high-noise, low-sample-rate conditions, not a harness bug, and it is reported as-is.

      Counterfactual explanations, not bare scores

      Every alert carries an ablation-replay counterfactual, implemented in src/precausal/counterfactual.py. A fresh WorldState and a forked predictor replay the alert's own retained observation history with a candidate cause surgically removed (presence ablation) or frozen at its first position (motion ablation), through the real production Predictor.step, no reimplementation. A cause only lands in would_not_fire_if when its ablation actually killed the alert on replay:

      would_not_fire_if: [primary track removed]
      still_fires_despite: [secondary track removed]
      kindn explainedablations checkedpresence-removal necessarymotion-freeze necessary
      zone_entry4545100.0%100.0%
      convergence3570 (2 entities x 35)100.0%98.6%
      convergence_multi1020 (2 entities x 10)100.0%100.0%
      crowd_buildup45267 (per-occupant)37.5%n/a (presence only)

      Source: BATTERY_RESULTS.md, "Counterfactual stability (ablation-replay necessity)," and evidence/scenario_battery_summary.json, counterfactual_stability.

      The repo's own read, reproduced numerically here: a crowd buildup with margin above its threshold genuinely does not depend on any single occupant, so the engine correctly reports necessity of 37.5% rather than inflating per-person blame, up from 21.7% pre-fix because the confirmation-tick requirement means the alert that finally fires is, on average, closer to its threshold (more accumulated occupancy) than the pre-fix alert that fired on the very first eligible window, and buildups closer to threshold are exactly the regime where individual occupants are more likely to be necessary; that is consistent with the pre-existing unit test test_crowd_counterfactual_no_single_person_necessary. Convergence's one asymmetric case (98.6%, not 100%, motion-freeze) is a real, explainable geometric property: in 1 of 70 checks, freezing the slower of two converging tracks did not kill the alert on replay, because the faster track's own continued approach was sufficient to close the gap on its own. explain() correctly reported this as still_fires_despite rather than manufacturing a false necessity claim.

      Honest limitations

      FAQ

      What data was this measured on?

      210 seeded scenarios generated by experiments/scenario_battery.py (seed 0), driven through the real production code (WorldState, Predictor, FalsificationLedger, counterfactual.explain), plus an earlier, narrower 200-scenario battery (evidence/battery_200.jsonl) used for the ledger illustration above. Every row is a JSON object with the scenario's parameters, the alert's prediction, the ground-truth outcome, and the ledger's verdict. Full rows are embedded in the interactive demo on this page and in the companion dataset linked in the footer.

      Is this real-world or synthetic?

      Synthetic. Every scenario is a procedurally generated kinematic trajectory (speed, heading, zone geometry, observation noise, and sample rate all swept by a seeded random generator), not footage from a real camera or a real incident. That is stated plainly in the disclaimer above and in the Honest limitations section: real-world camera and tracker validation has not been performed yet.

      Can I run it on my own cameras or event streams?

      The engine's input contract is a plain observation stream, JSONL rows of {t, track_id, x, y[, class]} in a metric ground frame, so any tracker that can emit that (or be adapted to emit it) is a valid input. Pair it with a ground-projection product such as fixed-camera-3d to turn pixel tracks into a metric frame, or feed it any consistent planar frame already available. The code itself is proprietary and not publicly released; see "How do I get access" below.

      How do I get access?

      The source is proprietary and is not published anywhere public. The public artifacts are the evidence, the benchmark dataset, and this page. For access or a partnership inquiry, contact dhi-tech.com.

      What does the falsification ledger actually do when an alert's promise turns out wrong?

      Nothing is hidden or softened. Every PredictedAlert is filed with an objective fulfillment condition and a deadline. If real observations show the deadline passed without the condition becoming true, the ledger resolves that prediction to falsified, and stats() counts it against that alert kind's precision exactly as it would count a fulfilled prediction in its favor. Falsified counts are published as prominently as fulfilled counts everywhere on this page (see the 87-falsified, 63-fulfilled ledger figures above), and no aggregate number on this page is computed by excluding or downweighting falsified rows.

      Why disclose per-scenario-kind precision instead of one aggregate number?

      Because a single blended number would hide exactly the information an operator needs: which kinds of alerts to trust more. Precision ranges from 0.500 (convergence_multi) to 0.667 (zone_entry) across the four kinds with confusion-matrix metrics in this battery. A blended average across all of them would land somewhere in the middle and tell an operator nothing about which specific alert kind is unreliable. The Problem section above describes exactly this failure mode: alarm fatigue happens when operators cannot tell reliable warnings from unreliable ones, and a single flattering aggregate number recreates that problem instead of solving it.