use super::{Observation, Orient};
use crate::sdr::{self, CaCode, Cf64, CorrelatorDump, TrackConfig};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum IqFormat {
Int8,
Int16Le,
}
impl IqFormat {
pub fn bytes_per_sample(self) -> usize {
match self {
IqFormat::Int8 => 2,
IqFormat::Int16Le => 4,
}
}
}
pub fn load_iq(bytes: &[u8], fmt: IqFormat) -> Vec<Cf64> {
let step = fmt.bytes_per_sample();
let n = bytes.len() / step;
let mut out = Vec::with_capacity(n);
match fmt {
IqFormat::Int8 => {
for c in bytes.chunks_exact(2) {
out.push(Cf64::new(c[0] as i8 as f64, c[1] as i8 as f64));
}
}
IqFormat::Int16Le => {
for c in bytes.chunks_exact(4) {
let i = i16::from_le_bytes([c[0], c[1]]) as f64;
let q = i16::from_le_bytes([c[2], c[3]]) as f64;
out.push(Cf64::new(i, q));
}
}
}
out
}
#[derive(Clone, Copy, Debug)]
pub struct FeatureStageConfig {
pub fs_hz: f64,
pub if_hz: f64,
pub doppler_max_hz: f64,
pub doppler_step_hz: f64,
pub acq_threshold: f64,
pub n_epochs: usize,
pub track: TrackConfig,
}
impl FeatureStageConfig {
pub fn texbat_like() -> Self {
Self {
fs_hz: 25_000_000.0,
if_hz: 0.0,
doppler_max_hz: 6000.0,
doppler_step_hz: 250.0,
acq_threshold: 2.5,
n_epochs: 1000,
track: TrackConfig::default(),
}
}
}
pub fn dumps_for_prn(
iq: &[Cf64],
prn: u8,
cfg: &FeatureStageConfig,
) -> Option<Vec<CorrelatorDump>> {
let code = CaCode::new(prn)?;
let spe = (cfg.fs_hz / 1000.0).round() as usize;
if iq.len() < spe {
return None;
}
let acq = sdr::acquire(
&iq[..spe],
&code,
cfg.fs_hz,
cfg.if_hz,
cfg.doppler_max_hz,
cfg.doppler_step_hz,
cfg.acq_threshold,
);
if !acq.acquired {
return None;
}
Some(sdr::track(
iq,
&code,
&acq,
cfg.fs_hz,
cfg.if_hz,
&cfg.track,
cfg.n_epochs,
))
}
pub fn sqm_observations(dumps: &[CorrelatorDump]) -> Vec<Observation> {
dumps
.iter()
.map(|d| Observation::new("sqm", d.el_imbalance(), Orient::Raw))
.collect()
}
pub fn prompt_power_observations(dumps: &[CorrelatorDump]) -> Vec<Observation> {
dumps
.iter()
.map(|d| {
let p = d.prompt.abs().max(1e-12);
Observation::new("pwr", 20.0 * p.log10(), Orient::Negate)
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn int16_round_trips_through_the_loader() {
let mut bytes = Vec::new();
bytes.extend_from_slice(&1000i16.to_le_bytes());
bytes.extend_from_slice(&(-2000i16).to_le_bytes());
bytes.extend_from_slice(&(-3i16).to_le_bytes());
bytes.extend_from_slice(&4i16.to_le_bytes());
let iq = load_iq(&bytes, IqFormat::Int16Le);
assert_eq!(iq.len(), 2);
assert_eq!(iq[0], Cf64::new(1000.0, -2000.0));
assert_eq!(iq[1], Cf64::new(-3.0, 4.0));
}
#[test]
fn int8_decodes_signed_and_ignores_trailing_partial_sample() {
let bytes = [5u8, 0xFA, 7u8];
let iq = load_iq(&bytes, IqFormat::Int8);
assert_eq!(iq.len(), 1);
assert_eq!(iq[0], Cf64::new(5.0, -6.0)); }
#[test]
fn end_to_end_synthetic_if_acquires_tracks_and_scores_low_sqm() {
let prn = 21;
let code = CaCode::new(prn).unwrap();
let fs = 5_000_000.0;
let if_hz = 50_000.0;
let n_epochs = 30;
let n = (fs / 1000.0) as usize * n_epochs;
let sig = sdr::synth_if(
&code,
fs,
if_hz + 900.0,
sdr::CA_CHIP_RATE_HZ,
256.0,
1.0,
n,
0.05,
3,
);
let mut bytes = Vec::with_capacity(n * 4);
for s in &sig {
let i = (s.re * 4000.0).round().clamp(-32768.0, 32767.0) as i16;
let q = (s.im * 4000.0).round().clamp(-32768.0, 32767.0) as i16;
bytes.extend_from_slice(&i.to_le_bytes());
bytes.extend_from_slice(&q.to_le_bytes());
}
let iq = load_iq(&bytes, IqFormat::Int16Le);
assert_eq!(iq.len(), n);
let cfg = FeatureStageConfig {
fs_hz: fs,
if_hz,
doppler_max_hz: 5000.0,
doppler_step_hz: 250.0,
acq_threshold: 2.0,
n_epochs,
track: TrackConfig::default(),
};
let dumps = dumps_for_prn(&iq, prn, &cfg).expect("clean signal must acquire");
assert_eq!(dumps.len(), n_epochs);
let obs = sqm_observations(&dumps);
let settled = &obs[5..];
let mean: f64 = settled.iter().map(|o| o.score).sum::<f64>() / settled.len() as f64;
assert!(
mean < 0.15,
"clean end-to-end SQM mean {mean:.3} should be low"
);
assert!(obs.iter().all(|o| o.detector == "sqm"));
}
#[test]
fn absent_prn_does_not_acquire() {
let prn_tx = 21;
let prn_absent = 5;
let code = CaCode::new(prn_tx).unwrap();
let fs = 5_000_000.0;
let if_hz = 50_000.0;
let n = (fs / 1000.0) as usize * 5;
let sig = sdr::synth_if(
&code,
fs,
if_hz + 900.0,
sdr::CA_CHIP_RATE_HZ,
256.0,
1.0,
n,
0.05,
3,
);
let mut bytes = Vec::with_capacity(n * 4);
for s in &sig {
let i = (s.re * 4000.0).round().clamp(-32768.0, 32767.0) as i16;
let q = (s.im * 4000.0).round().clamp(-32768.0, 32767.0) as i16;
bytes.extend_from_slice(&i.to_le_bytes());
bytes.extend_from_slice(&q.to_le_bytes());
}
let iq = load_iq(&bytes, IqFormat::Int16Le);
let cfg = FeatureStageConfig {
fs_hz: fs,
if_hz,
doppler_max_hz: 5000.0,
doppler_step_hz: 250.0,
acq_threshold: 3.0,
n_epochs: 5,
track: TrackConfig::default(),
};
assert!(dumps_for_prn(&iq, prn_absent, &cfg).is_none());
}
}