mod atomic_zero_clock;
#[cfg(kshana_celeste)]
use crate::celeste_iod::fusion as celeste_iod;
mod centispace;
mod generic_cband;
mod iridium_stl;
mod starlink_sop;
mod xona_pulsar;
use serde::Serialize;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum SourceKind {
Public,
Workshop,
Derived,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
pub struct PresetSource {
pub kind: SourceKind,
pub citation: &'static str,
pub url: Option<&'static str>,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
pub struct PresetShell {
pub pattern: &'static str,
pub total: usize,
pub planes: usize,
pub phasing: usize,
pub altitude_km: f64,
pub inclination_deg: f64,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
pub struct PresetSignal {
pub name: &'static str,
pub carrier_hz: f64,
pub modulation: &'static str,
pub chip_rate_hz: Option<f64>,
pub bandwidth_hz: f64,
pub rx_power_dbw: Option<(f64, f64)>,
pub cn0_dbhz: Option<(f64, f64)>,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
pub struct LeoPreset {
pub id: &'static str,
pub name: &'static str,
pub summary: &'static str,
pub sources: &'static [PresetSource],
pub representative: &'static [&'static str],
pub shells: &'static [PresetShell],
pub signals: &'static [PresetSignal],
pub doppler_only: bool,
pub sigma_doppler_hz: Option<f64>,
pub sisre_m: f64,
pub ephemeris_model: &'static str,
pub clock_model: &'static str,
}
impl LeoPreset {
pub fn signal(&self, name: Option<&str>) -> Result<&'static PresetSignal, String> {
match name {
None => self
.signals
.first()
.ok_or_else(|| format!("preset {} carries no signal", self.id)),
Some(n) => self
.signals
.iter()
.find(|s| s.name.eq_ignore_ascii_case(n))
.ok_or_else(|| {
format!(
"preset {} has no signal {n:?}; it has {:?}",
self.id,
self.signals.iter().map(|s| s.name).collect::<Vec<_>>()
)
}),
}
}
pub fn uses_workshop_material(&self) -> bool {
self.sources.iter().any(|s| s.kind == SourceKind::Workshop)
}
}
pub fn all() -> Vec<&'static LeoPreset> {
vec![
&xona_pulsar::PRESET,
&iridium_stl::PRESET,
&starlink_sop::PRESET,
¢ispace::PRESET,
&generic_cband::PRESET,
&atomic_zero_clock::PRESET,
#[cfg(kshana_celeste)]
&celeste_iod::PRESET,
]
}
pub fn by_id(id: &str) -> Result<&'static LeoPreset, String> {
all()
.into_iter()
.find(|p| p.id.eq_ignore_ascii_case(id.trim()))
.ok_or_else(|| {
format!(
"unknown LEO preset {id:?}; known: {:?}",
all().iter().map(|p| p.id).collect::<Vec<_>>()
)
})
}
pub fn noise_density_dbw_hz(t_sys_k: f64) -> f64 {
10.0 * (1.380_649e-23 * t_sys_k).log10()
}
pub fn cn0_range_dbhz(sig: &PresetSignal, t_sys_k: f64) -> Option<(f64, f64)> {
sig.cn0_dbhz.or_else(|| {
sig.rx_power_dbw.map(|(lo, hi)| {
let n0 = noise_density_dbw_hz(t_sys_k);
(lo - n0, hi - n0)
})
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_preset_has_a_source_and_every_public_source_a_url() {
for p in all() {
assert!(!p.sources.is_empty(), "{}", p.id);
for s in p.sources {
if s.kind == SourceKind::Public {
assert!(
s.url.is_some_and(|u| u.starts_with("https://")),
"{}: {}",
p.id,
s.citation
);
}
}
for sig in p.signals {
assert!(sig.carrier_hz > 1e8 && sig.bandwidth_hz > 0.0, "{}", p.id);
}
for sh in p.shells {
assert!(sh.total % sh.planes == 0, "{}", p.id);
assert!(sh.altitude_km > 150.0 && sh.altitude_km < 2000.0);
}
assert!(p.sisre_m > 0.0);
}
assert!(by_id("XONA-PULSAR").is_ok());
assert!(by_id("galileo").is_err());
}
#[test]
fn only_one_preset_uses_workshop_material() {
for p in all() {
assert_eq!(
p.uses_workshop_material(),
p.id == "celeste-iod",
"{}",
p.id
);
}
}
#[test]
fn noise_density_at_290_k_is_minus_204_dbw_per_hz() {
assert!((noise_density_dbw_hz(290.0) + 203.975).abs() < 1e-3);
}
}