1+ import argparse
2+ import json
3+ import numpy as np
4+ import pandas as pd
5+ import matplotlib .pyplot as plt
6+ from pathlib import Path
7+
8+ from ofdm .simulation .solver import solve_tdoa
9+ from ofdm .viz .sim_plotter import plot_experiment_results , plot_tdoa_hyperbolas
10+
11+ def load_rover_position (roaming_positions_path : Path ) -> np .ndarray :
12+ with open (roaming_positions_path ) as f :
13+ data = json .load (f )
14+ pos = next (iter (data .values ()))
15+ return np .array ([pos ["x" ], pos ["y" ]])
16+
17+ def main ():
18+ parser = argparse .ArgumentParser (description = "Run TDOA multilateration on a processed experiment." )
19+ parser .add_argument ("--experiment" , type = str , required = True , help = "Path to experiment directory" )
20+ parser .add_argument ("--devices" , nargs = "+" , required = True , help = "Device archive names e.g. RX3ch1 RX5ch1, RX7ch1" )
21+ parser .add_argument ("--anchor" , type = str , required = True , help = "Anchor device key in fixed_positions.json e.g ANCHORch0" )
22+ parser .add_argument ("--bias-ns" , type = float , default = 3.661 , help = "Hardware TDOA bias to subtract (nanoseconds)" )
23+ args = parser .parse_args ()
24+
25+ experiment_dir = Path (args .experiment )
26+
27+ with open (experiment_dir / "fixed_positions.json" ) as f :
28+ fixed = json .load (f )
29+
30+ anchor_pos = np .array ([fixed [args .anchor ]["x" ], fixed [args .anchor ]["y" ]])
31+ tx_true = np .array ([fixed ["TX" ]["x" ], fixed ["TX" ]["y" ]])
32+
33+ rover_positions = []
34+ device_dataframes = []
35+
36+ for device in args .devices :
37+ archive_dir = experiment_dir / f"{ device } _archive"
38+ rover_positions .append (load_rover_position (archive_dir / "roaming_positions.json" ))
39+
40+ df = pd .read_csv (archive_dir / "delays.csv" )
41+ device_dataframes .append (df [["run" , "delay0" , "delay1" ]].rename (columns = {"delay0" : f"delay0_{ device } " , "delay1" : f"delay1_{ device } " }))
42+
43+ rx_coords = np .array ([anchor_pos ] + rover_positions )
44+
45+ print (f"ROVER POSITIONS { rover_positions } " )
46+
47+ merged = device_dataframes [0 ][["run" ]].copy ()
48+ for df in device_dataframes :
49+ merged = merged .merge (df , on = "run" )
50+
51+ estimated_positions = []
52+
53+ MAX_TDOA_NS = 30
54+
55+ for _ , row in merged .iterrows ():
56+ delay_diffs_s = []
57+ valid = True
58+ for device in args .devices :
59+ tdoa_ns = row [f"delay0_{ device } " ] - row [f"delay1_{ device } " ] - args .bias_ns
60+ if abs (tdoa_ns ) > MAX_TDOA_NS :
61+ valid = False
62+ break
63+ delay_diffs_s .append (tdoa_ns * 1e-9 )
64+ if not valid :
65+ continue
66+ result = solve_tdoa (rx_coords , np .array (delay_diffs_s ))
67+ if result is not None :
68+ estimated_positions .append (result )
69+
70+ estimated_positions = np .array (estimated_positions )
71+ n_converged = len (estimated_positions )
72+ n_total = len (merged )
73+
74+ if n_converged == 0 :
75+ print ("No successful solves." )
76+ return
77+
78+ centroid = np .mean (estimated_positions , axis = 0 )
79+ errors = np .linalg .norm (estimated_positions - tx_true , axis = 1 )
80+ centroid_error = np .linalg .norm (centroid - tx_true )
81+
82+ print (f"Converged: { n_converged } /{ n_total } " )
83+ print (f"Centroid: X={ centroid [0 ]:.4f} m Y={ centroid [1 ]:.4f} m" )
84+ print (f"Mean error: { np .mean (errors )* 100 :.2f} cm" )
85+ print (f"RMSE: { np .sqrt (np .mean (errors ** 2 ))* 100 :.2f} cm" )
86+ print (f"P95 error: { np .percentile (errors , 95 )* 100 :.2f} cm" )
87+ print (f"Centroid error: { centroid_error * 100 :.2f} cm" )
88+
89+ results = {
90+ "estimates" : estimated_positions ,
91+ "centroid" : centroid ,
92+ "rmse" : np .sqrt (np .mean (errors ** 2 )),
93+ "mean_error" : np .mean (errors ),
94+ "p95_error" : np .percentile (errors , 95 ),
95+ "centroid_error" : centroid_error ,
96+ "n_converged" : n_converged ,
97+ "n_trials" : n_total ,
98+ }
99+
100+ fig , ax = plt .subplots (figsize = (9 , 9 ))
101+ plot_experiment_results (results , tx_true , rx_coords , ax = ax )
102+ plot_tdoa_hyperbolas (tx_true , rx_coords , results , ax )
103+ ax .set_title (f"OFDM Localization" )
104+ plt .tight_layout ()
105+ plt .show ()
106+
107+ if __name__ == "__main__" :
108+ main ()
0 commit comments