kshana 0.33.1

Open, reproducible PNT-resilience simulator with quantum-sensor performance models
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
//! Scenario pack registry — the dispatch seam.
//!
//! Historically `crate::api::run_toml` matched a resolved [`crate::api::ScenarioKind`]
//! against one big exhaustive table. This module introduces a thin, object-safe
//! registry over that table so out-of-tree packs can interpose without forking core,
//! while the built-ins keep running byte-for-byte the same code. The seam is
//! `no_std`/WASM-friendly: it uses [`std::collections::BTreeMap`] and
//! [`std::borrow::Cow`] and never reads the wall clock.

use std::borrow::Cow;
use std::collections::BTreeMap;

/// A stable, interned identifier for a scenario pack — the registry key. For the
/// built-ins this is exactly the kebab-case `kind` string from
/// [`crate::api::ScenarioKind::as_str`]; third-party packs choose their own.
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct ScenarioId(Cow<'static, str>);

impl ScenarioId {
    /// Construct an id from a `'static` string in `const` context (used by the
    /// [`ids`] table below). No allocation.
    pub const fn from_static(s: &'static str) -> Self {
        ScenarioId(Cow::Borrowed(s))
    }

    /// The id as a string slice.
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl std::fmt::Display for ScenarioId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&self.0)
    }
}

/// The single source of truth for which kinds are built in. Both [`ids::all`] and
/// [`PackRegistry::with_builtins`] are derived from this, so they cannot drift.
const BUILTIN_KINDS: &[crate::api::ScenarioKind] = {
    use crate::api::ScenarioKind::*;
    &[
        Clock,
        Inertial,
        Integrity,
        TimeTransfer,
        QuantumTimeTransfer,
        QuantumGnssFreeNav,
        QuantumAnomalyDetect,
        Hybrid,
        Fusion,
        HybridUkf,
        GnssIns,
        GnssSim,
        Jamming,
        Spoof,
        SpoofDetect,
        Sweep,
        SweepNd,
        Orbit,
        Ephemeris,
        LunarIntegrity,
        LunarTime,
        LunarVlbi,
        LunarCombination,
        LunarFrameRealise,
        LunarFrameCampaign,
        LunarService,
        LunarDpnt,
        LunarBeacon,
        EarthGnssLunar,
        LunarInterop,
        GravityMap,
        Terrain,
        TerrainSlam,
        CombinedAltPnt,
        Pvt,
        MarsPnt,
        ImpairmentEval,
        QuantumTrade,
        SpaceWeather,
        OemInterop,
        LaunchWindow,
        Reentry,
        EoCoverage,
        SpacePacket,
        AttitudeBudget,
        Passes,
        LinkBudget,
        LunarTimeBudget,
        RealtimeFrameEop,
        HybridOpticalRf,
        CislunarObservability,
        CislunarArcRecovery,
        ConflictResilience,
        LunarAttackSurface,
        ApertureDutyCycle,
        LunarJamming,
        InsTrnCoast,
        LunarVlbiFim,
        TrackingLoop,
        AraimReferenceCheck,
        LunarLlrDatum,
        TelecomTiming,
        SlotTiming,
        Spectrum,
        SolarSystem,
        BodyPnt,
        ConstellationDesign,
        Campaign,
        LeoSignal,
        LeoPass,
        LeoNavmsg,
        LeoPvt,
        LeoPpp,
        NtnPositioning,
        LeoPntChain,
    ]
};

/// The canonical id constants — one per built-in [`crate::api::ScenarioKind`]. The
/// string values are the exact kebab strings returned by
/// [`crate::api::ScenarioKind::as_str`].
pub mod ids {
    use super::ScenarioId;

    pub const CLOCK: ScenarioId = ScenarioId::from_static("clock");
    pub const INERTIAL: ScenarioId = ScenarioId::from_static("inertial");
    pub const INTEGRITY: ScenarioId = ScenarioId::from_static("integrity");
    pub const TIMETRANSFER: ScenarioId = ScenarioId::from_static("timetransfer");
    pub const QUANTUM_TIME_TRANSFER: ScenarioId = ScenarioId::from_static("quantum-time-transfer");
    pub const QUANTUM_GNSS_FREE_NAV: ScenarioId = ScenarioId::from_static("quantum-gnss-free-nav");
    pub const QUANTUM_ANOMALY_DETECT: ScenarioId =
        ScenarioId::from_static("quantum-anomaly-detect");
    pub const HYBRID: ScenarioId = ScenarioId::from_static("hybrid");
    pub const FUSION: ScenarioId = ScenarioId::from_static("fusion");
    pub const HYBRID_UKF: ScenarioId = ScenarioId::from_static("hybrid-ukf");
    pub const GNSS_INS: ScenarioId = ScenarioId::from_static("gnss-ins");
    pub const GNSS_SIM: ScenarioId = ScenarioId::from_static("gnss-sim");
    pub const JAMMING: ScenarioId = ScenarioId::from_static("jamming");
    pub const SPOOF: ScenarioId = ScenarioId::from_static("spoof");
    pub const SPOOF_DETECT: ScenarioId = ScenarioId::from_static("spoof-detect");
    pub const SWEEP: ScenarioId = ScenarioId::from_static("sweep");
    pub const SWEEP_ND: ScenarioId = ScenarioId::from_static("sweep-nd");
    pub const ORBIT: ScenarioId = ScenarioId::from_static("orbit");
    pub const EPHEMERIS: ScenarioId = ScenarioId::from_static("ephemeris");
    pub const LUNAR_INTEGRITY: ScenarioId = ScenarioId::from_static("lunar-integrity");
    pub const LUNAR_TIME_OFFSET: ScenarioId = ScenarioId::from_static("lunar-time-offset");
    pub const LUNAR_VLBI: ScenarioId = ScenarioId::from_static("lunar-vlbi");
    pub const LUNAR_JOINT_OD_CLOCK: ScenarioId = ScenarioId::from_static("lunar-joint-od-clock");
    pub const LUNAR_FRAME_REALISATION: ScenarioId =
        ScenarioId::from_static("lunar-frame-realisation");
    /// Seven-parameter Helmert frame datum propagated from a simulated lunar-VLBI
    /// observing campaign rather than recovered from an injected transform.
    pub const LUNAR_FRAME_CAMPAIGN: ScenarioId = ScenarioId::from_static("lunar-frame-campaign");
    pub const MOONLIGHT_SERVICE_VOLUME: ScenarioId =
        ScenarioId::from_static("moonlight-service-volume");
    pub const LUNAR_DIFFERENTIAL_PNT: ScenarioId =
        ScenarioId::from_static("lunar-differential-pnt");
    pub const LUNAR_INTEROP_EXPORT: ScenarioId = ScenarioId::from_static("lunar-interop-export");
    pub const GRAVITY_MAP: ScenarioId = ScenarioId::from_static("gravity-map");
    pub const TERRAIN_NAV: ScenarioId = ScenarioId::from_static("terrain-nav");
    pub const TERRAIN_SLAM: ScenarioId = ScenarioId::from_static("terrain-slam");
    pub const COMBINED_ALTPNT: ScenarioId = ScenarioId::from_static("combined-altpnt");
    pub const PVT: ScenarioId = ScenarioId::from_static("pvt");
    pub const MARS_PNT: ScenarioId = ScenarioId::from_static("mars-pnt");
    pub const IMPAIRMENT_EVAL: ScenarioId = ScenarioId::from_static("impairment-eval");
    pub const QUANTUM_TRADE: ScenarioId = ScenarioId::from_static("quantum-trade");
    pub const SPACE_WEATHER: ScenarioId = ScenarioId::from_static("space-weather");
    pub const OEM_INTEROP: ScenarioId = ScenarioId::from_static("oem-interop");
    pub const LAUNCH_WINDOW: ScenarioId = ScenarioId::from_static("launch-window");
    pub const REENTRY: ScenarioId = ScenarioId::from_static("reentry");
    pub const EO_COVERAGE: ScenarioId = ScenarioId::from_static("eo-coverage");
    pub const SPACE_PACKET: ScenarioId = ScenarioId::from_static("space-packet");
    pub const ATTITUDE_BUDGET: ScenarioId = ScenarioId::from_static("attitude-budget");
    pub const PASSES: ScenarioId = ScenarioId::from_static("passes");
    pub const LINK_BUDGET: ScenarioId = ScenarioId::from_static("link-budget");
    pub const LUNAR_TIME_BUDGET: ScenarioId = ScenarioId::from_static("lunar-time-budget");
    pub const REALTIME_FRAME_EOP: ScenarioId = ScenarioId::from_static("realtime-frame-eop");
    pub const HYBRID_OPTICAL_RF: ScenarioId = ScenarioId::from_static("hybrid-optical-rf");
    pub const CISLUNAR_OBSERVABILITY: ScenarioId =
        ScenarioId::from_static("cislunar-observability");
    /// Independent-estimator corroboration of the cislunar arc-length observability
    /// threshold: a finite-difference batch corrector that shares no measurement
    /// Jacobian with the Gramian.
    pub const CISLUNAR_ARC_RECOVERY: ScenarioId = ScenarioId::from_static("cislunar-arc-recovery");
    pub const CONFLICT_RESILIENCE: ScenarioId = ScenarioId::from_static("conflict-resilience");
    pub const LUNAR_ATTACK_SURFACE: ScenarioId = ScenarioId::from_static("lunar-attack-surface");
    /// Aperture navigation-versus-communications duty cycle from a contact plan.
    pub const APERTURE_DUTY_CYCLE: ScenarioId = ScenarioId::from_static("aperture-duty-cycle");
    /// Lunar surface-navigation RF jamming with a per-satellite J/S table.
    pub const LUNAR_JAMMING: ScenarioId = ScenarioId::from_static("lunar-jamming");
    /// INS/TRN coasting error growth and the coast durations reaching stated thresholds.
    pub const INS_TRN_COAST: ScenarioId = ScenarioId::from_static("ins-trn-coast");
    /// Lunar-VLBI station-coordinate covariance accumulated from the delay partials over
    /// a schedule of baselines and epochs.
    pub const LUNAR_VLBI_FIM: ScenarioId = ScenarioId::from_static("lunar-vlbi-fim");
    /// Tracking-loop loss of lock, hysteresis, spoof pull-in and the loop-dynamics
    /// denial radius alongside the power-ratio one.
    pub const TRACKING_LOOP: ScenarioId = ScenarioId::from_static("tracking-loop");
    /// The engine's ARAIM protection levels against the published WG-C reference
    /// airborne-algorithm worked examples, at the tolerance those documents state.
    pub const ARAIM_REFERENCE_CHECK: ScenarioId = ScenarioId::from_static("araim-reference-check");
    /// The seven-parameter Helmert lunar frame datum driven by archived ILRS lunar laser
    /// ranging normal points -- a real observing campaign rather than a simulated one.
    pub const LUNAR_LLR_DATUM: ScenarioId = ScenarioId::from_static("lunar-llr-datum");
    /// Time error, MTIE and TDEV of a synthetic GNSS holdover or an ingested series,
    /// checked against transcribed ITU-T G.8272 / G.8272.1 / G.8273.2 / G.8271.1 masks.
    pub const TELECOM_TIMING: ScenarioId = ScenarioId::from_static("telecom-timing");
    /// Seconds until a free-running clock leaves a time-indexed slot's guard, the fix
    /// cadence that keeps it inside, and an optional orbital timing protection level.
    pub const SLOT_TIMING: ScenarioId = ScenarioId::from_static("slot-timing");
    /// L-band power spectral density waterfall under a scripted jammer timeline, with
    /// per-band J/S and effective C/N₀ from the spectral separation coefficient.
    pub const SPECTRUM: ScenarioId = ScenarioId::from_static("spectrum");
    /// The whole solar system at one epoch: positions, constants, light times and orbit
    /// tracks from the Standish elements and the major-moon models.
    pub const SOLAR_SYSTEM: ScenarioId = ScenarioId::from_static("solar-system");
    /// Positioning around any solar-system body with a local navigation constellation and
    /// an optional deep-space ranging link from Earth.
    pub const BODY_PNT: ScenarioId = ScenarioId::from_static("body-pnt");
    /// Walker, explicit and preset constellations around any body, with a coverage and
    /// dilution-of-precision grid map.
    pub const CONSTELLATION_DESIGN: ScenarioId = ScenarioId::from_static("constellation-design");
    /// Existing kinds composed into a chained mission timeline, a parameter grid, a
    /// seeded Monte Carlo ensemble, or members run under shared conditions.
    pub const CAMPAIGN: ScenarioId = ScenarioId::from_static("campaign");
    /// Low Earth orbit positioning, navigation and timing signal designs: band-limited
    /// spectra, code tracking, acquisition, GNSS compatibility and a band trade.
    pub const LEO_SIGNAL: ScenarioId = ScenarioId::from_static("leo-signal");
    /// A LEO pass and its per-band link budget beside the MEO GNSS satellites in view.
    pub const LEO_PASS: ScenarioId = ScenarioId::from_static("leo-pass");
    /// LEO navigation message: fitter, user algorithm, mid-pass update, binary and text
    /// encodings.
    pub const LEO_NAVMSG: ScenarioId = ScenarioId::from_static("leo-navmsg");
    /// Fused MEO and LEO positioning: Doppler, joint pseudorange, polar coverage and LEO
    /// time transfer over any constellations.
    pub const LEO_PVT: ScenarioId = ScenarioId::from_static("leo-pvt");
    /// Precise point positioning convergence with GNSS only and with LEO augmentation.
    pub const LEO_PPP: ScenarioId = ScenarioId::from_static("leo-ppp");
    /// Positioning from a 5G non-terrestrial-network downlink via the Cramér-Rao bound.
    pub const NTN_POSITIONING: ScenarioId = ScenarioId::from_static("ntn-positioning");
    /// One LEO-PNT system end to end: signal, pass, navigation message and fused fix.
    pub const LEO_PNT_CHAIN: ScenarioId = ScenarioId::from_static("leo-pnt-chain");

