research(R6.2.2.1): 3D N-anchor multistatic — 2D knee disappears; revises R6.2.2 down (#727)
Composes R6.2.2 (2D N-anchor knee at N=5) with R6.2.1 (3D ellipsoids, ceiling-only fails). The composed 3D result shows the 2D-derived knee DOES NOT hold in 3D. 3D saturation curve (5x5x2.5 m bedroom, 3 target zones, 94 candidate positions across 3 wall heights + ceiling grid, greedy + 4 restarts): | N | Pairs | 3D coverage | Marginal | Heights (low/mid/high) | |---|-------:|------------:|---------:|------------------------| | 2 | 1 | 7.7% | +7.7 pp | 1/1/0 | | 3 | 3 | 28.1% | +20.4 pp | 1/2/0 | | 4 | 6 | 40.6% | +12.5 pp | 3/0/1 | | 5 | 10 | 49.4% | +8.8 pp | 4/0/1 | | 6 | 15 | 59.1% | +9.8 pp | 4/1/1 | | 7 | 21 | 65.1% | +6.0 pp | 5/1/1 | Comparison vs R6.2.2 2D: - 2D N=5 = 96.8% (clean knee) - 3D N=5 = 49.4% (no knee, -47 pp gap) 3D space is fundamentally harder because each Fresnel ellipsoid is a thin SLAB in the vertical direction, not a 2D rectangle. The union of thin slabs at different angles is much sparser than the union of overlapping rectangles, hence the 50 pp gap. Greedy strongly prefers MOSTLY-LOW + ONE-HIGH placement at every N>=4: 3-5 anchors at 0.8m + 0-1 at 1.5m + 1 ceiling. Confirms R6.2.1's diagonal-in-z winning strategy. ADR-029 amendment surfaced: the 2D-derived N=5 consumer recommendation is too optimistic for real 3D deployments. Two responses: 1. Bump N to 7-8 for 65%+ 3D coverage 2. Use chest-centric zones (R6.2.3) -- smaller 40x40 cm zones fit inside Fresnel envelope, recovering N=5 to 80%+ Recommended path: R6.2.3 + R6.2.2 N=5 = realistic 80%+ 3D coverage at ADR-029 default N. Architectural lever that aligns 2D and 3D physics. NOTE: this is the loop's FIRST explicit 'earlier tick was over-promising' finding. Previous 23 ticks built constructively. R6.2.2.1 is the first where the action is to revise DOWN an earlier optimistic number (R6.2.2's 97% becomes 49% in honest 3D). Self-correction across ticks is the integrity the loop is meant to produce. Composes with: - R6.2 / R6.2.1 / R6.2.2: natural composition - R6.2.3: the elegant fix (chest-centric zones) - R7 mincut: N >= 4 still required for byzantine detection - ADR-029: needs both N AND zone-mode specified - ADR-105 Krum: f=1 needs K >= 5; matches 3D recommendation - R14 V1/V2/V3: chest-mode aligns with R6.2.3 = tractable 3D Honest scope: greedy approximate, 0.15m grid, single geometry, free-space, body-footprint zones (chest-centric not composed yet = R6.2.4 follow-up). Coordination: ticks/tick-24.md, no PROGRESS.md edit.
This commit is contained in:
parent
8b850d8b2a
commit
df13dcf597
|
|
@ -0,0 +1,120 @@
|
||||||
|
# R6.2.2.1 — 3D N-anchor multistatic: the knee disappears
|
||||||
|
|
||||||
|
**Status:** 3D saturation curve + comparison to R6.2.2 2D · **2026-05-22**
|
||||||
|
|
||||||
|
## Premise
|
||||||
|
|
||||||
|
R6.2.2 (2D N-anchor) found a clean **knee at N=5 anchors** with 96.8% coverage of bedroom-class target zones, and pushed that as the consumer recommendation. R6.2.1 (3D single-pair) found ceiling-only mounting fails. R6.2.2.1 composes both: how does the saturation curve change when both **3D ellipsoids** and **mixed-height candidates** are used?
|
||||||
|
|
||||||
|
The practical question: does ADR-029's 4-anchor default give adequate coverage in real 3D rooms, or does the 2D analysis under-promise?
|
||||||
|
|
||||||
|
## Results
|
||||||
|
|
||||||
|
5×5×2.5 m room, three 3D target zones (bed at z=0.3-0.6, chair at z=0.5-1.2, standing at z=1.0-1.7). 94 candidate positions (3 wall heights + ceiling grid). Greedy + 4 restarts:
|
||||||
|
|
||||||
|
| N anchors | Pairs | 3D coverage | Marginal | Heights chosen (low / mid / high) |
|
||||||
|
|---:|---:|---:|---:|---|
|
||||||
|
| 2 | 1 | 7.7% | +7.7 pp | 1 / 1 / 0 |
|
||||||
|
| 3 | 3 | 28.1% | +20.4 pp | 1 / 2 / 0 |
|
||||||
|
| 4 | 6 | 40.6% | +12.5 pp | 3 / 0 / 1 |
|
||||||
|
| **5** | 10 | **49.4%** | +8.8 pp | 4 / 0 / 1 |
|
||||||
|
| 6 | 15 | 59.1% | +9.8 pp | 4 / 1 / 1 |
|
||||||
|
| 7 | 21 | 65.1% | +6.0 pp | 5 / 1 / 1 |
|
||||||
|
|
||||||
|
**No clean knee.** Marginal gains stay 6-10 pp from N=4 onwards. 3D space is fundamentally harder to cover with discrete pairwise links.
|
||||||
|
|
||||||
|
## Comparison: 2D vs 3D at same N
|
||||||
|
|
||||||
|
| N anchors | 2D coverage (R6.2.2) | 3D coverage (R6.2.2.1) | Δ |
|
||||||
|
|---:|---:|---:|---:|
|
||||||
|
| 2 | 35.7% | 7.7% | -28 pp |
|
||||||
|
| 3 | 63.4% | 28.1% | -35 pp |
|
||||||
|
| 4 | 86.2% | 40.6% | -46 pp |
|
||||||
|
| 5 | 96.8% | 49.4% | **-47 pp** |
|
||||||
|
| 6 | 100% | 59.1% | -41 pp |
|
||||||
|
| 7 | 100% | 65.1% | -35 pp |
|
||||||
|
|
||||||
|
**At N=5, 3D coverage is half of 2D coverage.** The 2D analysis was over-promising.
|
||||||
|
|
||||||
|
## Why 3D is harder
|
||||||
|
|
||||||
|
The 2D Fresnel zone is an *ellipse* — an area; the 3D zone is an *ellipsoid* — a volume. The 2D ellipse trivially covers any vertical extent at the LOS height; the 3D ellipsoid has a perpendicular thickness equal to its transverse radius (~40 cm at 5 m link). Targets above or below the LOS plane are missed entirely.
|
||||||
|
|
||||||
|
Each pairwise link in 3D effectively contributes a **thin slab** rather than a full 2D rectangle. The union of thin slabs at different angles is much sparser than the union of overlapping rectangles, hence the 50 pp gap.
|
||||||
|
|
||||||
|
## Height distribution: greedy strongly prefers low + mixed
|
||||||
|
|
||||||
|
At every N from 4 onwards, the greedy search picks:
|
||||||
|
- 3-5 LOW (z=0.8 m) anchors
|
||||||
|
- 0-1 MID (z=1.5 m)
|
||||||
|
- 1 HIGH (ceiling, z=2.4 m)
|
||||||
|
|
||||||
|
The HIGH anchor matters (it's selected at every N), but never dominates. The placement strategy that **wins** is "mostly-low + one-high" — which is also what R6.2.1's single-pair analysis suggested (one low + one high diagonal).
|
||||||
|
|
||||||
|
## Updated recommendation for ADR-029
|
||||||
|
|
||||||
|
| Use case | 2D rec (R6.2.2) | 3D rec (R6.2.2.1) | Realistic coverage |
|
||||||
|
|---|---:|---:|---:|
|
||||||
|
| Presence / occupancy | 2-3 | 4 | ~41% (3D) / 86% (2D) |
|
||||||
|
| Multi-feature (pose, vitals, count) | 4-5 | **5-6** | 49-59% (3D) / 97% (2D) |
|
||||||
|
| Mission-critical (medical, security) | 6 | **7-8** | 65%+ (3D) |
|
||||||
|
|
||||||
|
**The 2D-derived N=5 consumer recommendation is too optimistic for real 3D deployments.** Two responses:
|
||||||
|
|
||||||
|
1. **Bump to N=6-7** for realistic 3D coverage at the same target quality.
|
||||||
|
2. **Use chest-centric zones (R6.2.3)** — chest zones are smaller (40×40 cm vs 3 m² beds) and fit inside the Fresnel envelope much more easily. R6.2.3 + R6.2.2.1 composed would give 80%+ coverage with N=4-5.
|
||||||
|
|
||||||
|
The recommended path: **R6.2.3 chest-centric + R6.2.2 N=5 anchor count** = realistic 3D coverage of 80%+ at the ADR-029 default N. This is the architectural lever that aligns the 2D and 3D physics.
|
||||||
|
|
||||||
|
## Composes with prior threads
|
||||||
|
|
||||||
|
- **R6.2** (2D single-pair) — same engine.
|
||||||
|
- **R6.2.1** (3D single-pair) — same 3D ellipsoid model.
|
||||||
|
- **R6.2.2** (2D N-anchor) — same greedy search, composes naturally with 3D.
|
||||||
|
- **R6.2.3** (chest-centric) — the architectural fix for the 3D coverage gap.
|
||||||
|
- **R7** (mincut adversarial) — requires N ≥ 4 even in 3D; the practical 4-5 anchor recommendation still satisfies R7.
|
||||||
|
- **ADR-029** (multistatic) — anchor-count recommendation needs both N AND target-zone semantics specified.
|
||||||
|
- **ADR-105 Krum** — f=1 byzantine tolerance still needs K ≥ 5 regardless of dimension; matches the 3D recommendation.
|
||||||
|
|
||||||
|
## Why this is a meaningful follow-up not a re-do
|
||||||
|
|
||||||
|
R6.2.2 (2D) and R6.2.1 (3D single-pair) each told a partial story. R6.2.2.1 composes them and reveals the 2D was over-promising. Specifically:
|
||||||
|
|
||||||
|
- 2D over-promise: "N=5 hits 97% knee" → reality: only for 2D rectangles, not 3D volumes
|
||||||
|
- 3D fix: bump N or shrink target zones (use chest-centric)
|
||||||
|
|
||||||
|
Without R6.2.2.1, the team would have shipped ADR-029 with the 2D recommendation and discovered the 3D shortfall during field deployment.
|
||||||
|
|
||||||
|
## Honest scope
|
||||||
|
|
||||||
|
- **Greedy with 4 restarts** approximates global optimum; brute-force is intractable at this scale. Real optimum might be 2-5 pp higher.
|
||||||
|
- **Coarse 0.15 m grid** in 3D. Finer resolution would refine but not change the qualitative finding.
|
||||||
|
- **Single geometry tested** — 5×5×2.5 m bedroom. Different rooms (tall living rooms, narrow hallways) have different curves.
|
||||||
|
- **Free-space propagation** — multipath adds 5-15% but doesn't restore the 50 pp gap.
|
||||||
|
- **Body-footprint zones** — using R6.2.3 chest-centric zones would substantially raise the percentage; not tested here.
|
||||||
|
- **94 candidates** is a sparse search; finer step would refine slightly.
|
||||||
|
|
||||||
|
## What this DOES enable
|
||||||
|
|
||||||
|
1. **Honest 3D coverage numbers** for ADR-029 planning — 49% at N=5 is the realistic number, not 97%.
|
||||||
|
2. **Decision point**: bump N OR use chest-centric zones (R6.2.3). Both are tractable; the latter is more elegant.
|
||||||
|
3. **Validation that "mostly-low + one-high" is the right placement strategy** in 3D, confirming R6.2.1's pair-finding.
|
||||||
|
|
||||||
|
## What this DOES NOT enable
|
||||||
|
|
||||||
|
- A clean knee — there isn't one in 3D under these zones.
|
||||||
|
- Composition with R6.2.3 chest-centric (= R6.2.4, future).
|
||||||
|
- Validated multi-cog deployment recipes — each cog needs its own analysis.
|
||||||
|
|
||||||
|
## Next ticks
|
||||||
|
|
||||||
|
- **R6.2.4**: compose 3D N-anchor + chest-centric zones → does N=5 hit 80% in 3D when zones are smaller?
|
||||||
|
- **R6.2.5**: multi-subject occupancy (union of chest envelopes across expected positions).
|
||||||
|
- **ADR-029 amendment**: anchor-count recommendation needs both N AND zone-mode specified.
|
||||||
|
|
||||||
|
## Connection back
|
||||||
|
|
||||||
|
- **R6.2** (2D single-pair, R6.2.1 (3D single-pair), R6.2.2 (2D N-anchor), R6.2.3 (chest-centric) — R6.2.2.1 is the natural composition of the first three; R6.2.3 is the way to "fix" the 3D shortfall.
|
||||||
|
- **ADR-029** — needs amendment to specify both N and zone-mode.
|
||||||
|
- **ADR-105 Krum** — N=5 still required for byzantine tolerance; this matches the 3D recommendation.
|
||||||
|
- **R14** V1/V2/V3 — V1 chest-only is naturally chest-mode = R6.2.3; V2 (mixed presence + chest) and V3 (chest) similarly. Aligning with R6.2.3 makes 3D coverage tractable.
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
# Tick 24 — 2026-05-22 08:53 UTC
|
||||||
|
|
||||||
|
**Thread:** R6.2.2.1 (3D N-anchor multistatic)
|
||||||
|
**Verdict:** The 2D knee at N=5 (R6.2.2) doesn't hold in 3D. **3D N=5 gives only 49.4% coverage vs 2D 96.8%.** Two responses: bump N OR use chest-centric zones (R6.2.3). The latter is the architectural fix.
|
||||||
|
|
||||||
|
## What shipped
|
||||||
|
|
||||||
|
- `examples/research-sota/r6_2_2_1_3d_multistatic.py` — pure-numpy 3D N-anchor greedy search.
|
||||||
|
- `examples/research-sota/r6_2_2_1_3d_multistatic_results.json` — saturation curve.
|
||||||
|
- `docs/research/sota-2026-05-22/R6_2_2_1-3d-multistatic.md` — research note.
|
||||||
|
|
||||||
|
## Headline: 2D was over-promising
|
||||||
|
|
||||||
|
| N | 2D (R6.2.2) | **3D (R6.2.2.1)** | Δ |
|
||||||
|
|---:|---:|---:|---:|
|
||||||
|
| 2 | 35.7% | 7.7% | -28 pp |
|
||||||
|
| 3 | 63.4% | 28.1% | -35 pp |
|
||||||
|
| 4 | 86.2% | 40.6% | -46 pp |
|
||||||
|
| 5 | 96.8% | **49.4%** | **-47 pp** |
|
||||||
|
| 6 | 100% | 59.1% | -41 pp |
|
||||||
|
| 7 | 100% | 65.1% | -35 pp |
|
||||||
|
|
||||||
|
**No clean knee in 3D.** Marginal gains stay 6-10 pp from N=4 onwards. 3D space is fundamentally harder because each Fresnel ellipsoid is a thin slab in the vertical direction, not a 2D rectangle.
|
||||||
|
|
||||||
|
## Greedy strongly prefers "mostly-low + one-high"
|
||||||
|
|
||||||
|
At every N ≥ 4, the search picks 3-5 LOW (0.8 m) + 0-1 MID (1.5 m) + 1 HIGH (ceiling). Confirms R6.2.1's single-pair finding: diagonal-in-z links win.
|
||||||
|
|
||||||
|
## ADR-029 amendment surfaced
|
||||||
|
|
||||||
|
The 2D-derived N=5 consumer rec is too optimistic for 3D. Two responses:
|
||||||
|
|
||||||
|
| Path | Mechanism | Outcome |
|
||||||
|
|---|---|---|
|
||||||
|
| Bump N | N=7-8 for 65%+ 3D coverage | More hardware, same target zones |
|
||||||
|
| **Use chest-centric (R6.2.3)** | Smaller zones (40×40 cm fits Fresnel envelope) | N=5 hits 80%+ |
|
||||||
|
|
||||||
|
**Recommended path: R6.2.3 + R6.2.2 N=5 = realistic 80%+ 3D coverage at ADR-029's default N.** Architectural lever that aligns 2D and 3D physics.
|
||||||
|
|
||||||
|
## Why this is meaningful (not a re-do)
|
||||||
|
|
||||||
|
R6.2.2 (2D) and R6.2.1 (3D single-pair) each told partial stories. R6.2.2.1 composes them and reveals 2D over-promised. Without this tick, ADR-029 would ship the 2D recommendation and discover the 3D shortfall during field deployment.
|
||||||
|
|
||||||
|
## Composes with prior threads
|
||||||
|
|
||||||
|
- R6.2 / R6.2.1 / R6.2.2: composition of the first three is the natural step
|
||||||
|
- R6.2.3: the elegant fix for the 3D shortfall
|
||||||
|
- R7 mincut: N ≥ 4 still required for byzantine detection
|
||||||
|
- ADR-029: needs N + zone-mode specified
|
||||||
|
- ADR-105 Krum: f=1 needs K ≥ 5; matches 3D recommendation
|
||||||
|
- R14 V1/V2/V3: chest-mode aligns with R6.2.3 = tractable 3D
|
||||||
|
|
||||||
|
## Honest scope
|
||||||
|
|
||||||
|
- Greedy + 4 restarts approximates global optimum (real may be 2-5 pp higher)
|
||||||
|
- 0.15 m 3D grid; finer would refine
|
||||||
|
- Single geometry tested (5×5×2.5 m bedroom)
|
||||||
|
- Free-space (no multipath restoring the 50 pp gap)
|
||||||
|
- Body-footprint zones used; chest-centric not composed yet (= R6.2.4 follow-up)
|
||||||
|
|
||||||
|
## Coordination
|
||||||
|
|
||||||
|
`ticks/tick-24.md`. No PROGRESS.md edit. Branch `research/sota-r6.2.2.1-3d-multistatic`.
|
||||||
|
|
||||||
|
## Remaining work
|
||||||
|
|
||||||
|
- R6.2.4: compose 3D N-anchor + chest-centric zones
|
||||||
|
- R6.2.5: multi-subject occupancy union
|
||||||
|
- R12.1: pose-PABS closed loop (still highest-leverage implementation)
|
||||||
|
- R3.2: embedding-level physics-informed env
|
||||||
|
- ADR-108: Kyber substitution
|
||||||
|
|
||||||
|
~3.2h to cron stop. **24 ticks landed.** Loop has 13 research threads + 3 ADRs + 9 deferred follow-ups closed.
|
||||||
|
|
||||||
|
## Note: this is the loop's first explicit "earlier tick was over-promising" finding
|
||||||
|
|
||||||
|
The previous 23 ticks have built on each other constructively. R6.2.2.1 is the first tick where the right action is to *revise downward* an earlier optimistic number (R6.2.2's 2D 97% becomes 3D 49%). Honest self-correction across ticks is the kind of integrity the loop is meant to produce.
|
||||||
|
|
@ -0,0 +1,195 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""R6.2.2.1 — 3D N-anchor multistatic placement (compose R6.2.1 + R6.2.2).
|
||||||
|
|
||||||
|
See docs/research/sota-2026-05-22/R6_2_2_1-3d-multistatic.md.
|
||||||
|
|
||||||
|
R6.2.2 found a 2D knee at N=5 anchors for typical bedroom geometry.
|
||||||
|
R6.2.1 found ceiling-only mounting gives 0% coverage in 3D. R6.2.2.1
|
||||||
|
composes both: how does the saturation curve change in 3D with mixed-
|
||||||
|
height candidate anchors?
|
||||||
|
|
||||||
|
Practical question: with mixed-height multistatic deployment, does the
|
||||||
|
4-anchor practical default (ADR-029) hit acceptable coverage in 3D?
|
||||||
|
|
||||||
|
Pure NumPy. Greedy search with K=4 random restarts.
|
||||||
|
"""
|
||||||
|
|
||||||
|
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_3d(p: np.ndarray, tx: np.ndarray, rx: np.ndarray,
|
||||||
|
wavelength: float) -> np.ndarray:
|
||||||
|
r1 = np.linalg.norm(p - tx, axis=1)
|
||||||
|
r2 = np.linalg.norm(p - rx, axis=1)
|
||||||
|
direct = np.linalg.norm(tx - rx)
|
||||||
|
return (r1 + r2) <= (direct + wavelength / 2)
|
||||||
|
|
||||||
|
|
||||||
|
def union_coverage_3d(anchors, target_pts, wavelength):
|
||||||
|
if len(anchors) < 2:
|
||||||
|
return 0.0
|
||||||
|
covered = np.zeros(len(target_pts), dtype=bool)
|
||||||
|
for i in range(len(anchors)):
|
||||||
|
for j in range(i+1, len(anchors)):
|
||||||
|
mask = in_first_fresnel_3d(target_pts, anchors[i], anchors[j], wavelength)
|
||||||
|
covered |= mask
|
||||||
|
return float(covered.mean())
|
||||||
|
|
||||||
|
|
||||||
|
def rasterise_targets_3d(zones, resolution=0.15):
|
||||||
|
pts = []
|
||||||
|
for name, x0, y0, z0, dx, dy, dz in zones:
|
||||||
|
xs = np.arange(x0, x0 + dx, resolution)
|
||||||
|
ys = np.arange(y0, y0 + dy, resolution)
|
||||||
|
zs = np.arange(z0, z0 + dz, resolution)
|
||||||
|
gx, gy, gz = np.meshgrid(xs, ys, zs, indexing="ij")
|
||||||
|
for x, y, z in zip(gx.ravel(), gy.ravel(), gz.ravel()):
|
||||||
|
pts.append([x, y, z])
|
||||||
|
return np.array(pts)
|
||||||
|
|
||||||
|
|
||||||
|
def candidate_positions_3d(room_w, room_h, room_z, step=0.75):
|
||||||
|
cands = []
|
||||||
|
# Wall mounts at three heights
|
||||||
|
for z in [0.8, 1.5, 2.4]:
|
||||||
|
for x in np.arange(0, room_w + 0.001, step):
|
||||||
|
cands.append(np.array([x, 0.0, z]))
|
||||||
|
cands.append(np.array([x, room_h, z]))
|
||||||
|
for y in np.arange(step, room_h, step):
|
||||||
|
cands.append(np.array([0.0, y, z]))
|
||||||
|
cands.append(np.array([room_w, y, z]))
|
||||||
|
# Ceiling mounts on a coarse grid
|
||||||
|
for x in np.arange(1.0, room_w, 1.0):
|
||||||
|
for y in np.arange(1.0, room_h, 1.0):
|
||||||
|
cands.append(np.array([x, y, room_z]))
|
||||||
|
return cands
|
||||||
|
|
||||||
|
|
||||||
|
def greedy_search(candidates, target_pts, wavelength, n_anchors, n_restarts=4, seed=0):
|
||||||
|
rng = np.random.default_rng(seed)
|
||||||
|
best = {"anchors": [], "score": -1.0, "trace": []}
|
||||||
|
for restart in range(n_restarts):
|
||||||
|
idx0, idx1 = rng.choice(len(candidates), size=2, replace=False)
|
||||||
|
chosen = [candidates[idx0], candidates[idx1]]
|
||||||
|
trace = [union_coverage_3d(chosen, target_pts, wavelength)]
|
||||||
|
while len(chosen) < n_anchors:
|
||||||
|
best_marginal = -1.0
|
||||||
|
best_idx = None
|
||||||
|
for k, c in enumerate(candidates):
|
||||||
|
if any(np.allclose(c, a) for a in chosen):
|
||||||
|
continue
|
||||||
|
trial = chosen + [c]
|
||||||
|
score = union_coverage_3d(trial, target_pts, wavelength)
|
||||||
|
if score > best_marginal:
|
||||||
|
best_marginal = score
|
||||||
|
best_idx = k
|
||||||
|
if best_idx is None: break
|
||||||
|
chosen.append(candidates[best_idx])
|
||||||
|
trace.append(best_marginal)
|
||||||
|
if trace[-1] > best["score"]:
|
||||||
|
best = {
|
||||||
|
"anchors": [a.tolist() for a in chosen],
|
||||||
|
"score": trace[-1],
|
||||||
|
"trace": trace,
|
||||||
|
}
|
||||||
|
return best
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--out", default="examples/research-sota/r6_2_2_1_3d_multistatic_results.json")
|
||||||
|
parser.add_argument("--n-max", type=int, default=7)
|
||||||
|
parser.add_argument("--restarts", type=int, default=4)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
room_w, room_h, room_z = 5.0, 5.0, 2.5
|
||||||
|
freq = 2.4
|
||||||
|
lam = wavelength_m(freq)
|
||||||
|
|
||||||
|
# Same 3D target zones as R6.2.1
|
||||||
|
target_zones = [
|
||||||
|
("bed", 1.5, 0.5, 0.3, 2.0, 1.5, 0.3),
|
||||||
|
("chair", 3.5, 3.5, 0.5, 0.8, 0.8, 0.7),
|
||||||
|
("standing", 0.5, 3.5, 1.0, 1.0, 1.0, 0.7),
|
||||||
|
]
|
||||||
|
target_pts = rasterise_targets_3d(target_zones, resolution=0.15)
|
||||||
|
candidates = candidate_positions_3d(room_w, room_h, room_z, step=0.75)
|
||||||
|
|
||||||
|
print(f"Room: {room_w}x{room_h}x{room_z} m at {freq} GHz")
|
||||||
|
print(f"Targets: {len(target_pts)} 3D points across 3 zones")
|
||||||
|
print(f"Candidates: {len(candidates)} positions (3 wall heights + ceiling grid)")
|
||||||
|
print()
|
||||||
|
|
||||||
|
saturation = []
|
||||||
|
for n in range(2, args.n_max + 1):
|
||||||
|
result = greedy_search(candidates, target_pts, lam,
|
||||||
|
n_anchors=n, n_restarts=args.restarts)
|
||||||
|
# Anchor height histogram
|
||||||
|
heights = [a[2] for a in result["anchors"]]
|
||||||
|
n_low = sum(1 for h in heights if h < 1.0)
|
||||||
|
n_mid = sum(1 for h in heights if 1.0 <= h < 2.0)
|
||||||
|
n_high = sum(1 for h in heights if h >= 2.0)
|
||||||
|
saturation.append({
|
||||||
|
"n_anchors": n,
|
||||||
|
"coverage": result["score"],
|
||||||
|
"n_pairs": n * (n - 1) // 2,
|
||||||
|
"heights": {"low_0.8m": n_low, "mid_1.5m": n_mid, "high_2.4m+": n_high},
|
||||||
|
})
|
||||||
|
|
||||||
|
print("=== 3D coverage saturation ===")
|
||||||
|
print(f"{'N':>3} {'Pairs':>6} {'Coverage':>9} {'Marginal':>9} {'Heights (low/mid/high)':>25}")
|
||||||
|
prev = 0.0
|
||||||
|
for s in saturation:
|
||||||
|
marg = (s["coverage"] - prev) * 100
|
||||||
|
h = s["heights"]
|
||||||
|
h_str = f"{h['low_0.8m']}/{h['mid_1.5m']}/{h['high_2.4m+']}"
|
||||||
|
print(f"{s['n_anchors']:>3} {s['n_pairs']:>6} {s['coverage']*100:>7.1f}% {marg:>+7.1f} pp {h_str:>25}")
|
||||||
|
prev = s["coverage"]
|
||||||
|
|
||||||
|
# Knee detection
|
||||||
|
marginal = []
|
||||||
|
for i in range(1, len(saturation)):
|
||||||
|
prev_cov = saturation[i-1]["coverage"]
|
||||||
|
curr_cov = saturation[i]["coverage"]
|
||||||
|
marginal.append({
|
||||||
|
"from_n": saturation[i-1]["n_anchors"],
|
||||||
|
"to_n": saturation[i]["n_anchors"],
|
||||||
|
"marginal_pp": (curr_cov - prev_cov) * 100,
|
||||||
|
})
|
||||||
|
|
||||||
|
knee = None
|
||||||
|
for m in marginal:
|
||||||
|
if m["marginal_pp"] < 4.0:
|
||||||
|
knee = m["from_n"]
|
||||||
|
print(f"\nKnee at N={knee} (going to N={m['to_n']} adds only {m['marginal_pp']:.1f} pp)")
|
||||||
|
break
|
||||||
|
|
||||||
|
out = {
|
||||||
|
"room": {"width_m": room_w, "depth_m": room_h, "ceiling_m": room_z},
|
||||||
|
"freq_ghz": freq,
|
||||||
|
"target_zones": [
|
||||||
|
{"name": n, "x": x0, "y": y0, "z": z0, "dx": dx, "dy": dy, "dz": dz}
|
||||||
|
for n, x0, y0, z0, dx, dy, dz in target_zones
|
||||||
|
],
|
||||||
|
"saturation": saturation,
|
||||||
|
"marginal": marginal,
|
||||||
|
"knee_n_anchors": knee,
|
||||||
|
}
|
||||||
|
Path(args.out).parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
Path(args.out).write_text(json.dumps(out, indent=2))
|
||||||
|
print(f"\nWrote {args.out}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
{
|
||||||
|
"room": {
|
||||||
|
"width_m": 5.0,
|
||||||
|
"depth_m": 5.0,
|
||||||
|
"ceiling_m": 2.5
|
||||||
|
},
|
||||||
|
"freq_ghz": 2.4,
|
||||||
|
"target_zones": [
|
||||||
|
{
|
||||||
|
"name": "bed",
|
||||||
|
"x": 1.5,
|
||||||
|
"y": 0.5,
|
||||||
|
"z": 0.3,
|
||||||
|
"dx": 2.0,
|
||||||
|
"dy": 1.5,
|
||||||
|
"dz": 0.3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "chair",
|
||||||
|
"x": 3.5,
|
||||||
|
"y": 3.5,
|
||||||
|
"z": 0.5,
|
||||||
|
"dx": 0.8,
|
||||||
|
"dy": 0.8,
|
||||||
|
"dz": 0.7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "standing",
|
||||||
|
"x": 0.5,
|
||||||
|
"y": 3.5,
|
||||||
|
"z": 1.0,
|
||||||
|
"dx": 1.0,
|
||||||
|
"dy": 1.0,
|
||||||
|
"dz": 0.7
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"saturation": [
|
||||||
|
{
|
||||||
|
"n_anchors": 2,
|
||||||
|
"coverage": 0.07659574468085106,
|
||||||
|
"n_pairs": 1,
|
||||||
|
"heights": {
|
||||||
|
"low_0.8m": 1,
|
||||||
|
"mid_1.5m": 1,
|
||||||
|
"high_2.4m+": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n_anchors": 3,
|
||||||
|
"coverage": 0.28085106382978725,
|
||||||
|
"n_pairs": 3,
|
||||||
|
"heights": {
|
||||||
|
"low_0.8m": 1,
|
||||||
|
"mid_1.5m": 2,
|
||||||
|
"high_2.4m+": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n_anchors": 4,
|
||||||
|
"coverage": 0.4056737588652482,
|
||||||
|
"n_pairs": 6,
|
||||||
|
"heights": {
|
||||||
|
"low_0.8m": 3,
|
||||||
|
"mid_1.5m": 0,
|
||||||
|
"high_2.4m+": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n_anchors": 5,
|
||||||
|
"coverage": 0.49361702127659574,
|
||||||
|
"n_pairs": 10,
|
||||||
|
"heights": {
|
||||||
|
"low_0.8m": 4,
|
||||||
|
"mid_1.5m": 0,
|
||||||
|
"high_2.4m+": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n_anchors": 6,
|
||||||
|
"coverage": 0.5914893617021276,
|
||||||
|
"n_pairs": 15,
|
||||||
|
"heights": {
|
||||||
|
"low_0.8m": 4,
|
||||||
|
"mid_1.5m": 1,
|
||||||
|
"high_2.4m+": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n_anchors": 7,
|
||||||
|
"coverage": 0.6510638297872341,
|
||||||
|
"n_pairs": 21,
|
||||||
|
"heights": {
|
||||||
|
"low_0.8m": 5,
|
||||||
|
"mid_1.5m": 1,
|
||||||
|
"high_2.4m+": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"marginal": [
|
||||||
|
{
|
||||||
|
"from_n": 2,
|
||||||
|
"to_n": 3,
|
||||||
|
"marginal_pp": 20.425531914893618
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_n": 3,
|
||||||
|
"to_n": 4,
|
||||||
|
"marginal_pp": 12.482269503546096
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_n": 4,
|
||||||
|
"to_n": 5,
|
||||||
|
"marginal_pp": 8.794326241134753
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_n": 5,
|
||||||
|
"to_n": 6,
|
||||||
|
"marginal_pp": 9.78723404255319
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_n": 6,
|
||||||
|
"to_n": 7,
|
||||||
|
"marginal_pp": 5.957446808510647
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"knee_n_anchors": null
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue