188 lines
8.3 KiB
Python
188 lines
8.3 KiB
Python
#!/usr/bin/env python3
|
|
"""R3 — Cross-room CSI re-identification: simulation of the embedding-overlap problem.
|
|
|
|
See docs/research/sota-2026-05-22/R3-crossroom-reid.md.
|
|
|
|
Simulates the core problem: a CSI embedding is a sum of two contributions:
|
|
embedding = person_signature + environment_signature
|
|
|
|
Within a single room, the environment signature is constant across all
|
|
subjects, so K-NN works (~95% acc per AETHER, ADR-024). Across rooms,
|
|
the environment signature changes by O(1) -- larger than the
|
|
per-person signature variation -- so naive K-NN collapses to chance.
|
|
|
|
This script:
|
|
1. Generates synthetic embeddings for 10 subjects across 3 rooms
|
|
2. Measures within-room K-NN accuracy (baseline)
|
|
3. Measures cross-room K-NN accuracy (raw embeddings)
|
|
4. Applies domain-invariance via MERIDIAN-style environment subtraction
|
|
5. Reports the accuracy gap
|
|
|
|
Pure NumPy, no ML deps. The simulation makes physically-realistic
|
|
assumptions about embedding dimensions and noise floors.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import numpy as np
|
|
from pathlib import Path
|
|
|
|
|
|
def generate_synthetic_embeddings(n_subjects: int, n_rooms: int,
|
|
n_samples_per_subject_per_room: int,
|
|
embedding_dim: int = 128,
|
|
person_signature_scale: float = 0.35,
|
|
environment_signature_scale: float = 1.5,
|
|
noise_scale: float = 0.3,
|
|
seed: int = 42) -> np.ndarray:
|
|
"""Generate (n_subjects, n_rooms, n_samples, embedding_dim) tensor.
|
|
Each embedding = person_sig[subject] + env_sig[room] + noise."""
|
|
rng = np.random.default_rng(seed)
|
|
person_sigs = rng.standard_normal((n_subjects, embedding_dim)) * person_signature_scale
|
|
env_sigs = rng.standard_normal((n_rooms, embedding_dim)) * environment_signature_scale
|
|
embeddings = np.zeros((n_subjects, n_rooms, n_samples_per_subject_per_room, embedding_dim))
|
|
for s in range(n_subjects):
|
|
for r in range(n_rooms):
|
|
base = person_sigs[s] + env_sigs[r]
|
|
noise = rng.standard_normal((n_samples_per_subject_per_room, embedding_dim)) * noise_scale
|
|
embeddings[s, r] = base + noise
|
|
return embeddings, person_sigs, env_sigs
|
|
|
|
|
|
def cosine_knn_accuracy(query: np.ndarray, gallery: np.ndarray,
|
|
query_labels: np.ndarray, gallery_labels: np.ndarray,
|
|
k: int = 1) -> float:
|
|
"""1-shot cosine K-NN accuracy. Returns fraction of queries correctly matched."""
|
|
q_norm = query / (np.linalg.norm(query, axis=1, keepdims=True) + 1e-9)
|
|
g_norm = gallery / (np.linalg.norm(gallery, axis=1, keepdims=True) + 1e-9)
|
|
sims = q_norm @ g_norm.T # (n_query, n_gallery)
|
|
top_k_indices = np.argsort(-sims, axis=1)[:, :k]
|
|
correct = 0
|
|
for i, top_k in enumerate(top_k_indices):
|
|
top_k_labels = gallery_labels[top_k]
|
|
vals, counts = np.unique(top_k_labels, return_counts=True)
|
|
majority = vals[np.argmax(counts)]
|
|
if majority == query_labels[i]:
|
|
correct += 1
|
|
return correct / len(query)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--out", default="examples/research-sota/r3_reid_results.json")
|
|
args = parser.parse_args()
|
|
|
|
n_subjects = 10
|
|
n_rooms = 3
|
|
n_samples = 20
|
|
emb_dim = 128
|
|
|
|
emb, person_sigs, env_sigs = generate_synthetic_embeddings(
|
|
n_subjects, n_rooms, n_samples, emb_dim,
|
|
)
|
|
|
|
# ===== 1. Within-room K-NN baseline =====
|
|
# Train on first 10 samples of each (subject, room), query on the rest
|
|
within_accuracies = []
|
|
for r in range(n_rooms):
|
|
train = emb[:, r, :10, :].reshape(-1, emb_dim)
|
|
query = emb[:, r, 10:, :].reshape(-1, emb_dim)
|
|
train_labels = np.repeat(np.arange(n_subjects), 10)
|
|
query_labels = np.repeat(np.arange(n_subjects), 10)
|
|
acc = cosine_knn_accuracy(query, train, query_labels, train_labels, k=1)
|
|
within_accuracies.append(acc)
|
|
within_mean = float(np.mean(within_accuracies))
|
|
|
|
# ===== 2. Cross-room K-NN (raw, no domain invariance) =====
|
|
# Train on room 0, query on rooms 1 + 2
|
|
cross_accuracies_raw = []
|
|
train = emb[:, 0, :, :].reshape(-1, emb_dim)
|
|
train_labels = np.repeat(np.arange(n_subjects), n_samples)
|
|
for r in [1, 2]:
|
|
query = emb[:, r, :, :].reshape(-1, emb_dim)
|
|
query_labels = np.repeat(np.arange(n_subjects), n_samples)
|
|
acc = cosine_knn_accuracy(query, train, query_labels, train_labels, k=1)
|
|
cross_accuracies_raw.append(acc)
|
|
cross_raw_mean = float(np.mean(cross_accuracies_raw))
|
|
|
|
# ===== 3. Cross-room with environment subtraction (MERIDIAN-style) =====
|
|
# Compute per-room mean (across all subjects in that room)
|
|
# and subtract it from each embedding. This removes the env_sig
|
|
# contribution exactly, leaving person_sig + noise.
|
|
cross_accuracies_meridian = []
|
|
train_centroid = emb[:, 0, :, :].reshape(-1, emb_dim).mean(axis=0)
|
|
train_clean = emb[:, 0, :, :].reshape(-1, emb_dim) - train_centroid
|
|
for r in [1, 2]:
|
|
query_centroid = emb[:, r, :, :].reshape(-1, emb_dim).mean(axis=0)
|
|
query_clean = emb[:, r, :, :].reshape(-1, emb_dim) - query_centroid
|
|
query_labels = np.repeat(np.arange(n_subjects), n_samples)
|
|
acc = cosine_knn_accuracy(query_clean, train_clean, query_labels, train_labels, k=1)
|
|
cross_accuracies_meridian.append(acc)
|
|
cross_meridian_mean = float(np.mean(cross_accuracies_meridian))
|
|
|
|
# ===== 4. Cross-room with PARTIAL invariance (incomplete env subtraction) =====
|
|
# Real MERIDIAN can't perfectly recover the env signal -- it's
|
|
# estimated from labeled examples. Simulate a 70% effective subtraction.
|
|
partial_strength = 0.7
|
|
cross_accuracies_partial = []
|
|
train_partial = emb[:, 0, :, :].reshape(-1, emb_dim) - partial_strength * train_centroid
|
|
for r in [1, 2]:
|
|
query_centroid = emb[:, r, :, :].reshape(-1, emb_dim).mean(axis=0)
|
|
query_partial = emb[:, r, :, :].reshape(-1, emb_dim) - partial_strength * query_centroid
|
|
query_labels = np.repeat(np.arange(n_subjects), n_samples)
|
|
acc = cosine_knn_accuracy(query_partial, train_partial, query_labels, train_labels, k=1)
|
|
cross_accuracies_partial.append(acc)
|
|
cross_partial_mean = float(np.mean(cross_accuracies_partial))
|
|
|
|
# ===== 5. Embedding distance breakdown =====
|
|
# How big is environment_sig vs person_sig?
|
|
person_sig_norm = float(np.linalg.norm(person_sigs, axis=1).mean())
|
|
env_sig_norm = float(np.linalg.norm(env_sigs, axis=1).mean())
|
|
|
|
out = {
|
|
"config": {
|
|
"n_subjects": n_subjects, "n_rooms": n_rooms, "n_samples_per_room": n_samples,
|
|
"embedding_dim": emb_dim,
|
|
"person_signature_scale": 0.35,
|
|
"environment_signature_scale": 1.5,
|
|
"noise_scale": 0.3,
|
|
},
|
|
"signature_norms": {
|
|
"person_norm_avg": person_sig_norm,
|
|
"environment_norm_avg": env_sig_norm,
|
|
"env_to_person_ratio": env_sig_norm / person_sig_norm,
|
|
},
|
|
"accuracy": {
|
|
"within_room_baseline": within_mean,
|
|
"cross_room_raw": cross_raw_mean,
|
|
"cross_room_meridian_perfect": cross_meridian_mean,
|
|
"cross_room_meridian_70pct": cross_partial_mean,
|
|
"chance": 1.0 / n_subjects,
|
|
},
|
|
}
|
|
Path(args.out).parent.mkdir(parents=True, exist_ok=True)
|
|
Path(args.out).write_text(json.dumps(out, indent=2))
|
|
|
|
print("=== Cross-room re-ID simulation ===")
|
|
print(f" Embedding dim: {emb_dim}")
|
|
print(f" Subjects: {n_subjects}")
|
|
print(f" Rooms: {n_rooms}")
|
|
print(f" Samples per subject per room: {n_samples}")
|
|
print()
|
|
print(f" Person signature norm avg: {person_sig_norm:.2f}")
|
|
print(f" Environment signature norm: {env_sig_norm:.2f}")
|
|
print(f" Env/Person ratio: {env_sig_norm / person_sig_norm:.2f}x")
|
|
print()
|
|
print(f" Within-room 1-shot K-NN: {within_mean*100:.1f}% (matches AETHER ~95% target)")
|
|
print(f" Cross-room RAW: {cross_raw_mean*100:.1f}% (chance is {100/n_subjects:.1f}%)")
|
|
print(f" Cross-room with MERIDIAN 100%: {cross_meridian_mean*100:.1f}%")
|
|
print(f" Cross-room with MERIDIAN 70%: {cross_partial_mean*100:.1f}%")
|
|
print()
|
|
print(f"Wrote {args.out}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|