    /// Every built-in scenario id, in the engine's canonical order.
    pub fn all() -> Vec<ScenarioId> {
        super::BUILTIN_KINDS
            .iter()
            .map(|k| ScenarioId::from_static(k.as_str()))
            .collect()
    }
}

/// A factory that knows how to build one scenario pack from its TOML source. The
/// trait is object-safe so a [`PackRegistry`] can hold a heterogeneous set behind
/// `Box<dyn ScenarioFactory>`.
pub trait ScenarioFactory: Send + Sync {
    /// The id this factory answers to.
    fn id(&self) -> &ScenarioId;

    /// Parse `src` and return a ready-to-run [`crate::api::Scenario`].
    fn build(&self, src: &str) -> Result<Box<dyn crate::api::Scenario>, String>;

    /// Introspection metadata. The default is an empty placeholder; packs that want
    /// to surface richer metadata override this.
    fn metadata(&self) -> crate::api::ScenarioMeta {
        crate::api::ScenarioMeta {
            name: "",
            description: "",
            required_fields: &[],
            optional_fields: &[],
        }
    }
}

/// A registry mapping [`ScenarioId`] to the factory that builds it.
#[derive(Default)]
pub struct PackRegistry {
    scenarios: BTreeMap<ScenarioId, Box<dyn ScenarioFactory>>,
}

