#![allow(clippy::useless_conversion)]
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyList};
fn json_to_py<'py>(py: Python<'py>, v: &serde_json::Value) -> PyResult<pyo3::Bound<'py, PyAny>> {
use serde_json::Value;
Ok(match v {
Value::Null => py.None().into_bound(py),
Value::Bool(b) => (*b).into_pyobject(py)?.to_owned().into_any(),
Value::Number(n) => {
if let Some(i) = n.as_i64() {
i.into_pyobject(py)?.into_any()
} else if let Some(u) = n.as_u64() {
u.into_pyobject(py)?.into_any()
} else {
n.as_f64().unwrap_or(f64::NAN).into_pyobject(py)?.into_any()
}
}
Value::String(s) => s.into_pyobject(py)?.into_any(),
Value::Array(a) => {
let list = PyList::empty(py);
for item in a {
list.append(json_to_py(py, item)?)?;
}
list.into_any()
}
Value::Object(o) => {
let dict = PyDict::new(py);
for (k, val) in o {
dict.set_item(k, json_to_py(py, val)?)?;
}
dict.into_any()
}
})
}
#[pyclass(name = "RunOutput", skip_from_py_object)]
#[derive(Clone)]
struct PyRunOutput {
#[pyo3(get)]
json: String,
#[pyo3(get)]
svg: String,
#[pyo3(get)]
summary: String,
#[pyo3(get)]
csv: Option<String>,
}
#[pymethods]
impl PyRunOutput {
fn data<'py>(&self, py: Python<'py>) -> PyResult<pyo3::Bound<'py, PyAny>> {
let v: serde_json::Value =
serde_json::from_str(&self.json).map_err(|e| PyValueError::new_err(e.to_string()))?;
json_to_py(py, &v)
}
fn write_csv(&self, path: &str) -> PyResult<usize> {
match &self.csv {
Some(csv) => std::fs::write(path, csv)
.map(|()| csv.len())
.map_err(|e| PyValueError::new_err(e.to_string())),
None => Ok(0),
}
}
fn __repr__(&self) -> String {
format!(
"RunOutput(json={} bytes, svg={} bytes, csv={} bytes, summary={:?})",
self.json.len(),
self.svg.len(),
self.csv.as_ref().map_or(0, |c| c.len()),
self.summary.chars().take(48).collect::<String>()
)
}
}
#[pyfunction]
fn run_typed(toml: &str) -> PyResult<PyRunOutput> {
crate::api::run_toml(toml)
.map(|o| PyRunOutput {
json: o.json,
svg: o.svg,
summary: o.summary,
csv: o.csv,
})
.map_err(PyValueError::new_err)
}
#[pyfunction]
fn receiver_trust(toml: &str) -> PyResult<PyRunOutput> {
crate::receiver_trust::scenario::run_toml(toml)
.map(|o| PyRunOutput {
json: o.json,
svg: o.svg,
summary: o.summary,
csv: Some(o.csv),
})
.map_err(PyValueError::new_err)
}
#[pyfunction]
fn scenario_kinds<'py>(py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
let json = crate::api::list_scenario_kinds_json();
let v: serde_json::Value =
serde_json::from_str(&json).map_err(|e| PyValueError::new_err(e.to_string()))?;
json_to_py(py, &v)
}
#[pyfunction]
fn validate_toml(toml: &str) -> Vec<String> {
match crate::api::run_scenario(toml) {
Ok(_) => vec![],
Err(e) => vec![e.to_string()],
}
}
#[pyfunction]
fn run(toml: &str) -> PyResult<String> {
crate::api::run_toml(toml)
.map(|o| o.json)
.map_err(PyValueError::new_err)
}
#[pyfunction]
fn run_full(toml: &str) -> PyResult<(String, String, String)> {
crate::api::run_toml(toml)
.map(|o| (o.json, o.svg, o.summary))
.map_err(PyValueError::new_err)
}
#[pyfunction]
fn list_kinds() -> String {
crate::api::list_scenario_kinds_json()
}
#[pyfunction]
fn error_kind(toml: &str) -> Option<String> {
crate::api::run_scenario(toml)
.err()
.map(|e| e.kind_tag().to_string())
}
#[pyfunction]
fn version() -> &'static str {
env!("CARGO_PKG_VERSION")
}
use crate::iq::acq::{acquire_peak, AcqConfig};
use crate::iq::cli::{
build_broadcast_scene, build_chain, build_channel, build_code, build_scene, BroadcastParams,
ChannelParams, FrontendParams, SceneParams,
};
use crate::iq::scene::TruthRecord;
use crate::iq::signals::SignalCode;
use crate::iq::track::{ChannelInit, EpochOutput};
use crate::iq::{Cf64, SampleSpec, SpreadingCode, VecSink};
fn codes_for(signal: &str, prns: &[i64]) -> PyResult<Vec<SignalCode>> {
if prns.is_empty() {
return Err(PyValueError::new_err("prns must list at least one PRN"));
}
prns.iter()
.map(|&id| build_code(signal, id).map_err(PyValueError::new_err))
.collect()
}
fn samples_from(i: &[f64], q: &[f64]) -> PyResult<Vec<Cf64>> {
if i.len() != q.len() {
return Err(PyValueError::new_err(format!(
"i and q must be the same length ({} vs {})",
i.len(),
q.len()
)));
}
Ok(i.iter()
.zip(q)
.map(|(&re, &im)| Cf64::new(re, im))
.collect())
}
#[pyfunction]
#[pyo3(signature = (fs_hz, duration_s, signal, prns, dopplers=None, cn0_dbhz=None, center_hz=None, if_hz=0.0, noise=true, noise_figure_db=2.0, seed=1, data=false, threads=1, iono_stec=None, iono_vtec=None, iono_klobuchar=false, tropo=false, tropo_doy=180.0, s4=None, scint_tau0=1.0, sigma_phi=0.0, multipath_height=None, multipath_ground="dry".to_string(), land_mobile=false, nlos=false, cn0_profile=None))]
#[allow(clippy::too_many_arguments)]
fn iq_scene<'py>(
py: Python<'py>,
fs_hz: f64,
duration_s: f64,
signal: String,
prns: Vec<i64>,
dopplers: Option<Vec<f64>>,
cn0_dbhz: Option<f64>,
center_hz: Option<f64>,
if_hz: f64,
noise: bool,
noise_figure_db: f64,
seed: u64,
data: bool,
threads: usize,
iono_stec: Option<f64>,
iono_vtec: Option<f64>,
iono_klobuchar: bool,
tropo: bool,
tropo_doy: f64,
s4: Option<f64>,
scint_tau0: f64,
sigma_phi: f64,
multipath_height: Option<f64>,
multipath_ground: String,
land_mobile: bool,
nlos: bool,
cn0_profile: Option<String>,
) -> PyResult<Bound<'py, PyAny>> {
use crate::iq::channel::cn0_profile::{Cn0Profile, Cn0ProfileChannel, ProfiledTruth};
let profile = cn0_profile
.map(|t| Cn0Profile::parse(&t).map_err(|e| PyValueError::new_err(e.to_string())))
.transpose()?;
let params = SceneParams {
fs_hz,
duration_s,
signal,
ids: prns,
dopplers: dopplers.unwrap_or_default(),
center_hz,
if_hz,
cn0_dbhz,
noise_figure_db,
noise,
seed,
data,
threads,
};
let mut scene = build_scene(¶ms).map_err(PyValueError::new_err)?;
let spec = scene.config().spec;
let chan = ChannelParams {
iono_stec_tecu: iono_stec,
iono_vtec_tecu: iono_vtec,
iono_klobuchar,
tropo,
tropo_doy,
s4,
scint_tau0_s: scint_tau0,
sigma_phi_rad: sigma_phi,
multipath_height_m: multipath_height,
multipath_ground,
land_mobile,
nlos,
};
let mut inner = None;
if chan.any() {
let carrier = scene
.satellites()
.first()
.map(|s| s.code.carrier_hz())
.unwrap_or(spec.center_hz);
let start_tow = scene.config().start_tow_s;
inner =
build_channel(&chan, carrier, params.seed, start_tow).map_err(PyValueError::new_err)?;
}
match (&profile, inner) {
(Some(p), inner) => scene.set_channel(Box::new(Cn0ProfileChannel::new(p.clone(), inner))),
(None, Some(ch)) => scene.set_channel(ch),
(None, None) => {}
}
let mut sink = VecSink::default();
let mut truth: Vec<TruthRecord> = Vec::new();
match profile {
Some(p) => scene.generate(&mut sink, &mut ProfiledTruth::new(p, &mut truth)),
None => scene.generate(&mut sink, &mut truth),
}
.map_err(|e| PyValueError::new_err(e.to_string()))?;
let i: Vec<f64> = sink.samples.iter().map(|s| s.re).collect();
let q: Vec<f64> = sink.samples.iter().map(|s| s.im).collect();
let truth_json: Vec<serde_json::Value> = truth
.iter()
.map(|r| serde_json::to_value(r).unwrap_or(serde_json::Value::Null))
.collect();
let v = serde_json::json!({
"fs_hz": spec.fs_hz,
"center_hz": spec.center_hz,
"if_hz": spec.if_hz,
"samples_i": i,
"samples_q": q,
"truth": truth_json,
});
json_to_py(py, &v)
}
#[pyfunction]
#[pyo3(signature = (fs_hz, window_s, nav_text, rx_lat, rx_lon, rx_alt, prns=None, start_tow=0.0, cn0_dbhz=None, center_hz=None, if_hz=0.0, noise=true, noise_figure_db=2.0, seed=1, mask_deg=5.0, threads=1))]
#[allow(clippy::too_many_arguments)]
fn iq_scene_broadcast<'py>(
py: Python<'py>,
fs_hz: f64,
window_s: f64,
nav_text: String,
rx_lat: f64,
rx_lon: f64,
rx_alt: f64,
prns: Option<Vec<i64>>,
start_tow: f64,
cn0_dbhz: Option<f64>,
center_hz: Option<f64>,
if_hz: f64,
noise: bool,
noise_figure_db: f64,
seed: u64,
mask_deg: f64,
threads: usize,
) -> PyResult<Bound<'py, PyAny>> {
let bp = BroadcastParams {
fs_hz,
duration_s: window_s,
nav_text,
prns: prns.unwrap_or_default(),
start_tow_s: start_tow,
rx_llh_deg: (rx_lat, rx_lon, rx_alt),
center_hz,
if_hz,
cn0_dbhz,
noise,
noise_figure_db,
seed,
elevation_mask_deg: mask_deg,
threads,
};
let scene = build_broadcast_scene(&bp).map_err(PyValueError::new_err)?;
let spec = scene.config().spec;
let mut sink = VecSink::default();
let mut truth: Vec<TruthRecord> = Vec::new();
scene
.generate(&mut sink, &mut truth)
.map_err(|e| PyValueError::new_err(e.to_string()))?;
let i: Vec<f64> = sink.samples.iter().map(|s| s.re).collect();
let q: Vec<f64> = sink.samples.iter().map(|s| s.im).collect();
let truth_json: Vec<serde_json::Value> = truth
.iter()
.map(|r| serde_json::to_value(r).unwrap_or(serde_json::Value::Null))
.collect();
let v = serde_json::json!({
"fs_hz": spec.fs_hz,
"center_hz": spec.center_hz,
"if_hz": spec.if_hz,
"samples_i": i,
"samples_q": q,
"truth": truth_json,
});
json_to_py(py, &v)
}
#[pyfunction]
#[pyo3(signature = (i, q, fs_hz, signal, prns, if_hz=0.0, center_hz=None, coherent=1, noncoherent=1, doppler_max=5000.0, doppler_step=None, pfa=1e-3))]
#[allow(clippy::too_many_arguments)]
fn iq_acquire<'py>(
py: Python<'py>,
i: Vec<f64>,
q: Vec<f64>,
fs_hz: f64,
signal: String,
prns: Vec<i64>,
if_hz: f64,
center_hz: Option<f64>,
coherent: usize,
noncoherent: usize,
doppler_max: f64,
doppler_step: Option<f64>,
pfa: f64,
) -> PyResult<Bound<'py, PyAny>> {
let codes = codes_for(&signal, &prns)?;
let samples = samples_from(&i, &q)?;
let spec = SampleSpec {
fs_hz,
center_hz: center_hz.unwrap_or_else(|| codes[0].carrier_hz()),
if_hz,
};
let coherent = coherent.max(1);
let cfg = AcqConfig {
coherent_periods: coherent,
noncoherent: noncoherent.max(1),
doppler_max_hz: doppler_max,
doppler_step_hz: doppler_step
.unwrap_or(2.0 / (3.0 * coherent as f64 * codes[0].period_s())),
pfa,
};
let mut out = Vec::new();
for code in &codes {
let r = acquire_peak(&samples, &spec, code, &cfg).map_err(PyValueError::new_err)?;
out.push(serde_json::json!({
"code": r.code_name,
"acquired": r.acquired,
"doppler_hz": r.doppler_hz,
"code_phase_chips": r.code_phase_chips,
"delay_samples": r.delay_samples,
"statistic": r.statistic,
"threshold": r.threshold,
"peak_ratio": r.peak_ratio,
"second_peak": r.second_peak,
"sample_power": r.sample_power,
}));
}
json_to_py(py, &serde_json::Value::Array(out))
}
#[pyfunction]
#[pyo3(signature = (i, q, fs_hz, signal, prn, if_hz=0.0, center_hz=None, coherent=1, noncoherent=1, doppler_max=5000.0, doppler_step=None, pfa=1e-3))]
#[allow(clippy::too_many_arguments)]
fn iq_acq_surface<'py>(
py: Python<'py>,
i: Vec<f64>,
q: Vec<f64>,
fs_hz: f64,
signal: String,
prn: i64,
if_hz: f64,
center_hz: Option<f64>,
coherent: usize,
noncoherent: usize,
doppler_max: f64,
doppler_step: Option<f64>,
pfa: f64,
) -> PyResult<Bound<'py, PyAny>> {
let codes = codes_for(&signal, &[prn])?;
let samples = samples_from(&i, &q)?;
let spec = SampleSpec {
fs_hz,
center_hz: center_hz.unwrap_or_else(|| codes[0].carrier_hz()),
if_hz,
};
let coherent = coherent.max(1);
let cfg = AcqConfig {
coherent_periods: coherent,
noncoherent: noncoherent.max(1),
doppler_max_hz: doppler_max,
doppler_step_hz: doppler_step
.unwrap_or(2.0 / (3.0 * coherent as f64 * codes[0].period_s())),
pfa,
};
let surface = crate::iq::acq_surface::Surface::compute(&samples, &spec, &codes[0], &cfg)
.map_err(PyValueError::new_err)?;
json_to_py(
py,
&serde_json::json!({ "header": surface.header, "rows": surface.grid }),
)
}
#[pyfunction]
#[pyo3(signature = (i, q, fs_hz, signal, prns, if_hz=0.0, center_hz=None, pll_bw=None, fll_bw=None, dll_bw=None, spacing=None, coherent=None, periods_per_bit=None, acq_coherent=None, acq_noncoherent=None, doppler_max=None, max_seconds=None, design=None, design_name=None, reacquire=None, extra_taps=None, threads=1))]
#[allow(clippy::too_many_arguments)]
fn iq_track<'py>(
py: Python<'py>,
i: Vec<f64>,
q: Vec<f64>,
fs_hz: f64,
signal: String,
prns: Vec<i64>,
if_hz: f64,
center_hz: Option<f64>,
pll_bw: Option<f64>,
fll_bw: Option<f64>,
dll_bw: Option<f64>,
spacing: Option<f64>,
coherent: Option<usize>,
periods_per_bit: Option<usize>,
acq_coherent: Option<usize>,
acq_noncoherent: Option<usize>,
doppler_max: Option<f64>,
max_seconds: Option<f64>,
design: Option<String>,
design_name: Option<String>,
reacquire: Option<bool>,
extra_taps: Option<Vec<f64>>,
threads: usize,
) -> PyResult<Bound<'py, PyAny>> {
use crate::iq::track::design::{Design, DesignFile};
use crate::iq::track::sink::CollectSink;
use crate::iq::track::{SessionChannel, TrackSession};
let codes = codes_for(&signal, &prns)?;
let samples = samples_from(&i, &q)?;
let spec = SampleSpec {
fs_hz,
center_hz: center_hz.unwrap_or_else(|| codes[0].carrier_hz()),
if_hz,
};
let base = match design {
None => {
if design_name.is_some() {
return Err(PyValueError::new_err("design_name needs design"));
}
Design::builtin_default()
}
Some(d) => {
let text = if std::path::Path::new(&d).is_file() {
std::fs::read_to_string(&d).map_err(|e| PyValueError::new_err(e.to_string()))?
} else {
d
};
DesignFile::parse(&text)
.and_then(|f| f.select(design_name.as_deref()).cloned())
.map_err(PyValueError::new_err)?
}
};
let mut o = String::new();
let mut section = |name: &str, kv: Vec<(&str, Option<String>)>| {
let set: Vec<String> = kv
.into_iter()
.filter_map(|(k, v)| v.map(|v| format!("{k} = {v}")))
.collect();
if !set.is_empty() {
o.push_str(&format!("[{name}]\n{}\n", set.join("\n")));
}
};
let f = |v: Option<f64>| v.map(|x| format!("{x:?}"));
let n = |v: Option<usize>| v.map(|x| x.max(1).to_string());
section(
"carrier",
vec![("pll_bw_hz", f(pll_bw)), ("fll_bw_hz", f(fll_bw))],
);
section("code", vec![("bw_hz", f(dll_bw))]);
section(
"integration",
vec![
("spacing_chips", f(spacing)),
("coherent_periods", n(coherent)),
(
"extra_taps_chips",
extra_taps.map(|t| {
format!(
"[{}]",
t.iter()
.map(|v| format!("{v:?}"))
.collect::<Vec<_>>()
.join(", ")
)
}),
),
],
);
section(
"lock",
vec![("reacquire", reacquire.map(|b| b.to_string()))],
);
section(
"acquisition",
vec![
("coherent_periods", n(acq_coherent)),
("noncoherent", n(acq_noncoherent)),
("doppler_max_hz", f(doppler_max)),
],
);
let design = if o.is_empty() {
base
} else {
base.with_overrides(&o).map_err(PyValueError::new_err)?
};
let acq = design.acq_config(codes[0].period_s());
let mut inits = Vec::new();
for code in &codes {
let found = acquire_peak(&samples, &spec, code, &acq).map_err(PyValueError::new_err)?;
if !found.acquired {
return Err(PyValueError::new_err(format!(
"{}: not acquired (statistic {:.2} < threshold {:.2})",
code.name(),
found.statistic,
found.threshold
)));
}
let arc: std::sync::Arc<dyn SpreadingCode + Send + Sync> =
std::sync::Arc::new(code.clone());
inits.push(ChannelInit::from_acquisition(
arc,
&found,
&spec,
0,
periods_per_bit,
));
}
let channels = inits
.into_iter()
.map(|init| SessionChannel::from_design(init, &design))
.collect();
let mut session = TrackSession::new(spec, channels)
.map_err(PyValueError::new_err)?
.with_threads(threads);
let max_samples = max_seconds.map(|s| (s * fs_hz).round() as u64);
let mut src = crate::iq::VecSource::new(spec, samples);
let mut sink = CollectSink::default();
session
.run(&mut src, max_samples, &mut sink)
.map_err(|e| PyValueError::new_err(e.to_string()))?;
let chans: Vec<serde_json::Value> = codes
.iter()
.enumerate()
.map(|(k, code)| {
let epochs: Vec<serde_json::Value> = sink
.channels
.get(k)
.map(|v| {
v.iter()
.map(|(e, st)| {
let mut j = epoch_value(e);
j["state"] = st.as_str().into();
j
})
.collect()
})
.unwrap_or_default();
serde_json::json!({ "code": code.name(), "epochs": epochs })
})
.collect();
let events: Vec<serde_json::Value> = sink
.events
.iter()
.map(|e| serde_json::to_value(e).unwrap_or(serde_json::Value::Null))
.collect();
json_to_py(
py,
&serde_json::json!({
"fs_hz": fs_hz,
"design": design.to_json(),
"events": events,
"warnings": crate::iq::cli::sampling_warnings(&spec, &codes),
"channels": chans,
}),
)
}
#[pyfunction]
fn iq_loop_designs<'py>(py: Python<'py>, toml: &str) -> PyResult<Bound<'py, PyAny>> {
let text = if std::path::Path::new(toml).is_file() {
std::fs::read_to_string(toml).map_err(|e| PyValueError::new_err(e.to_string()))?
} else {
toml.to_string()
};
let file = crate::iq::track::design::DesignFile::parse(&text).map_err(PyValueError::new_err)?;
let v: Vec<serde_json::Value> = file.designs().iter().map(|d| d.to_json()).collect();
json_to_py(py, &serde_json::Value::Array(v))
}
#[pyfunction]
fn iq_read_epochs<'py>(py: Python<'py>, path: &str) -> PyResult<Bound<'py, PyAny>> {
use crate::iq::track::sink::BinaryEpochReader;
let f = std::fs::File::open(path).map_err(|e| PyValueError::new_err(format!("{path}: {e}")))?;
let reader = BinaryEpochReader::new(std::io::BufReader::new(f))
.map_err(|e| PyValueError::new_err(e.to_string()))?;
let header = serde_json::to_value(reader.header()).unwrap_or(serde_json::Value::Null);
let records = reader
.map(|r| {
r.map(|rec| serde_json::to_value(rec).unwrap_or(serde_json::Value::Null))
.map_err(|e| PyValueError::new_err(e.to_string()))
})
.collect::<PyResult<Vec<_>>>()?;
json_to_py(
py,
&serde_json::json!({ "header": header, "records": records }),
)
}
fn epoch_value(e: &EpochOutput) -> serde_json::Value {
let mut v = serde_json::json!({
"epoch": e.epoch,
"sample_index": e.sample_index,
"code_epoch_s": e.code_epoch_s,
"t_coh_s": e.t_coh_s,
"doppler_hz": e.doppler_hz,
"code_rate_hz": e.code_rate_hz,
"code_phase_chips": e.code_phase_chips,
"periods": e.periods,
"i_early": e.early.re,
"q_early": e.early.im,
"i_prompt": e.prompt.re,
"q_prompt": e.prompt.im,
"i_late": e.late.re,
"q_late": e.late.im,
"carrier_phase_cycles": e.carrier_phase_cycles,
"bit_edge": e.bit_edge,
"bit": e.bit,
"dll_chips": e.disc.dll_chips,
"pll_rad": e.disc.pll_rad,
"fll_hz": e.disc.fll_hz,
"pli": e.pli,
"phase_lock": e.phase_lock,
"code_lock": e.code_lock,
"cn0_nwpr_dbhz": e.cn0_nwpr_dbhz,
"cn0_beaulieu_dbhz": e.cn0_beaulieu_dbhz,
"cn0_m2m4_dbhz": e.cn0_m2m4_dbhz,
});
if !e.extra.is_empty() {
v["extra"] = e
.extra
.iter()
.map(|&(offset_chips, c)| {
serde_json::json!({ "offset_chips": offset_chips, "i": c.re, "q": c.im })
})
.collect();
}
v
}
#[pyfunction]
#[pyo3(signature = (path, signal=None, prns=None, power=false, spectral=false, settings=None, baseline=None, max_seconds=None, spacing=None, pll_bw=None, fll_bw=None, dll_bw=None, coherent=None, cn0_windows=None, doppler_max=None, periods_per_bit=None, format=None, rate=None, center_hz=None, if_hz=None, header_bytes=None))]
#[allow(clippy::too_many_arguments)]
fn iq_monitor<'py>(
py: Python<'py>,
path: String,
signal: Option<String>,
prns: Option<Vec<i64>>,
power: bool,
spectral: bool,
settings: Option<String>,
baseline: Option<f64>,
max_seconds: Option<f64>,
spacing: Option<f64>,
pll_bw: Option<f64>,
fll_bw: Option<f64>,
dll_bw: Option<f64>,
coherent: Option<usize>,
cn0_windows: Option<usize>,
doppler_max: Option<f64>,
periods_per_bit: Option<usize>,
format: Option<String>,
rate: Option<f64>,
center_hz: Option<f64>,
if_hz: Option<f64>,
header_bytes: Option<u64>,
) -> PyResult<Bound<'py, PyAny>> {
let mut args = vec![path];
let mut opt = |k: &str, v: Option<String>| {
if let Some(v) = v {
args.push(k.to_string());
args.push(v);
}
};
opt("--signal", signal);
opt(
"--prn",
prns.map(|p| {
p.iter()
.map(|x| x.to_string())
.collect::<Vec<_>>()
.join(",")
}),
);
opt("--baseline", baseline.map(|v| v.to_string()));
opt("--max-seconds", max_seconds.map(|v| v.to_string()));
opt("--spacing", spacing.map(|v| v.to_string()));
opt("--pll-bw", pll_bw.map(|v| v.to_string()));
opt("--fll-bw", fll_bw.map(|v| v.to_string()));
opt("--dll-bw", dll_bw.map(|v| v.to_string()));
opt("--coherent", coherent.map(|v| v.to_string()));
opt("--cn0-windows", cn0_windows.map(|v| v.to_string()));
opt("--doppler-max", doppler_max.map(|v| v.to_string()));
opt("--periods-per-bit", periods_per_bit.map(|v| v.to_string()));
opt("--format", format);
opt("--rate", rate.map(|v| v.to_string()));
opt("--center", center_hz.map(|v| v.to_string()));
opt("--if", if_hz.map(|v| v.to_string()));
opt("--header", header_bytes.map(|v| v.to_string()));
if power {
args.push("--power".into());
}
if spectral {
args.push("--spectral".into());
}
let fail = |f: crate::iq::cli::CliFail| match f {
crate::iq::cli::CliFail::Usage(m) | crate::iq::cli::CliFail::Run(m) => {
PyValueError::new_err(m)
}
};
let a = crate::iq::cli::monitor_parse_args(&args).map_err(fail)?;
let report = crate::iq::cli::monitor_report(&a, settings.as_deref()).map_err(fail)?;
let v = serde_json::to_value(&report).map_err(|e| PyValueError::new_err(e.to_string()))?;
json_to_py(py, &v)
}
#[pyfunction]
#[pyo3(signature = (i, q, fs_hz, signal, prns, if_hz=0.0, center_hz=None, pll_bws=None, dll_bws=None, spacings=None, coherents=None, design=None, reacquire=None, periods_per_bit=None, max_seconds=None, threads=1))]
#[allow(clippy::too_many_arguments)]
fn iq_sweep<'py>(
py: Python<'py>,
i: Vec<f64>,
q: Vec<f64>,
fs_hz: f64,
signal: String,
prns: Vec<i64>,
if_hz: f64,
center_hz: Option<f64>,
pll_bws: Option<Vec<f64>>,
dll_bws: Option<Vec<f64>>,
spacings: Option<Vec<f64>>,
coherents: Option<Vec<usize>>,
design: Option<String>,
reacquire: Option<bool>,
periods_per_bit: Option<usize>,
max_seconds: Option<f64>,
threads: usize,
) -> PyResult<Bound<'py, PyAny>> {
use crate::iq::track::design::{Design, DesignFile};
use crate::iq::track::sink::{Fanout, Summary};
use crate::iq::track::{SessionChannel, TrackSession};
let codes = codes_for(&signal, &prns)?;
let samples = samples_from(&i, &q)?;
let spec = SampleSpec {
fs_hz,
center_hz: center_hz.unwrap_or_else(|| codes[0].carrier_hz()),
if_hz,
};
let bad = |e: String| PyValueError::new_err(e);
let designs: Vec<Design> = match design {
Some(d) => {
if pll_bws.is_some() || dll_bws.is_some() || spacings.is_some() || coherents.is_some() {
return Err(bad(
"pll_bws, dll_bws, spacings and coherents cannot be combined with design: \
the sweep runs every design in the file"
.into(),
));
}
let text = if std::path::Path::new(&d).is_file() {
std::fs::read_to_string(&d).map_err(|e| bad(e.to_string()))?
} else {
d
};
DesignFile::parse(&text).map_err(bad)?.designs().to_vec()
}
None => {
let base = Design::builtin_default();
let pll = pll_bws
.filter(|v| !v.is_empty())
.unwrap_or_else(|| vec![15.0]);
let dll = dll_bws
.filter(|v| !v.is_empty())
.unwrap_or_else(|| vec![base.loop_config().dll_bn_hz]);
let sp = spacings
.filter(|v| !v.is_empty())
.unwrap_or_else(|| vec![base.loop_config().spacing_chips]);
let coh = coherents
.filter(|v| !v.is_empty())
.unwrap_or_else(|| vec![base.loop_config().coherent_periods]);
let mut out = Vec::new();
for &p in &pll {
for &d in &dll {
for &s in &sp {
for &c in &coh {
let c = c.max(1);
let dd = base
.with_overrides(&format!(
"[carrier]\npll_bw_hz = {p:?}\n[code]\nbw_hz = {d:?}\n\
[integration]\nspacing_chips = {s:?}\ncoherent_periods = {c}\n"
))
.map_err(bad)?;
out.push(dd.renamed(&format!("pll{p}_dll{d}_sp{s}_coh{c}")));
}
}
}
}
out
}
};
if designs.is_empty() {
return Err(bad("no designs to sweep".into()));
}
if designs.len() * codes.len() > 256 {
return Err(bad(format!(
"{} (design, PRN) channels; at most 256 per sweep",
designs.len() * codes.len()
)));
}
let acq = designs[0].acq_config(codes[0].period_s());
let mut inits = Vec::new();
for code in &codes {
let found = acquire_peak(&samples, &spec, code, &acq).map_err(bad)?;
if !found.acquired {
return Err(bad(format!(
"{}: not acquired (statistic {:.2} < threshold {:.2})",
code.name(),
found.statistic,
found.threshold
)));
}
let arc: std::sync::Arc<dyn SpreadingCode + Send + Sync> =
std::sync::Arc::new(code.clone());
inits.push(ChannelInit::from_acquisition(
arc,
&found,
&spec,
0,
periods_per_bit,
));
}
let n_total = samples.len() as u64;
let max_samples = max_seconds.map(|s| (s * fs_hz).round() as u64);
let tracked = max_samples.map_or(n_total, |m| m.min(n_total));
let mut channels = Vec::new();
let mut meta = Vec::new();
for d in &designs {
let d = if reacquire.unwrap_or(false) {
d.with_overrides("[lock]\nreacquire = true\n")
.map_err(bad)?
} else {
d.clone()
};
for (init, code) in inits.iter().zip(&codes) {
channels.push(SessionChannel::from_design(init.clone(), &d));
meta.push((d.name().to_string(), d.hash().to_string(), code.name()));
}
}
let mut session = TrackSession::new(spec, channels)
.map_err(bad)?
.with_threads(threads);
let mut summary = Summary::new(0.5 * tracked as f64 / fs_hz);
let mut src = crate::iq::VecSource::new(spec, samples);
{
let mut fan = Fanout::new();
fan.push(&mut summary);
session
.run(&mut src, max_samples, &mut fan)
.map_err(|e| PyValueError::new_err(e.to_string()))?;
}
let rows: Vec<serde_json::Value> = meta
.iter()
.enumerate()
.map(|(k, (name, hash, code))| {
let c = summary.channels.get(k).cloned().unwrap_or_default();
serde_json::json!({
"design": name,
"code": code,
"epochs": c.epochs,
"phase_jitter_deg": c.phase_jitter_deg,
"code_jitter_chips": c.code_jitter_chips,
"phase_lock_frac": c.phase_lock_fraction(),
"code_lock_frac": c.code_lock_fraction(),
"mean_cn0_dbhz": c.mean_cn0_dbhz,
"design_hash": hash,
})
})
.collect();
json_to_py(
py,
&serde_json::json!({
"designs": rows,
"warnings": crate::iq::cli::sampling_warnings(&spec, &codes),
}),
)
}
#[pyfunction]
#[pyo3(signature = (path, hash=false))]
fn iq_info<'py>(py: Python<'py>, path: &str, hash: bool) -> PyResult<Bound<'py, PyAny>> {
let e = crate::iq::io::inventory::inventory_entry(std::path::Path::new(path), hash);
let v = serde_json::to_value(&e).map_err(|err| PyValueError::new_err(err.to_string()))?;
json_to_py(py, &v)
}
#[pyfunction]
#[pyo3(signature = (directory, recursive=false, hash=false))]
fn iq_inventory<'py>(
py: Python<'py>,
directory: &str,
recursive: bool,
hash: bool,
) -> PyResult<Bound<'py, PyAny>> {
let paths =
crate::iq::io::inventory::list_recordings(std::path::Path::new(directory), recursive)
.map_err(|e| PyValueError::new_err(e.to_string()))?;
let rows: Vec<serde_json::Value> = paths
.iter()
.map(|p| {
serde_json::to_value(crate::iq::io::inventory::inventory_entry(p, hash))
.unwrap_or(serde_json::Value::Null)
})
.collect();
json_to_py(py, &serde_json::Value::Array(rows))
}
#[pyfunction]
fn iq_labfit<'py>(py: Python<'py>, toml: &str) -> PyResult<Bound<'py, PyAny>> {
let out = crate::iq::labfit::run_toml(toml).map_err(PyValueError::new_err)?;
let report: serde_json::Value =
serde_json::from_str(&out.json).map_err(|e| PyValueError::new_err(e.to_string()))?;
let v = serde_json::json!({
"report": report,
"residuals_csv": out.residuals_csv,
"predictions_csv": out.predictions_csv,
"markdown": out.markdown,
});
json_to_py(py, &v)
}
#[pyfunction]
fn iq_test_conditions<'py>(py: Python<'py>, conditions: &str) -> PyResult<Bound<'py, PyAny>> {
use crate::iq::campaign::TestConditions;
let path = std::path::Path::new(conditions);
let tc = if !conditions.contains('\n') && path.is_file() {
TestConditions::load(path)
} else {
TestConditions::parse(conditions)
}
.map_err(PyValueError::new_err)?;
let mut v = serde_json::to_value(&tc).map_err(|e| PyValueError::new_err(e.to_string()))?;
if let Some(m) = v.as_object_mut() {
m.insert("hash".into(), tc.hash().into());
}
json_to_py(py, &v)
}
#[pyfunction]
#[pyo3(signature = (campaign, out_dir, workers=0, resume=true, max_cells=None, dry_run=false))]
fn iq_campaign<'py>(
py: Python<'py>,
campaign: &str,
out_dir: &str,
workers: usize,
resume: bool,
max_cells: Option<usize>,
dry_run: bool,
) -> PyResult<Bound<'py, PyAny>> {
use crate::iq::campaign::{run, LoadedCampaign, RunOptions};
let path = std::path::Path::new(campaign);
let loaded = if !campaign.contains('\n') && path.is_file() {
LoadedCampaign::load(path)
} else {
LoadedCampaign::load_text(campaign, std::path::Path::new("./campaign.toml"))
}
.map_err(PyValueError::new_err)?;
let opts = RunOptions {
workers,
no_resume: !resume,
max_cells,
dry_run,
};
let out = std::path::PathBuf::from(out_dir);
let summary = py
.detach(|| run(&loaded, &out, &opts))
.map_err(pyo3::exceptions::PyRuntimeError::new_err)?;
let v = serde_json::to_value(&summary).map_err(|e| PyValueError::new_err(e.to_string()))?;
json_to_py(py, &v)
}
#[pyfunction]
fn iq_campaign_report<'py>(py: Python<'py>, out_dir: &str) -> PyResult<Bound<'py, PyAny>> {
let s = crate::iq::campaign::report::build(std::path::Path::new(out_dir))
.map_err(pyo3::exceptions::PyRuntimeError::new_err)?;
let v = serde_json::to_value(&s).map_err(|e| PyValueError::new_err(e.to_string()))?;
json_to_py(py, &v)
}
#[pyfunction]
#[pyo3(signature = (i, q, fs_hz, bandpass_lo=None, bandpass_hi=None, bandpass_transition=None, bandpass_atten=60.0, notch=false, notch_r=0.95, notch_mu=0.05, blank=None, blank_hold=0, excise=false, excise_fft=256, excise_pfa=1e-3, agc=false, agc_tau=1e-3, bits=None, quant_step=None, no_agc=false))]
#[allow(clippy::too_many_arguments)]
fn iq_frontend<'py>(
py: Python<'py>,
i: Vec<f64>,
q: Vec<f64>,
fs_hz: f64,
bandpass_lo: Option<f64>,
bandpass_hi: Option<f64>,
bandpass_transition: Option<f64>,
bandpass_atten: f64,
notch: bool,
notch_r: f64,
notch_mu: f64,
blank: Option<f64>,
blank_hold: usize,
excise: bool,
excise_fft: usize,
excise_pfa: f64,
agc: bool,
agc_tau: f64,
bits: Option<u32>,
quant_step: Option<f64>,
no_agc: bool,
) -> PyResult<Bound<'py, PyAny>> {
use crate::iq::frontend::Stage;
let mut samples = samples_from(&i, &q)?;
let bandpass = match (bandpass_lo, bandpass_hi) {
(Some(lo), Some(hi)) => Some((lo, hi)),
(None, None) => None,
_ => {
return Err(PyValueError::new_err(
"bandpass needs both bandpass_lo and bandpass_hi",
))
}
};
let params = FrontendParams {
bandpass,
bandpass_transition_hz: bandpass_transition,
bandpass_atten_db: bandpass_atten,
notch,
notch_r,
notch_mu,
blank,
blank_hold,
excise,
excise_fft,
excise_pfa,
agc,
agc_tau_s: agc_tau,
bits,
quant_step,
no_agc,
};
let mut chain = build_chain(¶ms, fs_hz).map_err(PyValueError::new_err)?;
chain.process(&mut samples);
let oi: Vec<f64> = samples.iter().map(|s| s.re).collect();
let oq: Vec<f64> = samples.iter().map(|s| s.im).collect();
json_to_py(py, &serde_json::json!({ "samples_i": oi, "samples_q": oq }))
}
#[derive(FromPyObject)]
enum TextOrBytes {
Text(String),
Bytes(Vec<u8>),
}
impl TextOrBytes {
fn bytes(&self) -> &[u8] {
match self {
Self::Text(t) => t.as_bytes(),
Self::Bytes(b) => b,
}
}
}
#[pyfunction]
fn receiver_trust_replay<'py>(
py: Python<'py>,
session_toml: &str,
nmea: TextOrBytes,
) -> PyResult<Bound<'py, PyAny>> {
let r = crate::surface::assess_vessel_excerpt(
session_toml,
nmea.bytes(),
crate::surface::MAX_INPUT_BYTES,
)
.map_err(PyValueError::new_err)?;
let v = serde_json::to_value(&r).map_err(|e| PyValueError::new_err(e.to_string()))?;
json_to_py(py, &v)
}
#[pyfunction]
fn assess_vessel_log<'py>(
py: Python<'py>,
session_toml: &str,
log: TextOrBytes,
) -> PyResult<Bound<'py, PyAny>> {
let j = crate::surface::assess_vessel_log_json(
session_toml,
log.bytes(),
crate::surface::MAX_INPUT_BYTES,
)
.map_err(PyValueError::new_err)?;
let v: serde_json::Value =
serde_json::from_str(&j).map_err(|e| PyValueError::new_err(e.to_string()))?;
json_to_py(py, &v)
}
#[pyfunction]
#[pyo3(signature = (session_toml, log, from_s, to_s, title=None, created_utc=None, seed_hex=None))]
#[allow(clippy::too_many_arguments)]
fn evidence_create<'py>(
py: Python<'py>,
session_toml: &str,
log: TextOrBytes,
from_s: f64,
to_s: f64,
title: Option<String>,
created_utc: Option<String>,
seed_hex: Option<String>,
) -> PyResult<Bound<'py, PyAny>> {
use pyo3::types::{PyBytes, PyDict};
let seed = seed_hex
.as_deref()
.map(|h| crate::surface::hex32("seed_hex", h))
.transpose()
.map_err(PyValueError::new_err)?;
let created = match created_utc.as_deref() {
None => Some(crate::surface::now_rfc3339_utc()),
Some("none") => None,
Some(t) => Some(t.to_string()),
};
let p = crate::surface::evidence_create(
session_toml,
log.bytes(),
from_s,
to_s,
title.as_deref(),
created.as_deref(),
seed,
crate::surface::MAX_INPUT_BYTES,
)
.map_err(PyValueError::new_err)?;
let files = PyDict::new(py);
for (k, v) in &p.files {
files.set_item(k, PyBytes::new(py, v))?;
}
let out = PyDict::new(py);
out.set_item("files", files)?;
out.set_item("public_key", p.public_key)?;
out.set_item("seed_hex", hex::encode(p.seed))?;
out.set_item("epochs_in_window", p.epochs_in_window)?;
out.set_item("slice", p.slice.map(|(a, b)| vec![a, b]))?;
Ok(out.into_any())
}
#[pyfunction]
#[pyo3(signature = (files, public_key=None, full_log=None, require_timestamp=false))]
fn evidence_verify<'py>(
py: Python<'py>,
files: std::collections::BTreeMap<String, TextOrBytes>,
public_key: Option<String>,
full_log: Option<TextOrBytes>,
require_timestamp: bool,
) -> PyResult<Bound<'py, PyAny>> {
let files: crate::evidence::Files = files
.into_iter()
.map(|(k, v)| (k, v.bytes().to_vec()))
.collect();
let r = crate::surface::evidence_verify(
&files,
public_key.as_deref(),
full_log.as_ref().map(TextOrBytes::bytes),
require_timestamp,
)
.map_err(PyValueError::new_err)?;
json_to_py(py, &r)
}
#[pyfunction]
#[pyo3(signature = (files, token, replace=false))]
fn evidence_attach_timestamp<'py>(
py: Python<'py>,
files: std::collections::BTreeMap<String, TextOrBytes>,
token: TextOrBytes,
replace: bool,
) -> PyResult<Bound<'py, PyAny>> {
use pyo3::types::{PyBytes, PyDict};
let files: crate::evidence::Files = files
.into_iter()
.map(|(k, v)| (k, v.bytes().to_vec()))
.collect();
let token = match &token {
TextOrBytes::Bytes(b) => b.clone(),
TextOrBytes::Text(t) => crate::permalink::base64_decode(t.trim())
.ok_or_else(|| PyValueError::new_err("token text is not base64"))?,
};
let t = crate::surface::evidence_attach_timestamp(
&files,
&token,
replace,
crate::surface::MAX_INPUT_BYTES,
)
.map_err(PyValueError::new_err)?;
let out_files = PyDict::new(py);
for (k, v) in &t.files {
out_files.set_item(k, PyBytes::new(py, v))?;
}
let out = PyDict::new(py);
out.set_item("files", out_files)?;
out.set_item("notes", t.notes)?;
Ok(out.into_any())
}
#[pyfunction]
fn evidence_keygen<'py>(py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
let k = crate::surface::evidence_keygen();
json_to_py(
py,
&serde_json::json!({
"seed_hex": k.seed_hex,
"public_key": k.public_key,
"fingerprint": k.fingerprint,
"warning": "seed_hex is the PRIVATE signing key. Keep it secret: never log it, paste it into a chat or commit it. Give verifiers only public_key, by a route they trust.",
}),
)
}
#[pyfunction]
#[pyo3(signature = (source, csv, dataset, cell_deg=None, licence=None, licence_url=None, attribution=None, land_geojson=None))]
#[allow(clippy::too_many_arguments)]
fn interference_map<'py>(
py: Python<'py>,
source: &str,
csv: &str,
dataset: &str,
cell_deg: Option<f64>,
licence: Option<String>,
licence_url: Option<String>,
attribution: Option<String>,
land_geojson: Option<String>,
) -> PyResult<Bound<'py, PyAny>> {
use crate::surface::{interference_map as build, CustomDataset, MapSource, MAX_INPUT_BYTES};
let custom = (licence.is_some() || licence_url.is_some() || attribution.is_some()).then(|| {
CustomDataset {
licence: licence.unwrap_or_default(),
licence_url: licence_url.unwrap_or_default(),
attribution: attribution.unwrap_or_default(),
}
});
let days = build(
MapSource::parse(source).map_err(PyValueError::new_err)?,
csv,
dataset,
cell_deg,
custom.as_ref(),
land_geojson.as_deref(),
MAX_INPUT_BYTES,
)
.map_err(PyValueError::new_err)?;
let v = serde_json::Value::Array(
days.into_iter()
.map(|d| {
serde_json::json!({
"file_name": d.file_name,
"date": d.date,
"cells_published": d.cells_published,
"cells_flagged": d.cells_flagged,
"geojson": d.geojson,
})
})
.collect(),
);
json_to_py(py, &v)
}
#[pyfunction]
#[pyo3(signature = (toml, epoch=None))]
fn bench_export<'py>(
py: Python<'py>,
toml: &str,
epoch: Option<&str>,
) -> PyResult<Bound<'py, PyAny>> {
use crate::surface::{BENCH_NOTICE, MAX_INPUT_BYTES};
let e = crate::surface::bench_export(toml, epoch, MAX_INPUT_BYTES)
.map_err(PyValueError::new_err)?;
let files: serde_json::Map<String, serde_json::Value> =
e.files.into_iter().map(|(k, v)| (k, v.into())).collect();
json_to_py(
py,
&serde_json::json!({"files": files, "notes": e.notes, "notice": BENCH_NOTICE}),
)
}
#[pyfunction]
fn export_sp3(toml: &str) -> PyResult<String> {
crate::api::export_sp3(toml).map_err(PyValueError::new_err)
}
#[pyfunction]
fn export_omm(toml: &str) -> PyResult<String> {
crate::api::export_omm(toml).map_err(PyValueError::new_err)
}
#[pyfunction]
fn export_oem(toml: &str) -> PyResult<String> {
crate::api::export_oem(toml).map_err(PyValueError::new_err)
}
#[pyfunction]
fn export_formats<'py>(py: Python<'py>, toml: &str) -> PyResult<Bound<'py, PyAny>> {
let v = crate::surface::export_formats(toml, crate::surface::MAX_INPUT_BYTES)
.map_err(PyValueError::new_err)?;
json_to_py(py, &v)
}
#[pyfunction]
fn export_scenario<'py>(py: Python<'py>, toml: &str, format: &str) -> PyResult<Bound<'py, PyAny>> {
let e = crate::surface::export_scenario(toml, format, crate::surface::MAX_INPUT_BYTES)
.map_err(PyValueError::new_err)?;
json_to_py(py, &e.to_json())
}
#[pyfunction]
fn import_route(toml: &str, geojson: &str) -> PyResult<String> {
crate::surface::import_route(toml, geojson, crate::surface::MAX_INPUT_BYTES)
.map_err(PyValueError::new_err)
}
#[pyfunction]
#[pyo3(signature = (toml, format="svg", fps=None, duration_s=None, width=None))]
fn animate_scenario<'py>(
py: Python<'py>,
toml: &str,
format: &str,
fps: Option<u32>,
duration_s: Option<f64>,
width: Option<u32>,
) -> PyResult<Bound<'py, PyAny>> {
let a = crate::surface::animate_scenario(
toml,
Some(format),
fps,
duration_s,
width,
crate::surface::MAX_INPUT_BYTES,
)
.map_err(PyValueError::new_err)?;
let files: serde_json::Map<String, serde_json::Value> =
a.files.into_iter().map(|(k, v)| (k, v.into())).collect();
json_to_py(
py,
&serde_json::json!({"summary": a.summary, "files": files}),
)
}
#[pyfunction]
#[pyo3(signature = (kind=None))]
fn list_examples<'py>(py: Python<'py>, kind: Option<&str>) -> PyResult<Bound<'py, PyAny>> {
let v = crate::surface::list_examples(kind).map_err(PyValueError::new_err)?;
json_to_py(py, &v)
}
#[pyfunction]
fn get_example(name: &str) -> PyResult<&'static str> {
crate::surface::get_example(name).map_err(PyValueError::new_err)
}
#[pyfunction]
fn compliance_report<'py>(
py: Python<'py>,
runs: Vec<std::collections::BTreeMap<String, String>>,
) -> PyResult<Bound<'py, PyAny>> {
use crate::surface::{ComplianceRunText, MAX_INPUT_BYTES};
let mut texts = Vec::with_capacity(runs.len());
for (i, r) in runs.iter().enumerate() {
let get = |k: &str| r.get(k).cloned();
texts.push(ComplianceRunText {
label: get("label")
.ok_or_else(|| PyValueError::new_err(format!("runs[{i}] needs `label`")))?,
result_json: get("result")
.ok_or_else(|| PyValueError::new_err(format!("runs[{i}] needs `result`")))?,
scenario_toml: get("scenario"),
});
}
let out = crate::surface::compliance_report(&texts, MAX_INPUT_BYTES)
.map_err(PyValueError::new_err)?;
json_to_py(
py,
&serde_json::json!({"report": out.report, "markdown": out.markdown}),
)
}
#[pyfunction]
#[allow(clippy::too_many_arguments)]
#[pyo3(signature = (data, format="", start_gst=None, merkle_root_hex="", public_keys=None, reference_time=None, max_time_error=None))]
fn osnma_verify<'py>(
py: Python<'py>,
data: TextOrBytes,
format: &str,
start_gst: Option<i64>,
merkle_root_hex: &str,
public_keys: Option<Vec<String>>,
reference_time: Option<i64>,
max_time_error: Option<i64>,
) -> PyResult<Bound<'py, PyAny>> {
let to_u32 = |name: &str, v: Option<i64>| -> PyResult<Option<u32>> {
v.map(|n| {
u32::try_from(n).map_err(|_| {
PyValueError::new_err(format!("{name} must be between 0 and 2**32 - 1"))
})
})
.transpose()
};
let (start_gst, reference_time, max_time_error) = (
to_u32("start_gst", start_gst)?,
to_u32("reference_time", reference_time)?,
to_u32("max_time_error", max_time_error)?,
);
let out = crate::surface::osnma_verify(
data.bytes(),
format,
start_gst,
merkle_root_hex,
&public_keys.unwrap_or_default(),
reference_time,
max_time_error,
crate::surface::MAX_INPUT_BYTES,
)
.map_err(PyValueError::new_err)?;
let v: serde_json::Value =
serde_json::from_str(&out).map_err(|e| PyValueError::new_err(e.to_string()))?;
json_to_py(py, &v)
}
#[pyfunction]
#[pyo3(signature = (sources=false))]
fn compliance_mapping(sources: bool) -> String {
crate::surface::compliance_mapping(sources)
}
#[pyfunction]
#[pyo3(signature = (input, format, host="", input_kind="jsonl", expose_position=false, syslog_all=false, unix_nano=0))]
fn trust_telemetry(
input: &str,
format: &str,
host: &str,
input_kind: &str,
expose_position: bool,
syslog_all: bool,
unix_nano: i128,
) -> PyResult<String> {
use crate::surface::{self, TelemetryFormat, TelemetryInput, TelemetryOptions};
let bad = |e: String| PyValueError::new_err(e);
let unix_nano = u64::try_from(unix_nano)
.map_err(|_| PyValueError::new_err("unix_nano must be between 0 and 2**64 - 1"))?;
surface::trust_telemetry(
input,
TelemetryFormat::parse(format).map_err(bad)?,
host,
TelemetryInput::parse(input_kind).map_err(bad)?,
TelemetryOptions {
expose_position,
syslog_all,
unix_nano,
},
surface::MAX_INPUT_BYTES,
)
.map_err(bad)
}
#[pyfunction]
#[pyo3(signature = (route, maps, date_from=None, date_to=None))]
fn route_exposure(
route: &str,
maps: Vec<String>,
date_from: Option<String>,
date_to: Option<String>,
) -> PyResult<String> {
let refs: Vec<&str> = maps.iter().map(String::as_str).collect();
crate::surface::route_exposure(
route,
&refs,
date_from.as_deref(),
date_to.as_deref(),
crate::surface::MAX_INPUT_BYTES,
)
.map_err(PyValueError::new_err)
}
#[pyfunction]
#[pyo3(signature = (toml, seed=None))]
fn nmea_training<'py>(
py: Python<'py>,
toml: &str,
seed: Option<u64>,
) -> PyResult<Bound<'py, PyAny>> {
let t = crate::surface::nmea_training(toml, seed, crate::surface::MAX_INPUT_BYTES)
.map_err(PyValueError::new_err)?;
json_to_py(
py,
&serde_json::json!({"nmea": t.nmea, "log_json": t.log_json, "log_text": t.log_text}),
)
}
#[pyfunction]
fn iq_signals() -> Vec<String> {
crate::iq::cli::signal_names()
.iter()
.map(|s| s.to_string())
.collect()
}
#[pymodule]
fn kshana(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyRunOutput>()?;
m.add_function(wrap_pyfunction!(run, m)?)?;
m.add_function(wrap_pyfunction!(run_full, m)?)?;
m.add_function(wrap_pyfunction!(run_typed, m)?)?;
m.add_function(wrap_pyfunction!(scenario_kinds, m)?)?;
m.add_function(wrap_pyfunction!(validate_toml, m)?)?;
m.add_function(wrap_pyfunction!(list_kinds, m)?)?;
m.add_function(wrap_pyfunction!(error_kind, m)?)?;
m.add_function(wrap_pyfunction!(version, m)?)?;
m.add_function(wrap_pyfunction!(receiver_trust, m)?)?;
m.add_function(wrap_pyfunction!(receiver_trust_replay, m)?)?;
m.add_function(wrap_pyfunction!(assess_vessel_log, m)?)?;
m.add_function(wrap_pyfunction!(evidence_create, m)?)?;
m.add_function(wrap_pyfunction!(evidence_verify, m)?)?;
m.add_function(wrap_pyfunction!(interference_map, m)?)?;
m.add_function(wrap_pyfunction!(compliance_report, m)?)?;
m.add_function(wrap_pyfunction!(bench_export, m)?)?;
m.add_function(wrap_pyfunction!(iq_sweep, m)?)?;
m.add_function(wrap_pyfunction!(iq_info, m)?)?;
m.add_function(wrap_pyfunction!(iq_inventory, m)?)?;
m.add_function(wrap_pyfunction!(evidence_attach_timestamp, m)?)?;
m.add_function(wrap_pyfunction!(evidence_keygen, m)?)?;
m.add_function(wrap_pyfunction!(export_sp3, m)?)?;
m.add_function(wrap_pyfunction!(export_omm, m)?)?;
m.add_function(wrap_pyfunction!(export_oem, m)?)?;
m.add_function(wrap_pyfunction!(export_formats, m)?)?;
m.add_function(wrap_pyfunction!(export_scenario, m)?)?;
m.add_function(wrap_pyfunction!(import_route, m)?)?;
m.add_function(wrap_pyfunction!(animate_scenario, m)?)?;
m.add_function(wrap_pyfunction!(list_examples, m)?)?;
m.add_function(wrap_pyfunction!(get_example, m)?)?;
m.add_function(wrap_pyfunction!(compliance_mapping, m)?)?;
m.add_function(wrap_pyfunction!(osnma_verify, m)?)?;
m.add_function(wrap_pyfunction!(trust_telemetry, m)?)?;
m.add_function(wrap_pyfunction!(route_exposure, m)?)?;
m.add_function(wrap_pyfunction!(nmea_training, m)?)?;
m.add_function(wrap_pyfunction!(iq_scene, m)?)?;
m.add_function(wrap_pyfunction!(iq_scene_broadcast, m)?)?;
m.add_function(wrap_pyfunction!(iq_acquire, m)?)?;
m.add_function(wrap_pyfunction!(iq_acq_surface, m)?)?;
m.add_function(wrap_pyfunction!(iq_track, m)?)?;
m.add_function(wrap_pyfunction!(iq_loop_designs, m)?)?;
m.add_function(wrap_pyfunction!(iq_read_epochs, m)?)?;
m.add_function(wrap_pyfunction!(iq_labfit, m)?)?;
m.add_function(wrap_pyfunction!(iq_frontend, m)?)?;
m.add_function(wrap_pyfunction!(iq_monitor, m)?)?;
m.add_function(wrap_pyfunction!(iq_signals, m)?)?;
m.add_function(wrap_pyfunction!(iq_test_conditions, m)?)?;
m.add_function(wrap_pyfunction!(iq_campaign, m)?)?;
m.add_function(wrap_pyfunction!(iq_campaign_report, m)?)?;
m.add("__version__", env!("CARGO_PKG_VERSION"))?;
Ok(())
}