Skip to content

Commit 8ea50f9

Browse files
authored
Merge pull request #11 from bprobert97/swap-violin-to-boxplot
Swap violin to boxplot
2 parents 144af46 + 055a5f2 commit 8ea50f9

File tree

2 files changed

+61
-85
lines changed

2 files changed

+61
-85
lines changed

accord_demo.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
import numpy as np
3030
from src.plotting import \
3131
plot_aggregated_reputation, check_consensus_outcomes, \
32-
plot_nis_violin, plot_ground_tracks, \
32+
plot_nis_boxplot, plot_ground_tracks, \
3333
calculate_convergence_index, \
3434
calculate_nis_convergence_index
3535
from src.consensus_mech import ConsensusMechanism
@@ -417,10 +417,10 @@ def __init__(self, ledger: dict): # pylint: disable=super-init-not-called
417417

418418
# Use the results for plotting
419419
if FINAL_DAG is not None and FAULTY_IDS is not None:
420-
plot_nis_violin(FINAL_DAG, faulty_ids=FAULTY_IDS)
420+
plot_nis_boxplot(FINAL_DAG, faulty_ids=FAULTY_IDS)
421421
NIS_CONVERGENCE_INDEX = calculate_nis_convergence_index(FINAL_DAG,\
422422
faulty_ids=FAULTY_IDS)
423-
plot_nis_violin(FINAL_DAG, faulty_ids=FAULTY_IDS, \
423+
plot_nis_boxplot(FINAL_DAG, faulty_ids=FAULTY_IDS, \
424424
convergence_index=NIS_CONVERGENCE_INDEX)
425425
check_consensus_outcomes(FINAL_DAG)
426426
if REP_HIST and FAULTY_IDS is not None:

src/plotting.py

Lines changed: 58 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@ def plot_nis_vs_consensus(df: pd.DataFrame) -> None:
6565
cbar = fig.colorbar(scatter, ax=ax)
6666
cbar.set_label("Correctness", fontsize=16)
6767

68-
ax.set_xlabel("Normalised Innovation Squared", fontsize=16)
69-
ax.set_ylabel("Consensus Score", fontsize=16)
68+
ax.set_xlabel("Normalised Innovation Squared [-]", fontsize=16)
69+
ax.set_ylabel("Consensus Score [-]", fontsize=16)
7070
ax.set_xscale('symlog')
7171
plt.tick_params(axis='x', labelsize=16)
7272
plt.tick_params(axis='y', labelsize=16)
@@ -281,32 +281,24 @@ def plot_nis_consistency_by_satellite(dag: DAG, confidence: float = 0.95) -> Non
281281
plt.show()
282282

283283

284-
def plot_nis_boxplot(dag: DAG) -> None:
284+
def plot_nis_boxplot(dag: DAG, faulty_ids: set[int],
285+
convergence_index: Optional[int] = None) -> None:
285286
"""
286-
Generates box plots visualizing the distribution of Normalised Innovation Squared (NIS)
287-
values for each simulated satellite.
288-
289-
This function collects NIS data from the DAG for honest and intermittently faulty
290-
satellites and loads pre-recorded malicious satellite NIS data from
291-
'sat1_nis_data.json'. The box plots illustrate the spread of NIS values,
292-
with different satellite types (honest, faulty, malicious) clearly labeled.
293-
294-
The plot includes horizontal lines indicating:
295-
- The 95% chi-squared confidence interval (for DOF=2), providing statistical bounds
296-
for expected NIS values.
297-
- The expected median of the chi-squared distribution (for DOF=2).
298-
299-
The y-axis uses a symmetrical log scale to better visualize a wide range of NIS values.
287+
Generates a grouped box plot for NIS values, separating honest and faulty satellites.
300288
301289
Args:
302-
dag (DAG): The DAG object containing transaction data, including NIS metadata
303-
for honest and intermittently faulty satellites.
290+
dag (DAG): The DAG object containing transaction data.
291+
faulty_ids (set[int]): A set of IDs for faulty satellites.
292+
convergence_index (int): Optional index to only plot data
293+
after filter convergence.
304294
305295
Returns:
306-
None: Displays a matplotlib box plot figure.
296+
None: Displays a matplotlib plot.
307297
"""
308-
# Collect data by satellite
309-
nis_data_by_sat: dict[str, list[float]] = {}
298+
honest_nis = []
299+
faulty_nis = []
300+
start_index = convergence_index if convergence_index is not None else 0
301+
310302
for _, tx_list in dag.ledger.items():
311303
for tx in tx_list:
312304
if not hasattr(tx.metadata, "nis"):
@@ -318,72 +310,56 @@ def plot_nis_boxplot(dag: DAG) -> None:
318310
continue
319311

320312
sid = tx_data.get("observer")
321-
if sid is None:
322-
continue
323-
324313
nis = getattr(tx.metadata, "nis", None)
325-
if nis is None:
326-
continue
327314

328-
nis_data_by_sat.setdefault(str(sid), []).append(nis)
315+
if sid is None or nis is None:
316+
continue
329317

330-
# Filter out satellites with no data
331-
nis_data_by_sat = {sid: vals for sid, vals in nis_data_by_sat.items() if vals}
318+
if int(sid) in faulty_ids:
319+
faulty_nis.append(nis)
320+
else:
321+
honest_nis.append(nis)
332322

333-
# Load data for the malicious satellite from file and slice it
334-
malicious_nis_data = None
335-
try:
336-
with open('sat1_nis_data.json', 'r', encoding='utf-8') as f:
337-
malicious_nis_data = json.load(f)
338-
if malicious_nis_data:
339-
malicious_nis_data = malicious_nis_data[100:]
340-
except FileNotFoundError:
341-
print("Warning: sat1_nis_data.json not found. Cannot plot malicious data.")
342-
except json.JSONDecodeError:
343-
print("Warning: Could not decode sat1_nis_data.json. Cannot plot malicious data.")
344-
345-
# Sort satellites by ID, then move sat 1 to the end of the DAG-based data.
346-
sorted_sids = sorted(nis_data_by_sat.keys(), key=int)
347-
if '1' in sorted_sids:
348-
sorted_sids.remove('1')
349-
sorted_sids.append('1')
350-
351-
nis_values_for_plot = [nis_data_by_sat[sid] for sid in sorted_sids]
352-
labels = [f"Honest Satellite\n(ID: Sat_{sid})" if sid != "1" else \
353-
"Satellite with \nIntermittent Fault\n(ID: Sat_1)" for sid in sorted_sids]
354-
355-
# Add malicious data if loaded and it has points left
356-
if malicious_nis_data:
357-
nis_values_for_plot.append(malicious_nis_data)
358-
labels.append("Malicious Satellite\n(ID: Sat_1)")
359-
360-
if not nis_values_for_plot:
323+
if not honest_nis and not faulty_nis:
361324
print("No NIS data available to create a box plot.")
362325
return
363326

364-
plt.figure(figsize=(10, 6))
365-
bp = plt.boxplot(nis_values_for_plot,
366-
labels=labels) # type: ignore [call-arg]
367-
for median in bp['medians']:
368-
median.set_color('blue')
369-
370-
# Add chi-squared bounds
371-
dof = 2
372-
confidence = 0.95
327+
honest_nis = honest_nis[start_index:]
328+
faulty_nis = faulty_nis[start_index:]
329+
330+
plot_data = []
331+
labels = []
332+
333+
if honest_nis:
334+
plot_data.append(honest_nis)
335+
labels.append("Honest Satellites")
336+
if faulty_nis:
337+
plot_data.append(faulty_nis)
338+
labels.append("Faulty Satellites")
339+
340+
_, ax = plt.subplots(figsize=(10, 6))
341+
342+
# Create box plot
343+
parts = ax.boxplot(plot_data, labels=labels) # type: ignore [call-arg]
344+
345+
for partname in ('cbars', 'cmins', 'cmaxes', 'cmedians'):
346+
if partname in parts:
347+
parts[partname].set_color('black')
348+
parts[partname].set_linewidth(1.5)
349+
350+
# Add expected median (assuming DOF=2)
373351
expected_median = 1.298
374-
chi2_lower = chi2.ppf((1 - confidence) / 2, df=dof)
375-
chi2_upper = chi2.ppf((1 + confidence) / 2, df=dof)
376-
plt.axhline(chi2_lower, color='r', linestyle='--',
377-
label='95% Chi-squared Confidence Interval (DOF=2)')
378-
plt.axhline(chi2_upper, color='r', linestyle='--')
379-
plt.axhline(expected_median, color='black', linestyle=':', label='Expected Median (DOF=2)')
380-
381-
plt.ylabel("Normalised Innovation Squared", fontsize=18)
382-
plt.yscale("symlog")
383-
plt.tick_params(axis='x', labelsize=18)
384-
plt.tick_params(axis='y', labelsize=18)
385-
plt.legend(fontsize=18)
386-
plt.grid(True, linestyle=":", alpha=0.7)
352+
353+
ax.axhline(expected_median, color='black', linestyle=':', label='Expected Median')
354+
355+
ax.set_xticks(np.arange(1, len(labels) + 1))
356+
ax.set_xticklabels(labels, fontsize=16)
357+
ax.set_ylabel("Normalised Innovation Squared [-]", fontsize=16)
358+
ax.set_yscale("symlog")
359+
360+
ax.legend(fontsize=14)
361+
ax.grid(True, linestyle=":", alpha=0.7)
362+
387363
plt.tight_layout()
388364
plt.show()
389365

@@ -830,8 +806,8 @@ def plot_ground_tracks(truth: np.ndarray, n: int) -> None:
830806
]
831807
ax.legend(handles=handles, loc='upper right', framealpha=1.0, facecolor='white')
832808

833-
ax.set_xlabel("Longitude (Degrees)", fontsize=12)
834-
ax.set_ylabel("Latitude (Degrees)", fontsize=12)
809+
ax.set_xlabel("Longitude [Degrees]", fontsize=12)
810+
ax.set_ylabel("Latitude [Degrees]", fontsize=12)
835811
# White grid looks better on dark maps
836812
ax.grid(True, linestyle=":", alpha=0.4, color='white')
837813

0 commit comments

Comments
 (0)