@@ -3,9 +3,13 @@ use chrono::{DateTime, Utc};
33use nalgebra:: Vector3 ;
44use rand:: SeedableRng ;
55use rand_distr:: { Distribution , Normal } ;
6+ use serde:: { Deserialize , Serialize } ;
7+ use std:: fs:: File ;
8+ use std:: io:: { self , Read , Write } ;
9+ use std:: path:: Path ;
610
711use crate :: IMUData ;
8- use crate :: earth:: { meters_ned_to_dlat_dlon, METERS_TO_DEGREES } ;
12+ use crate :: earth:: meters_ned_to_dlat_dlon;
913use crate :: filter:: {
1014 GPSPositionAndVelocityMeasurement , GravityAnomalyMeasurement , MagneticAnomalyMeasurement ,
1115 RelativeAltitudeMeasurement ,
@@ -45,7 +49,8 @@ use crate::sim::TestDataRecord;
4549/// // Alternate 5 s ON, 15 s OFF, starting in ON state at t=0
4650/// let sched = GnssScheduler::DutyCycle { on_s: 5.0, off_s: 15.0, start_phase_s: 0.0 };
4751/// ```
48- #[ derive( Clone , Debug ) ]
52+ #[ derive( Clone , Debug , Serialize , Deserialize ) ]
53+ #[ serde( tag = "kind" , rename_all = "snake_case" ) ]
4954pub enum GnssScheduler {
5055 /// Pass every GNSS fix through to the filter with no rate reduction.
5156 ///
@@ -81,6 +86,66 @@ pub enum GnssScheduler {
8186 start_phase_s : f64 ,
8287 } ,
8388}
89+
90+ #[ cfg( test) ]
91+ mod serialization_tests {
92+ use super :: * ;
93+ use tempfile:: NamedTempFile ;
94+
95+ fn sample_cfg ( ) -> GnssDegradationConfig {
96+ GnssDegradationConfig {
97+ scheduler : GnssScheduler :: PassThrough ,
98+ fault : GnssFaultModel :: Degraded {
99+ rho_pos : 0.99 ,
100+ sigma_pos_m : 3.0 ,
101+ rho_vel : 0.95 ,
102+ sigma_vel_mps : 0.3 ,
103+ r_scale : 5.0 ,
104+ } ,
105+ seed : 42 ,
106+ }
107+ }
108+
109+ #[ test]
110+ fn json_roundtrip ( ) {
111+ let cfg = sample_cfg ( ) ;
112+ let f = NamedTempFile :: new ( ) . unwrap ( ) ;
113+ let path = f. path ( ) . with_extension ( "json" ) ;
114+ cfg. to_json ( & path) . unwrap ( ) ;
115+ let loaded = GnssDegradationConfig :: from_json ( & path) . unwrap ( ) ;
116+ assert_eq ! ( cfg. seed, loaded. seed) ;
117+ }
118+
119+ #[ test]
120+ fn yaml_roundtrip ( ) {
121+ let cfg = sample_cfg ( ) ;
122+ let f = NamedTempFile :: new ( ) . unwrap ( ) ;
123+ let path = f. path ( ) . with_extension ( "yaml" ) ;
124+ cfg. to_yaml ( & path) . unwrap ( ) ;
125+ let loaded = GnssDegradationConfig :: from_yaml ( & path) . unwrap ( ) ;
126+ assert_eq ! ( cfg. seed, loaded. seed) ;
127+ }
128+
129+ #[ test]
130+ fn toml_roundtrip ( ) {
131+ let cfg = sample_cfg ( ) ;
132+ let f = NamedTempFile :: new ( ) . unwrap ( ) ;
133+ let path = f. path ( ) . with_extension ( "toml" ) ;
134+ cfg. to_toml ( & path) . unwrap ( ) ;
135+ let loaded = GnssDegradationConfig :: from_toml ( & path) . unwrap ( ) ;
136+ assert_eq ! ( cfg. seed, loaded. seed) ;
137+ }
138+
139+ #[ test]
140+ fn generic_dispatch_roundtrip ( ) {
141+ let cfg = sample_cfg ( ) ;
142+ let f = NamedTempFile :: new ( ) . unwrap ( ) ;
143+ let path = f. path ( ) . with_extension ( "json" ) ;
144+ cfg. to_file ( & path) . unwrap ( ) ;
145+ let loaded = GnssDegradationConfig :: from_file ( & path) . unwrap ( ) ;
146+ assert_eq ! ( cfg. seed, loaded. seed) ;
147+ }
148+ }
84149/// Models how GNSS measurement *content* is corrupted before it reaches the filter.
85150///
86151/// This is complementary to [`GnssScheduler`], which decides *when* GNSS
@@ -144,7 +209,8 @@ pub enum GnssScheduler {
144209/// GnssFaultModel::Hijack { offset_n_m: 50.0, offset_e_m: 0.0, start_s: 120.0, duration_s: 60.0 },
145210/// ]);
146211/// ```
147- #[ derive( Clone , Debug ) ]
212+ #[ derive( Clone , Debug , Serialize , Deserialize ) ]
213+ #[ serde( tag = "kind" , rename_all = "snake_case" ) ]
148214pub enum GnssFaultModel {
149215 /// No corruption; GNSS fixes are passed through unchanged.
150216 None ,
@@ -236,7 +302,7 @@ pub enum GnssFaultModel {
236302/// seed: 42,
237303/// };
238304/// ```
239- #[ derive( Clone , Debug ) ]
305+ #[ derive( Clone , Debug , Serialize , Deserialize ) ]
240306pub struct GnssDegradationConfig {
241307 /// Scheduler that determines when GNSS measurements are emitted
242308 /// (e.g., pass-through, fixed interval, or duty-cycled).
@@ -253,6 +319,74 @@ pub struct GnssDegradationConfig {
253319 pub seed : u64 ,
254320}
255321
322+ impl GnssDegradationConfig {
323+ /// Write the configuration to a JSON file (pretty-printed).
324+ pub fn to_json < P : AsRef < Path > > ( & self , path : P ) -> io:: Result < ( ) > {
325+ let file = File :: create ( path) ?;
326+ serde_json:: to_writer_pretty ( file, self ) . map_err ( |e| io:: Error :: new ( io:: ErrorKind :: Other , e) )
327+ }
328+
329+ /// Read the configuration from a JSON file.
330+ pub fn from_json < P : AsRef < Path > > ( path : P ) -> io:: Result < Self > {
331+ let file = File :: open ( path) ?;
332+ serde_json:: from_reader ( file) . map_err ( |e| io:: Error :: new ( io:: ErrorKind :: Other , e) )
333+ }
334+ /// Write the configuration as YAML.
335+ pub fn to_yaml < P : AsRef < Path > > ( & self , path : P ) -> io:: Result < ( ) > {
336+ let mut file = File :: create ( path) ?;
337+ let s = serde_yaml:: to_string ( self ) . map_err ( |e| io:: Error :: new ( io:: ErrorKind :: Other , e) ) ?;
338+ file. write_all ( s. as_bytes ( ) )
339+ }
340+
341+ /// Read the configuration from YAML.
342+ pub fn from_yaml < P : AsRef < Path > > ( path : P ) -> io:: Result < Self > {
343+ let file = File :: open ( path) ?;
344+ serde_yaml:: from_reader ( file) . map_err ( |e| io:: Error :: new ( io:: ErrorKind :: Other , e) )
345+ }
346+ /// Write the configuration as TOML.
347+ pub fn to_toml < P : AsRef < Path > > ( & self , path : P ) -> io:: Result < ( ) > {
348+ let mut file = File :: create ( path) ?;
349+ let s = toml:: to_string ( self ) . map_err ( |e| io:: Error :: new ( io:: ErrorKind :: Other , e) ) ?;
350+ file. write_all ( s. as_bytes ( ) )
351+ }
352+ /// Read the configuration from TOML.
353+ pub fn from_toml < P : AsRef < Path > > ( path : P ) -> io:: Result < Self > {
354+ let mut s = String :: new ( ) ;
355+ let mut file = File :: open ( path) ?;
356+ file. read_to_string ( & mut s) ?;
357+ toml:: from_str ( & s) . map_err ( |e| io:: Error :: new ( io:: ErrorKind :: Other , e) )
358+ }
359+ /// Generic write: choose format by file extension (.json/.yaml/.yml/.toml)
360+ pub fn to_file < P : AsRef < Path > > ( & self , path : P ) -> io:: Result < ( ) > {
361+ let p = path. as_ref ( ) ;
362+ let ext = p
363+ . extension ( )
364+ . and_then ( |s| s. to_str ( ) )
365+ . map ( |s| s. to_lowercase ( ) ) ;
366+ match ext. as_deref ( ) {
367+ Some ( "json" ) => self . to_json ( p) ,
368+ Some ( "yaml" ) | Some ( "yml" ) => self . to_yaml ( p) ,
369+ Some ( "toml" ) => self . to_toml ( p) ,
370+ _ => Err ( io:: Error :: new ( io:: ErrorKind :: InvalidInput , "unsupported file extension" ) ) ,
371+ }
372+ }
373+
374+ /// Generic read: choose format by file extension (.json/.yaml/.yml/.toml)
375+ pub fn from_file < P : AsRef < Path > > ( path : P ) -> io:: Result < Self > {
376+ let p = path. as_ref ( ) ;
377+ let ext = p
378+ . extension ( )
379+ . and_then ( |s| s. to_str ( ) )
380+ . map ( |s| s. to_lowercase ( ) ) ;
381+ match ext. as_deref ( ) {
382+ Some ( "json" ) => Self :: from_json ( p) ,
383+ Some ( "yaml" ) | Some ( "yml" ) => Self :: from_yaml ( p) ,
384+ Some ( "toml" ) => Self :: from_toml ( p) ,
385+ _ => Err ( io:: Error :: new ( io:: ErrorKind :: InvalidInput , "unsupported file extension" ) ) ,
386+ }
387+ }
388+ }
389+
256390/// A simulation event delivered to the filter in time order.
257391///
258392/// Events represent sensor updates or other observations that occur during
@@ -473,7 +607,7 @@ impl FaultState {
473607///
474608/// ```ignore
475609/// use rand::SeedableRng;
476- ///
610+ ///
477611/// let mut x = 0.0;
478612/// let mut rng = SeedableRng::seed_from_u64(42);
479613/// for _ in 0..5 {
@@ -713,7 +847,7 @@ fn apply_fault(
713847 }
714848}
715849// --------------------------- public API ---------------------------
716- /// Build a time-ordered event stream from recorded data and a GNSS degradation
850+ /// Build a time-ordered event stream from recorded data and a GNSS degradation
717851/// configuration.
718852///
719853/// This function converts raw `records` into a vector of [`Event`]s suitable
@@ -886,8 +1020,8 @@ pub fn build_event_stream(records: &[TestDataRecord], cfg: &GnssDegradationConfi
8861020#[ cfg( test) ]
8871021mod tests {
8881022 use super :: * ;
889- use chrono:: { TimeZone , Utc } ;
8901023 use assert_approx_eq:: assert_approx_eq;
1024+ use chrono:: { TimeZone , Utc } ;
8911025
8921026 fn create_test_records ( count : usize , interval_secs : f64 ) -> Vec < TestDataRecord > {
8931027 let base_time = Utc . with_ymd_and_hms ( 2023 , 1 , 1 , 0 , 0 , 0 ) . unwrap ( ) ;
@@ -994,7 +1128,11 @@ mod tests {
9941128 #[ test]
9951129 fn test_duty_cycle_scheduler ( ) {
9961130 let records = create_test_records ( 60 , 1.0 ) ; // 60 records, 1s apart, 60 seconds total
997- assert ! ( records. len( ) == 60 , "{}" , format!( "Expected 60 records, found: {}" , records. len( ) ) ) ;
1131+ assert ! (
1132+ records. len( ) == 60 ,
1133+ "{}" ,
1134+ format!( "Expected 60 records, found: {}" , records. len( ) )
1135+ ) ;
9981136
9991137 let config = GnssDegradationConfig {
10001138 scheduler : GnssScheduler :: DutyCycle {
@@ -1005,7 +1143,7 @@ mod tests {
10051143 fault : GnssFaultModel :: None ,
10061144 seed : 42 ,
10071145 } ;
1008- //
1146+ //
10091147 let events = build_event_stream ( & records, & config) ;
10101148 //assert!(events.len() == 60, "{}", format!("Expected 60 events, found: {}", events.len()));
10111149 // We should only have GNSS events when turning ON
@@ -1018,7 +1156,11 @@ mod tests {
10181156 // We initialize off of the first event and only get GNSS updates every two seconds starting from 1.0
10191157 // (60 - 1) // 2 = 29
10201158 assert ! ( gnss_events. len( ) >= 2 ) ;
1021- assert ! ( gnss_events. len( ) == 29 , "{}" , format!( "Expected 29 GNSS events, found: {}" , gnss_events. len( ) ) ) ;
1159+ assert ! (
1160+ gnss_events. len( ) == 29 ,
1161+ "{}" ,
1162+ format!( "Expected 29 GNSS events, found: {}" , gnss_events. len( ) )
1163+ ) ;
10221164
10231165 // Extract elapsed_s from GNSS events and verify they occur at expected times
10241166 let gnss_times: Vec < f64 > = gnss_events
@@ -1126,18 +1268,15 @@ mod tests {
11261268
11271269 // zero “truth” velocity
11281270 let ( _lat, _lon, _alt, vn_c, ve_c, _hstd, _vstd) = apply_fault (
1129- & fault, & mut st,
1130- /*t*/ 10.0 , /*dt*/ 1.0 ,
1131- /*lat_deg*/ 40.0 , /*lon_deg*/ -75.0 , /*alt_m*/ 0.0 ,
1132- /*vn_mps*/ 0.0 , /*ve_mps*/ 0.0 ,
1271+ & fault, & mut st, /*t*/ 10.0 , /*dt*/ 1.0 , /*lat_deg*/ 40.0 ,
1272+ /*lon_deg*/ -75.0 , /*alt_m*/ 0.0 , /*vn_mps*/ 0.0 , /*ve_mps*/ 0.0 ,
11331273 /*horiz_std_m*/ 3.0 , /*vert_std_m*/ 5.0 , /*vel_std_mps*/ 0.2 ,
11341274 ) ;
11351275
1136- assert_approx_eq ! ( vn_c, 0.02 , 0.001 ) ;
1276+ assert_approx_eq ! ( vn_c, 0.02 , 0.001 ) ;
11371277 assert_approx_eq ! ( ve_c, -0.01 , 0.001 ) ;
11381278 }
11391279
1140-
11411280 #[ test]
11421281 fn test_hijack_fault_model ( ) {
11431282 let records = create_test_records ( 30 , 0.1 ) ; // 30 records, 0.1s apart, total 3.0 seconds
@@ -1184,8 +1323,6 @@ mod tests {
11841323 }
11851324 }
11861325
1187-
1188-
11891326 #[ test]
11901327 fn test_combo_fault_model ( ) {
11911328 // Test that the combo fault model functionality exists
0 commit comments