diff --git a/docs/research/sota-2026-05-22/R6_2_5-multi-subject-union.md b/docs/research/sota-2026-05-22/R6_2_5-multi-subject-union.md new file mode 100644 index 00000000..fdcd073b --- /dev/null +++ b/docs/research/sota-2026-05-22/R6_2_5-multi-subject-union.md @@ -0,0 +1,129 @@ +# R6.2.5 — Multi-subject occupancy union: N=5 hits 100% for 4 occupants + +**Status:** clean positive result · **2026-05-22** + +## Premise + +R6.2 / R6.2.3 picked one chest position per zone. Real households have 2-4 occupants who can be in different positions simultaneously. R6.2.5 extends to **union of chest envelopes** across all expected occupant positions. The practical question: does coverage degrade gracefully as occupant count grows? + +## Result: graceful saturation at N=5 + +| Scenario | # zones | Total area | Coverage @ N=5 | +|---|---:|---:|---:| +| 1 occupant (chair) | 1 | 0.16 m² | **100%** | +| 2 occupants (chair + bed) | 2 | 0.40 m² | **100%** | +| 3 occupants (chair + bed + desk) | 3 | 0.48 m² | **100%** | +| 4 occupants (+ 2nd chair) | 4 | 0.64 m² | **100%** | + +**N=5 hits 100% coverage for all configurations up to 4 occupants.** The chest-centric small-zone approach (R6.2.3) generalises trivially to multi-subject. + +## 4-occupant saturation curve + +| N | Coverage | Marginal | +|---:|---:|---:| +| 2 | 14.5% | +14.5 pp | +| 3 | 72.9% | +58.4 pp | +| **4** | **99.0%** | **+26.1 pp** | +| 5 | 100% | +1.0 pp | +| 6 | 100% | +0 pp | +| 7 | 100% | +0 pp | + +**Knee returns to N=4** — even for 4 occupants, 4 anchors get us to 99%. This is the **2D chest-centric multi-subject** regime, which is the most demanding 2D configuration tested in the R6 family — and it still hits the knee at N=4. + +## Cross-eval: single-subject placement is bad for multi-subject + +| Placement | Coverage on 4-zone target | +|---|---:| +| Single-subject-optimised | 70.6% | +| Multi-subject-optimised | **100%** | +| **Gain from multi-subject optimisation** | **+29.4 pp** | + +The CLI must accept multiple `--target` arguments and optimise for their **union** — not pick a representative zone and hope. + +## Updated CLI recommendation + +```bash +wifi-densepose plan-antennas \ + --room 5 5 \ + --target chair_chest 3.7 3.7 0.4 0.4 \ + --target bed_chest 2.2 0.8 0.6 0.4 \ + --target desk_chest 0.5 2.7 0.4 0.2 \ + --target chair2_chest 1.0 4.2 0.4 0.4 \ + --freq-ghz 2.4 +``` + +Output: N=5 anchors hitting 100% coverage of the union. + +## R6 family summary (8 ticks + this) + +| Tick | Configuration | Headline number | +|---|---|---:| +| R6.2 | 2D body, single-subject | 51% N=5 | +| R6.2.1 | 3D body, single-subject | 26% N=2 (mixed-height) | +| R6.2.2 | 2D body, N-anchor | 97% N=5 | +| R6.2.2.1 | 3D body, N-anchor | 49% N=5 | +| R6.2.3 | 2D chest, single-subject | 82% N=5 | +| R6.2.4 | 3D chest, N-anchor | 77% N=5 / 82% N=6 | +| **R6.2.5 (this)** | **2D chest, multi-subject (1-4)** | **100% N=5** | + +The R6 family's headline finding: **2D chest-centric + multi-subject + N=5 = 100% coverage**. This is the placement recipe to ship. + +## Composes with prior threads + +- **R6.2 / R6.2.3**: directly extends — single-subject → multi-subject union +- **R6.2.2 / R6.2.4**: same saturation behaviour at the multi-subject level +- **R14 (empathic appliances)**: V1 lighting / V2 HVAC / V3 attention in households of 2-4 occupants → use multi-subject placement +- **R3 / ADR-024**: per-subject identity (AETHER) + multi-subject placement = full empathic-appliance stack +- **ADR-105 / ADR-106 / ADR-107**: federation operates on the same model across occupant counts; placement is orthogonal +- **R12 PABS**: works per-subject within the union; multi-subject coverage = multi-subject intrusion detection + +## Why N=4 knee returns for multi-subject + +Each chest zone is small (40×40 cm) and fits inside a single Fresnel ellipsoid (which is ~40 cm wide at midpoint of a 5 m link). With N=4 anchors, we get 6 pairwise links — enough Fresnel ellipsoids to cover 4 disjoint 40×40 cm zones without much waste. Beyond N=4 the marginal gain drops to <1 pp. + +This is *more saturated* than the single-subject R6.2 setup (which used 3 m² bed footprint and couldn't be covered fully even at N=8 with body-centric zones). **Chest-centric multi-subject is the sweet spot for the Fresnel envelope geometry.** + +## Honest scope + +- **2D only** — multi-subject 3D not benchmarked (extension is mechanical; expect N=6 to retain the chest-centric N=5 advantage). +- **Static positions** — real occupants move; the union should be conservative (larger than any instantaneous configuration). +- **Single 5×5 m geometry** — larger or oddly-shaped rooms need separate benchmarks. +- **Greedy + 4 restarts** — global optimum may be 1-2 pp higher. +- **4 occupants** — beyond 4-5 the coverage may degrade. Extreme density (e.g. classroom with 20 people) is a different regime. + +## What this DOES enable + +1. **A clean cap on the placement complexity story**: 4-occupant households are fully sensable at N=5 with multi-subject-aware placement. +2. **A required CLI feature**: support multiple `--target` arguments. +3. **An updated installer recipe**: for households of 1-4, the same N=5 chest-centric placement works. +4. **R6 family closes with a positive result** that ships directly. + +## What this DOES NOT enable + +- Beyond 4-5 occupants — separate regime, not tested. +- Time-varying occupancy (people moving between zones) — would benefit from pose-trajectory data (out of scope). +- 3D multi-subject — mechanical extension, not done here. + +## Final R6.2 CLI surface + +After this tick, the productisation of R6.2 should support: + +``` +wifi-densepose plan-antennas + --room W H [Z] # 2D or 3D + --target NAME X Y W H [DX DY DZ] # repeatable + --target-mode {body, chest} # R6.2.3 + --freq-ghz F # 2.4, 5.0, 6.0 + --n-anchors N # auto-saturation if omitted + --restarts K # 4 default +``` + +This covers the R6.2 / R6.2.1 / R6.2.2 / R6.2.2.1 / R6.2.3 / R6.2.4 / R6.2.5 use cases in a single CLI tool. ~50 LOC over the original R6.2. + +## Connection back + +- **R6 / R6.1**: physical foundation +- **R6.2 / R6.2.3**: single-subject body / chest +- **R6.2.1 / R6.2.2 / R6.2.2.1 / R6.2.4**: 3D / N-anchor / composition +- **R6.2.5 (this)**: multi-subject completes the matrix +- **R14**: empathic-appliance deployment recipe is now: N=5 + chest-centric + multi-subject-union targets, with mixed-height anchors for full-body coverage when needed diff --git a/docs/research/sota-2026-05-22/ticks/tick-27.md b/docs/research/sota-2026-05-22/ticks/tick-27.md new file mode 100644 index 00000000..2152f327 --- /dev/null +++ b/docs/research/sota-2026-05-22/ticks/tick-27.md @@ -0,0 +1,103 @@ +# Tick 27 — 2026-05-22 09:32 UTC + +**Thread:** R6.2.5 (multi-subject occupancy union) +**Verdict:** Clean positive — **N=5 hits 100% coverage** for households of 1-4 occupants with chest-centric zones. N=4 knee returns. R6 family completes with this tick. + +## What shipped + +- `examples/research-sota/r6_2_5_multi_subject.py` +- `examples/research-sota/r6_2_5_multi_subject_results.json` +- `docs/research/sota-2026-05-22/R6_2_5-multi-subject-union.md` + +## Headline + +| Scenario | # zones | Coverage @ N=5 | +|---|---:|---:| +| 1 occupant | 1 | **100%** | +| 2 occupants | 2 | **100%** | +| 3 occupants | 3 | **100%** | +| 4 occupants | 4 | **100%** | + +4-occupant saturation curve: + +| N | Coverage | +|---:|---:| +| 2 | 14.5% | +| 3 | 72.9% | +| **4** | **99.0%** ← knee | +| 5 | 100% | + +**Knee at N=4** even for 4 occupants. The chest-centric small-zone approach generalises trivially. + +## Cross-eval: multi-subject optimisation matters + +| Placement | Coverage on 4 zones | +|---|---:| +| Single-subject-optimised | 70.6% | +| **Multi-subject-optimised** | **100%** | +| **Gain** | **+29.4 pp** | + +CLI must accept multiple `--target` args and compute union. + +## R6 family complete (9 ticks) + +| Tick | Config | Result | +|---|---|---:| +| R6.2 | 2D body, single | 51% N=5 | +| R6.2.1 | 3D body, single | 26% N=2 | +| R6.2.2 | 2D body, N-anchor | 97% N=5 | +| R6.2.2.1 | 3D body, N-anchor | 49% N=5 | +| R6.2.3 | 2D chest, single | 82% N=5 | +| R6.2.4 | 3D chest, N-anchor | 77/82% N=5/6 | +| **R6.2.5** | **2D chest, multi-subject** | **100% N=5** | + +**R6 family's ship recipe**: 2D chest-centric + multi-subject + N=5 = 100% coverage. + +## Why N=4 knee returns for multi-subject + +Each chest zone is 40×40 cm and fits inside one Fresnel ellipsoid (~40 cm wide at midpoint of 5 m link). N=4 anchors → 6 pairwise links → enough to cover 4 disjoint chest zones without much waste. Beyond N=4 the marginal gain drops to <1 pp. + +**Chest-centric multi-subject is the sweet spot for the Fresnel envelope geometry.** + +## Final R6.2 CLI surface (productisation spec) + +``` +wifi-densepose plan-antennas + --room W H [Z] # 2D or 3D + --target NAME X Y W H [DX DY DZ] # repeatable + --target-mode {body, chest} # R6.2.3 + --freq-ghz F # 2.4, 5.0, 6.0 + --n-anchors N # auto-saturation if omitted + --restarts K # 4 default +``` + +~50 LOC over the original R6.2. + +## Composes with prior threads + +- R6.2 / R6.2.3: direct extension (single → multi) +- R6.2.2 / R6.2.4: same saturation behaviour +- R14: V1/V2/V3 in households of 2-4 use this recipe +- R3 / ADR-024: per-subject identity + multi-subject placement = full empathic-appliance stack +- ADR-105/106/107: federation orthogonal to placement +- R12 PABS: multi-subject coverage = multi-subject intrusion detection + +## Honest scope + +- 2D only (3D multi-subject is mechanical extension) +- Static positions (real movement = conservative union) +- Single 5×5 m geometry +- Greedy + 4 restarts +- 4 occupants; beyond may degrade + +## Coordination + +`ticks/tick-27.md`. No PROGRESS.md edit. Branch `research/sota-r6.2.5-multi-subject`. + +## Remaining loop work + +- R12.1: pose-PABS closed loop (needs Rust integration, out of scope for synthetic ticks) +- ADR-108: Kyber substitution (quantum-resistant) +- Loop retrospective / 00-summary.md (still ~2.5h until cron stop) + +~2.5h to cron stop. **27 ticks landed.** R6 family + R3 arc both substantively complete. diff --git a/examples/research-sota/r6_2_5_multi_subject.py b/examples/research-sota/r6_2_5_multi_subject.py new file mode 100644 index 00000000..77e3800c --- /dev/null +++ b/examples/research-sota/r6_2_5_multi_subject.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +"""R6.2.5 — Multi-subject occupancy union. + +See docs/research/sota-2026-05-22/R6_2_5-multi-subject-union.md. + +R6.2 / R6.2.3 picked one chest position per zone. Real households +have 2-4 occupants who can be in different positions simultaneously +(spouse in bed + child at desk + visitor on chair). R6.2.5 extends to +**union of chest envelopes** across all expected occupant positions. + +Practical question: does the optimal placement degrade gracefully +when target zones multiply? Does N=5 still hit a useful coverage? + +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 in_first_fresnel(x, y, tx, rx, wavelength): + r1 = np.sqrt((x - tx[0])**2 + (y - tx[1])**2) + r2 = np.sqrt((x - rx[0])**2 + (y - rx[1])**2) + direct = np.linalg.norm(tx - rx) + return (r1 + r2) <= (direct + wavelength / 2) + + +def union_coverage(anchors, target_x, target_y, wavelength): + if len(anchors) < 2: return 0.0 + covered = np.zeros(len(target_x), dtype=bool) + for i in range(len(anchors)): + for j in range(i+1, len(anchors)): + covered |= in_first_fresnel(target_x, target_y, + anchors[i], anchors[j], wavelength) + return float(covered.mean()) + + +def rasterise_zones(zones, resolution=0.05): + xs, ys = [], [] + for name, x0, y0, w, h in zones: + zx = np.arange(x0, x0 + w, resolution) + zy = np.arange(y0, y0 + h, resolution) + gx, gy = np.meshgrid(zx, zy) + xs.append(gx.ravel()) + ys.append(gy.ravel()) + return np.concatenate(xs), np.concatenate(ys) + + +def candidates(room_w, room_h, step): + cands = [] + for x in np.arange(0, room_w + 0.001, step): + cands.append(np.array([x, 0.0])) + cands.append(np.array([x, room_h])) + for y in np.arange(step, room_h, step): + cands.append(np.array([0.0, y])) + cands.append(np.array([room_w, y])) + return cands + + +def greedy_search(cands, target_x, target_y, lam, n_anchors, restarts=4, seed=0): + rng = np.random.default_rng(seed) + best = {"score": -1.0, "anchors": []} + for r in range(restarts): + idx0, idx1 = rng.choice(len(cands), size=2, replace=False) + chosen = [cands[idx0], cands[idx1]] + while len(chosen) < n_anchors: + best_marg = -1 + best_idx = None + for k, c in enumerate(cands): + if any(np.allclose(c, a) for a in chosen): continue + s = union_coverage(chosen + [c], target_x, target_y, lam) + if s > best_marg: + best_marg = s + best_idx = k + if best_idx is None: break + chosen.append(cands[best_idx]) + score = union_coverage(chosen, target_x, target_y, lam) + if score > best["score"]: + best = {"score": score, "anchors": [a.tolist() for a in chosen]} + return best + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--out", default="examples/research-sota/r6_2_5_multi_subject_results.json") + args = parser.parse_args() + + room_w, room_h = 5.0, 5.0 + freq = 2.4 + lam = wavelength_m(freq) + step = 0.25 + cands = candidates(room_w, room_h, step) + + # Scenarios with increasing occupant count + # Each "chest zone" is a 40x40 cm patch + scenarios = { + "1 occupant (chair)": [ + ("chair_chest", 3.7, 3.7, 0.4, 0.4), + ], + "2 occupants (chair + bed)": [ + ("chair_chest", 3.7, 3.7, 0.4, 0.4), + ("bed_chest", 2.2, 0.8, 0.6, 0.4), + ], + "3 occupants (chair + bed + desk)": [ + ("chair_chest", 3.7, 3.7, 0.4, 0.4), + ("bed_chest", 2.2, 0.8, 0.6, 0.4), + ("desk_chest", 0.5, 2.7, 0.4, 0.2), + ], + "4 occupants (+ 2nd chair)": [ + ("chair_chest", 3.7, 3.7, 0.4, 0.4), + ("bed_chest", 2.2, 0.8, 0.6, 0.4), + ("desk_chest", 0.5, 2.7, 0.4, 0.2), + ("chair2_chest", 1.0, 4.2, 0.4, 0.4), + ], + } + + print(f"Room {room_w}x{room_h} m, freq {freq} GHz, chest-centric zones") + print() + + # For each scenario, find optimum at N=5 + results = [] + for name, zones in scenarios.items(): + tx, ty = rasterise_zones(zones) + result = greedy_search(cands, tx, ty, lam, n_anchors=5) + # Total zone area + zone_area = sum(w * h for _, _, _, w, h in zones) + results.append({ + "scenario": name, + "n_zones": len(zones), + "total_zone_area_m2": zone_area, + "coverage_n5": result["score"], + "best_anchors": result["anchors"], + }) + + print(f"{'Scenario':<40} {'#zones':>6} {'Area':>7} {'Cov@N=5':>9}") + print("-" * 75) + for r in results: + print(f"{r['scenario']:<40} {r['n_zones']:>6} {r['total_zone_area_m2']:>5.2f} m2 {r['coverage_n5']*100:>7.1f}%") + print() + + # Stress test: scale N for the 4-occupant scenario + print(f"=== 4-occupant scenario, scaling N from 2..7 ===") + zones4 = scenarios["4 occupants (+ 2nd chair)"] + tx, ty = rasterise_zones(zones4) + print(f"{'N':>3} {'Coverage':>9} {'Marginal':>9}") + prev = 0.0 + scale_curve = [] + for n in range(2, 8): + result = greedy_search(cands, tx, ty, lam, n_anchors=n) + marg = (result["score"] - prev) * 100 + print(f"{n:>3} {result['score']*100:>7.1f}% {marg:>+7.1f} pp") + scale_curve.append({"n_anchors": n, "coverage": result["score"]}) + prev = result["score"] + print() + + # Cross-eval: how does a single-subject-optimised placement perform on 4 subjects? + single_zone = [("chair_chest", 3.7, 3.7, 0.4, 0.4)] + tx1, ty1 = rasterise_zones(single_zone) + single_opt = greedy_search(cands, tx1, ty1, lam, n_anchors=5) + tx4, ty4 = rasterise_zones(zones4) + cov_single_on_multi = union_coverage( + [np.array(a) for a in single_opt["anchors"]], tx4, ty4, lam + ) + print(f"=== Cross-eval ===") + print(f" Single-subject placement on 4-subject zones: {cov_single_on_multi*100:.1f}%") + print(f" 4-subject-optimised placement on 4 zones: {results[-1]['coverage_n5']*100:.1f}%") + print(f" Gain from multi-subject optimisation: {(results[-1]['coverage_n5'] - cov_single_on_multi)*100:+.1f} pp") + print() + + out = { + "room": {"width_m": room_w, "height_m": room_h}, + "freq_ghz": freq, + "scenarios_n5": results, + "saturation_4subj": scale_curve, + "cross_eval": { + "single_opt_on_multi": cov_single_on_multi, + "multi_opt_on_multi": results[-1]["coverage_n5"], + "gain_pp": (results[-1]["coverage_n5"] - cov_single_on_multi) * 100, + }, + } + Path(args.out).parent.mkdir(parents=True, exist_ok=True) + Path(args.out).write_text(json.dumps(out, indent=2)) + print(f"Wrote {args.out}") + + +if __name__ == "__main__": + main() diff --git a/examples/research-sota/r6_2_5_multi_subject_results.json b/examples/research-sota/r6_2_5_multi_subject_results.json new file mode 100644 index 00000000..e5b71ad3 --- /dev/null +++ b/examples/research-sota/r6_2_5_multi_subject_results.json @@ -0,0 +1,152 @@ +{ + "room": { + "width_m": 5.0, + "height_m": 5.0 + }, + "freq_ghz": 2.4, + "scenarios_n5": [ + { + "scenario": "1 occupant (chair)", + "n_zones": 1, + "total_zone_area_m2": 0.16000000000000003, + "coverage_n5": 1.0, + "best_anchors": [ + [ + 5.0, + 3.25 + ], + [ + 0.0, + 1.25 + ], + [ + 2.0, + 5.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 5.0 + ] + ] + }, + { + "scenario": "2 occupants (chair + bed)", + "n_zones": 2, + "total_zone_area_m2": 0.4, + "coverage_n5": 1.0, + "best_anchors": [ + [ + 5.0, + 3.25 + ], + [ + 0.0, + 1.25 + ], + [ + 5.0, + 0.5 + ], + [ + 2.0, + 5.0 + ], + [ + 0.0, + 0.0 + ] + ] + }, + { + "scenario": "3 occupants (chair + bed + desk)", + "n_zones": 3, + "total_zone_area_m2": 0.48000000000000004, + "coverage_n5": 1.0, + "best_anchors": [ + [ + 5.0, + 3.25 + ], + [ + 0.0, + 1.25 + ], + [ + 2.0, + 5.0 + ], + [ + 5.0, + 0.5 + ], + [ + 0.0, + 0.0 + ] + ] + }, + { + "scenario": "4 occupants (+ 2nd chair)", + "n_zones": 4, + "total_zone_area_m2": 0.6400000000000001, + "coverage_n5": 1.0, + "best_anchors": [ + [ + 3.0, + 0.0 + ], + [ + 2.5, + 5.0 + ], + [ + 0.0, + 3.75 + ], + [ + 4.25, + 5.0 + ], + [ + 0.75, + 5.0 + ] + ] + } + ], + "saturation_4subj": [ + { + "n_anchors": 2, + "coverage": 0.14516129032258066 + }, + { + "n_anchors": 3, + "coverage": 0.7290322580645161 + }, + { + "n_anchors": 4, + "coverage": 0.9903225806451613 + }, + { + "n_anchors": 5, + "coverage": 1.0 + }, + { + "n_anchors": 6, + "coverage": 1.0 + }, + { + "n_anchors": 7, + "coverage": 1.0 + } + ], + "cross_eval": { + "single_opt_on_multi": 0.7064516129032258, + "multi_opt_on_multi": 1.0, + "gain_pp": 29.354838709677423 + } +} \ No newline at end of file