use crate::portable_math::PortableFloat;
use serde::{Deserialize, Serialize};
use crate::jamming::{q_factor, CA_CHIP_RATE_HZ};
use crate::receiver_trust::scenario::LogCfg;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum LevelKind {
#[default]
Cn0Drop,
JOverS,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Interp {
#[default]
Step,
Linear,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LevelPoint {
pub t_s: f64,
pub level_db: f64,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Conditions {
pub onset_s: f64,
#[serde(default)]
pub offset_s: Option<f64>,
#[serde(default)]
pub level_kind: LevelKind,
#[serde(default)]
pub levels: Vec<LevelPoint>,
#[serde(default)]
pub interp: Interp,
#[serde(default)]
pub jammer_type: Option<String>,
#[serde(default)]
pub doppler_rate_hz_per_s: f64,
#[serde(default)]
pub code_slew_chips_per_s: f64,
}
impl Conditions {
pub fn event_end_s(&self) -> f64 {
self.offset_s.unwrap_or(f64::INFINITY)
}
pub fn sorted_levels(&self) -> Vec<LevelPoint> {
let mut v = self.levels.clone();
v.sort_by(|a, b| a.t_s.total_cmp(&b.t_s));
v
}
pub fn level_at(&self, t_s: f64) -> Option<f64> {
if t_s < self.onset_s || t_s >= self.event_end_s() {
return None;
}
level_in(&self.sorted_levels(), self.interp, t_s)
}
pub fn peak_level_db(&self) -> Option<f64> {
self.levels
.iter()
.map(|p| p.level_db)
.fold(None, |m, v| Some(m.map_or(v, |m: f64| m.max(v))))
}
pub fn q(&self) -> f64 {
q_factor(self.jammer_type.as_deref().unwrap_or("broadband"), None)
}
pub fn cn0_at_level(&self, nominal_dbhz: f64, level_db: f64, offset_db: f64) -> f64 {
match self.level_kind {
LevelKind::Cn0Drop => nominal_dbhz - (level_db + offset_db),
LevelKind::JOverS => crate::jamming::effective_cn0_dbhz(
nominal_dbhz,
level_db + offset_db,
self.q(),
CA_CHIP_RATE_HZ,
),
}
}
pub fn cn0_at(&self, nominal_dbhz: f64, t_s: f64, offset_db: f64) -> f64 {
match self.level_at(t_s) {
Some(l) => self.cn0_at_level(nominal_dbhz, l, offset_db),
None => nominal_dbhz,
}
}
pub fn level_threshold(&self, nominal_dbhz: f64, threshold_dbhz: f64, offset_db: f64) -> f64 {
if !threshold_dbhz.is_finite() {
return if threshold_dbhz > 0.0 {
f64::NEG_INFINITY
} else {
f64::INFINITY
};
}
match self.level_kind {
LevelKind::Cn0Drop => nominal_dbhz - threshold_dbhz - offset_db,
LevelKind::JOverS => {
let t_lin = 10f64.ppowf(threshold_dbhz / 10.0);
let n_lin = 10f64.ppowf(nominal_dbhz / 10.0);
let js_lin = self.q() * CA_CHIP_RATE_HZ * (1.0 / t_lin - 1.0 / n_lin);
if js_lin <= 0.0 {
f64::NEG_INFINITY
} else {
10.0 * js_lin.plog10() - offset_db
}
}
}
}
}
pub(crate) fn level_in(points: &[LevelPoint], interp: Interp, t: f64) -> Option<f64> {
let first = points.first()?;
let last = points.last()?;
if t <= first.t_s {
return Some(first.level_db);
}
if t >= last.t_s {
return Some(last.level_db);
}
let k = points.partition_point(|p| p.t_s <= t);
let (a, b) = (points[k - 1], points[k]);
Some(match interp {
Interp::Step => a.level_db,
Interp::Linear => {
let span = b.t_s - a.t_s;
if span <= 0.0 {
b.level_db
} else {
a.level_db + (b.level_db - a.level_db) * (t - a.t_s) / span
}
}
})
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RunCfg {
pub label: String,
pub log: LogCfg,
pub conditions: Conditions,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct ObserveCfg {
pub band: Option<String>,
pub pre_window_s: Option<f64>,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct LoopFixedCfg {
pub integration_s: f64,
pub spacing_chips: f64,
pub dll_bandwidth_hz: f64,
pub carrier_allowance_deg: f64,
pub code_allowance_chips: Option<f64>,
}
impl Default for LoopFixedCfg {
fn default() -> Self {
Self {
integration_s: 0.001,
spacing_chips: 0.5,
dll_bandwidth_hz: 1.0,
carrier_allowance_deg: crate::tracking_loop::COSTAS_THRESHOLD_DEG,
code_allowance_chips: None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct BoundsCfg {
pub pll_bandwidth_hz: [f64; 2],
pub pullin_ratio: [f64; 2],
pub drop_cn0_dbhz: [f64; 2],
pub hysteresis_db: [f64; 2],
pub drop_dwell_s: [f64; 2],
pub relock_dwell_s: [f64; 2],
pub level_offset_db: [f64; 2],
}
impl Default for BoundsCfg {
fn default() -> Self {
Self {
pll_bandwidth_hz: [1.0, 40.0],
pullin_ratio: [1.0, 8.0],
drop_cn0_dbhz: [10.0, 45.0],
hysteresis_db: [0.0, 15.0],
drop_dwell_s: [0.0, 20.0],
relock_dwell_s: [0.0, 60.0],
level_offset_db: [-30.0, 30.0],
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct FitCfg {
pub seed: u64,
pub bootstrap: usize,
pub folds: usize,
pub grid_points: usize,
pub max_evals: usize,
}
impl Default for FitCfg {
fn default() -> Self {
Self {
seed: 1,
bootstrap: 50,
folds: 0,
grid_points: 5,
max_evals: 2000,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct ScaleCfg {
pub level_db: f64,
pub doppler_rate_hz_per_s: f64,
pub code_slew_chips_per_s: f64,
}
impl Default for ScaleCfg {
fn default() -> Self {
Self {
level_db: 1.0,
doppler_rate_hz_per_s: 1.0,
code_slew_chips_per_s: 1.0,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PredictCfg {
pub label: String,
#[serde(default)]
pub nominal_cn0_dbhz: Option<f64>,
pub conditions: Conditions,
pub run_end_s: f64,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LabFitScenario {
#[serde(default)]
pub kind: Option<String>,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub observe: ObserveCfg,
#[serde(default)]
pub loop_fixed: LoopFixedCfg,
#[serde(default)]
pub bounds: BoundsCfg,
#[serde(default)]
pub fit: FitCfg,
#[serde(default)]
pub scales: ScaleCfg,
#[serde(default)]
pub runs: Vec<RunCfg>,
#[serde(default)]
pub predict: Vec<PredictCfg>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input: Option<InputCfg>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub campaign_fit: Option<CampaignFitCfg>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub validation: Option<ValidationCfg>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mode: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct InputCfg {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub campaign: Option<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct CampaignFitCfg {
pub seed: u64,
pub bootstrap: usize,
pub min_recordings: usize,
pub q_bounds: [f64; 2],
pub delta_bounds_db: [f64; 2],
pub lock_models: bool,
}
impl Default for CampaignFitCfg {
fn default() -> Self {
Self {
seed: 1,
bootstrap: 1000,
min_recordings: 5,
q_bounds: [0.1, 10.0],
delta_bounds_db: [-10.0, 10.0],
lock_models: true,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ValidationCfg {
pub registered: String,
pub cn0_rms_db_max: f64,
pub pi90_coverage: [f64; 2],
pub loss_time_rms_s_max: f64,
}