use kshana::clock_integrity::{sky_at, IntegrityCfg};
use serde_json::Value;
const SECTION: &str =
"\n[integrity]\nsite = { lat_deg = 59.44, lon_deg = 24.75, height_m = 30.0 }\nstep_s = 60.0\n";
fn clock() -> String {
std::fs::read_to_string("scenarios/clock-holdover.toml").unwrap()
}
fn doc(src: &str) -> Value {
serde_json::from_str(&kshana::api::run_toml(src).expect("runs").json).unwrap()
}
fn nominal_all(extra: &str) -> String {
clock().replace(
" { t0 = 0.0, t1 = 600.0, state = \"nominal\" },\n { t0 = 600.0, t1 = 7200.0, state = \"denied\" },",
" { t0 = 0.0, t1 = 7200.0, state = \"nominal\" },",
) + SECTION
+ extra
}
#[test]
fn the_block_is_separate_and_absent_by_default() {
let plain = doc(&clock());
assert!(plain.get("integrity_fom").is_none());
let with = doc(&(clock() + SECTION));
for k in ["quantum", "classical", "scenario_hash"] {
if k == "scenario_hash" {
assert_ne!(
with[k], plain[k],
"the section is an input, so it is hashed"
);
} else {
assert_eq!(with[k], plain[k], "{k} moved with [integrity]");
}
}
let f = &with["integrity_fom"];
assert_eq!(f["label"], "MODELLED");
assert_eq!(f["n_epochs"], 121);
assert_eq!(f["n_evaluated"], 10);
assert_eq!(f["n_denied"], 111);
for e in f["epochs"].as_array().unwrap() {
if e["gnss_state"] == "denied" {
assert_eq!(e["verdict"], "unavailable-gnss-denied");
assert!(e.get("hpl_m").is_none() && e.get("vpl_m").is_none());
}
}
let ens = clock().replace("seed = 42", "seed = 42\nruns = 3") + SECTION;
assert!(
kshana::api::run_toml(&ens).is_err(),
"[integrity] needs runs = 1"
);
}
#[test]
fn protection_levels_are_the_snapshot_raims_on_the_same_sky() {
let src = nominal_all("");
let f = doc(&src)["integrity_fom"].clone();
let cfg: IntegrityCfg =
toml::from_str(SECTION.trim_start().trim_start_matches("[integrity]\n")).unwrap();
let user = kshana::geometry::site_ecef(&cfg.site).unwrap();
for e in f["epochs"]
.as_array()
.unwrap()
.iter()
.step_by(20)
.filter(|e| e["gnss_state"] != "denied")
{
let t = e["t_s"].as_f64().unwrap();
let sky = sky_at(&cfg, user, t).unwrap();
assert_eq!(e["n_sats"].as_u64().unwrap() as usize, sky.len());
let sats: Vec<[f64; 3]> = sky.iter().map(|s| s.1).collect();
let zero = vec![0.0; sats.len()];
let r = kshana::raim::snapshot_raim(user, &sats, &zero, 1.0, 1e-5, 1e-3).expect("raim");
assert_eq!(e["hpl_m"].as_f64().unwrap(), r.hpl_m, "t {t}");
assert_eq!(e["vpl_m"].as_f64().unwrap(), r.vpl_m, "t {t}");
}
assert_eq!(f["n_alarm"], 0, "fault-free epochs must not alarm");
assert!(f["n_available"].as_u64().unwrap() > 100);
}
#[test]
fn a_stated_bias_is_detected_while_it_lasts() {
let cfg: IntegrityCfg =
toml::from_str(SECTION.trim_start().trim_start_matches("[integrity]\n")).unwrap();
let user = kshana::geometry::site_ecef(&cfg.site).unwrap();
let id = sky_at(&cfg, user, 3000.0).unwrap()[0].0.clone();
let src = nominal_all(&format!(
"injected_bias = {{ sat_id = \"{id}\", bias_m = 60.0, t0_s = 3000.0, t1_s = 3600.0 }}\n"
));
let f = doc(&src)["integrity_fom"].clone();
for e in f["epochs"].as_array().unwrap() {
let t = e["t_s"].as_f64().unwrap();
let inside = (3000.0..3600.0).contains(&t);
let alarm = e["verdict"] == "alarm";
assert_eq!(alarm, inside, "t {t}: verdict {}", e["verdict"]);
}
assert_eq!(f["n_alarm"], 10);
}
#[test]
fn every_integrity_field_has_a_unit() {
let src = clock()
+ SECTION
+ "injected_bias = { sat_id = \"gps-baseline-2\", bias_m = 60.0, t0_s = 0.0, t1_s = 120.0 }\n";
let a = kshana::field_schema::audit_document(&doc(&src));
assert!(a.missing.is_empty(), "missing units: {:?}", a.missing);
let plain = doc(&clock());
assert!(!plain["units"]
.as_object()
.unwrap()
.keys()
.any(|k| k.starts_with("integrity_fom")));
}