Skip to content

Commit ce8b7f5

Browse files
authored
Merge pull request #8 from osherlock1/add-simulation
add: Monte Carlo Simulatioins
2 parents 74e64da + 7660649 commit ce8b7f5

15 files changed

Lines changed: 516 additions & 5 deletions

File tree

configs/layout.json.example

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"rx_coords" : [
3+
[0.0, 0.0],
4+
[0.508, 0.137],
5+
[0.0, 0.615],
6+
[0.7, 0.1]
7+
],
8+
"tx_true" :
9+
[0.565, 0.906]
10+
}

scripts/experiment_scripts/collect_raw_data.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
SOURCE_DAT_FILE = "./data_files/rand_ofdm_packet_rx"
1212

1313
# --------- MODIFY --------------
14-
EXPERIMENT_NAME = "virtual_multilateration_3" # CHOOSE NAME OF EXPERIMENT TO BE RUN
15-
ROAMING_DEVICES = ["RX2ch1"] # NAME OF DEVICE THAT IS MOVED (WILL ASK FOR POSITIONS EACH RUN)
14+
EXPERIMENT_NAME = "rj_virtual_multilateration_300" # CHOOSE NAME OF EXPERIMENT TO BE RUN
15+
ROAMING_DEVICES = ["RX4ch1"] # NAME OF DEVICE THAT IS MOVED (WILL ASK FOR POSITIONS EACH RUN)
1616
FIXED_DEVICES = ["ANCHORch0", "TX"] # NAME OF DEVICES THAT ARE FIXED (WILL ONLY ASK ONCE PER EXPERIMENT)
1717
# -------------------------------
1818

scripts/simulation/heapmap.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import numpy as np
2+
import matplotlib.pyplot as plt
3+
from ofdm.simulation.monte_carlo import run_monte_carlo
4+
from ofdm.config import loadLayout
5+
6+
7+
def main():
8+
layout_config_pth = "./configs/layout.json"
9+
rx_coords, tx_true = loadLayout(layout_config_pth)
10+
11+
rx_x = rx_coords[:, 0].reshape(-1, 1, 1)
12+
rx_y = rx_coords[:, 1].reshape(-1, 1, 1)
13+
14+
x_range = np.linspace(0, 1, num=40)
15+
y_range = np.linspace(0, 1, num=40)
16+
X, Y = np.meshgrid(x_range, y_range)
17+
18+
error_heatmap = np.zeros(X.shape)
19+
20+
for i in range(len(x_range)):
21+
for j in range(len(y_range)):
22+
tx_coords = np.array([X[i][j], Y[i][j]])
23+
results = run_monte_carlo(
24+
rx_coords=rx_coords,
25+
tx_pos=tx_coords,
26+
sigma_ns=0.1,
27+
n_trials=10,
28+
seed=42,
29+
)
30+
error_heatmap[i][j] = results['rmse']
31+
32+
plt.figure(figsize=(10, 8))
33+
v_max = 1
34+
v_min = 0
35+
cp = plt.pcolormesh(X, Y, error_heatmap,vmin=v_min, vmax=v_max, shading='auto', cmap='viridis')
36+
plt.colorbar(cp, label='RMSE (meters)')
37+
plt.scatter(rx_x, rx_y, marker='^', color='red', label='Receivers')
38+
plt.title('OFDM Localization Error Heatmap')
39+
plt.xlabel('X Position (m)')
40+
plt.ylabel('Y Position (m)')
41+
plt.legend()
42+
plt.show()
43+
44+
45+
if __name__ == "__main__":
46+
main()

scripts/simulation/monte_carlo.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import argparse
2+
import numpy as np
3+
import matplotlib.pyplot as plt
4+
from ofdm.viz.sim_plotter import plot_mc_results, plot_tdoa_hyperbolas, DraggableSimulation
5+
from ofdm.simulation.monte_carlo import run_monte_carlo
6+
from ofdm.config import loadLayout
7+
8+
def main():
9+
parser = argparse.ArgumentParser()
10+
parser.add_argument("--tx", nargs=2, type=float)
11+
parser.add_argument("--sigma-ns", type=float, default=0.05)
12+
parser.add_argument("--trials", type=int, default=1000)
13+
args = parser.parse_args()
14+
15+
layout_conf_path = "./configs/layout.json"
16+
rx_coords, tx_true = loadLayout(layout_conf_path)
17+
18+
if args.tx is not None:
19+
tx_true = np.array(args.tx)
20+
21+
results = run_monte_carlo(
22+
tx_pos=tx_true,
23+
rx_coords=rx_coords,
24+
sigma_ns=args.sigma_ns,
25+
n_trials=args.trials,
26+
seed=42,
27+
)
28+
29+
print(f"Converged: {results['n_converged']}/{results['n_trials']}")
30+
print(f"RMSE: {results['rmse']*100:.2f} cm")
31+
print(f"Mean error: {results['mean_error']*100:.2f} cm")
32+
print(f"P95 errors: {results['p95_error']*100:.2f} cm")
33+
print(f"Centroid: X={results['centroid'][0]:.4f} cm, Y={results['centroid'][1]:.4f} cm")
34+
35+
ax = plot_mc_results(results, tx_true, rx_coords, args.sigma_ns)
36+
plot_tdoa_hyperbolas(tx_true, rx_coords, results, ax)
37+
plt.tight_layout()
38+
sim = DraggableSimulation(
39+
ax=ax,
40+
tx_pos=tx_true,
41+
rx_coords=rx_coords,
42+
sigma_ns=args.sigma_ns,
43+
n_trials=args.trials
44+
)
45+
46+
plt.show()
47+
48+
if __name__ == "__main__":
49+
main()

