@@ -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 \n Intermittent 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