use super::channel::{MAX_EXTRA_TAPS, MAX_TAP_OFFSET_CHIPS};
use super::cn0::BitSyncConfig;
use super::discrim::{DllDiscriminator, FllDiscriminator, PllDiscriminator};
use super::sink::ResolvedRun;
use super::{CarrierLoop, FllAssist, FllGate, LoopConfig};
use crate::iq::acq::{auto_coherent_periods, default_step_hz, AcqConfig};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
pub const SCHEMA: &str = "kshana.loop-design/1";
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct LockConfig {
pub pull_in_max_s: f64,
pub loss_dwell_s: f64,
pub false_lock_check: bool,
pub false_lock_margin_db: f64,
pub reacquire: bool,
pub reacq_doppler_window_hz: f64,
pub reacq_window_s: f64,
pub reacq_interval_s: f64,
pub reacq_max_interval_s: f64,
pub max_reacq_attempts: u32,
}
impl Default for LockConfig {
fn default() -> Self {
Self {
pull_in_max_s: 2.0,
loss_dwell_s: 0.2,
false_lock_check: true,
false_lock_margin_db: 3.0,
reacquire: false,
reacq_doppler_window_hz: 500.0,
reacq_window_s: 30.0,
reacq_interval_s: 0.1,
reacq_max_interval_s: 0.1,
max_reacq_attempts: 0,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Auto<T> {
Value(T),
Keyword(AutoKeyword),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AutoKeyword {
Auto,
}
impl<T: Copy> Auto<T> {
pub fn value(&self) -> Option<T> {
match self {
Auto::Value(v) => Some(*v),
Auto::Keyword(_) => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct AcquisitionDesign {
pub coherent_periods: Auto<usize>,
pub noncoherent: usize,
pub doppler_max_hz: f64,
pub doppler_step_hz: Auto<f64>,
pub pfa: f64,
}
impl Default for AcquisitionDesign {
fn default() -> Self {
Self {
coherent_periods: Auto::Keyword(AutoKeyword::Auto),
noncoherent: 1,
doppler_max_hz: 5000.0,
doppler_step_hz: Auto::Keyword(AutoKeyword::Auto),
pfa: 1e-3,
}
}
}
impl AcquisitionDesign {
pub fn acq_config(&self, code_period_s: f64, t_track_s: f64) -> AcqConfig {
let n = self
.coherent_periods
.value()
.unwrap_or_else(|| auto_coherent_periods(code_period_s))
.max(1);
AcqConfig {
coherent_periods: n,
noncoherent: self.noncoherent.max(1),
doppler_max_hz: self.doppler_max_hz,
doppler_step_hz: self
.doppler_step_hz
.value()
.unwrap_or_else(|| default_step_hz(code_period_s, n, t_track_s)),
pfa: self.pfa,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Design {
name: String,
description: Option<String>,
resolved: Resolved,
hash: String,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
struct Resolved {
schema: &'static str,
integration: Integration,
carrier: Carrier,
code: Code,
lock: Lock,
bit_sync: BitSync,
acquisition: AcquisitionDesign,
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct RawFile {
schema: Option<String>,
#[serde(default)]
design: Vec<RawDesign>,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct RawDesign {
name: Option<String>,
extends: Option<String>,
description: Option<String>,
#[serde(default)]
integration: RawIntegration,
#[serde(default)]
carrier: RawCarrier,
#[serde(default)]
code: RawCode,
#[serde(default)]
lock: RawLock,
#[serde(default)]
bit_sync: RawBitSync,
#[serde(default)]
acquisition: RawAcquisition,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct RawIntegration {
coherent_periods: Option<usize>,
spacing_chips: Option<f64>,
extra_taps_chips: Option<Vec<f64>>,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct RawCarrier {
kind: Option<String>,
pll_order: Option<u8>,
pll_bw_hz: Option<f64>,
fll_order: Option<u8>,
fll_bw_hz: Option<f64>,
pll_discriminator: Option<String>,
fll_discriminator: Option<String>,
fll_assist: Option<String>,
fll_off_pli: Option<f64>,
fll_on_pli: Option<f64>,
fll_gate_dwell_s: Option<f64>,
bn_t_max: Option<f64>,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct RawCode {
order: Option<u8>,
bw_hz: Option<f64>,
discriminator: Option<String>,
carrier_aiding: Option<bool>,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct RawLock {
pli_threshold: Option<f64>,
code_lock_cn0_dbhz: Option<f64>,
cn0_windows: Option<usize>,
cn0_window_periods: Option<usize>,
pull_in_max_s: Option<f64>,
loss_dwell_s: Option<f64>,
false_lock_check: Option<bool>,
false_lock_margin_db: Option<f64>,
reacquire: Option<bool>,
reacq_doppler_window_hz: Option<f64>,
reacq_window_s: Option<f64>,
reacq_interval_s: Option<f64>,
reacq_max_interval_s: Option<f64>,
max_reacq_attempts: Option<u32>,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct RawBitSync {
min_votes: Option<u32>,
ratio: Option<f64>,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct RawAcquisition {
coherent_periods: Option<Auto<usize>>,
noncoherent: Option<usize>,
doppler_max_hz: Option<f64>,
doppler_step_hz: Option<Auto<f64>>,
pfa: Option<f64>,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
struct Integration {
coherent_periods: usize,
spacing_chips: f64,
#[serde(skip_serializing_if = "Vec::is_empty")]
extra_taps_chips: Vec<f64>,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
struct Carrier {
kind: String,
pll_order: Option<u8>,
pll_bw_hz: Option<f64>,
fll_order: Option<u8>,
fll_bw_hz: Option<f64>,
pll_discriminator: String,
fll_discriminator: String,
fll_assist: Option<String>,
fll_off_pli: Option<f64>,
fll_on_pli: Option<f64>,
fll_gate_dwell_s: Option<f64>,
#[serde(skip_serializing_if = "is_default_bn_t_max")]
bn_t_max: f64,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
struct Code {
order: u8,
bw_hz: f64,
discriminator: String,
carrier_aiding: bool,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
struct Lock {
pli_threshold: f64,
code_lock_cn0_dbhz: f64,
cn0_windows: usize,
cn0_window_periods: usize,
#[serde(flatten)]
machine: LockConfig,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
struct BitSync {
min_votes: u32,
ratio: f64,
}
fn pll_disc(s: &str) -> Result<PllDiscriminator, String> {
Ok(match s {
"atan2" => PllDiscriminator::Atan2,
"costas-atan" => PllDiscriminator::CostasAtan,
"costas-decision-directed" => PllDiscriminator::CostasDecisionDirected,
o => {
return Err(format!(
"carrier.pll_discriminator {o:?}: expected atan2, costas-atan or costas-decision-directed"
))
}
})
}
fn pll_disc_name(d: PllDiscriminator) -> &'static str {
match d {
PllDiscriminator::Atan2 => "atan2",
PllDiscriminator::CostasAtan => "costas-atan",
PllDiscriminator::CostasDecisionDirected => "costas-decision-directed",
}
}
fn fll_disc(s: &str) -> Result<FllDiscriminator, String> {
Ok(match s {
"cross-product" => FllDiscriminator::CrossProduct,
"atan2" => FllDiscriminator::Atan2,
"atan2-pilot" => FllDiscriminator::Atan2Pilot,
o => {
return Err(format!(
"carrier.fll_discriminator {o:?}: expected cross-product, atan2 or atan2-pilot"
))
}
})
}
fn fll_disc_name(d: FllDiscriminator) -> &'static str {
match d {
FllDiscriminator::CrossProduct => "cross-product",
FllDiscriminator::Atan2 => "atan2",
FllDiscriminator::Atan2Pilot => "atan2-pilot",
}
}
fn fll_assist(s: &str) -> Result<FllAssist, String> {
match s {
"pull-in" => Ok(FllAssist::default()),
"always" => Ok(FllAssist::Always),
o => Err(format!(
"carrier.fll_assist {o:?}: expected pull-in or always"
)),
}
}
fn fll_assist_name(a: FllAssist) -> &'static str {
match a {
FllAssist::PullIn(_) => "pull-in",
FllAssist::Always => "always",
}
}
fn dll_disc(s: &str) -> Result<DllDiscriminator, String> {
Ok(match s {
"eml-power" => DllDiscriminator::EarlyMinusLatePower,
"dot-product" => DllDiscriminator::DotProduct,
"eml-envelope" => DllDiscriminator::EarlyMinusLateEnvelope,
o => {
return Err(format!(
"code.discriminator {o:?}: expected eml-power, dot-product or eml-envelope"
))
}
})
}
fn dll_disc_name(d: DllDiscriminator) -> &'static str {
match d {
DllDiscriminator::EarlyMinusLatePower => "eml-power",
DllDiscriminator::DotProduct => "dot-product",
DllDiscriminator::EarlyMinusLateEnvelope => "eml-envelope",
}
}
impl Resolved {
fn builtin() -> Self {
let c = LoopConfig::default();
let (kind, po, pb, fo, fb) = carrier_fields(&c.carrier);
let assisted = c.carrier.has_pll() && c.carrier.has_fll();
let gate = match c.fll_assist {
FllAssist::PullIn(g) => g,
FllAssist::Always => FllGate::default(),
};
Self {
schema: SCHEMA,
integration: Integration {
coherent_periods: c.coherent_periods,
spacing_chips: c.spacing_chips,
extra_taps_chips: c.extra_taps_chips.clone(),
},
carrier: Carrier {
kind: kind.into(),
pll_order: po,
pll_bw_hz: pb,
fll_order: fo,
fll_bw_hz: fb,
pll_discriminator: pll_disc_name(c.pll_discriminator).into(),
fll_discriminator: fll_disc_name(c.fll_discriminator).into(),
fll_assist: assisted.then(|| fll_assist_name(c.fll_assist).into()),
fll_off_pli: assisted.then_some(gate.off_pli),
fll_on_pli: assisted.then_some(gate.on_pli),
fll_gate_dwell_s: assisted.then_some(gate.dwell_s),
bn_t_max: BN_T_MAX_DEFAULT,
},
code: Code {
order: c.dll_order,
bw_hz: c.dll_bn_hz,
discriminator: dll_disc_name(c.dll).into(),
carrier_aiding: c.carrier_aiding,
},
lock: Lock {
pli_threshold: c.pli_threshold,
code_lock_cn0_dbhz: c.code_lock_cn0_dbhz,
cn0_windows: c.cn0_windows,
cn0_window_periods: c.cn0_window_periods,
machine: LockConfig::default(),
},
bit_sync: BitSync {
min_votes: c.bit_sync.min_votes,
ratio: c.bit_sync.ratio,
},
acquisition: AcquisitionDesign::default(),
}
}
fn apply(mut self, raw: &RawDesign, who: &str) -> Result<Self, String> {
let at = |m: String| format!("design {who:?}: {m}");
let i = &raw.integration;
set(&mut self.integration.coherent_periods, i.coherent_periods);
set(&mut self.integration.spacing_chips, i.spacing_chips);
if let Some(t) = &i.extra_taps_chips {
self.integration.extra_taps_chips = t.clone();
}
let c = &raw.carrier;
if let Some(k) = &c.kind {
if !["pll", "fll", "fll-assisted-pll"].contains(&k.as_str()) {
return Err(at(format!(
"carrier.kind {k:?}: expected pll, fll or fll-assisted-pll"
)));
}
if *k != self.carrier.kind {
let base = Self::builtin().carrier;
self.carrier.kind = k.clone();
let has_pll = k != "fll";
let has_fll = k != "pll";
self.carrier.pll_order =
has_pll.then(|| self.carrier.pll_order.or(base.pll_order).unwrap_or(2));
self.carrier.pll_bw_hz =
has_pll.then(|| self.carrier.pll_bw_hz.or(base.pll_bw_hz).unwrap_or(15.0));
self.carrier.fll_order =
has_fll.then(|| self.carrier.fll_order.or(base.fll_order).unwrap_or(1));
self.carrier.fll_bw_hz =
has_fll.then(|| self.carrier.fll_bw_hz.or(base.fll_bw_hz).unwrap_or(10.0));
let assisted = has_pll && has_fll;
self.carrier.fll_assist = assisted.then(|| {
self.carrier
.fll_assist
.clone()
.or(base.fll_assist)
.unwrap_or_else(|| "pull-in".into())
});
self.carrier.fll_off_pli = assisted
.then(|| self.carrier.fll_off_pli.or(base.fll_off_pli))
.flatten();
self.carrier.fll_on_pli = assisted
.then(|| self.carrier.fll_on_pli.or(base.fll_on_pli))
.flatten();
self.carrier.fll_gate_dwell_s = assisted
.then(|| self.carrier.fll_gate_dwell_s.or(base.fll_gate_dwell_s))
.flatten();
}
}
let has_pll = self.carrier.kind != "fll";
let has_fll = self.carrier.kind != "pll";
for (key, given, allowed) in [
("pll_order", c.pll_order.is_some(), has_pll),
("pll_bw_hz", c.pll_bw_hz.is_some(), has_pll),
("fll_order", c.fll_order.is_some(), has_fll),
("fll_bw_hz", c.fll_bw_hz.is_some(), has_fll),
("fll_assist", c.fll_assist.is_some(), has_pll && has_fll),
("fll_off_pli", c.fll_off_pli.is_some(), has_pll && has_fll),
("fll_on_pli", c.fll_on_pli.is_some(), has_pll && has_fll),
(
"fll_gate_dwell_s",
c.fll_gate_dwell_s.is_some(),
has_pll && has_fll,
),
] {
if given && !allowed {
return Err(at(format!(
"carrier.{key} does not apply to carrier.kind = {:?}",
self.carrier.kind
)));
}
}
if c.pll_order.is_some() {
self.carrier.pll_order = c.pll_order;
}
if c.pll_bw_hz.is_some() {
self.carrier.pll_bw_hz = c.pll_bw_hz;
}
if c.fll_order.is_some() {
self.carrier.fll_order = c.fll_order;
}
if c.fll_bw_hz.is_some() {
self.carrier.fll_bw_hz = c.fll_bw_hz;
}
if let Some(d) = &c.pll_discriminator {
pll_disc(d).map_err(at)?;
self.carrier.pll_discriminator = d.clone();
}
if let Some(d) = &c.fll_discriminator {
fll_disc(d).map_err(at)?;
self.carrier.fll_discriminator = d.clone();
}
if let Some(a) = &c.fll_assist {
fll_assist(a).map_err(at)?;
self.carrier.fll_assist = Some(a.clone());
}
set_some(&mut self.carrier.fll_off_pli, c.fll_off_pli);
set_some(&mut self.carrier.fll_on_pli, c.fll_on_pli);
set_some(&mut self.carrier.fll_gate_dwell_s, c.fll_gate_dwell_s);
if let Some(v) = c.bn_t_max {
if !(v.is_finite() && v >= 0.0) {
return Err(at(format!(
"carrier.bn_t_max {v}: expected a number >= 0 (0 disables the clamp)"
)));
}
self.carrier.bn_t_max = v;
}
let k = &raw.code;
set(&mut self.code.order, k.order);
set(&mut self.code.bw_hz, k.bw_hz);
set(&mut self.code.carrier_aiding, k.carrier_aiding);
if let Some(d) = &k.discriminator {
dll_disc(d).map_err(at)?;
self.code.discriminator = d.clone();
}
let l = &raw.lock;
set(&mut self.lock.pli_threshold, l.pli_threshold);
set(&mut self.lock.code_lock_cn0_dbhz, l.code_lock_cn0_dbhz);
set(&mut self.lock.cn0_windows, l.cn0_windows);
set(&mut self.lock.cn0_window_periods, l.cn0_window_periods);
let m = &mut self.lock.machine;
set(&mut m.pull_in_max_s, l.pull_in_max_s);
set(&mut m.loss_dwell_s, l.loss_dwell_s);
set(&mut m.false_lock_check, l.false_lock_check);
set(&mut m.false_lock_margin_db, l.false_lock_margin_db);
set(&mut m.reacquire, l.reacquire);
set(&mut m.reacq_doppler_window_hz, l.reacq_doppler_window_hz);
set(&mut m.reacq_window_s, l.reacq_window_s);
set(&mut m.reacq_interval_s, l.reacq_interval_s);
set(&mut m.reacq_max_interval_s, l.reacq_max_interval_s);
set(&mut m.max_reacq_attempts, l.max_reacq_attempts);
set(&mut self.bit_sync.min_votes, raw.bit_sync.min_votes);
set(&mut self.bit_sync.ratio, raw.bit_sync.ratio);
let a = &raw.acquisition;
set(&mut self.acquisition.coherent_periods, a.coherent_periods);
set(&mut self.acquisition.noncoherent, a.noncoherent);
set(&mut self.acquisition.doppler_max_hz, a.doppler_max_hz);
set(&mut self.acquisition.doppler_step_hz, a.doppler_step_hz);
set(&mut self.acquisition.pfa, a.pfa);
self.validate().map_err(at)?;
Ok(self)
}
fn validate(&self) -> Result<(), String> {
let pos = |v: f64, k: &str| {
if v.is_finite() && v > 0.0 {
Ok(())
} else {
Err(format!("{k} must be a positive number (got {v})"))
}
};
if self.integration.coherent_periods == 0 {
return Err("integration.coherent_periods must be at least 1".into());
}
let d = self.integration.spacing_chips;
if !(d.is_finite() && d > 0.0 && d <= 2.0) {
return Err(format!(
"integration.spacing_chips must lie in (0, 2] (got {d})"
));
}
let taps = &self.integration.extra_taps_chips;
if taps.len() > MAX_EXTRA_TAPS {
return Err(format!(
"integration.extra_taps_chips holds at most {MAX_EXTRA_TAPS} offsets (got {})",
taps.len()
));
}
if let Some(t) = taps
.iter()
.find(|t| !(t.is_finite() && t.abs() <= MAX_TAP_OFFSET_CHIPS))
{
return Err(format!(
"integration.extra_taps_chips offsets must be finite and within \
±{MAX_TAP_OFFSET_CHIPS} chips (got {t})"
));
}
if let (Some(off), Some(on)) = (self.carrier.fll_off_pli, self.carrier.fll_on_pli) {
if !(on.is_finite() && off.is_finite() && on > -1.0 && on < off && off <= 1.0) {
return Err(format!(
"carrier.fll_on_pli ({on}) must be below carrier.fll_off_pli ({off}), \
both in (-1, 1]"
));
}
}
if let Some(t) = self.carrier.fll_gate_dwell_s {
if !(t.is_finite() && t >= 0.0) {
return Err(format!(
"carrier.fll_gate_dwell_s must be zero or positive (got {t})"
));
}
}
if let (Some(o), Some(b)) = (self.carrier.pll_order, self.carrier.pll_bw_hz) {
if !(1..=3).contains(&o) {
return Err(format!("carrier.pll_order must be 1, 2 or 3 (got {o})"));
}
pos(b, "carrier.pll_bw_hz")?;
}
if let (Some(o), Some(b)) = (self.carrier.fll_order, self.carrier.fll_bw_hz) {
if !(1..=2).contains(&o) {
return Err(format!("carrier.fll_order must be 1 or 2 (got {o})"));
}
pos(b, "carrier.fll_bw_hz")?;
}
if !(1..=2).contains(&self.code.order) {
return Err(format!(
"code.order must be 1 or 2 (got {})",
self.code.order
));
}
pos(self.code.bw_hz, "code.bw_hz")?;
let pli = self.lock.pli_threshold;
if !(pli.is_finite() && (-1.0..=1.0).contains(&pli)) {
return Err(format!(
"lock.pli_threshold must lie in [-1, 1] (got {pli})"
));
}
if self.lock.cn0_windows == 0 {
return Err("lock.cn0_windows must be at least 1".into());
}
if self.lock.cn0_window_periods < 2 {
return Err("lock.cn0_window_periods must be at least 2".into());
}
let m = &self.lock.machine;
pos(m.pull_in_max_s, "lock.pull_in_max_s")?;
pos(m.loss_dwell_s, "lock.loss_dwell_s")?;
pos(m.reacq_doppler_window_hz, "lock.reacq_doppler_window_hz")?;
pos(m.reacq_window_s, "lock.reacq_window_s")?;
pos(m.reacq_interval_s, "lock.reacq_interval_s")?;
pos(m.reacq_max_interval_s, "lock.reacq_max_interval_s")?;
if m.reacq_max_interval_s < m.reacq_interval_s {
return Err(format!(
"lock.reacq_max_interval_s ({}) must not be below lock.reacq_interval_s ({})",
m.reacq_max_interval_s, m.reacq_interval_s
));
}
if !m.false_lock_margin_db.is_finite() {
return Err("lock.false_lock_margin_db must be finite".into());
}
if self.bit_sync.min_votes == 0 {
return Err("bit_sync.min_votes must be at least 1".into());
}
pos(self.bit_sync.ratio, "bit_sync.ratio")?;
let a = &self.acquisition;
if a.coherent_periods.value() == Some(0) {
return Err("acquisition.coherent_periods must be at least 1 (or \"auto\")".into());
}
if a.noncoherent == 0 {
return Err("acquisition.noncoherent must be at least 1".into());
}
pos(a.doppler_max_hz, "acquisition.doppler_max_hz")?;
if let Some(s) = a.doppler_step_hz.value() {
pos(s, "acquisition.doppler_step_hz")?;
}
if !(a.pfa > 0.0 && a.pfa < 1.0) {
return Err(format!(
"acquisition.pfa must lie in (0, 1) (got {})",
a.pfa
));
}
Ok(())
}
}
pub const BN_CLAMP_ABOVE_T_S: f64 = 4.0e-3;
pub const BN_T_MAX_DEFAULT: f64 = 0.1;
fn is_default_bn_t_max(v: &f64) -> bool {
*v == BN_T_MAX_DEFAULT
}
fn set_some<T>(slot: &mut Option<T>, v: Option<T>) {
if v.is_some() {
*slot = v;
}
}
fn set<T: Clone>(slot: &mut T, v: Option<T>) {
if let Some(v) = v {
*slot = v;
}
}
pub(crate) fn canonical_json(v: &serde_json::Value) -> String {
fn write(v: &serde_json::Value, out: &mut String) {
match v {
serde_json::Value::Object(map) => {
let mut keys: Vec<&String> = map.keys().collect();
keys.sort_unstable_by(|a, b| a.as_bytes().cmp(b.as_bytes()));
out.push('{');
for (i, k) in keys.into_iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push_str(&serde_json::Value::String(k.clone()).to_string());
out.push(':');
write(&map[k], out);
}
out.push('}');
}
serde_json::Value::Array(items) => {
out.push('[');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
write(item, out);
}
out.push(']');
}
scalar => out.push_str(&scalar.to_string()),
}
}
let mut out = String::new();
write(v, &mut out);
out
}
fn carrier_fields(
c: &CarrierLoop,
) -> (
&'static str,
Option<u8>,
Option<f64>,
Option<u8>,
Option<f64>,
) {
match *c {
CarrierLoop::Pll { order, bn_hz } => ("pll", Some(order), Some(bn_hz), None, None),
CarrierLoop::Fll { order, bn_hz } => ("fll", None, None, Some(order), Some(bn_hz)),
CarrierLoop::FllAssistedPll {
pll_order,
pll_bn_hz,
fll_order,
fll_bn_hz,
} => (
"fll-assisted-pll",
Some(pll_order),
Some(pll_bn_hz),
Some(fll_order),
Some(fll_bn_hz),
),
}
}
impl Design {
fn from_resolved(name: String, description: Option<String>, resolved: Resolved) -> Self {
let value = serde_json::to_value(&resolved).unwrap_or_default();
let hash = crate::advanced_report::sha256_hex(canonical_json(&value).as_bytes());
Self {
name,
description,
resolved,
hash,
}
}
pub fn builtin_default() -> Self {
Self::from_resolved("default".into(), None, Resolved::builtin())
}
pub fn name(&self) -> &str {
&self.name
}
pub fn description(&self) -> Option<&str> {
self.description.as_deref()
}
pub fn hash(&self) -> &str {
&self.hash
}
pub fn to_json(&self) -> serde_json::Value {
let mut v = serde_json::to_value(&self.resolved).unwrap_or_default();
if let Some(o) = v.as_object_mut() {
o.insert("name".into(), self.name.clone().into());
o.insert("hash".into(), self.hash.clone().into());
if let Some(d) = &self.description {
o.insert("description".into(), d.clone().into());
}
}
v
}
pub fn resolved_run(&self, code_period_s: f64) -> ResolvedRun {
let acq = self.acq_config(code_period_s);
let cfg = self.loop_config_for(code_period_s);
let (pll_bn_hz, fll_bn_hz) = match cfg.carrier {
CarrierLoop::Pll { bn_hz, .. } => (Some(bn_hz), None),
CarrierLoop::Fll { bn_hz, .. } => (None, Some(bn_hz)),
CarrierLoop::FllAssistedPll {
pll_bn_hz,
fll_bn_hz,
..
} => (Some(pll_bn_hz), Some(fll_bn_hz)),
};
ResolvedRun {
acq_coherent_periods: acq.coherent_periods,
acq_doppler_step_hz: acq.doppler_step_hz,
t_track_s: cfg.coherent_periods as f64 * code_period_s,
pll_bn_hz,
fll_bn_hz,
}
}
pub fn loop_config_for(&self, code_period_s: f64) -> LoopConfig {
let mut cfg = self.loop_config();
let t = cfg.coherent_periods as f64 * code_period_s;
let k = self.resolved.carrier.bn_t_max;
if k > 0.0 && t.is_finite() && t > BN_CLAMP_ABOVE_T_S * (1.0 + 1e-9) {
let cap = k / t;
match &mut cfg.carrier {
CarrierLoop::Pll { bn_hz, .. } | CarrierLoop::Fll { bn_hz, .. } => {
*bn_hz = bn_hz.min(cap)
}
CarrierLoop::FllAssistedPll {
pll_bn_hz,
fll_bn_hz,
..
} => {
*pll_bn_hz = pll_bn_hz.min(cap);
*fll_bn_hz = fll_bn_hz.min(cap);
}
}
}
cfg
}
pub fn loop_config(&self) -> LoopConfig {
let r = &self.resolved;
let c = &r.carrier;
let carrier = match c.kind.as_str() {
"pll" => CarrierLoop::Pll {
order: c.pll_order.unwrap_or(2),
bn_hz: c.pll_bw_hz.unwrap_or(15.0),
},
"fll" => CarrierLoop::Fll {
order: c.fll_order.unwrap_or(1),
bn_hz: c.fll_bw_hz.unwrap_or(10.0),
},
_ => CarrierLoop::FllAssistedPll {
pll_order: c.pll_order.unwrap_or(2),
pll_bn_hz: c.pll_bw_hz.unwrap_or(15.0),
fll_order: c.fll_order.unwrap_or(1),
fll_bn_hz: c.fll_bw_hz.unwrap_or(10.0),
},
};
LoopConfig {
label: self.name.clone(),
coherent_periods: r.integration.coherent_periods,
spacing_chips: r.integration.spacing_chips,
extra_taps_chips: r.integration.extra_taps_chips.clone(),
dll: dll_disc(&r.code.discriminator).unwrap_or(DllDiscriminator::EarlyMinusLatePower),
dll_order: r.code.order,
dll_bn_hz: r.code.bw_hz,
carrier_aiding: r.code.carrier_aiding,
carrier,
pll_discriminator: pll_disc(&c.pll_discriminator)
.unwrap_or(PllDiscriminator::CostasAtan),
fll_discriminator: fll_disc(&c.fll_discriminator).unwrap_or(FllDiscriminator::Atan2),
fll_assist: match c.fll_assist.as_deref() {
Some("always") => FllAssist::Always,
_ => {
let d = FllGate::default();
FllAssist::PullIn(FllGate {
off_pli: c.fll_off_pli.unwrap_or(d.off_pli),
on_pli: c.fll_on_pli.unwrap_or(d.on_pli),
dwell_s: c.fll_gate_dwell_s.unwrap_or(d.dwell_s),
})
}
},
pli_threshold: r.lock.pli_threshold,
code_lock_cn0_dbhz: r.lock.code_lock_cn0_dbhz,
cn0_windows: r.lock.cn0_windows,
cn0_window_periods: r.lock.cn0_window_periods,
bit_sync: BitSyncConfig {
min_votes: r.bit_sync.min_votes,
ratio: r.bit_sync.ratio,
},
}
}
pub fn lock_config(&self) -> LockConfig {
self.resolved.lock.machine.clone()
}
pub fn acquisition(&self) -> &AcquisitionDesign {
&self.resolved.acquisition
}
pub fn acq_config(&self, code_period_s: f64) -> AcqConfig {
let t_track_s = self.resolved.integration.coherent_periods as f64 * code_period_s;
self.resolved
.acquisition
.acq_config(code_period_s, t_track_s)
}
pub fn renamed(&self, name: &str) -> Self {
Self {
name: name.to_string(),
..self.clone()
}
}
pub fn with_overrides(&self, overrides: &str) -> Result<Self, String> {
let raw: RawDesign =
toml::from_str(overrides).map_err(|e| format!("design overrides: {e}"))?;
if raw.name.is_some() || raw.extends.is_some() {
return Err("design overrides may not set name or extends".into());
}
let resolved = self.resolved.clone().apply(&raw, &self.name)?;
Ok(Self::from_resolved(
self.name.clone(),
self.description.clone(),
resolved,
))
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct DesignFile {
designs: Vec<Design>,
}
impl DesignFile {
pub fn parse(text: &str) -> Result<Self, String> {
let raw: RawFile = toml::from_str(text).map_err(|e| format!("loop-design file: {e}"))?;
match raw.schema.as_deref() {
Some(SCHEMA) => {}
Some(other) => {
return Err(format!(
"loop-design file: schema {other:?} is not supported; expected {SCHEMA:?}"
))
}
None => return Err(format!("loop-design file: missing schema = {SCHEMA:?}")),
}
if raw.design.is_empty() {
return Err("loop-design file: no [[design]] tables".into());
}
let mut by_name: BTreeMap<String, usize> = BTreeMap::new();
for (i, d) in raw.design.iter().enumerate() {
let name = d.name.clone().ok_or_else(|| {
format!("loop-design file: [[design]] number {} has no name", i + 1)
})?;
if name.trim().is_empty() || name == "default" {
return Err(format!(
"loop-design file: design name {name:?} is reserved or empty"
));
}
if by_name.insert(name.clone(), i).is_some() {
return Err(format!(
"loop-design file: design {name:?} is defined twice"
));
}
}
let mut resolved: Vec<Option<Resolved>> = vec![None; raw.design.len()];
for i in 0..raw.design.len() {
resolve(i, &raw.design, &by_name, &mut resolved, &mut Vec::new())?;
}
let designs = raw
.design
.iter()
.zip(resolved)
.map(|(d, r)| {
Design::from_resolved(
d.name.clone().unwrap_or_default(),
d.description.clone(),
r.unwrap_or_else(Resolved::builtin),
)
})
.collect();
Ok(Self { designs })
}
pub fn designs(&self) -> &[Design] {
&self.designs
}
pub fn get(&self, name: &str) -> Option<&Design> {
self.designs.iter().find(|d| d.name == name)
}
pub fn select(&self, name: Option<&str>) -> Result<&Design, String> {
match name {
None => self
.designs
.first()
.ok_or_else(|| "loop-design file holds no designs".to_string()),
Some(n) => self.get(n).ok_or_else(|| {
format!(
"no design named {n:?}; the file holds {}",
self.designs
.iter()
.map(|d| d.name.as_str())
.collect::<Vec<_>>()
.join(", ")
)
}),
}
}
}
fn resolve(
i: usize,
raw: &[RawDesign],
by_name: &BTreeMap<String, usize>,
done: &mut Vec<Option<Resolved>>,
stack: &mut Vec<usize>,
) -> Result<Resolved, String> {
if let Some(r) = &done[i] {
return Ok(r.clone());
}
let name = raw[i].name.clone().unwrap_or_default();
if stack.contains(&i) {
return Err(format!(
"loop-design file: design {name:?} extends itself (a cycle)"
));
}
stack.push(i);
let base = match raw[i].extends.as_deref() {
None | Some("default") => Resolved::builtin(),
Some(parent) => {
let j = *by_name.get(parent).ok_or_else(|| {
format!("loop-design file: design {name:?} extends unknown design {parent:?}")
})?;
resolve(j, raw, by_name, done, stack)?
}
};
stack.pop();
let r = base.apply(&raw[i], &name)?;
done[i] = Some(r.clone());
Ok(r)
}
#[cfg(test)]
mod tests {
use super::*;
const HEAD: &str = "schema = \"kshana.loop-design/1\"\n";
#[test]
fn canonical_json_sorts_keys_at_every_level_and_ignores_insertion_order() {
let mut a = serde_json::Map::new();
a.insert("z".into(), serde_json::json!(1.0));
a.insert("a".into(), serde_json::json!({"y": 2, "b": [3, "x"]}));
let mut b = serde_json::Map::new();
b.insert("a".into(), serde_json::json!({"b": [3, "x"], "y": 2}));
b.insert("z".into(), serde_json::json!(1.0));
let (a, b) = (serde_json::Value::Object(a), serde_json::Value::Object(b));
assert_eq!(canonical_json(&a), r#"{"a":{"b":[3,"x"],"y":2},"z":1.0}"#);
assert_eq!(canonical_json(&a), canonical_json(&b));
assert_ne!(
canonical_json(&serde_json::json!(15)),
canonical_json(&serde_json::json!(15.0))
);
}
#[test]
fn the_builtin_default_hash_is_pinned() {
let d = Design::builtin_default();
let canonical = canonical_json(&serde_json::to_value(&d.resolved).unwrap());
assert!(canonical.starts_with(r#"{"acquisition":{"coherent_periods":"auto""#));
assert_eq!(
d.hash(),
crate::advanced_report::sha256_hex(canonical.as_bytes())
);
assert_eq!(
d.hash(),
"33261cd171a53803a6c262686e878e01f37d902a93d5918c20a44297b8ef8e80"
);
}
#[test]
fn extra_taps_join_the_hash_only_when_set() {
let base = format!("{HEAD}[[design]]\nname = \"a\"\n");
let plain = DesignFile::parse(&base).unwrap();
let empty = DesignFile::parse(&format!(
"{base}[design.integration]\nextra_taps_chips = []\n"
))
.unwrap();
let tapped = DesignFile::parse(&format!(
"{base}[design.integration]\nextra_taps_chips = [0.25, -0.25]\n"
))
.unwrap();
assert_eq!(plain.designs()[0].hash(), empty.designs()[0].hash());
assert_eq!(
plain.designs()[0].hash(),
Design::builtin_default().hash(),
"an empty tap list leaves the default's hash alone"
);
assert_ne!(plain.designs()[0].hash(), tapped.designs()[0].hash());
assert_eq!(
tapped.designs()[0].loop_config().extra_taps_chips,
vec![0.25, -0.25]
);
let canon = |d: &Design| canonical_json(&serde_json::to_value(&d.resolved).unwrap());
assert!(!canon(&plain.designs()[0]).contains("extra_taps_chips"));
assert!(canon(&tapped.designs()[0]).contains("\"extra_taps_chips\":[0.25,-0.25]"));
}
#[test]
fn bad_extra_taps_are_refused() {
let bad = |t: &str| {
DesignFile::parse(&format!(
"{HEAD}[[design]]\nname = \"a\"\n[design.integration]\nextra_taps_chips = {t}\n"
))
.unwrap_err()
};
assert!(bad("[3.0]").contains("extra_taps_chips"));
assert!(bad("[nan]").contains("extra_taps_chips"));
let many = format!("[{}]", vec!["0.1"; 17].join(","));
assert!(bad(&many).contains("at most 16"));
}
#[test]
fn an_empty_design_is_the_builtin_default() {
let f = DesignFile::parse(&format!("{HEAD}[[design]]\nname = \"a\"\n")).unwrap();
let d = &f.designs()[0];
let want = LoopConfig {
label: "a".into(),
..LoopConfig::default()
};
assert_eq!(d.loop_config(), want);
assert_eq!(d.lock_config(), LockConfig::default());
assert!(
!d.lock_config().reacquire,
"re-acquisition is off by default"
);
assert_eq!(d.hash(), Design::builtin_default().hash());
assert_eq!(d.acq_config(1e-3).coherent_periods, 4);
}
#[test]
fn the_auto_step_follows_the_signal_and_is_recorded() {
let d = Design::builtin_default();
let ca = d.resolved_run(1e-3);
assert_eq!(ca.acq_coherent_periods, 4);
assert!((ca.acq_doppler_step_hz - 2.0 / 0.012).abs() < 1e-9);
let e1b = d.resolved_run(4e-3);
assert_eq!(e1b.acq_coherent_periods, 1);
assert!((e1b.acq_doppler_step_hz - 50.0).abs() < 1e-9);
assert!((e1b.t_track_s - 4e-3).abs() < 1e-15);
let fixed = d
.with_overrides("[acquisition]\ndoppler_step_hz = 250.0\n")
.unwrap();
assert_eq!(fixed.resolved_run(4e-3).acq_doppler_step_hz, 250.0);
}
fn bws(c: &LoopConfig) -> (f64, f64) {
match c.carrier {
CarrierLoop::FllAssistedPll {
pll_bn_hz,
fll_bn_hz,
..
} => (pll_bn_hz, fll_bn_hz),
ref other => panic!("not fll-assisted: {other:?}"),
}
}
#[test]
fn the_default_clamps_bn_times_t_above_4_ms_only() {
let d = Design::builtin_default();
assert_eq!(bws(&d.loop_config_for(1e-3)), (15.0, 10.0));
assert_eq!(bws(&d.loop_config_for(4e-3)), (15.0, 10.0));
assert_eq!(bws(&d.loop_config_for(10e-3)), (10.0, 10.0));
let (pll, fll) = bws(&d.loop_config_for(20e-3));
assert!((pll - 5.0).abs() < 1e-12 && (fll - 5.0).abs() < 1e-12);
for t in [5e-3, 10e-3, 20e-3, 100e-3] {
let (p, f) = bws(&d.loop_config_for(t));
assert!(p * t <= 0.1 + 1e-12 && f * t <= 0.1 + 1e-12, "T = {t}");
}
let r = d.resolved_run(20e-3);
assert_eq!((r.pll_bn_hz.unwrap(), r.fll_bn_hz.unwrap()), (pll, fll));
assert_eq!(d.resolved_run(1e-3).pll_bn_hz, Some(15.0));
assert_eq!(bws(&d.loop_config()), (15.0, 10.0));
}
#[test]
fn bn_t_max_zero_disables_the_clamp_and_changes_the_hash() {
let d = Design::builtin_default();
let same = d.with_overrides("[carrier]\nbn_t_max = 0.1\n").unwrap();
assert_eq!(
same.hash(),
d.hash(),
"the default value is omitted from the hash"
);
assert!(!same.to_json()["carrier"]
.as_object()
.unwrap()
.contains_key("bn_t_max"));
let off = d.with_overrides("[carrier]\nbn_t_max = 0.0\n").unwrap();
assert_ne!(off.hash(), d.hash());
assert_eq!(
bws(&off.loop_config_for(20e-3)),
(15.0, 10.0),
"0 disables the clamp"
);
assert_eq!(off.to_json()["carrier"]["bn_t_max"], 0.0);
let loose = d.with_overrides("[carrier]\nbn_t_max = 0.3\n").unwrap();
assert_ne!(loose.hash(), d.hash());
assert_ne!(loose.hash(), off.hash());
assert_eq!(bws(&loose.loop_config_for(20e-3)), (15.0, 10.0));
let explicit = d.with_overrides("[carrier]\npll_bw_hz = 15.0\n").unwrap();
assert_eq!(explicit.hash(), d.hash());
assert_eq!(bws(&explicit.loop_config_for(20e-3)).0, 5.0);
assert!(d.with_overrides("[carrier]\nbn_t_max = -1.0\n").is_err());
}
#[test]
fn every_field_round_trips_into_the_loop_config() {
let text = format!(
"{HEAD}[[design]]
name = \"all\"
[design.integration]
coherent_periods = 2
spacing_chips = 0.2
[design.carrier]
kind = \"pll\"
pll_order = 3
pll_bw_hz = 18.0
pll_discriminator = \"atan2\"
fll_discriminator = \"atan2-pilot\"
[design.code]
order = 2
bw_hz = 1.0
discriminator = \"dot-product\"
carrier_aiding = false
[design.lock]
pli_threshold = 0.7
code_lock_cn0_dbhz = 30.0
cn0_windows = 20
cn0_window_periods = 10
pull_in_max_s = 3.0
loss_dwell_s = 0.5
false_lock_check = false
false_lock_margin_db = 6.0
reacquire = true
reacq_doppler_window_hz = 300.0
reacq_window_s = 12.0
reacq_interval_s = 0.5
reacq_max_interval_s = 4.0
max_reacq_attempts = 5
[design.bit_sync]
min_votes = 8
ratio = 2.0
[design.acquisition]
coherent_periods = 2
noncoherent = 3
doppler_max_hz = 4000.0
doppler_step_hz = 100.0
pfa = 0.01
"
);
let f = DesignFile::parse(&text).unwrap();
let c = f.designs()[0].loop_config();
assert_eq!(c.coherent_periods, 2);
assert_eq!(c.spacing_chips, 0.2);
assert_eq!(
c.carrier,
CarrierLoop::Pll {
order: 3,
bn_hz: 18.0
}
);
assert_eq!(c.pll_discriminator, PllDiscriminator::Atan2);
assert_eq!(c.fll_discriminator, FllDiscriminator::Atan2Pilot);
assert_eq!((c.dll_order, c.dll_bn_hz), (2, 1.0));
assert_eq!(c.dll, DllDiscriminator::DotProduct);
assert!(!c.carrier_aiding);
assert_eq!((c.pli_threshold, c.code_lock_cn0_dbhz), (0.7, 30.0));
assert_eq!((c.cn0_windows, c.cn0_window_periods), (20, 10));
assert_eq!((c.bit_sync.min_votes, c.bit_sync.ratio), (8, 2.0));
let l = f.designs()[0].lock_config();
assert_eq!(
l,
LockConfig {
pull_in_max_s: 3.0,
loss_dwell_s: 0.5,
false_lock_check: false,
false_lock_margin_db: 6.0,
reacquire: true,
reacq_doppler_window_hz: 300.0,
reacq_window_s: 12.0,
reacq_interval_s: 0.5,
reacq_max_interval_s: 4.0,
max_reacq_attempts: 5,
}
);
let a = f.designs()[0].acq_config(1e-3);
assert_eq!(
(
a.coherent_periods,
a.noncoherent,
a.doppler_max_hz,
a.doppler_step_hz,
a.pfa
),
(2, 3, 4000.0, 100.0, 0.01)
);
}
#[test]
fn extends_inherits_and_the_hash_ignores_the_name() {
let text = format!(
"{HEAD}[[design]]\nname = \"a\"\n[design.carrier]\npll_bw_hz = 5.0\n\
[[design]]\nname = \"b\"\nextends = \"a\"\n[design.code]\nbw_hz = 4.0\n\
[[design]]\nname = \"c\"\nextends = \"a\"\n"
);
let f = DesignFile::parse(&text).unwrap();
let b = f.get("b").unwrap().loop_config();
assert_eq!(b.dll_bn_hz, 4.0);
assert!(
matches!(b.carrier, CarrierLoop::FllAssistedPll { pll_bn_hz, .. } if pll_bn_hz == 5.0)
);
assert_eq!(f.get("a").unwrap().hash(), f.get("c").unwrap().hash());
assert_ne!(f.get("a").unwrap().hash(), f.get("b").unwrap().hash());
}
#[test]
fn mistakes_are_refused_with_the_design_and_key() {
let bad = |body: &str| {
DesignFile::parse(&format!("{HEAD}[[design]]\nname = \"x\"\n{body}")).unwrap_err()
};
assert!(bad("[design.carrier]\npll_bw = 5.0\n").contains("pll_bw"));
assert!(
bad("[design.carrier]\nkind = \"fll\"\npll_bw_hz = 5.0\n").contains("does not apply")
);
assert!(bad("[design.carrier]\npll_order = 4\n").contains("pll_order"));
assert!(bad("[design.code]\ndiscriminator = \"nope\"\n").contains("code.discriminator"));
assert!(bad("[design.integration]\nspacing_chips = 3.0\n").contains("spacing_chips"));
assert!(bad("[design.acquisition]\ncoherent_periods = \"often\"\n")
.contains("coherent_periods"));
assert!(bad("extends = \"x\"\n").contains("cycle"));
assert!(bad("extends = \"y\"\n").contains("unknown design"));
assert!(DesignFile::parse("[[design]]\nname = \"x\"\n")
.unwrap_err()
.contains("schema"));
assert!(
DesignFile::parse(&format!("{HEAD}[[design]]\nname = \"default\"\n"))
.unwrap_err()
.contains("reserved")
);
assert!(DesignFile::parse(&format!(
"{HEAD}[[design]]\nname = \"a\"\n[[design]]\nname = \"a\"\n"
))
.unwrap_err()
.contains("twice"));
}
#[test]
fn switching_carrier_kind_keeps_only_the_keys_it_uses() {
let f = DesignFile::parse(&format!(
"{HEAD}[[design]]\nname = \"f\"\n[design.carrier]\nkind = \"fll\"\nfll_bw_hz = 4.0\n"
))
.unwrap();
assert_eq!(
f.designs()[0].loop_config().carrier,
CarrierLoop::Fll {
order: 1,
bn_hz: 4.0
}
);
}
#[test]
fn fll_assist_defaults_to_pull_in_and_belongs_to_the_assisted_kind() {
let parse =
|body: &str| DesignFile::parse(&format!("{HEAD}[[design]]\nname = \"a\"\n{body}"));
let d = Design::builtin_default();
assert_eq!(
d.loop_config().fll_assist,
FllAssist::PullIn(FllGate::default())
);
let tuned = parse("[design.carrier]\nfll_on_pli = 0.5\nfll_gate_dwell_s = 0.3\n").unwrap();
assert_eq!(
tuned.designs()[0].loop_config().fll_assist,
FllAssist::PullIn(FllGate {
off_pli: 0.8,
on_pli: 0.5,
dwell_s: 0.3
})
);
let err = parse("[design.carrier]\nfll_on_pli = 0.9\n").unwrap_err();
assert!(err.contains("must be below"), "{err}");
assert_eq!(d.to_json()["carrier"]["fll_assist"], "pull-in");
let always = parse("[design.carrier]\nfll_assist = \"always\"\n").unwrap();
assert_eq!(
always.designs()[0].loop_config().fll_assist,
FllAssist::Always
);
assert_ne!(always.designs()[0].hash(), d.hash());
let pll = parse("[design.carrier]\nkind = \"pll\"\n").unwrap();
assert!(pll.designs()[0].to_json()["carrier"]["fll_assist"].is_null());
let err = parse("[design.carrier]\nkind = \"pll\"\nfll_assist = \"always\"\n").unwrap_err();
assert!(err.contains("does not apply"), "{err}");
let err = parse("[design.carrier]\nfll_assist = \"sometimes\"\n").unwrap_err();
assert!(err.contains("pull-in or always"), "{err}");
}
#[test]
fn overrides_apply_on_top_and_rehash() {
let d = Design::builtin_default();
let o = d.with_overrides("[carrier]\npll_bw_hz = 25.0\n").unwrap();
assert!(
matches!(o.loop_config().carrier, CarrierLoop::FllAssistedPll { pll_bn_hz, .. } if pll_bn_hz == 25.0)
);
assert_ne!(o.hash(), d.hash());
assert!(d.with_overrides("name = \"z\"\n").is_err());
}
}