scripts/trilateration.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,6 @@ def toa_cost_function(guess, rx_coords, measured_distance):
121121
Calculates difference between theoretical distances based on guessed (x,y) and the actual measured distnace.
122122
123123
"""
124-
125124
x, y = guess
126125
residuals = np.zeros(len(rx_coords))
127126

src/ofdm/config.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from dataclasses import dataclass
22
import random
3+
import json
4+
import numpy as np
35

46

57
@dataclass
@@ -49,8 +51,6 @@ def _load_random_map(self):
4951
self.pilot_carriers.sort()
5052
self.data_carriers.sort()
5153

52-
53-
5454
def _load_default_map(self):
5555
"""
5656
Define Default OFDM mapping if no Config is provided
@@ -71,3 +71,12 @@ def _idx(self, k: int) -> int:
7171
Helper to convert from python indexing to freq bin indexing
7272
"""
7373
return (k + self.N) % self.N
74+
75+
76+
def loadLayout(layout_config_path:str)->np.ndarray:
77+
"""
78+
Loads configs/layout.json. Returns rx_coords, tx_true as np.ndarrays
79+
"""
80+
with open(layout_config_path, "r") as f:
81+
coords = json.load(f)
82+
return np.array(coords['rx_coords']), np.array(coords['tx_true'])

src/ofdm/simulation/__init__.py

Whitespace-only changes.

src/ofdm/simulation/geometry.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import numpy as np
2+
from scipy import constants
3+
4+
C = constants.c
5+
6+
def compute_toa(tx_pos, rx_coords):
7+
distances = np.linalg.norm(rx_coords - tx_pos, axis = 1)
8+
return distances / C
9+
10+
def compute_tdoa(tx_pos, rx_coords):
11+
toa = compute_toa(tx_pos, rx_coords)
12+
return toa[1:] - toa[0] # roaming rx - anchor
13+
14+
def compute_distances(tx_pos, rx_coords):
15+
return np.linalg.norm(rx_coords - tx_pos, axis = 1)

src/ofdm/simulation/monte_carlo.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import numpy as np
2+
from ofdm.simulation.geometry import compute_tdoa
3+
from ofdm.simulation.noise_model import add_gausian_nosie
4+
from ofdm.simulation.solver import solve_tdoa
5+
6+
7+
def run_monte_carlo(tx_pos, rx_coords, sigma_ns, n_trials=1000, seed=None):
8+
"""
9+
Run monte carlo TDOA localization simluation.
10+
Returns dict with estimates, errors, and summary stats
11+
"""
12+
rng = np.random.default_rng(seed)
13+
ideal_tdoas = compute_tdoa(tx_pos, rx_coords)
14+
15+
estimates = []
16+
for _ in range(n_trials):
17+
noisy_tdoas = add_gausian_nosie(ideal_tdoas, sigma_ns, rng=rng)
18+
est = solve_tdoa(rx_coords, noisy_tdoas)
19+
if est is not None:
20+
estimates.append(est)
21+
estimates = np.array(estimates)
22+
errors = np.linalg.norm(estimates - tx_pos, axis=1)
23+
return {
24+
"estimates": estimates,
25+
"errors": errors,
26+
"rmse": np.sqrt(np.mean(errors**2)),
27+
"mean_error": np.mean(errors),
28+
"p95_error": np.percentile(errors, 95),
29+
"centroid": np.mean(estimates, axis=0),
30+
"n_converged": len(estimates),
31+
"n_trials": n_trials,
32+
}
33+

src/ofdm/simulation/noise_model.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import numpy as np
2+
3+
def add_gausian_nosie(tdoa_values, sigma_ns, rng=None):
4+
"""
5+
Add gausian noise to time difference of arrival delay values.
6+
signa_ns: noise standard deviation in ns
7+
rng: optional rng for repoducibility
8+
"""
9+
if rng is None:
10+
rng = np.random.default_rng()
11+
sigma_sec = sigma_ns * 1e-9
12+
noise = rng.normal(0, sigma_sec, size=tdoa_values.shape)
13+
return tdoa_values + noise
14+
15+
16+

0 commit comments

Comments
 (0)