impl PackRegistry {
    /// Register a factory, returning `self` for chaining. In debug builds, asserts
    /// that no id is registered twice.
    pub fn register(&mut self, factory: Box<dyn ScenarioFactory>) -> &mut Self {
        let id = factory.id().clone();
        debug_assert!(
            !self.scenarios.contains_key(&id),
            "duplicate scenario id registered: {id}"
        );
        self.scenarios.insert(id, factory);
        self
    }

    /// Build a scenario for `id` from `src`. Returns `Err` if the id is unknown.
    pub fn build(
        &self,
        id: &ScenarioId,
        src: &str,
    ) -> Result<Box<dyn crate::api::Scenario>, String> {
        match self.scenarios.get(id) {
            Some(factory) => factory.build(src),
            None => Err(format!("unknown scenario id: {id}")),
        }
    }

    /// Whether an id is registered.
    pub fn contains(&self, id: &ScenarioId) -> bool {
        self.scenarios.contains_key(id)
    }

    /// Iterate the registered ids in sorted order.
    pub fn ids(&self) -> impl Iterator<Item = &ScenarioId> {
        self.scenarios.keys()
    }

    /// A registry pre-populated with every built-in pack.
    pub fn with_builtins() -> Self {
        let mut reg = PackRegistry::default();
        for &kind in BUILTIN_KINDS {
            reg.register(Box::new(BuiltinFactory {
                id: ScenarioId::from_static(kind.as_str()),
                kind,
            }));
        }
        reg
    }
}

