use chrono::{DateTime, Utc};
use nalgebra::Vector3;
use rand::SeedableRng;
use rand_distr::{Distribution, Normal};
use crate::IMUData;
use crate::earth::{meters_ned_to_dlat_dlon, METERS_TO_DEGREES};
use crate::filter::{
GPSPositionAndVelocityMeasurement, GravityAnomalyMeasurement, MagneticAnomalyMeasurement,
RelativeAltitudeMeasurement,
};
use crate::sim::TestDataRecord;
#[derive(Clone, Debug)]
pub enum GnssScheduler {
PassThrough,
FixedInterval {
interval_s: f64,
phase_s: f64,
},
DutyCycle {
on_s: f64,
off_s: f64,
start_phase_s: f64,
},
}
#[derive(Clone, Debug)]
pub enum GnssFaultModel {
None,
Degraded {
rho_pos: f64,
sigma_pos_m: f64,
rho_vel: f64,
sigma_vel_mps: f64,
r_scale: f64,
},
SlowBias {
drift_n_mps: f64,
drift_e_mps: f64,
q_bias: f64,
rotate_omega_rps: f64,
},
Hijack {
offset_n_m: f64,
offset_e_m: f64,
start_s: f64,
duration_s: f64,
},
Combo(Vec<GnssFaultModel>),
}
#[derive(Clone, Debug)]
pub struct GnssDegradationConfig {
pub scheduler: GnssScheduler,
pub fault: GnssFaultModel,
pub seed: u64,
}
#[derive(Clone, Debug)]
pub enum Event {
Imu {
dt_s: f64,
imu: IMUData,
elapsed_s: f64,
},
Gnss {
meas: GPSPositionAndVelocityMeasurement,
elapsed_s: f64,
},
Altitude {
meas: RelativeAltitudeMeasurement,
elapsed_s: f64,
},
GravityAnomaly {
meas: GravityAnomalyMeasurement,
elapsed_s: f64,
},
MagneticAnomaly {
meas: MagneticAnomalyMeasurement,
elapsed_s: f64,
},
}
pub struct EventStream {
pub start_time: DateTime<Utc>,
pub events: Vec<Event>,
}
#[derive(Clone, Debug)]
struct FaultState {
e_n_m: f64,
e_e_m: f64,
e_u_m: f64,
ev_n_mps: f64,
ev_e_mps: f64,
ev_u_mps: f64,
b_n_m: f64,
b_e_m: f64,
rng: rand::rngs::StdRng,
}
impl FaultState {
fn new(seed: u64) -> Self {
Self {
e_n_m: 0.0,
e_e_m: 0.0,
e_u_m: 0.0,
ev_n_mps: 0.0,
ev_e_mps: 0.0,
ev_u_mps: 0.0,
b_n_m: 0.0,
b_e_m: 0.0,
rng: rand::rngs::StdRng::seed_from_u64(seed),
}
}
}
fn ar1_step(x: &mut f64, rho: f64, sigma: f64, rng: &mut rand::rngs::StdRng) {
let n = Normal::new(0.0, sigma.max(0.0)).unwrap();
*x = rho * *x + n.sample(rng);
}
fn apply_fault(
fault: &GnssFaultModel,
st: &mut FaultState,
t: f64,
dt: f64,
lat_deg: f64,
lon_deg: f64,
alt_m: f64,
vn_mps: f64,
ve_mps: f64,
horiz_std_m: f64,
vert_std_m: f64,
vel_std_mps: f64,
) -> (f64, f64, f64, f64, f64, f64, f64) {
match fault {
GnssFaultModel::None => (
lat_deg,
lon_deg,
alt_m,
vn_mps,
ve_mps,
horiz_std_m,
vel_std_mps,
),
GnssFaultModel::Degraded {
rho_pos,
sigma_pos_m,
rho_vel,
sigma_vel_mps,
r_scale,
} => {
ar1_step(&mut st.e_n_m, *rho_pos, *sigma_pos_m, &mut st.rng);
ar1_step(&mut st.e_e_m, *rho_pos, *sigma_pos_m, &mut st.rng);
ar1_step(&mut st.e_u_m, *rho_pos, *sigma_pos_m, &mut st.rng);
ar1_step(&mut st.ev_n_mps, *rho_vel, *sigma_vel_mps, &mut st.rng);
ar1_step(&mut st.ev_e_mps, *rho_vel, *sigma_vel_mps, &mut st.rng);
let (dlat, dlon) =
meters_ned_to_dlat_dlon(lat_deg.to_radians(), alt_m, st.e_n_m, st.e_e_m);
let lat_c = lat_deg + dlat.to_degrees();
let lon_c = lon_deg + dlon.to_degrees();
let alt_c = alt_m + st.e_u_m;
let vn_c = vn_mps + st.ev_n_mps;
let ve_c = ve_mps + st.ev_e_mps;
(
lat_c,
lon_c,
alt_c,
vn_c,
ve_c,
horiz_std_m * r_scale,
vel_std_mps * r_scale,
)
}
GnssFaultModel::SlowBias {
drift_n_mps,
drift_e_mps,
q_bias,
rotate_omega_rps,
} => {
let (mut bn_dot, mut be_dot) = (*drift_n_mps, *drift_e_mps);
if *rotate_omega_rps != 0.0 {
let th = rotate_omega_rps * t;
let c = th.cos();
let s = th.sin();
let (n0, e0) = (*drift_n_mps, *drift_e_mps);
bn_dot = c * n0 - s * e0;
be_dot = s * n0 + c * e0;
}
st.b_n_m += bn_dot * dt;
st.b_e_m += be_dot * dt;
if *q_bias > 0.0 {
ar1_step(&mut st.b_n_m, 1.0, (q_bias * dt).sqrt(), &mut st.rng);
ar1_step(&mut st.b_e_m, 1.0, (q_bias * dt).sqrt(), &mut st.rng);
}
let (dlat, dlon) =
meters_ned_to_dlat_dlon(lat_deg.to_radians(), alt_m, st.b_n_m, st.b_e_m);
let lat_c = lat_deg + dlat.to_degrees();
let lon_c = lon_deg + dlon.to_degrees();
let vn_c = vn_mps + bn_dot;
let ve_c = ve_mps + be_dot;
(lat_c, lon_c, alt_m, vn_c, ve_c, horiz_std_m, vel_std_mps)
}
GnssFaultModel::Hijack {
offset_n_m,
offset_e_m,
start_s,
duration_s,
} => {
if t >= *start_s && t <= (start_s + duration_s) {
let (dlat, dlon) =
meters_ned_to_dlat_dlon(lat_deg.to_radians(), alt_m, *offset_n_m, *offset_e_m);
let lat_c = lat_deg + dlat.to_degrees();
let lon_c = lon_deg + dlon.to_degrees();
(
lat_c,
lon_c,
alt_m,
vn_mps,
ve_mps,
horiz_std_m,
vel_std_mps,
)
} else {
(
lat_deg,
lon_deg,
alt_m,
vn_mps,
ve_mps,
horiz_std_m,
vel_std_mps,
)
}
}
GnssFaultModel::Combo(models) => {
let mut out = (
lat_deg,
lon_deg,
alt_m,
vn_mps,
ve_mps,
horiz_std_m,
vel_std_mps,
);
for m in models {
}
out
}
}
}
pub fn build_event_stream(records: &[TestDataRecord], cfg: &GnssDegradationConfig) -> EventStream {
let start_time = records[0].time;
let records_with_elapsed: Vec<(f64, &TestDataRecord)> = records
.iter()
.map(|r| ((r.time - start_time).num_milliseconds() as f64 / 1000.0, r))
.collect();
let mut events = Vec::with_capacity(records_with_elapsed.len() * 2);
let mut st = FaultState::new(cfg.seed);
let mut next_emit_time = match cfg.scheduler {
GnssScheduler::PassThrough => 0.0,
GnssScheduler::FixedInterval {
interval_s,
phase_s,
} => phase_s,
GnssScheduler::DutyCycle { start_phase_s, .. } => start_phase_s,
};
let mut duty_on = true;
for w in records_with_elapsed.windows(2) {
let (t0, r0) = (&w[0].0, &w[0].1);
let (t1, r1) = (&w[1].0, &w[1].1);
let dt = t1 - t0;
let imu = IMUData {
accel: Vector3::new(r1.acc_x, r1.acc_y, r1.acc_z),
gyro: Vector3::new(r1.gyro_x, r1.gyro_y, r1.gyro_z),
};
events.push(Event::Imu {
dt_s: dt,
imu,
elapsed_s: *t1,
});
let should_emit = match cfg.scheduler {
GnssScheduler::PassThrough => true,
GnssScheduler::FixedInterval { interval_s, .. } => {
if *t1 + 1e-9 >= next_emit_time {
next_emit_time += interval_s;
true
} else {
false
}
}
GnssScheduler::DutyCycle { on_s, off_s, .. } => {
let window = if duty_on { on_s } else { off_s };
if *t1 + 1e-9 >= next_emit_time {
duty_on = !duty_on;
next_emit_time += window;
duty_on } else {
false
}
}
};
if should_emit {
let lat = r1.latitude;
let lon = r1.longitude;
let alt = r1.altitude;
let bearing_rad = r1.bearing.to_radians();
let vn = r1.speed * bearing_rad.cos();
let ve = r1.speed * bearing_rad.sin();
let horiz_std = r1.horizontal_accuracy.max(1e-3);
let vert_std = r1.vertical_accuracy.max(1e-3);
let vel_std = r1.speed_accuracy.max(0.1);
let (lat_c, lon_c, alt_c, vn_c, ve_c, horiz_c, vel_c) = apply_fault(
&cfg.fault, &mut st, *t1, dt, lat, lon, alt, vn, ve, horiz_std, vert_std, vel_std,
);
let meas = GPSPositionAndVelocityMeasurement {
latitude: lat_c,
longitude: lon_c,
altitude: alt_c,
northward_velocity: vn_c,
eastward_velocity: ve_c,
horizontal_noise_std: horiz_c,
vertical_noise_std: vert_std, velocity_noise_std: vel_c,
};
events.push(Event::Gnss {
meas,
elapsed_s: *t1,
});
}
}
EventStream { start_time, events }
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::{TimeZone, Utc};
use assert_approx_eq::assert_approx_eq;
fn create_test_records(count: usize, interval_secs: f64) -> Vec<TestDataRecord> {
let base_time = Utc.with_ymd_and_hms(2023, 1, 1, 0, 0, 0).unwrap();
let mut records = Vec::with_capacity(count);
for i in 0..count {
let time_ms = (i as f64 * interval_secs * 1000.0) as i64;
let time = base_time + chrono::Duration::milliseconds(time_ms);
let record = TestDataRecord {
time,
latitude: 37.0,
longitude: -122.0,
altitude: 100.0,
bearing: 45.0,
speed: 5.0,
acc_x: 0.0,
acc_y: 0.0,
acc_z: 9.81,
gyro_x: 0.0,
gyro_y: 0.0,
gyro_z: 0.01,
qx: 0.0,
qy: 0.0,
qz: 0.0,
qw: 1.0,
roll: 0.0,
pitch: 0.0,
yaw: 0.0,
mag_x: 0.0,
mag_y: 0.0,
mag_z: 0.0,
relative_altitude: 0.0,
pressure: 1013.25,
grav_x: 0.0,
grav_y: 0.0,
grav_z: 9.81,
horizontal_accuracy: 2.0,
vertical_accuracy: 4.0,
speed_accuracy: 0.5,
bearing_accuracy: 1.0,
};
records.push(record);
}
records
}
#[test]
fn test_passthrough_scheduler() {
let records = create_test_records(10, 0.1); let config = GnssDegradationConfig {
scheduler: GnssScheduler::PassThrough,
fault: GnssFaultModel::None,
seed: 42,
};
let events = build_event_stream(&records, &config);
assert_eq!(events.events.len(), 18);
let imu_count = events
.events
.iter()
.filter(|e| matches!(e, Event::Imu { .. }))
.count();
let gnss_count = events
.events
.iter()
.filter(|e| matches!(e, Event::Gnss { .. }))
.count();
assert_eq!(imu_count, 9);
assert_eq!(gnss_count, 9);
}
#[test]
fn test_fixed_interval_scheduler() {
let records = create_test_records(20, 0.1); let config = GnssDegradationConfig {
scheduler: GnssScheduler::FixedInterval {
interval_s: 0.5,
phase_s: 0.0,
},
fault: GnssFaultModel::None,
seed: 42,
};
let events = build_event_stream(&records, &config);
let imu_count = events
.events
.iter()
.filter(|e| matches!(e, Event::Imu { .. }))
.count();
let gnss_count = events
.events
.iter()
.filter(|e| matches!(e, Event::Gnss { .. }))
.count();
assert_eq!(imu_count, 19);
assert_eq!(gnss_count, 4); }
#[test]
fn test_duty_cycle_scheduler() {
let records = create_test_records(60, 1.0); assert!(records.len() == 60, "{}", format!("Expected 60 records, found: {}", records.len()));
let config = GnssDegradationConfig {
scheduler: GnssScheduler::DutyCycle {
on_s: 1.0,
off_s: 1.0,
start_phase_s: 0.0,
},
fault: GnssFaultModel::None,
seed: 42,
};
let events = build_event_stream(&records, &config);
let gnss_events: Vec<&Event> = events
.events
.iter()
.filter(|e| matches!(e, Event::Gnss { .. }))
.collect();
assert!(gnss_events.len() >= 2);
assert!(gnss_events.len() == 29, "{}", format!("Expected 29 GNSS events, found: {}", gnss_events.len()));
let gnss_times: Vec<f64> = gnss_events
.iter()
.map(|e| match e {
Event::Gnss { elapsed_s, .. } => *elapsed_s,
_ => unreachable!(),
})
.collect();
for i in 0..gnss_times.len().min(6) {
let expected_time = (i + 1) as f64 * 2.0;
assert_approx_eq!(expected_time, gnss_times[i], 0.1); }
}
#[test]
fn test_degraded_fault_model() {
let records = create_test_records(10, 0.1);
let config = GnssDegradationConfig {
scheduler: GnssScheduler::PassThrough,
fault: GnssFaultModel::Degraded {
rho_pos: 0.99,
sigma_pos_m: 3.0,
rho_vel: 0.95,
sigma_vel_mps: 0.3,
r_scale: 5.0,
},
seed: 500,
};
let events = build_event_stream(&records, &config);
let gnss_events: Vec<&Event> = events
.events
.iter()
.filter(|e| matches!(e, Event::Gnss { .. }))
.collect();
for event in &gnss_events {
if let Event::Gnss { meas, .. } = event {
assert_approx_eq!(meas.horizontal_noise_std, 9.0, 2.0); assert_approx_eq!(meas.velocity_noise_std, 2.5, 0.1); }
}
let original_lat = 37.0;
let original_lon = -122.0;
let mut all_same = true;
let mut prev_lat = None;
let mut prev_lon = None;
for event in &gnss_events {
if let Event::Gnss { meas, .. } = event {
assert_approx_eq!(meas.latitude, original_lat, 1e-3);
assert_approx_eq!(meas.longitude, original_lon, 1e-3);
if let Some(prev_lat) = prev_lat {
if (meas.latitude - prev_lat as f64).abs() > 1e-10 {
all_same = false;
}
}
prev_lat = Some(meas.latitude);
if let Some(prev_lon) = prev_lon {
if (meas.longitude - prev_lon as f64).abs() > 1e-10 {
all_same = false;
}
}
prev_lon = Some(meas.longitude);
}
}
assert!(!all_same);
}
#[test]
fn slow_bias_adds_velocity_bias() {
let mut st = FaultState::new(123);
let fault = GnssFaultModel::SlowBias {
drift_n_mps: 0.02,
drift_e_mps: -0.01,
q_bias: 0.0,
rotate_omega_rps: 0.0,
};
let (_lat, _lon, _alt, vn_c, ve_c, _hstd, _vstd) = apply_fault(
&fault, &mut st,
10.0, 1.0,
40.0, -75.0, 0.0,
0.0, 0.0,
3.0, 5.0, 0.2,
);
assert_approx_eq!(vn_c, 0.02, 0.001);
assert_approx_eq!(ve_c, -0.01, 0.001);
}
#[test]
fn test_hijack_fault_model() {
let records = create_test_records(30, 0.1); let offset_n = 50.0;
let offset_e = 30.0;
let config = GnssDegradationConfig {
scheduler: GnssScheduler::PassThrough,
fault: GnssFaultModel::Hijack {
offset_n_m: offset_n,
offset_e_m: offset_e,
start_s: 1.0,
duration_s: 1.0, },
seed: 42,
};
let events = build_event_stream(&records, &config);
let mut gnss_by_time: Vec<(f64, &GPSPositionAndVelocityMeasurement)> = Vec::new();
for event in &events.events {
if let Event::Gnss { meas, elapsed_s } = event {
gnss_by_time.push((*elapsed_s, meas));
}
}
gnss_by_time.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
let original_lat = 37.0;
let original_lon = -122.0;
for (time, meas) in gnss_by_time {
if time < 1.0 - 1e-6 || time > 2.0 + 1e-6 {
assert!((meas.latitude - original_lat).abs() < 1e-6);
assert!((meas.longitude - original_lon).abs() < 1e-6);
} else {
assert!((meas.latitude - original_lat).abs() > 1e-6);
assert!((meas.longitude - original_lon).abs() > 1e-6);
}
}
}
#[test]
fn test_combo_fault_model() {
let records = create_test_records(10, 0.1);
let config = GnssDegradationConfig {
scheduler: GnssScheduler::PassThrough,
fault: GnssFaultModel::Combo(vec![GnssFaultModel::None, GnssFaultModel::None]),
seed: 42,
};
let events = build_event_stream(&records, &config);
assert!(events.events.len() > 0);
}
#[test]
fn test_ar1_step() {
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
let mut x = 10.0;
ar1_step(&mut x, 0.0, 1.0, &mut rng);
assert!(x != 10.0); assert!(x.abs() < 5.0);
let mut x = 10.0;
ar1_step(&mut x, 1.0, 0.0, &mut rng);
assert_eq!(x, 10.0);
let mut x = 10.0;
ar1_step(&mut x, 0.5, -1.0, &mut rng);
assert_eq!(x, 5.0); }
}