Skip to content

Commit 7a85b3e

Browse files
committed
Add script for comparing 1000km and 2000km results
1 parent a152ee9 commit 7a85b3e

4 files changed

Lines changed: 201 additions & 2 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ coverage.xml
2020
/sim_data/mc_results/*.png
2121
/sim_data/mc_results_1000km
2222
/sim_data/mc_results_2000km
23+
/sim_data/comparison

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ If you use this work, please cite it as:
5555
├── accord_demo.py # Demonstration of ACCORD
5656
├── changelog.md # Release change log
5757
├── LICENSE.MD # License file
58+
├── mc_comparison.py # Code for generating comparison plots for different Monte Carlo data sets
5859
├── mc_demo.py # Monte Carlo Simulation of ACCORD
5960
├── mypy.ini # Mypy configuration
6061
├── README.md # Project overview

mc_comparison.py

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
# pylint: disable=protected-access, too-many-locals, too-many-statements, too-many-arguments, too-many-positional-arguments, broad-exception-caught, duplicate-code
2+
"""
3+
The Autonomous Cooperative Consensus Orbit Determination (ACCORD) framework.
4+
Author: Beth Probert
5+
Email: beth.probert@strath.ac.uk
6+
7+
Copyright (C) 2025 Applied Space Technology Laboratory
8+
9+
This program is free software: you can redistribute it and/or modify
10+
it under the terms of the GNU General Public License as published by
11+
the Free Software Foundation, either version 3 of the License, or
12+
(at your option) any later version.
13+
14+
This program is distributed in the hope that it will be useful,
15+
but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+
GNU General Public License for more details.
18+
19+
You should have received a copy of the GNU General Public License
20+
along with this program. If not, see <http://www.gnu.org/licenses/>.
21+
22+
"""
23+
import os
24+
import numpy as np
25+
import matplotlib.pyplot as plt
26+
from typing import Dict, List, Any
27+
28+
# Paths
29+
PATH_100KM = "sim_data/mc_results/mc_results.npz"
30+
PATH_2000KM = "sim_data/mc_results_2000km/mc_results.npz"
31+
OUTPUT_DIR = "sim_data/comparison"
32+
33+
def load_results(path: str) -> List[Dict[str, Any]]:
34+
if not os.path.exists(path):
35+
print(f"Warning: File not found at {path}")
36+
return []
37+
try:
38+
with np.load(path, allow_pickle=True) as data:
39+
results = list(data['results'])
40+
return [res for res in results if res is not None]
41+
except Exception as e:
42+
print(f"Error loading {path}: {e}")
43+
return []
44+
45+
def plot_reputation_comparison(results_100km: List[Dict[str, Any]],
46+
results_2000km: List[Dict[str, Any]]) -> None:
47+
"""
48+
Plots reputation history for both datasets on the same graph.
49+
"""
50+
plt.figure(figsize=(12, 7))
51+
52+
def get_aggregated_reps(results):
53+
honest_means = []
54+
faulty_means = []
55+
for kpi in results:
56+
honest_means.append(np.mean(kpi["honest_matrix"], axis=0))
57+
faulty_means.append(np.mean(kpi["faulty_matrix"], axis=0))
58+
return np.array(honest_means), np.array(faulty_means)
59+
60+
if results_100km:
61+
h_100, f_100 = get_aggregated_reps(results_100km)
62+
steps_100 = np.arange(h_100.shape[1])
63+
64+
h_mean_100 = np.mean(h_100, axis=0)
65+
h_std_100 = np.std(h_100, axis=0)
66+
plt.plot(steps_100, h_mean_100, color="green", label="Honest (100km ISL)")
67+
plt.fill_between(steps_100, h_mean_100 - h_std_100, h_mean_100 + h_std_100, color="green", alpha=0.1)
68+
69+
f_mean_100 = np.mean(f_100, axis=0)
70+
f_std_100 = np.std(f_100, axis=0)
71+
plt.plot(steps_100, f_mean_100, color="red", label="Faulty (100km ISL)")
72+
plt.fill_between(steps_100, f_mean_100 - f_std_100, f_mean_100 + f_std_100, color="red", alpha=0.1)
73+
74+
if results_2000km:
75+
h_2000, f_2000 = get_aggregated_reps(results_2000km)
76+
steps_2000 = np.arange(h_2000.shape[1])
77+
78+
h_mean_2000 = np.mean(h_2000, axis=0)
79+
h_std_2000 = np.std(h_2000, axis=0)
80+
plt.plot(steps_2000, h_mean_2000, color="green", linestyle="--", label="Honest (2000km ISL)")
81+
plt.fill_between(steps_2000, h_mean_2000 - h_std_2000, h_mean_2000 + h_std_2000, color="green", alpha=0.1)
82+
83+
f_mean_2000 = np.mean(f_2000, axis=0)
84+
f_std_2000 = np.std(f_2000, axis=0)
85+
plt.plot(steps_2000, f_mean_2000, color="red", linestyle="--", label="Faulty (2000km ISL)")
86+
plt.fill_between(steps_2000, f_mean_2000 - f_std_2000, f_mean_2000 + f_std_2000, color="red", alpha=0.1)
87+
88+
plt.axhline(0.5, color="gray", linestyle=":", label="Neutral")
89+
plt.xlabel("Timestep [-]", fontsize=14)
90+
plt.ylabel("Reputation [-]", fontsize=14)
91+
plt.legend(loc='upper left')
92+
plt.grid(True, alpha=0.3)
93+
plt.tight_layout()
94+
plt.savefig(os.path.join(OUTPUT_DIR, "reputation_comparison.png"))
95+
plt.show()
96+
97+
def plot_kpi_comparison(results_100km: List[Dict[str, Any]],
98+
results_2000km: List[Dict[str, Any]]) -> None:
99+
"""
100+
Plots a bar chart comparison of key KPIs, including TTD as a percentage of runtime.
101+
"""
102+
metrics = ["recall", "precision", "fpr"]
103+
labels = ["Recall", "Precision", "False Positive Rate", "Normalised TTD"]
104+
105+
# Calculate means for standard metrics
106+
means_100 = [np.mean([k[m] for k in results_100km]) for m in metrics]
107+
means_2000 = [np.mean([k[m] for k in results_2000km]) for m in metrics]
108+
109+
# Calculate Normalised TTD (as % of total steps)
110+
def get_ttd_percent(results):
111+
ttd_pcts = []
112+
for k in results:
113+
if k.get("avg_ttd") is not None:
114+
total_steps = k["honest_matrix"].shape[1]
115+
ttd_pcts.append((k["avg_ttd"] / total_steps) * 100)
116+
return np.mean(ttd_pcts) if ttd_pcts else 0
117+
118+
means_100.append(get_ttd_percent(results_100km))
119+
means_2000.append(get_ttd_percent(results_2000km))
120+
121+
x = np.arange(len(labels))
122+
width = 0.35
123+
124+
_, ax = plt.subplots(figsize=(12, 6))
125+
ax.bar(x - width/2, means_100, width, label='100km ISL', color='skyblue')
126+
ax.bar(x + width/2, means_2000, width, label='2000km ISL', color='salmon')
127+
128+
ax.set_ylabel('Percentage [%]')
129+
ax.set_xticks(x)
130+
ax.set_xticklabels(labels)
131+
ax.legend()
132+
ax.grid(axis='y', alpha=0.3)
133+
ax.set_ylim(0, 105) # Metrics are percentages
134+
135+
plt.tight_layout()
136+
plt.savefig(os.path.join(OUTPUT_DIR, "kpi_percentage_comparison.png"))
137+
plt.show()
138+
139+
# TTD Comparison
140+
ttds_100 = [float(k.get("avg_ttd", 0)) for k in results_100km if k.get("avg_ttd") is not None]
141+
ttds_2000 = [float(k.get("avg_ttd", 0)) for k in results_2000km if k.get("avg_ttd") is not None]
142+
143+
if ttds_100 or ttds_2000:
144+
plt.figure(figsize=(6, 6))
145+
data_to_plot = []
146+
labels_ttd = []
147+
if ttds_100:
148+
data_to_plot.append(ttds_100)
149+
labels_ttd.append("100km ISL")
150+
if ttds_2000:
151+
data_to_plot.append(ttds_2000)
152+
labels_ttd.append("2000km ISL")
153+
154+
# Reduce gap by setting positions closer and increasing widths
155+
positions = [1, 1.5]
156+
plt.boxplot(data_to_plot, tick_labels=labels_ttd, positions=positions[:len(data_to_plot)], widths=0.35)
157+
plt.xlim(0.5, 2.0)
158+
plt.ylabel("Time to Detection [Timesteps]")
159+
plt.grid(axis='y', alpha=0.3)
160+
plt.tight_layout()
161+
plt.savefig(os.path.join(OUTPUT_DIR, "ttd_comparison.png"))
162+
plt.show()
163+
164+
def main():
165+
os.makedirs(OUTPUT_DIR, exist_ok=True)
166+
167+
print("Loading 100km results...")
168+
res_100 = load_results(PATH_100KM)
169+
print("Loading 2000km results...")
170+
res_2000 = load_results(PATH_2000KM)
171+
172+
if not res_100 and not res_2000:
173+
print("No results found to compare.")
174+
return
175+
176+
print(f"Comparing {len(res_100)} runs (100km) vs {len(res_2000)} runs (2000km)")
177+
178+
plot_reputation_comparison(res_100, res_2000)
179+
plot_kpi_comparison(res_100, res_2000)
180+
181+
# Also print summary
182+
def print_summary(label, results):
183+
if not results: return
184+
print(f"\n--- {label} Summary ---")
185+
print(f"Mean Recall: {np.mean([k['recall'] for k in results]):.2f}%")
186+
print(f"Mean Precision: {np.mean([k['precision'] for k in results]):.2f}%")
187+
print(f"Mean FPR: {np.mean([k['fpr'] for k in results]):.2f}%")
188+
ttds = [float(k.get("avg_ttd", 0)) for k in results if k.get("avg_ttd") is not None]
189+
if ttds:
190+
print(f"Mean TTD: {np.mean(ttds):.2f} steps")
191+
print(f"Avg Detection Margin: {np.mean([k.get('detection_margin', 0) for k in results]):.4f}")
192+
193+
print_summary("100km ISL", res_100)
194+
print_summary("2000km ISL", res_2000)
195+
196+
if __name__ == "__main__":
197+
main()

sim_data/mc_results/mc_results.npz

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
version https://git-lfs.github.com/spec/v1
2-
oid sha256:f81865d996490e2bfab301208fc733ef15a9aed64770016b16aa58129aac48ae
3-
size 95624151
2+
oid sha256:522d12c0e48ae67607237ab4e6b126d35a38ccf4f72d526c7f632c8b66c0a3ee
3+
size 59687719

0 commit comments

Comments
 (0)