/// The built scenario for a built-in kind: it carries the resolved kind and the raw
/// source, and runs by routing back into the engine's exhaustive dispatch table.
struct BuiltinScenario {
    kind: crate::api::ScenarioKind,
    src: String,
}

impl crate::api::Scenario for BuiltinScenario {
    fn run(&self) -> Result<crate::api::RunOutput, crate::api::KshanaError> {
        crate::api::run_builtin_kind(self.kind, &self.src)
            .map_err(crate::api::KshanaError::InvalidInput)
    }
}

/// The factory for a built-in pack — a thin adapter over the engine's dispatch.
struct BuiltinFactory {
    id: ScenarioId,
    kind: crate::api::ScenarioKind,
}

impl ScenarioFactory for BuiltinFactory {
    fn id(&self) -> &ScenarioId {
        &self.id
    }

    fn build(&self, src: &str) -> Result<Box<dyn crate::api::Scenario>, String> {
        Ok(Box::new(BuiltinScenario {
            kind: self.kind,
            src: src.to_string(),
        }))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn ids_all_matches_as_str_table() {
        let all = ids::all();
        assert_eq!(all.len(), BUILTIN_KINDS.len());
        for (id, kind) in all.iter().zip(BUILTIN_KINDS.iter()) {
            assert_eq!(id.as_str(), kind.as_str());
        }
    }

    #[test]
    fn id_const_round_trips() {
        assert_eq!(ids::CLOCK.as_str(), "clock");
        assert_eq!(ids::LUNAR_TIME_OFFSET.as_str(), "lunar-time-offset");
        assert_eq!(ids::JAMMING.as_str(), "jamming");
    }

    #[test]
    fn with_builtins_registers_every_kind() {
        let reg = PackRegistry::with_builtins();
        assert_eq!(reg.ids().count(), BUILTIN_KINDS.len());
        for kind in BUILTIN_KINDS {
            assert!(reg.contains(&ScenarioId::from_static(kind.as_str())));
        }
    }

    #[test]
    fn ids_are_sorted_and_unique() {
        let reg = PackRegistry::with_builtins();
        let ids: Vec<&ScenarioId> = reg.ids().collect();
        for w in ids.windows(2) {
            assert!(
                w[0] < w[1],
                "ids must be strictly increasing: {} !< {}",
                w[0],
                w[1]
            );
        }
    }

    #[test]
    fn build_unknown_id_errors() {
        let reg = PackRegistry::with_builtins();
        let err = match reg.build(&ScenarioId::from_static("does-not-exist"), "") {
            Ok(_) => panic!("expected unknown-id error"),
            Err(e) => e,
        };
        assert_eq!(err, "unknown scenario id: does-not-exist");
    }

    #[test]
    fn default_is_empty() {
        let reg = PackRegistry::default();
        assert_eq!(reg.ids().count(), 0);
        assert!(!reg.contains(&ids::CLOCK));
    }

    #[test]
    fn build_is_lazy_parse_deferred_to_run() {
        // Building never parses — it only wraps (kind, src). Even gibberish builds;
        // the parse/validation error (if any) surfaces from run(), preserving the
        // engine's existing error strings.
        let reg = PackRegistry::with_builtins();
        assert!(reg.build(&ids::CLOCK, "not = valid = toml").is_ok());
    }
}