Executive summary
A Wi-Fi crowd-counting model does not stay accurate. It learns "this pattern of radio signal ≈ 5 people," but that pattern is a fingerprint of the room — walls, furniture, reflective surfaces. Rearrange the room, or just let a month pass, and the model silently drifts until its count is wrong. This experiment is the thesis's answer: keep a cheap, always-on second sensor (Bluetooth / BLE) running, and when it starts disagreeing with the Wi-Fi model, fire a short 15-minute recalibration campaign that pulls fresh labelled data from people already in the room via a phone app. Plot accuracy over 30 days and you should see the sawtooth: decay, trigger, recover, repeat.
Stated honestly: the real 30-day hardware sawtooth has not been captured yet. What exists today is a chain of four simulation campaigns that stress-test each moving part before we commit real device-months to it. Together they say something more nuanced — and more useful — than the original pitch:
- Drift is real but picky. In a ray-traced corridor, occupancy error grew from 0.29 to 0.93 persons only when furniture slid into the deep signal-blockage regime; a 27 dB signal shift out of the sensing path moved the count error not at all. Drift is "the change that pushes the model out of its calibrated regime," not "any change to the room."
- The trigger works as a detector, not as an accuracy win. The cross-modal disagreement statistic (
xmodal— the gap between what Wi-Fi thinks and what BLE thinks) tracked true error at Spearman ρ = 0.93 [0.72, 1.0] and stayed quiet on 94 % of error-irrelevant changes. But acting on it did not beat a fixed weekly clock on accuracy — even a perfect-knowledge trigger did not. The payoff is budget: same accuracy at roughly one-quarter the recalibrations across an estate where most zones never drift. - A powered re-run deflated the story further. When we grew the sample from 3 to 6 placement seeds, strict statistical equivalence with the clock failed (paired difference +0.034 persons, 90 % CI [+0.002, +0.066]), and a new failure mode surfaced: on one placement the disagreement statistic stayed below its alarm threshold while accuracy genuinely collapsed to 0.67 persons error — the trigger fired 0/6 times when it was most needed.
- Fusion is a hybrid, not a switch. Replacing the staleness clock with the disagreement condition only won at sparse Bluetooth cadence on bursty crowds; at dense cadence the clock won, because disagreement can't tell a stale anchor from a noisy Wi-Fi estimate.
So the mechanism is constructible in silico, the honest caveats are large, and the real defeater — a measured multi-day AX210/Pi5 capture per IP-106 — is exactly what this umbrella experiment is designed to run.
The problem, in plain words
Imagine calibrating a bathroom scale while standing on a soft carpet, then swapping the carpet for tile. The scale now reads wrong — not because your weight changed, but because its environment did. Wi-Fi crowd counting has the same disease, and worse: the "carpet" changes on its own as furniture moves, doors open, and seasons pass. This is textbook concept drift — the input–output relationship shifting over time — and in CSI sensing it is not hypothetical: a recent empirical study found that models trained on one time window degrade sharply when tested weeks later, purely from temporal change (brunello2025_d781 ↗).
The obvious fix is to recalibrate on a timer — "every Monday, re-measure with a known number of people." That works but is wasteful: most rooms are stable most of the time, so you pay for recalibrations you didn't need, and you can still be caught out between ticks. The smarter idea is an event-driven trigger that recalibrates only when a monitor says the model has actually broken. The hard part is building that monitor without ground truth — at runtime you don't know the real headcount, so you can't directly measure your own error. The thesis proposal: use a second, physically-different radio (BLE) as a witness. When two independently-calibrated sensors start disagreeing, something real has changed.
What we are trying to prove
- Primary hypothesis (falsifiable): a Day-0-frozen CSI counting model measurably degrades over 30 days, and short trigger-based BLE calibration campaigns restore it to within 5 % of Day-0 accuracy — producing a visible sawtooth. What a null means: if a frozen model doesn't drift over a month in a real room (never crosses the error budget), the whole calibration contribution is unmotivated and collapses to "just train once."
- Trigger sub-hypothesis: the cross-modal disagreement statistic, computable online with no ground truth, ranks time-periods by true error (high Spearman ρ) and stays quiet when the room shifts without breaking the model. If it fires on error-irrelevant change, it is a naive change-detector, not a drift trigger.
- Policy sub-hypothesis (the sceptical one): acting on the trigger lowers count error versus a fixed cadence at the same recalibration budget. We expected this to be the weak claim — and the simulation confirms it is weak (see below). A null here is not fatal: budget-parity at lower spend is still a deployable win.
- Mechanism-transfer check: a physics-informed model (PINN with a continuity-residual loss) should drift slower than a plain LGBM. If it doesn't in the field even though simulation says it should, that is a finding, not a failure.
How the experiment works (plain method)
The real deployment (planned, not yet run). Four Pi 5 + AX210 nodes and Bluetooth listeners sit in a fixed room for 30+ days. Day 0 trains two counting models on labelled CSI (ground truth from a co-located BLE device count) and freezes them. Every second, both the frozen model and a BLE-derived count are logged. Once an hour, a trigger checks whether |BLE count − model count| has exceeded 20 % for at least an hour; if so, the app nudges people already in the room through a 15-minute gamified walk, harvests fresh labelled CSI, and fine-tunes the models at a low learning rate. Afterwards, the same collected data is replayed offline through counterfactual strategies (daily / weekly / never) so the trigger competes against fixed clocks on identical data.
The simulation chain (executed) that de-risks it. Because a real device-month is expensive, each moving part was first tested in a coupled JuPedSim-crowd + Sionna-ray-traced-CSI + BLE sandbox:
- c-csi-layout-drift — build a controlled drift corpus: a synthetic corridor ray-traced under ordered furniture displacements, deliberately split into an arm where displacement grows count error (
losin, into deep blockage) and an arm where the signal shifts hugely but the count error stays flat (losperp). This measures how fast a Day-0-calibrated estimator drifts and produces the fixed-cadence prior. - c-ble-drift-trigger — over that corpus, compute four online drift monitors (Wi-Fi self-monitoring
csi-ks, BLE-onlyble-ks, cross-modalxmodal, and an idealisedoracle-counterceiling) and test both detectability (does the statistic rank-track true error and stay quiet on error-irrelevant change?) and policy (does triggering beat a fixed clock at matched budget?). - c-ble-trigger-equivalence — a pre-registered TOST equivalence re-run at 6 seeds, asking the sharper question: does the trigger reach parity with the clock (within ±0.05 persons) while spending a quarter of the budget?
- c-csi-ble-condition-switch — replace the staleness clock in the fusion weight with the disagreement condition, and test whether it matches a tuned clock without a tuned time constant, especially under bursty occupancy.
What we've found so far (honest, across the four campaigns)
All four campaigns ran real in-silico sessions with attached runs and figures — none are phantom write-ups. Every session's budget_used telemetry reads zero (a known instrumentation gap), but each carries dozens of gate-passed runs and rendered artefacts, so the results are genuine. The one provenance blemish: the equivalence session's figure array is empty in the session index even though its synthesis claims three figures under the S3 prefix — flagged below.
1. Drift is geometry-dependent (c-csi-layout-drift, 84 runs, all 4 criteria met). Occupancy error grew 0.29 → 0.93 persons (seed 0) and 0.37 → 0.82 (seed 2) only on the into-blockage losin arm; seed 1's placements happened to be drift-robust (→ 0.43). The losperp arm swung per-link mean amplitude ~27 dB yet left error flat — a large environment change with zero model degradation, the false-alarm trap. The recalibration cadence prior it produces, T*(ε=0.5), is seed-fragile: [0.25, 1.5, 0.25] m across three seeds. The honest deliverable is the spread, not a point estimate — and that spread is itself the argument for event-driven over fixed recalibration.
2. The trigger detects but does not beat the clock (c-ble-drift-trigger, 84-run corpus, 4/5 criteria — policy criterion FAILED).
| Monitor | Spearman ρ vs true error | False-fires on error-irrelevant shift |
|---|---|---|
csi-ks (Wi-Fi only) |
0.61 [0.15, 0.94] | 17 % — confuses "room changed" with "model broke" |
ble-ks (BLE only) |
0.62 | 0 % |
xmodal (cross-modal) |
0.93 [0.72, 1.0] | 5.6 % |
oracle-counter (ideal ceiling) |
0.97 [0.87, 0.99] | — |
Policy at matched budget on the signal arm: never-recalibrate 0.566 error; fixed clock 0.399 (2.0 recals); trigger[xmodal] 0.407 (1.33 recals); oracle-error 0.421 (1.0 recal). The strict "trigger beats fixed" inequality fails (0.407 > 0.399) — and tellingly, even the perfect-knowledge trigger loses on accuracy, because recalibrating on schedule also fixes below-threshold drift the trigger deliberately ignores. The real win is estate-wide budget: 2.0 vs 8.0 recalibrations across four zones where three never drift.
3. A powered re-run refuted equivalence and found a decoupling regime (c-ble-trigger-equivalence, 168 runs, 1/3 criteria met). Growing from 3 to 6 seeds, the paired (trigger − fixed) difference is +0.034 persons, 90 % CI [+0.002, +0.066] — the upper bound exceeds the ±0.05 margin, so TOST fails (p = 0.178). The trigger is cheaper (1.17 vs 2.0 recals) but worse by an amount not contained in the margin — two-edged, not parity. The smoking gun: seed 3's xmodal sat at 0.0062–0.0066, genuinely below its 0.0073 alarm threshold, so it fired 0/6 times while true error climbed to 0.67 persons — the statistic and the error decoupled. The cell-level ρ=0.93 also softened to a seed-cluster ρ=0.68, CI [0.10, 0.90], failing the 0.6 bar at the honest unit of replication. This is the most important negative in the chain: the trigger can stay silent on drift that hurts.
4. Condition-switch fusion is a hybrid, not a crown (c-csi-ble-condition-switch, 6 runs, 3/5 criteria met). Weighting the BLE anchor by observed disagreement instead of elapsed time beat the τ=16 s clock only on bursty occupancy at sparse cadence (≥25 s: 1.21 vs 1.32 persons; per-seed −0.26 / −0.00 / −0.08, CI [−0.26, −0.001], fragile at n=3), and lost at dense cadence on both arms (+0.17 to +0.34 vs the best-tuned clock). The reason is architectural: the disagreement statistic is symmetric — it rises when the anchor goes stale and when the Wi-Fi estimate is noisy, so it can't tell them apart. The design consequence is a hybrid: clock-gated freshness plus condition-modulated staleness, and the IP-106 capture should log both the cross-modal statistic and the anchor age.
What this means for the umbrella. The sim chain does not pre-empt the real experiment — it sharpens it. It gives the deployment (a) a cadence prior and its uncertainty, (b) a specific statistic to log (xmodal) plus a warning that it can fail silently, and (c) a fusion architecture (hybrid, not pure condition-switch). The 30-day sawtooth remains unmeasured; simulation cannot produce it because the "does it drift over real time in a real room" question is precisely what only hardware answers (brunello2025_d781 ↗).
Minimum viable sawtooth — decided 2026-09-04
The real cycle has not run, and the 2026-09-04 synthesis asked whether it should (IN-2026-09-04-003). Decision: scope the smallest cycle that produces every edge of the sawtooth, on the arm catalogue as it stands. No new playbook.
| Leg | Request | Arm | Not before | What it produces |
|---|---|---|---|---|
| 1 | CR-032 | lib-crowd-day |
2026-09-14 | Day-0 labelled staircase (0/2/4/6/8 people). The counting model is fitted here and frozen. |
| 2 | CR-033 | lib-crowd-day |
2026-09-28 | Falling edge: the frozen model is scored on the first hour before any refit. Then the staircase is the calibration campaign: handsets on air supply the BLE reference, fresh labelled CSI refits the model. |
| 3 | CR-034 | lib-crowd-day |
2026-10-05 | Rising edge: the staircase is scored by both the frozen Day-0 model and the leg-2 refit. The frozen model is the never-recalibrated counterfactual. |
Between the legs the drift series already runs (lib-empty-night CR-031,
lib-drift-day CR-007) and is not changed: the only variable allowed to differ
between legs is elapsed time, so channel (ch48), pace (250 Hz) and roster are held.
The cross-modal disagreement statistic and the anchor age are logged from leg 1
onward; whether the statistic crossed its threshold before leg 2 is the first field
ROC point (IP-106). The e-process trigger is the primary statistic, the fixed-z cut
the ablation (hypothesis note, 2026-08-07).
lib-crowd-day is the only arm whose measured variable is a headcount, which is why
all three legs use it. Each leg is attended: eight or more people and a named operator.
Until leg 3 is analysed every prose verb on recalibration-trigger-from-drift stays
on the cost side.
Hardware arms
First hardware drift data is being captured as of 2026-07-27 15:03 UTC — an overnight illuminated static-link arm on the office pair, launched the evening before the DEPLOY-001 AX210 Fleet Deployment FIIT Library install as both a drift baseline and the toolchain shakedown (EXP-P2 Illuminator Bring-Up and First Controlled Link).
- Session
monad02_drift-overnight-illum_20260727-150321— monad02 observing ch11 HT20 uncapped for 12 h (15:03 → 03:03 UTC), while monad01 illuminates with paced 25 Hz non-HT OFDM broadcast (self-recovering systemd units). S3:datasets/ax210-csi-captures/monad02/monad02_drift-overnight-illum_20260727-150321/after the ~03:15 auto-sync. - What it is: the empty-room static reference — the night-long trace of a fixed link through a fixed office, the baseline against which "the pattern is a fingerprint of the room" gets its first measured decay constant. What it is not: the 30-day sawtooth (no model, no trigger, no BLE arm yet), and not a diurnal cycle — 12 h, deliberately, because the pair is dismantled for DEPLOY-001 the next morning; the workday half belongs to the deployed library fleet.
- Design notes carried from EXP-P2: delivery of the 25 Hz pace is itself diurnal (96 % evening vs 56–72 % office hours), so every drift metric is reported alongside the delivered-rate timeline; records are scoped to the illuminator's 52-tone class via the
ef:be:ad:de:ad:desentinel; per-chain RSSI anchors absolute scale since |H| is AGC-relative; phase work uses the inter-chain conjugate product (hardware-offset-free per the CSI-ratio argument, zeng2021_1e4f ↗). - Analysis plan: diary 2026-07-27 - First illuminated CSI link - illuminator bring-up, broadcast physics, and the overnight drift arm §Analysis plan.
Results
First hardware result, 2026-08-28: the channel's wander saturates. It reaches about 0.2 dB within eight hours and does not grow further out to two days.
Nine receivers, channel 48, one record class (52-tone legacy OFDM), quarantined
sessions excluded, monad01 dropped because it is the illuminator and its
two-chain ratio while transmitting is a different quantity. 2026-08-22 to
2026-08-28, 1,276 hourly points across 36 node-bands. Reduction:
monad_knowledge/notebooks/python/expf2_drift_law.py; result JSON at
_attachments/expf2-drift/expf2_drift_law.json.
Aggregation window: hourly medians, lag in hours. Every dB figure in this table is a statistic of that window, and the same channel measured on minute blocks gives different numbers (below). A dB figure without its window is not a quantity.
| Elapsed time | RMS change in the two-chain ratio | host range | pairs |
|---|---|---|---|
| 1 h | 0.0800 dB | [0.073, 0.113] | 1,120 |
| 4 h | 0.1443 dB | [0.121, 0.234] | 804 |
| 8 h | 0.2193 dB | [0.156, 0.338] | 372 |
| 24 h | 0.1984 dB | [0.162, 0.278] | 844 |
| 48 h | 0.1914 dB | [0.098, 0.282] | 412 |
A variogram was chosen because it is shape-free. Mann-Kendall assumes monotone, Theil-Sen a constant rate, Pettitt exactly one step; on a series that is flat and then moves late all three fire, all three are significant, and every one describes the data wrongly.
The process behind the shape — an Ornstein–Uhlenbeck fit
Added 2026-08-29. The shape-free curve above says the wander saturates. This says what saturates. For a stationary OU process the variogram is γ(τ) = 2σ²(1 − e^(−τ/θ)), so fitting (σ, θ) to the same curve names a stationary spread and a correlation time instead of five points.
Aggregation window: minute blocks, lag in minutes. Source
notebooks/lake-deep-dive-20260828/run_ou.py, output
data/.out_ou.
| Stratum | Observable | σ (dB or rad) | θ (min) | R² | plateau RMS |
|---|---|---|---|---|---|
| ch48, night | amplitude | 0.118 dB | 2.4 | 0.61 | 0.167 dB |
| ch48, day | amplitude | 0.153 dB | 61.5 | 0.71 | 0.216 dB |
| ch48, all | amplitude | 0.143 dB | 40.4 | 0.61 | 0.203 dB |
| ch48, night | phase | 0.265 rad | 2.5 | 0.58 | 0.375 rad |
| ch48, day | phase | 0.246 rad | 4.2 | 0.51 | 0.348 rad |
| ch11, any | amplitude | — | no fit | — | — |
The day is two timescales and the night is one. That is the whole finding. The night's amplitude correlation time is 2.4 minutes; the day's is 61.5 minutes, twenty-five times longer, on the same channel, the same receivers and the same paced source. A room full of people does not merely add noise — it adds a slow component the empty room does not have, and that slow term is the occupancy footprint written as a process rather than as a level.
ch11 does not admit an OU fit and must not be quoted as if it did. The optimiser ran θ to its 10⁵-minute bound (99,907 min ≈ 69 days) on every 2.4 GHz amplitude stratum. That is a non-fit, not a 69-day correlation time: the plateau is never reached inside the observed lags, so the curve is still rising and the model is unidentified there.
The within-session figure rests on one host-night. The session-level fit — the one the diary quotes as σ ≈ 0.03 dB, θ ≈ 2–3 min — is σ = 0.0355 dB, θ = 1.70 min on monad10, 2026-08-27, n = 1 night, n = 1 host. Its reported "CI" is the same number twice, because a bootstrap over one value is that value. It agrees with the ch48-night stratum fit (2.4 min) and that agreement is the only reason to believe it. CR-018's six paced empty nights are what would turn it into an estimate.
The OU under-predicts persistence. Empirical lag-1 autocorrelation median 0.680 against the fitted model's 0.556. Consistent with the DFA exponent of 0.78, and the reason every false-alarm figure below is a lower bound.
The gate the process implies
evaluation.tex says a deployable trigger needs a magnitude gate on top of the
anytime-valid statistic, without saying what the gate should be. This is the
gate, in the units the trigger fires in.
Whiten before you gate. On the raw level series a CUSUM over-fires roughly threefold; the AR(1) innovation filter took residual autocorrelation from 0.78 to 0.11 and CUSUM false alarms on the empty night from 3.0/h to 0.0/h across five receivers — one night, so a pilot, but every receiver went to zero and the mechanism is the OU fit seen twice.
Aggregation window: one minute. σ = 0.0355 dB, θ = 1.70 min, innovation SD = 0.0295 dB.
| k | Threshold on the level | Threshold on the innovations | Mean minutes between false alarms |
|---|---|---|---|
| 2.0 | 0.071 dB | 0.059 dB | 22 |
| 2.5 | 0.089 dB | 0.0737 dB | 80 |
| 3.0 | 0.106 dB | 0.0884 dB | 370 |
| 3.5 | 0.124 dB | 0.1032 dB | 2,149 |
Why the gate is not the 0.0800 dB figure. That is an hourly-median statistic at a one-hour lag. A trigger fires on minute innovations, so it must be gated in minute-innovation units — 0.0884 dB at k = 3.0. Quoting the hourly figure as a minute gate is the same error as quoting a minute gate hourly, and the reason this section states a window on every number.
A second, independent statistic agrees. Comparing the spread of daily medians against the mean spread within one day gives a ratio of 1.14 — 7 of 9 hosts above 1.0, exact two-sided sign test p = 0.1797 against an attainable floor of 0.0039. The floor is below 0.05, so this design could have rejected and did not, which makes the null informative rather than an artefact of too few samples. The honest n is 9 receivers, not 36 node-bands and not the 1,120 hourly pairs — the four tone groups of one receiver are not four independent measurements.
T*(epsilon), read off the curve in dB: 0.10 dB → 2 h · 0.15 dB → 6 h · 0.20 dB → 8 h · 0.25 dB → never reached within 48 h.
The number the evaluation chapter was missing. evaluation.tex records the
anytime-valid statistical self-trigger firing on 8 of 25 certified-empty streams at
a median detected shift of 0.0031 dB, calls that physically meaningless micro-drift
and concludes a deployable trigger needs a magnitude gate on top — without
saying what the gate should be, because nothing had measured ordinary wander on
this hardware. Ordinary hour-to-hour wander is 0.0800 dB, twenty-six times
larger. That is where the gate comes from.
This contradicts a framing the thesis currently uses. ble-augmentation.tex
argues CSI-only operation "silently accumulates drift". Over six days on this
hardware it does not accumulate; it reaches a ceiling in hours. The finding
sharpens the argument rather than weakening it: the case for periodic
recalibration is not runaway drift, it is that the ceiling is high enough to
matter.
Limits
This does not test either limb of the hypothesis above, and the status says so.
The card asks for a counting model frozen at Day 0, accuracy against ground truth,
thirty days, BLE calibration campaigns and a sawtooth. None of those exists here:
there is no fitted model, no occupancy labels, six days rather than thirty, and
zero calibration events because EXP-F1 has no sessions. What is measured is the
channel-level precursor, which constrains both limbs and tests neither. The
card is running, not measured, for exactly this reason.
The daytime window carries occupancy: unknown by declaration. These are
day-ambient hours in an awake library, so 0.2 dB mixes channel drift with people
walking through the room. It is the operationally relevant figure rather than a
clean channel constant.
The night-empty subset cannot separate them. Only 280 hourly points survive the 22:00–04:00 filter; five hosts remain at a one-hour lag and nothing beyond two hours. That is a finding about the schedule, not the channel — the plans committed on 2026-08-28 add night hours every day of the following week, which is what would let the night-only law be computed.
EXP-LIB-01's 0.0056 dB is not comparable to the 0.0800 dB here and the two must not be divided. Different band (2.4 GHz ch11 against 5 GHz ch48), different statistic (SD of one-minute block medians against RMS of hourly differences), and different room condition (certified empty against ambient).
The null bounds accumulation as small; it does not establish zero. With n = 9 and a per-host range of [0.67, 1.75], a consistent 1.5x accumulation would have shown as nine hosts in one direction. A ten to twenty per cent accumulation would not be separable from this noise and should not be claimed either way.
How to read the figures
Real figures exist from the sim campaigns (rendered to each session's S3 artefacts/ prefix; the hardware sawtooth figure does not exist yet).
fig_layout_drift_multiseed(c-csi-layout-drift) — occupancy error vs furniture displacement, one panel per arm, three seed lines each, with the ε budget line marked. Read the seed spread, not any single line:losinrises, the other arms stay flat despite big signal shifts.fig_drift_trigger_detect(c-ble-drift-trigger) — four scatter panels (statistic on x, true error on y) with per-panel Spearman ρ. A good monitor makes the cloud rise left-to-right; notexmodal's tight rise vscsi-ks's scatter. n = 18 cells/arm — read the rank trend, not points.fig_drift_trigger_policy— bars of error and recalibrations per policy; the honest reading is overlapping error bars between trigger and fixed, with the recal-count bar far lower for the trigger.fig_fusion_condition_switch(c-csi-ble-condition-switch) — MAE vs BLE cadence (log-x) per arm: BLE-held, CSI-adapted, clock switch, condition switch, oracle envelope. The crossover near 25–50 s cadence on the bursty arm is the whole story.- Equivalence figures (
paired_diff,seed_trace,policy_smallmultiple) are named in the c-ble-trigger-equivalence synthesis but are not registered in the session's figure index — treat as claimed-but-unverified until re-fetched from S3.
Review panel
Each voice is a prepared expert with a one-line stance and the literature it argues from. Verdicts are about this experiment and its current evidence — mostly the four sim campaigns, since the hardware run is unexecuted.
Key references
- brunello2025_d781 ↗ — real empirical evidence that CSI models degrade over time; the drift premise's strongest anchor.
- zhang2025_a250 ↗ — continuous online adaptation, the ML baseline the trigger must beat.
- zou2018_1590 ↗ — device-free CSI counting; grounds the layout-dominated drift mechanism.
- khan2023_b7c5 ↗, zhang2018_138d ↗ — transfer/adaptation precedents for cross-domain counting.
- demrozi2021_bf55 ↗, longo2019_b72f ↗ — the second modality's real-world basis (BLE occupancy and WiFi+BLE ground truth).
- angelopoulos2025_8070 ↗ — the risk-control framing the mathematician wants for the trigger's miss guarantee.
- zhang2026_ccac ↗, guarino2026_e72c ↗ — the reproducibility bar the SWE voice invokes.
- hoydis2023_7aa4 — the ray-tracer behind the synthetic corpus; the red-team's sim-to-real caveat.
- huang2025_060d, meneghello2023_0a93 ↗ — public real-CSI anchors for the red-team's cross-check demand.
- zhang2024_1d32 ↗ — privacy-preserving BLE occupancy, the ethics voice's basis.
- wang2026_2758 ↗, chen2023_5cbd ↗, koo2026_a08d ↗ — generalization, cross-domain, and lightweight-self-calibration context.