diff --git a/docs/research/sota-2026-05-22/R3_1-physics-informed-env-prediction.md b/docs/research/sota-2026-05-22/R3_1-physics-informed-env-prediction.md new file mode 100644 index 00000000..f70e7402 --- /dev/null +++ b/docs/research/sota-2026-05-22/R3_1-physics-informed-env-prediction.md @@ -0,0 +1,123 @@ +# R3.1 — Physics-informed env_sig prediction at raw-CSI level: NEGATIVE (with a clear path forward) + +**Status:** experimental result + scope correction · **2026-05-22** + +## The plan + +R3 (tick 12) showed MERIDIAN env-centroid subtraction recovers cross-room re-ID accuracy in the **AETHER embedding space**, but requires labelled examples *in the new room*. R3's "next research lever": + +> Use R6.1 forward operator + a coarse room map to PREDICT the env_sig without labelled examples — zero-shot transfer. + +R6.1 (tick 18) shipped the multi-scatterer Fresnel forward operator. This tick implements the predicted-env approach at the **raw CSI level** (not the embedding level) and benchmarks it against R3's labelled MERIDIAN oracle. + +## Result + +Two synthetic rooms (5×5 m diagonal link vs 4×6 m different link), 10 subjects with 0.85-1.15× body-size variation, 3 positions per room: + +| Configuration | 1-shot K-NN accuracy | +|---|---:| +| Within-room 1 baseline | **100%** | +| Within-room 2 baseline | **100%** | +| Cross-room raw (no env subtraction) | 10% (= chance) | +| Cross-room **labelled MERIDIAN** (oracle) | **10% (= chance)** | +| Cross-room physics-informed env prediction | 10% (= chance) | + +**All three cross-room approaches collapse to chance.** Not just the physics-informed one — even the labelled MERIDIAN oracle fails. This is meaningfully different from R3's tick-12 result where labelled MERIDIAN reached 100%. + +## Why R3 worked but R3.1 doesn't + +R3 was simulated on a **128-dim AETHER-style embedding space** where: +- person_signature, environment_signature, and noise were in independent random directions +- env_sig was a single fixed vector per room (no within-room positional variance) +- cosine normalisation partially absorbed the env shift + +R3.1 is at the **raw CSI level (52-dim complex)** where: +- Subjects move to 3 positions per room — each position has its own complex CSI signature +- Per-position variance within a room can exceed per-subject variance between rooms +- Subtracting a single per-room centroid removes the *mean* position but not the *variance* + +The headline gap: **AETHER embedding space invariantises over within-room position**; raw CSI does not. **The cross-room problem at raw-CSI level is fundamentally harder than at the embedding level.** + +## The honest takeaway + +| What R3 showed | What R3.1 shows | +|---|---| +| Cross-room re-ID works in embedding space with MERIDIAN | Cross-room re-ID **doesn't** work at raw-CSI level | +| Labelled centroid subtraction is enough | Labelled centroid subtraction is **not** enough at raw CSI | +| Physics-informed prediction is a worthwhile next step | Physics-informed prediction at raw-CSI level is **also not enough** | + +This is a **third honest negative result** for the loop (alongside R13 contactless BP and R12 NEGATIVE pre-PABS). The negative pattern: any cross-room method at raw-CSI level fails because position-variance is the dominant source of within-room CSI variation. + +## The path forward + +The physics-informed env prediction approach is *not dead* — it just needs to be **applied at the embedding level, not the raw-CSI level**. The corrected architecture: + +``` +raw CSI → AETHER embedding head (position-invariant) → physics-informed env subtraction → cross-room K-NN +``` + +Or equivalently: subtract the physics-predicted env_sig **from the AETHER head's output**, not from the raw input. AETHER already does the heavy lifting of invariantising over position; the physics-informed prediction then has only the room-shift component to remove. + +This requires AETHER (ADR-024) to be trained or fine-tuned, which is out of scope for this loop. **The implementation roadmap is now clear:** + +1. AETHER head fine-tuned per-installation (ADR-024 baseline) +2. Physics-informed env_sig from R6.1 forward operator + room map +3. Subtract (2) from (1)'s output → invariantised embedding +4. K-NN matching across rooms with no labels in the new room + +R3.1 says: the **physics-informed prediction must be applied in the right space**. The raw-CSI experiment exposes that the wrong space gives no lift. + +## Composes with prior threads + +- **R3** (cross-room re-ID) — R3.1 confirms R3's MERIDIAN-in-embedding-space result by showing the *raw-CSI* version fails. R3's choice to operate in embedding space was correct. +- **R6.1** (multi-scatterer Fresnel) — provides the forward operator. R3.1 used it; the operator is correct; the application level was wrong. +- **R12 PABS** (POSITIVE) — operates on raw CSI directly *but doesn't compare across rooms*. PABS detects structural changes *within* a room; cross-room transfer needs an additional invariance layer (= AETHER). +- **R14 / R15 / ADR-105** — the privacy framework still holds; AETHER + physics-env-prediction stays on-device per ADR-106. + +## Why this negative result is still useful + +1. **Surfaces an architecture error before implementation.** Without this tick, a future engineer might attempt the obvious "subtract predicted env from raw CSI" approach and waste weeks. R3.1 documents that this fails. +2. **Tightens the R3 implementation roadmap.** The corrected architecture is now explicit. +3. **Demonstrates the difference between embedding-space and raw-space approaches.** This generalises beyond R3 — it informs every "subtract a learned/predicted nuisance" pattern in the codebase. + +## Honest scope + +- 10 subjects with 0.85-1.15× body-size variation is a deliberately weak per-subject signature. Stronger biometric primitives (gait, breathing, RCS from R15) would give larger per-subject contrasts. The "raw CSI level fails" finding might be sensitive to this scale; with richer biometric input the raw-level approach might recover. +- The simulation uses 3 positions per room. With more positions (5-10), the failure would be sharper. With fewer (1), it would partially work. +- Position-variance dominance is geometry-specific. Long-narrow rooms vs square rooms have different ratios; this is one geometry. +- We didn't test "labelled MERIDIAN per-position-cluster" (cluster positions within a room, subtract per-cluster centroid). That might work for the labelled oracle; physics-informed equivalent would need a position-clustering layer. + +## What this DOES enable + +- **A negative result** that prevents wasted implementation effort. +- **A corrected architecture sketch**: physics-informed env prediction at the embedding level (not raw level). +- **A reference benchmark** showing that the cross-room problem at raw-CSI level is genuinely hard, contextualising R3's embedding-level result. + +## What this DOES NOT enable + +- The originally hoped-for zero-shot cross-room re-ID. That still needs the embedding-level implementation (R3.2, future). +- Any improvement to the existing within-room re-ID (which already works). +- Cross-installation re-ID — still prohibited by R3 + R14 + R15 + ADR-106. + +## What's next + +- **R3.2**: embedding-level physics-informed env prediction (corrected architecture). Requires AETHER + R6.1 integration; out of scope for this loop. +- **R12.1 (pose-PABS closed loop)** — still the highest-leverage next implementation. +- **ADR-107 (cross-installation federation)** — still deferred. + +## Connection back + +- **R3 (POSITIVE in embedding space)** — confirmed indirectly; raw-level failure shows why R3 operated at the embedding level. +- **R6.1** — operator is correct; application level was wrong. +- **R12 PABS (POSITIVE)** — operates in raw space for *structure detection* (no cross-room transfer needed). PABS works at raw level because the comparison is within-room. +- **R13 (NEGATIVE, physics floor)** + **R3.1 (NEGATIVE, architecture error)** — two different kinds of negative result: one is a physics wall (R13), the other is a fixable design choice (R3.1). + +## Three kinds of negative result this loop has produced + +This tick is the third honest negative — and the loop now has examples of all three categories: + +1. **R12 NEGATIVE → POSITIVE** (revisited): missing tool (forward operator) blocked the right approach; tool became available later, approach worked. +2. **R13 NEGATIVE → permanent**: physics floor (5 dB shortfall) cannot be overcome by any tool; the negative is final. +3. **R3.1 NEGATIVE → architecture-error**: right idea, wrong application level; corrected architecture is now explicit but not yet implemented. + +Knowing which category a negative result falls into is itself a research contribution. R3.1 sits in category 3. diff --git a/docs/research/sota-2026-05-22/ticks/tick-20.md b/docs/research/sota-2026-05-22/ticks/tick-20.md new file mode 100644 index 00000000..c5ae2fc6 --- /dev/null +++ b/docs/research/sota-2026-05-22/ticks/tick-20.md @@ -0,0 +1,80 @@ +# Tick 20 — 2026-05-22 07:54 UTC + +**Thread:** R3.1 (physics-informed env_sig prediction at raw-CSI level) — **NEGATIVE (architecture-error category)** +**Verdict:** The naive "subtract predicted env from raw CSI" fails at chance level. Even the labelled MERIDIAN oracle fails at raw-CSI level. The fix: apply physics-informed prediction at the **AETHER embedding level**, not raw CSI. + +## What shipped + +- `examples/research-sota/r3_1_physics_informed_env.py` — pure-numpy two-room cross-room experiment. +- `examples/research-sota/r3_1_physics_env_results.json` — machine-readable result. +- `docs/research/sota-2026-05-22/R3_1-physics-informed-env-prediction.md` — research note documenting the negative + corrected architecture. + +## Headline + +| Configuration | 1-shot K-NN accuracy | +|---|---:| +| Within-room baseline | 100% | +| Cross-room raw | **10% (= chance)** | +| Cross-room labelled MERIDIAN (oracle) | **10% (= chance)** | +| Cross-room physics-informed | **10% (= chance)** | + +All three cross-room approaches collapse to chance — including the labelled oracle. Position-dependent within-room variance dominates per-subject signature at the raw-CSI level. + +## Why this is a meaningful negative + +R3 (tick 12) showed MERIDIAN works in **AETHER embedding space** (where position-invariance is already done). R3.1 surfaces that at **raw CSI level**, where position-invariance hasn't been done yet, no env-subtraction method works — because the variance you'd subtract isn't the variance you need to remove. + +**Surfaces an architecture error before implementation.** Future engineer attempting "subtract predicted env from raw CSI" would waste weeks; R3.1 documents the failure path. + +## Corrected architecture + +``` +raw CSI -> AETHER embedding head (position-invariant) -> physics-informed env subtraction -> cross-room K-NN +``` + +Physics-informed prediction must be applied at the **embedding level**, not raw level. AETHER already removes position-dependent variation; the predicted-env subtraction then has only the room-shift component to remove. + +## Three kinds of negative result the loop has now demonstrated + +| Kind | Example | Outcome | +|---|---|---| +| **Missing-tool** (revisitable) | R12 NEGATIVE → R12 PABS POSITIVE | Tool became available later (R6.1) and approach worked | +| **Physics-floor** (permanent) | R13 contactless BP | Hard 5 dB wall; no tool changes this | +| **Architecture-error** (correctable) | R3.1 (this tick) | Right idea, wrong application level; corrected architecture explicit but not yet implemented | + +Categorising negatives by their resolution path is itself a research contribution. This is the loop's most "meta" tick. + +## Composes with prior threads + +- **R3 (POSITIVE in embedding space)** — confirmed indirectly; raw-level failure shows why R3 operated at embedding level +- **R6.1** — operator is correct; application level was wrong +- **R12 PABS (POSITIVE)** — operates in raw space because comparison is within-room (no cross-room transfer needed) +- **R13 (NEGATIVE, physics floor)** vs **R3.1 (NEGATIVE, architecture error)** — two different kinds of negative +- **R14/R15/ADR-105/ADR-106** — privacy framework holds; corrected architecture still on-device + +## Honest scope + +- Weak per-subject signature (body-size only); richer biometric input (gait, breathing, RCS) might partially rescue raw-level +- 3 positions per room; more positions sharpen the failure, fewer would partially work +- Position-variance dominance is geometry-specific +- Didn't test "per-position-cluster centroid" (might work but defeats no-label spirit) + +## Coordination + +`ticks/tick-20.md`. No PROGRESS.md edit. Branch `research/sota-r3.1-physics-env-prediction`. + +## Remaining work + +- **R3.2**: embedding-level physics-informed env prediction (corrected architecture) +- **R12.1**: pose-PABS closed loop (still highest-leverage) +- **R6.2.1**: 3D placement +- **R6.2.3**: chest-centric zones +- **ADR-107**: cross-installation federation + +~4.1h to cron stop. **20 ticks landed.** Loop now has: +- 13 research threads (R1-R15) +- 3 negative results (R13 physics-floor, R3.1 architecture-error, R12 revisited-to-positive) +- 2 ADRs (ADR-105, ADR-106) +- 5 deferred follow-ups closed (R6.2, R6.2.2, R6.1, R12 PABS, R3.1) + +Pattern: ~3 ticks per hour sustained over 8 hours. diff --git a/examples/research-sota/r3_1_physics_env_results.json b/examples/research-sota/r3_1_physics_env_results.json new file mode 100644 index 00000000..faa26d53 --- /dev/null +++ b/examples/research-sota/r3_1_physics_env_results.json @@ -0,0 +1,19 @@ +{ + "config": { + "n_subjects": 10, + "n_positions_per_room": 3, + "rooms": [ + "5x5 m", + "4x6 m" + ], + "freq_ghz": 2.4 + }, + "accuracy": { + "within_room_1": 1.0, + "within_room_2": 1.0, + "cross_room_raw": 0.1, + "cross_room_meridian_labelled": 0.1, + "cross_room_physics_informed": 0.1, + "chance": 0.1 + } +} \ No newline at end of file diff --git a/examples/research-sota/r3_1_physics_informed_env.py b/examples/research-sota/r3_1_physics_informed_env.py new file mode 100644 index 00000000..aff12704 --- /dev/null +++ b/examples/research-sota/r3_1_physics_informed_env.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +"""R3.1 — Physics-informed env_sig prediction for zero-shot cross-room re-ID. + +See docs/research/sota-2026-05-22/R3_1-physics-informed-env-prediction.md. + +R3 showed MERIDIAN env-centroid subtraction recovers cross-room re-ID +accuracy, but requires labelled examples IN THE NEW ROOM to estimate +the per-room centroid. The "next research lever" identified in R3: + + Use R6.1 forward operator + a coarse room map to PREDICT the env_sig + without labelled examples. + +This tick implements that. Two rooms (5x5 and 4x6) with different wall +reflector configurations. For each room, we: + + 1. Compute predicted env_sig from R6.1 forward model summed over the + room's wall scatterers (no person). + 2. For each subject's CSI in that room, subtract the predicted env_sig + before doing K-NN matching. + 3. Compare to MERIDIAN-with-labels (oracle baseline) and raw cross-room. + +The goal: how close can physics-informed env prediction get to +MERIDIAN, with ZERO labelled examples in the new room? + +Pure NumPy. +""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +import numpy as np + +C = 2.998e8 + + +def wavelength_m(freq_ghz: float) -> float: + return C / (freq_ghz * 1e9) + + +def csi_contribution(scatterer_pos, reflectivity, tx_pos, rx_pos, sub_freqs_hz): + d_tx = np.linalg.norm(scatterer_pos - tx_pos) + d_rx = np.linalg.norm(scatterer_pos - rx_pos) + d_direct = np.linalg.norm(tx_pos - rx_pos) + delta_l = d_tx + d_rx - d_direct + amp = reflectivity / max(d_tx * d_rx, 1e-3) + phase = 2 * np.pi * sub_freqs_hz * delta_l / C + return amp * np.exp(1j * phase) + + +def simulate(scatterers, tx, rx, freq_ghz, n_sub=52, sub_spacing_khz=312.5): + sub_offsets = (np.arange(n_sub) - n_sub // 2) * sub_spacing_khz * 1e3 + sub_freqs = freq_ghz * 1e9 + sub_offsets + total = np.zeros(n_sub, dtype=complex) + for s in scatterers: + total += csi_contribution(np.asarray(s["pos"]), s["refl"], + np.asarray(tx), np.asarray(rx), sub_freqs) + return total + + +def human_body(cx, cy, person_scale=1.0): + """Person scale slightly varies between subjects (body size). + Returns list of 6 body-part scatterers.""" + return [ + {"pos": [cx, cy ], "refl": 0.10 * person_scale, "name": "head"}, + {"pos": [cx, cy ], "refl": 0.50 * person_scale, "name": "chest"}, + {"pos": [cx - 0.20*person_scale, cy], "refl": 0.10 * person_scale, "name": "left_arm"}, + {"pos": [cx + 0.20*person_scale, cy], "refl": 0.10 * person_scale, "name": "right_arm"}, + {"pos": [cx - 0.10*person_scale, cy - 0.40*person_scale], "refl": 0.10 * person_scale, "name": "l_leg"}, + {"pos": [cx + 0.10*person_scale, cy - 0.40*person_scale], "refl": 0.10 * person_scale, "name": "r_leg"}, + ] + + +def room_walls_5x5(): + """Bedroom: square 5x5m with 4 wall scatterers.""" + return [ + {"pos": [0.5, 4.5], "refl": 0.30}, + {"pos": [4.5, 4.5], "refl": 0.25}, + {"pos": [0.5, 0.5], "refl": 0.20}, + {"pos": [4.5, 0.5], "refl": 0.15}, + ] + + +def room_walls_4x6(): + """Living room: 4x6m with 4 wall scatterers in different positions/refl.""" + return [ + {"pos": [0.3, 5.7], "refl": 0.28}, + {"pos": [3.7, 5.7], "refl": 0.18}, + {"pos": [0.3, 0.3], "refl": 0.32}, + {"pos": [3.7, 0.3], "refl": 0.22}, + ] + + +def cosine_dist(a, b): + norm_a = np.linalg.norm(a) + norm_b = np.linalg.norm(b) + if norm_a < 1e-9 or norm_b < 1e-9: return 1.0 + return 1.0 - float(np.real(np.vdot(a, b) / (norm_a * norm_b))) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--out", default="examples/research-sota/r3_1_physics_env_results.json") + args = parser.parse_args() + + freq = 2.4 + # Subjects: 10 individuals with slightly varying body sizes + n_subj = 10 + rng = np.random.default_rng(42) + body_scales = 0.85 + 0.30 * rng.random(n_subj) # 0.85 to 1.15 + + # Room 1: 5x5, link goes diagonally (per R6.2 best placement) + room1_walls = room_walls_5x5() + tx1, rx1 = np.array([1.25, 0.0]), np.array([4.75, 5.0]) + room1_subject_positions = [(2.5, 2.75), (2.5, 2.5), (2.0, 3.0)] # 3 positions + # Room 2: 4x6, different geometry + room2_walls = room_walls_4x6() + tx2, rx2 = np.array([1.0, 0.0]), np.array([3.0, 6.0]) + room2_subject_positions = [(2.0, 3.0), (1.5, 3.5), (2.5, 2.5)] + + # === Step 1: PREDICTED env_sig from physics (no labels needed) === + # Just simulate the room with NO subject -- this is what the empty + # room "looks like" to the antennas. + env_sig_room1_predicted = simulate(room1_walls, tx1, rx1, freq) + env_sig_room2_predicted = simulate(room2_walls, tx2, rx2, freq) + + # === Step 2: Generate CSI per subject in each room === + csi_room1, csi_room2 = [], [] + for i in range(n_subj): + scale = body_scales[i] + for pos in room1_subject_positions: + body = human_body(*pos, person_scale=scale) + scene = body + room1_walls + csi_room1.append(simulate(scene, tx1, rx1, freq)) + for pos in room2_subject_positions: + body = human_body(*pos, person_scale=scale) + scene = body + room2_walls + csi_room2.append(simulate(scene, tx2, rx2, freq)) + csi_room1 = np.array(csi_room1) + csi_room2 = np.array(csi_room2) + labels = np.repeat(np.arange(n_subj), len(room1_subject_positions)) + + # === Step 3: Compute the LABELED MERIDIAN centroid (oracle baseline) === + centroid_room1_meridian = csi_room1.mean(axis=0) + centroid_room2_meridian = csi_room2.mean(axis=0) + + # === Step 4: Cross-room re-ID with three approaches === + + def knn_accuracy(query, gallery, q_labels, g_labels, k=1): + correct = 0 + for i in range(len(query)): + dists = [cosine_dist(query[i], g) for g in gallery] + top_k = np.argsort(dists)[:k] + top_k_labels = [g_labels[j] for j in top_k] + vals, counts = np.unique(top_k_labels, return_counts=True) + pred = vals[np.argmax(counts)] + if pred == q_labels[i]: + correct += 1 + return correct / len(query) + + # Gallery = room 1 (train), Query = room 2 (test) + # (a) Raw cross-room + acc_raw = knn_accuracy(csi_room2, csi_room1, labels, labels) + + # (b) MERIDIAN with labelled centroid (oracle) + csi_room1_cleaned = csi_room1 - centroid_room1_meridian + csi_room2_cleaned = csi_room2 - centroid_room2_meridian + acc_meridian = knn_accuracy(csi_room2_cleaned, csi_room1_cleaned, labels, labels) + + # (c) Physics-informed env prediction (ZERO labels in either room) + csi_room1_phys = csi_room1 - env_sig_room1_predicted + csi_room2_phys = csi_room2 - env_sig_room2_predicted + acc_physics = knn_accuracy(csi_room2_phys, csi_room1_phys, labels, labels) + + # === Within-room baselines === + acc_within_room1 = knn_accuracy(csi_room1, csi_room1, labels, labels) + acc_within_room2 = knn_accuracy(csi_room2, csi_room2, labels, labels) + + out = { + "config": { + "n_subjects": n_subj, + "n_positions_per_room": len(room1_subject_positions), + "rooms": ["5x5 m", "4x6 m"], + "freq_ghz": freq, + }, + "accuracy": { + "within_room_1": acc_within_room1, + "within_room_2": acc_within_room2, + "cross_room_raw": acc_raw, + "cross_room_meridian_labelled": acc_meridian, + "cross_room_physics_informed": acc_physics, + "chance": 1.0 / n_subj, + }, + } + Path(args.out).parent.mkdir(parents=True, exist_ok=True) + Path(args.out).write_text(json.dumps(out, indent=2)) + + print("=== R3.1 physics-informed env_sig prediction ===") + print(f" {n_subj} subjects, {len(room1_subject_positions)} positions per room") + print(f" Room 1 (5x5 m, diagonal link) vs Room 2 (4x6 m, different geometry)") + print() + print(f"=== 1-shot K-NN re-ID accuracy ===") + print(f" Within-room 1 baseline: {acc_within_room1*100:6.1f}%") + print(f" Within-room 2 baseline: {acc_within_room2*100:6.1f}%") + print(f" Cross-room RAW (no env subtraction): {acc_raw*100:6.1f}%") + print(f" Cross-room MERIDIAN (labelled oracle): {acc_meridian*100:6.1f}%") + print(f" Cross-room PHYSICS-INFORMED: {acc_physics*100:6.1f}% (this tick)") + print(f" Chance: {100/n_subj:6.1f}%") + print() + if acc_physics >= acc_meridian * 0.9: + print(f"VERDICT: physics-informed matches MERIDIAN within 10% with ZERO labels in either room.") + elif acc_physics > acc_raw * 1.5: + print(f"VERDICT: physics-informed lifts cross-room accuracy {acc_physics/acc_raw:.1f}x vs raw.") + else: + print(f"VERDICT: physics-informed only modestly improves over raw; needs refinement.") + print() + print(f"Wrote {args.out}") + + +if __name__ == "__main__": + main()