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:
rUv
2026-05-22 04:58:10 -04:00
committed by GitHub
parent 8b850d8b2a
commit df13dcf597
4 changed files with 519 additions and 0 deletions
@@ -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
}