use crate::constants::C_M_S;
use crate::validate;
pub const REFERENCE_CHIP_RATE_HZ: f64 = 1_023_000.0;
pub const BETZ_L1_RECEIVER_BANDWIDTH_HZ: f64 = 24_000_000.0;
const PI: f64 = std::f64::consts::PI;
const TWO_PI: f64 = 2.0 * PI;
const MAX_SUBCHIPS: usize = 4096;
const MAX_WEIGHTED_COMPONENTS: usize = 32;
const MAX_QUADRATURE_PANELS: usize = 32768;
const QUADRATURE_PANEL_HZ: f64 = REFERENCE_CHIP_RATE_HZ / 4.0;
const ROOT_SCAN_STEPS: usize = 96;
const ROOT_BISECTION_STEPS: usize = 48;
const WEIGHT_SUM_TOL: f64 = 1.0e-12;
const INTEGER_RATIO_TOL: f64 = 1.0e-12;
const DEGENERATE_DENOMINATOR: f64 = 1.0e-300;
#[allow(clippy::excessive_precision)]
const GL64_POSITIVE_NODES: [f64; 32] = [
2.43502926634244325e-02,
7.29931217877990424e-02,
1.21462819296120544e-01,
1.69644420423992831e-01,
2.17423643740007083e-01,
2.64687162208767424e-01,
3.11322871990210970e-01,
3.57220158337668126e-01,
4.02270157963991570e-01,
4.46366017253464087e-01,
4.89403145707052956e-01,
5.31279464019894565e-01,
5.71895646202634000e-01,
6.11155355172393278e-01,
6.48965471254657311e-01,
6.85236313054233270e-01,
7.19881850171610771e-01,
7.52819907260531940e-01,
7.83972358943341385e-01,
8.13265315122797539e-01,
8.40629296252580316e-01,
8.65999398154092770e-01,
8.89315445995114140e-01,
9.10522137078502825e-01,
9.29569172131939570e-01,
9.46411374858402765e-01,
9.61008799652053658e-01,
9.73326827789910975e-01,
9.83336253884625977e-01,
9.91013371476744287e-01,
9.96340116771955220e-01,
9.99305041735772170e-01,
];
#[allow(clippy::excessive_precision)]
const GL64_POSITIVE_WEIGHTS: [f64; 32] = [
4.86909570091397237e-02,
4.85754674415033935e-02,
4.83447622348029404e-02,
4.79993885964583034e-02,
4.75401657148303222e-02,
4.69681828162099788e-02,
4.62847965813143539e-02,
4.54916279274180727e-02,
4.45905581637566079e-02,
4.35837245293234157e-02,
4.24735151236535352e-02,
4.12625632426234581e-02,
3.99537411327204536e-02,
3.85501531786155635e-02,
3.70551285402399844e-02,
3.54722132568822401e-02,
3.38051618371418630e-02,
3.20579283548514254e-02,
3.02346570724024884e-02,
2.83396726142594486e-02,
2.63774697150548978e-02,
2.43527025687111306e-02,
2.22701738083829967e-02,
2.01348231535300216e-02,
1.79517157756973571e-02,
1.57260304760250269e-02,
1.34630478967191179e-02,
1.11681394601309738e-02,
8.84675982636339009e-03,
6.50445796897848854e-03,
4.14703326056443250e-03,
1.78328072169469699e-03,
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BocPhasing {
Sine,
Cosine,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CbocSign {
Plus,
Minus,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SignalModulation {
kind: ModulationKind,
label: &'static str,
}
#[derive(Debug, Clone, PartialEq)]
pub struct WeightedComponent {
pub weight: f64,
pub modulation: SignalModulation,
}
#[derive(Debug, Clone, PartialEq)]
pub struct InterferenceTerm {
pub modulation: SignalModulation,
pub power_ratio_to_carrier: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Cn0Degradation {
pub effective_cn0_hz: f64,
pub effective_cn0_db_hz: f64,
pub degradation_db: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DllProcessing {
Coherent,
NonCoherent,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DllTrackingOptions {
pub cn0_db_hz: f64,
pub loop_bandwidth_hz: f64,
pub integration_time_s: f64,
pub correlator_spacing_chips: f64,
pub receiver_bandwidth_hz: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DllJitter {
pub seconds: f64,
pub chips: f64,
pub meters: f64,
pub squaring_loss: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MultipathOptions {
pub multipath_to_direct_ratio: f64,
pub correlator_spacing_chips: f64,
pub receiver_bandwidth_hz: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MultipathEnvelopePoint {
pub delay_chips: f64,
pub delay_s: f64,
pub in_phase_chips: f64,
pub in_phase_s: f64,
pub in_phase_m: f64,
pub anti_phase_chips: f64,
pub anti_phase_s: f64,
pub anti_phase_m: f64,
pub running_average_chips: f64,
pub running_average_s: f64,
pub running_average_m: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub enum SignalAnalysisError {
InvalidInput {
field: &'static str,
reason: &'static str,
},
EmptyComponents,
NoDiscriminatorRoot {
delay_chips: f64,
phase_sign: f64,
},
}
impl core::fmt::Display for SignalAnalysisError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::InvalidInput { field, reason } => {
write!(f, "invalid signal-analysis input {field}: {reason}")
}
Self::EmptyComponents => write!(f, "weighted composite has no components"),
Self::NoDiscriminatorRoot {
delay_chips,
phase_sign,
} => write!(
f,
"no early-late discriminator root for delay {delay_chips} chips and phase sign {phase_sign}"
),
}
}
}
impl std::error::Error for SignalAnalysisError {}
#[derive(Debug, Clone, PartialEq)]
enum ModulationKind {
Pulse(ChipPulse),
Weighted(Vec<WeightedComponent>),
}
#[derive(Debug, Clone, PartialEq)]
struct ChipPulse {
code_rate_hz: f64,
amplitudes: Vec<f64>,
}
struct CorrelationKernel<'a> {
modulation: &'a SignalModulation,
receiver_bandwidth_hz: f64,
power: f64,
}
impl WeightedComponent {
pub fn new(weight: f64, modulation: SignalModulation) -> Self {
Self { weight, modulation }
}
}
impl InterferenceTerm {
pub fn new(modulation: SignalModulation, power_ratio_to_carrier: f64) -> Self {
Self {
modulation,
power_ratio_to_carrier,
}
}
}
impl SignalModulation {
pub fn bpsk(order: f64) -> Result<Self, SignalAnalysisError> {
let order = positive(order, "order")?;
Ok(Self::pulse(
"BPSK(n)",
order * REFERENCE_CHIP_RATE_HZ,
vec![1.0],
))
}
pub fn bpsk1() -> Self {
Self::pulse("BPSK(1)", REFERENCE_CHIP_RATE_HZ, vec![1.0])
}
pub fn boc(m: f64, n: f64, phasing: BocPhasing) -> Result<Self, SignalAnalysisError> {
let m = positive(m, "m")?;
let n = positive(n, "n")?;
let code_rate_hz = n * REFERENCE_CHIP_RATE_HZ;
let amplitudes = match phasing {
BocPhasing::Sine => {
let subchips = integer_ratio(2.0 * m / n, "2m/n")?;
alternating_subchips(subchips)
}
BocPhasing::Cosine => {
let subchips = integer_ratio(4.0 * m / n, "4m/n")?;
cosine_subchips(subchips)
}
};
Ok(Self::pulse("BOC(m,n)", code_rate_hz, amplitudes))
}
pub fn boc_sine(m: f64, n: f64) -> Result<Self, SignalAnalysisError> {
Self::boc(m, n, BocPhasing::Sine)
}
pub fn boc_cosine(m: f64, n: f64) -> Result<Self, SignalAnalysisError> {
Self::boc(m, n, BocPhasing::Cosine)
}
pub fn mboc_6_1_1_over_11() -> Self {
Self::weighted_unchecked(
"MBOC(6,1,1/11)",
vec![
WeightedComponent::new(10.0 / 11.0, Self::boc11()),
WeightedComponent::new(1.0 / 11.0, Self::boc61()),
],
)
}
pub fn tmboc_6_1_4_over_33() -> Self {
Self::weighted_unchecked(
"TMBOC(6,1,4/33)",
vec![
WeightedComponent::new(29.0 / 33.0, Self::boc11()),
WeightedComponent::new(4.0 / 33.0, Self::boc61()),
],
)
}
pub fn cboc_6_1_1_over_11(sign: CbocSign) -> Self {
let sign = match sign {
CbocSign::Plus => 1.0,
CbocSign::Minus => -1.0,
};
let low = libm::sqrt(10.0 / 11.0);
let high = libm::sqrt(1.0 / 11.0);
let mut amplitudes = Vec::with_capacity(12);
for k in 0..12 {
let boc11 = if k < 6 { 1.0 } else { -1.0 };
let boc61 = if k % 2 == 0 { 1.0 } else { -1.0 };
amplitudes.push(low * boc11 + sign * high * boc61);
}
Self::pulse("CBOC(6,1,1/11)", REFERENCE_CHIP_RATE_HZ, amplitudes)
}
pub fn weighted(components: Vec<WeightedComponent>) -> Result<Self, SignalAnalysisError> {
validate_weighted_components(&components)?;
Ok(Self::weighted_unchecked("weighted composite", components))
}
pub fn psd_hz(&self, offset_hz: f64) -> Result<f64, SignalAnalysisError> {
let offset_hz = finite(offset_hz, "offset_hz")?;
match &self.kind {
ModulationKind::Pulse(pulse) => pulse.psd_hz(offset_hz),
ModulationKind::Weighted(components) => {
let mut acc = 0.0;
for component in components {
acc += component.weight * component.modulation.psd_hz(offset_hz)?;
}
finite(acc, "psd_hz")
}
}
}
pub fn code_rate_hz(&self) -> Result<f64, SignalAnalysisError> {
match &self.kind {
ModulationKind::Pulse(pulse) => Ok(pulse.code_rate_hz),
ModulationKind::Weighted(components) => {
let mut iter = components.iter();
let first = iter
.next()
.ok_or(SignalAnalysisError::EmptyComponents)?
.modulation
.code_rate_hz()?;
for component in iter {
let rate = component.modulation.code_rate_hz()?;
if rate.to_bits() != first.to_bits() {
return Err(invalid("code_rate_hz", "mixed component rates"));
}
}
Ok(first)
}
}
}
pub fn label(&self) -> &'static str {
self.label
}
fn boc11() -> Self {
Self::pulse(
"BOCsin(1,1)",
REFERENCE_CHIP_RATE_HZ,
alternating_subchips(2),
)
}
fn boc61() -> Self {
Self::pulse(
"BOCsin(6,1)",
REFERENCE_CHIP_RATE_HZ,
alternating_subchips(12),
)
}
fn pulse(label: &'static str, code_rate_hz: f64, amplitudes: Vec<f64>) -> Self {
Self {
kind: ModulationKind::Pulse(ChipPulse {
code_rate_hz,
amplitudes,
}),
label,
}
}
fn weighted_unchecked(label: &'static str, components: Vec<WeightedComponent>) -> Self {
Self {
kind: ModulationKind::Weighted(components),
label,
}
}
}
impl ChipPulse {
fn psd_hz(&self, offset_hz: f64) -> Result<f64, SignalAnalysisError> {
let (real, imag) = self.transform(offset_hz);
let energy = self.energy_s();
let psd = (real * real + imag * imag) / energy;
finite(psd, "psd_hz")
}
fn transform(&self, offset_hz: f64) -> (f64, f64) {
let chip_s = 1.0 / self.code_rate_hz;
let dt = chip_s / self.amplitudes.len() as f64;
if offset_hz == 0.0 {
let real = self.amplitudes.iter().sum::<f64>() * dt;
return (real, 0.0);
}
let w = TWO_PI * offset_hz;
let mut real = 0.0;
let mut imag = 0.0;
for (idx, amplitude) in self.amplitudes.iter().enumerate() {
let a = idx as f64 * dt;
let b = (idx + 1) as f64 * dt;
real += amplitude * (libm::sin(w * b) - libm::sin(w * a)) / w;
imag += amplitude * (libm::cos(w * b) - libm::cos(w * a)) / w;
}
(real, imag)
}
fn energy_s(&self) -> f64 {
let chip_s = 1.0 / self.code_rate_hz;
let dt = chip_s / self.amplitudes.len() as f64;
self.amplitudes
.iter()
.map(|amplitude| amplitude * amplitude * dt)
.sum()
}
}
impl<'a> CorrelationKernel<'a> {
fn new(
modulation: &'a SignalModulation,
receiver_bandwidth_hz: f64,
) -> Result<Self, SignalAnalysisError> {
let power = power_in_band(modulation, receiver_bandwidth_hz)?;
if power <= 0.0 {
return Err(invalid("receiver_bandwidth_hz", "zero in-band power"));
}
Ok(Self {
modulation,
receiver_bandwidth_hz,
power,
})
}
fn autocorrelation(&self, delay_s: f64) -> Result<f64, SignalAnalysisError> {
let corr = integrate_symmetric(self.receiver_bandwidth_hz, |f| {
let psd = self.modulation.psd_hz(f)?;
finite(psd * libm::cos(TWO_PI * f * delay_s), "autocorrelation")
})?;
finite(corr / self.power, "autocorrelation")
}
}
pub fn power_in_band(
modulation: &SignalModulation,
receiver_bandwidth_hz: f64,
) -> Result<f64, SignalAnalysisError> {
let bandwidth = positive(receiver_bandwidth_hz, "receiver_bandwidth_hz")?;
integrate_symmetric(bandwidth, |f| modulation.psd_hz(f))
}
pub fn fraction_power_in_band(
modulation: &SignalModulation,
receiver_bandwidth_hz: f64,
) -> Result<f64, SignalAnalysisError> {
power_in_band(modulation, receiver_bandwidth_hz)
}
pub fn rms_bandwidth_hz(
modulation: &SignalModulation,
receiver_bandwidth_hz: f64,
) -> Result<f64, SignalAnalysisError> {
let bandwidth = positive(receiver_bandwidth_hz, "receiver_bandwidth_hz")?;
let power = power_in_band(modulation, bandwidth)?;
if power <= 0.0 {
return Err(invalid("receiver_bandwidth_hz", "zero in-band power"));
}
let moment2 = integrate_symmetric(bandwidth, |f| {
let psd = modulation.psd_hz(f)?;
finite(f * f * psd, "rms_bandwidth_hz")
})?;
finite(libm::sqrt(moment2 / power), "rms_bandwidth_hz")
}
pub fn autocorrelation(
modulation: &SignalModulation,
delay_s: f64,
receiver_bandwidth_hz: f64,
) -> Result<f64, SignalAnalysisError> {
let delay_s = finite(delay_s, "delay_s")?;
let bandwidth = positive(receiver_bandwidth_hz, "receiver_bandwidth_hz")?;
let power = power_in_band(modulation, bandwidth)?;
if power <= 0.0 {
return Err(invalid("receiver_bandwidth_hz", "zero in-band power"));
}
let corr = integrate_symmetric(bandwidth, |f| {
let psd = modulation.psd_hz(f)?;
finite(psd * libm::cos(TWO_PI * f * delay_s), "autocorrelation")
})?;
finite(corr / power, "autocorrelation")
}
pub fn spectral_separation_coefficient_hz(
desired: &SignalModulation,
interference: &SignalModulation,
receiver_bandwidth_hz: f64,
) -> Result<f64, SignalAnalysisError> {
let bandwidth = positive(receiver_bandwidth_hz, "receiver_bandwidth_hz")?;
integrate_symmetric(bandwidth, |f| {
let desired_psd = desired.psd_hz(f)?;
let interference_psd = interference.psd_hz(f)?;
finite(
desired_psd * interference_psd,
"spectral_separation_coefficient_hz",
)
})
}
pub fn spectral_separation_coefficient_db_hz(
desired: &SignalModulation,
interference: &SignalModulation,
receiver_bandwidth_hz: f64,
) -> Result<f64, SignalAnalysisError> {
let ssc = spectral_separation_coefficient_hz(desired, interference, receiver_bandwidth_hz)?;
if ssc <= 0.0 {
return Err(invalid(
"spectral_separation_coefficient_hz",
"not positive",
));
}
finite(
10.0 * libm::log10(ssc),
"spectral_separation_coefficient_db_hz",
)
}
pub fn white_noise_spectral_separation_hz(
desired: &SignalModulation,
receiver_bandwidth_hz: f64,
) -> Result<f64, SignalAnalysisError> {
let bandwidth = positive(receiver_bandwidth_hz, "receiver_bandwidth_hz")?;
let power = power_in_band(desired, bandwidth)?;
finite(power / bandwidth, "white_noise_spectral_separation_hz")
}
pub fn effective_cn0_degradation(
desired: &SignalModulation,
cn0_db_hz: f64,
receiver_bandwidth_hz: f64,
interferences: &[InterferenceTerm],
) -> Result<Cn0Degradation, SignalAnalysisError> {
let cn0_db_hz = finite(cn0_db_hz, "cn0_db_hz")?;
let cn0_hz = db_hz_to_hz(cn0_db_hz)?;
let bandwidth = positive(receiver_bandwidth_hz, "receiver_bandwidth_hz")?;
let signal_power = power_in_band(desired, bandwidth)?;
if signal_power <= 0.0 {
return Err(invalid("receiver_bandwidth_hz", "zero in-band power"));
}
let mut inverse_effective = 1.0 / cn0_hz;
for term in interferences {
let ratio = nonnegative(
term.power_ratio_to_carrier,
"interference_power_ratio_to_carrier",
)?;
let ssc =
spectral_separation_coefficient_hz(desired, &term.modulation, receiver_bandwidth_hz)?;
inverse_effective += ratio * ssc / signal_power;
}
if inverse_effective <= 0.0 {
return Err(invalid("effective_cn0_hz", "not positive"));
}
let effective_cn0_hz = 1.0 / inverse_effective;
let effective_cn0_db_hz = hz_to_db_hz(effective_cn0_hz)?;
Ok(Cn0Degradation {
effective_cn0_hz,
effective_cn0_db_hz,
degradation_db: cn0_db_hz - effective_cn0_db_hz,
})
}
pub fn dll_thermal_noise_jitter(
modulation: &SignalModulation,
options: DllTrackingOptions,
processing: DllProcessing,
) -> Result<DllJitter, SignalAnalysisError> {
let code_rate_hz = modulation.code_rate_hz()?;
let cn0_hz = db_hz_to_hz(finite(options.cn0_db_hz, "cn0_db_hz")?)?;
let loop_factor = dll_loop_factor(options.loop_bandwidth_hz, options.integration_time_s)?;
let spacing_chips = positive(options.correlator_spacing_chips, "correlator_spacing_chips")?;
let spacing_s = spacing_chips / code_rate_hz;
let bandwidth = positive(options.receiver_bandwidth_hz, "receiver_bandwidth_hz")?;
let noise_integral = integrate_symmetric(bandwidth, |f| {
let psd = modulation.psd_hz(f)?;
let s = libm::sin(PI * f * spacing_s);
finite(psd * s * s, "dll_noise_integral")
})?;
let discriminator_integral = integrate_symmetric(bandwidth, |f| {
let psd = modulation.psd_hz(f)?;
finite(
f * psd * libm::sin(PI * f * spacing_s),
"dll_discriminator_integral",
)
})?;
if discriminator_integral.abs() <= DEGENERATE_DENOMINATOR {
return Err(invalid(
"correlator_spacing_chips",
"degenerate discriminator",
));
}
let mut variance_s2 = loop_factor * noise_integral
/ ((TWO_PI * TWO_PI) * cn0_hz * discriminator_integral * discriminator_integral);
let squaring_loss = match processing {
DllProcessing::Coherent => 1.0,
DllProcessing::NonCoherent => {
let integration_time_s = positive(options.integration_time_s, "integration_time_s")?;
let cos2_integral = integrate_symmetric(bandwidth, |f| {
let psd = modulation.psd_hz(f)?;
let c = libm::cos(PI * f * spacing_s);
finite(psd * c * c, "dll_squaring_loss")
})?;
let cos_integral = integrate_symmetric(bandwidth, |f| {
let psd = modulation.psd_hz(f)?;
finite(psd * libm::cos(PI * f * spacing_s), "dll_squaring_loss")
})?;
if cos_integral.abs() <= DEGENERATE_DENOMINATOR {
return Err(invalid(
"correlator_spacing_chips",
"degenerate discriminator",
));
}
let loss =
1.0 + cos2_integral / (integration_time_s * cn0_hz * cos_integral * cos_integral);
variance_s2 *= loss;
finite(loss, "dll_squaring_loss")?
}
};
let seconds = finite(libm::sqrt(variance_s2), "dll_thermal_noise_jitter")?;
jitter_from_seconds(seconds, code_rate_hz, squaring_loss)
}
pub fn dll_lower_bound(
modulation: &SignalModulation,
options: DllTrackingOptions,
) -> Result<DllJitter, SignalAnalysisError> {
let code_rate_hz = modulation.code_rate_hz()?;
let cn0_hz = db_hz_to_hz(finite(options.cn0_db_hz, "cn0_db_hz")?)?;
let loop_factor = dll_loop_factor(options.loop_bandwidth_hz, options.integration_time_s)?;
let bandwidth = positive(options.receiver_bandwidth_hz, "receiver_bandwidth_hz")?;
let moment2 = integrate_symmetric(bandwidth, |f| {
let psd = modulation.psd_hz(f)?;
finite(f * f * psd, "dll_lower_bound")
})?;
if moment2 <= 0.0 {
return Err(invalid("receiver_bandwidth_hz", "zero spectral moment"));
}
let variance_s2 = loop_factor / ((TWO_PI * TWO_PI) * cn0_hz * moment2);
let seconds = finite(libm::sqrt(variance_s2), "dll_lower_bound")?;
jitter_from_seconds(seconds, code_rate_hz, 1.0)
}
pub fn multipath_error_envelope(
modulation: &SignalModulation,
options: MultipathOptions,
delay_chips: &[f64],
) -> Result<Vec<MultipathEnvelopePoint>, SignalAnalysisError> {
let code_rate_hz = modulation.code_rate_hz()?;
let amplitude = unit_interval_exclusive_upper(
options.multipath_to_direct_ratio,
"multipath_to_direct_ratio",
)?;
let spacing_chips = positive(options.correlator_spacing_chips, "correlator_spacing_chips")?;
let bandwidth = positive(options.receiver_bandwidth_hz, "receiver_bandwidth_hz")?;
let spacing_s = spacing_chips / code_rate_hz;
let kernel = CorrelationKernel::new(modulation, bandwidth)?;
let mut previous_delay = 0.0;
let mut area_chips = 0.0;
let mut previous_envelope = 0.0;
let mut out = Vec::with_capacity(delay_chips.len());
for (idx, &delay_chips) in delay_chips.iter().enumerate() {
let delay_chips = nonnegative(delay_chips, "delay_chips")?;
if idx > 0 && delay_chips < previous_delay {
return Err(invalid("delay_chips", "not non-decreasing"));
}
let delay_s = delay_chips / code_rate_hz;
let in_phase_s =
multipath_tracking_error_s(&kernel, amplitude, delay_s, delay_chips, spacing_s, 1.0)?;
let anti_phase_s =
multipath_tracking_error_s(&kernel, amplitude, delay_s, delay_chips, spacing_s, -1.0)?;
let in_phase_chips = in_phase_s * code_rate_hz;
let anti_phase_chips = anti_phase_s * code_rate_hz;
let envelope = in_phase_chips.abs().max(anti_phase_chips.abs());
if idx == 0 {
area_chips = 0.0;
} else {
area_chips += 0.5 * (previous_envelope + envelope) * (delay_chips - previous_delay);
}
let running_average_chips = if delay_chips > 0.0 {
area_chips / delay_chips
} else {
envelope
};
out.push(MultipathEnvelopePoint {
delay_chips,
delay_s,
in_phase_chips,
in_phase_s,
in_phase_m: in_phase_s * C_M_S,
anti_phase_chips,
anti_phase_s,
anti_phase_m: anti_phase_s * C_M_S,
running_average_chips,
running_average_s: running_average_chips / code_rate_hz,
running_average_m: running_average_chips / code_rate_hz * C_M_S,
});
previous_delay = delay_chips;
previous_envelope = envelope;
}
Ok(out)
}
fn multipath_tracking_error_s(
kernel: &CorrelationKernel<'_>,
amplitude: f64,
delay_s: f64,
delay_chips: f64,
spacing_s: f64,
phase_sign: f64,
) -> Result<f64, SignalAnalysisError> {
let search_half_width = delay_s + 2.0 * spacing_s + 2.0 / kernel.modulation.code_rate_hz()?;
let left = -search_half_width;
let right = search_half_width;
let step = (right - left) / ROOT_SCAN_STEPS as f64;
let mut best_root = None;
let mut best_abs = f64::INFINITY;
let mut prev_x = left;
let mut prev_y =
early_late_discriminator(kernel, amplitude, delay_s, spacing_s, phase_sign, prev_x)?;
if prev_y == 0.0 {
best_root = Some(prev_x);
best_abs = prev_x.abs();
}
for i in 1..=ROOT_SCAN_STEPS {
let x = left + i as f64 * step;
let y = early_late_discriminator(kernel, amplitude, delay_s, spacing_s, phase_sign, x)?;
if y == 0.0 {
let abs_x = x.abs();
if abs_x < best_abs {
best_root = Some(x);
best_abs = abs_x;
}
} else if (prev_y < 0.0 && y > 0.0) || (prev_y > 0.0 && y < 0.0) {
let root = bisect_root(
|candidate| {
early_late_discriminator(
kernel, amplitude, delay_s, spacing_s, phase_sign, candidate,
)
},
prev_x,
x,
)?;
let abs_root = root.abs();
if abs_root < best_abs {
best_root = Some(root);
best_abs = abs_root;
}
}
prev_x = x;
prev_y = y;
}
best_root.ok_or(SignalAnalysisError::NoDiscriminatorRoot {
delay_chips,
phase_sign,
})
}
fn early_late_discriminator(
kernel: &CorrelationKernel<'_>,
amplitude: f64,
delay_s: f64,
spacing_s: f64,
phase_sign: f64,
error_s: f64,
) -> Result<f64, SignalAnalysisError> {
let half = 0.5 * spacing_s;
let early = kernel.autocorrelation(error_s + half)?
+ phase_sign * amplitude * kernel.autocorrelation(error_s - delay_s + half)?;
let late = kernel.autocorrelation(error_s - half)?
+ phase_sign * amplitude * kernel.autocorrelation(error_s - delay_s - half)?;
finite(early * early - late * late, "early_late_discriminator")
}
fn bisect_root<F>(mut f: F, mut left: f64, mut right: f64) -> Result<f64, SignalAnalysisError>
where
F: FnMut(f64) -> Result<f64, SignalAnalysisError>,
{
let mut f_left = f(left)?;
for _ in 0..ROOT_BISECTION_STEPS {
let mid = 0.5 * (left + right);
let f_mid = f(mid)?;
if f_mid == 0.0 {
return Ok(mid);
}
if (f_left < 0.0 && f_mid > 0.0) || (f_left > 0.0 && f_mid < 0.0) {
right = mid;
} else {
left = mid;
f_left = f_mid;
}
}
Ok(0.5 * (left + right))
}
fn jitter_from_seconds(
seconds: f64,
code_rate_hz: f64,
squaring_loss: f64,
) -> Result<DllJitter, SignalAnalysisError> {
Ok(DllJitter {
seconds,
chips: seconds * code_rate_hz,
meters: seconds * C_M_S,
squaring_loss,
})
}
fn dll_loop_factor(
loop_bandwidth_hz: f64,
integration_time_s: f64,
) -> Result<f64, SignalAnalysisError> {
let loop_bandwidth_hz = positive(loop_bandwidth_hz, "loop_bandwidth_hz")?;
let integration_time_s = positive(integration_time_s, "integration_time_s")?;
let factor = loop_bandwidth_hz * (1.0 - 0.5 * loop_bandwidth_hz * integration_time_s);
if factor <= 0.0 {
return Err(invalid("loop_bandwidth_hz", "loop factor not positive"));
}
finite(factor, "dll_loop_factor")
}
fn integrate_symmetric<F>(bandwidth_hz: f64, f: F) -> Result<f64, SignalAnalysisError>
where
F: FnMut(f64) -> Result<f64, SignalAnalysisError>,
{
let half_band = 0.5 * bandwidth_hz;
let integral = integrate_interval(0.0, half_band, f)?;
finite(2.0 * integral, "quadrature")
}
fn integrate_interval<F>(a: f64, b: f64, mut f: F) -> Result<f64, SignalAnalysisError>
where
F: FnMut(f64) -> Result<f64, SignalAnalysisError>,
{
if b < a {
return Err(invalid("quadrature_interval", "out of range"));
}
if b == a {
return Ok(0.0);
}
let width = b - a;
let panels = panel_count(width)?;
let panel_width = width / panels as f64;
let mut acc = 0.0;
for panel in 0..panels {
let pa = a + panel as f64 * panel_width;
let pb = if panel + 1 == panels {
b
} else {
pa + panel_width
};
acc += integrate_panel(pa, pb, &mut f)?;
}
finite(acc, "quadrature")
}
fn integrate_panel<F>(a: f64, b: f64, f: &mut F) -> Result<f64, SignalAnalysisError>
where
F: FnMut(f64) -> Result<f64, SignalAnalysisError>,
{
let mid = 0.5 * (a + b);
let half = 0.5 * (b - a);
let mut acc = 0.0;
for (&node, &weight) in GL64_POSITIVE_NODES.iter().zip(GL64_POSITIVE_WEIGHTS.iter()) {
acc += weight * (f(mid - half * node)? + f(mid + half * node)?);
}
Ok(half * acc)
}
fn panel_count(width_hz: f64) -> Result<usize, SignalAnalysisError> {
let panels = libm::ceil(width_hz / QUADRATURE_PANEL_HZ).max(1.0);
if panels > MAX_QUADRATURE_PANELS as f64 {
return Err(invalid("receiver_bandwidth_hz", "too wide"));
}
Ok(panels as usize)
}
fn alternating_subchips(subchips: usize) -> Vec<f64> {
(0..subchips)
.map(|idx| if idx % 2 == 0 { 1.0 } else { -1.0 })
.collect()
}
fn cosine_subchips(subchips: usize) -> Vec<f64> {
(0..subchips)
.map(|idx| match idx % 4 {
0 | 3 => 1.0,
_ => -1.0,
})
.collect()
}
fn integer_ratio(value: f64, field: &'static str) -> Result<usize, SignalAnalysisError> {
let value = positive(value, field)?;
let rounded = libm::round(value);
if (value - rounded).abs() > INTEGER_RATIO_TOL {
return Err(invalid(field, "not an integer"));
}
if rounded < 1.0 || rounded > MAX_SUBCHIPS as f64 {
return Err(invalid(field, "out of range"));
}
Ok(rounded as usize)
}
fn validate_weighted_components(
components: &[WeightedComponent],
) -> Result<(), SignalAnalysisError> {
if components.is_empty() {
return Err(SignalAnalysisError::EmptyComponents);
}
if components.len() > MAX_WEIGHTED_COMPONENTS {
return Err(invalid("components", "too many"));
}
let mut sum = 0.0;
for component in components {
sum += nonnegative(component.weight, "component_weight")?;
}
if (sum - 1.0).abs() > WEIGHT_SUM_TOL {
return Err(invalid("component_weight", "weights must sum to one"));
}
Ok(())
}
fn db_hz_to_hz(db_hz: f64) -> Result<f64, SignalAnalysisError> {
let hz = libm::pow(10.0, db_hz / 10.0);
if hz > 0.0 {
finite(hz, "cn0_hz")
} else {
Err(invalid("cn0_db_hz", "out of range"))
}
}
fn hz_to_db_hz(hz: f64) -> Result<f64, SignalAnalysisError> {
let hz = positive(hz, "cn0_hz")?;
finite(10.0 * libm::log10(hz), "cn0_db_hz")
}
fn positive(x: f64, field: &'static str) -> Result<f64, SignalAnalysisError> {
validate::finite_positive(x, field).map_err(map_analysis_input)
}
fn nonnegative(x: f64, field: &'static str) -> Result<f64, SignalAnalysisError> {
validate::finite_nonneg(x, field).map_err(map_analysis_input)
}
fn finite(x: f64, field: &'static str) -> Result<f64, SignalAnalysisError> {
validate::finite(x, field).map_err(map_analysis_input)
}
fn unit_interval_exclusive_upper(x: f64, field: &'static str) -> Result<f64, SignalAnalysisError> {
validate::finite_in_range_exclusive_upper(x, 0.0, 1.0, field).map_err(map_analysis_input)
}
fn map_analysis_input(error: validate::FieldError) -> SignalAnalysisError {
invalid(error.field(), error.reason())
}
fn invalid(field: &'static str, reason: &'static str) -> SignalAnalysisError {
SignalAnalysisError::InvalidInput { field, reason }
}
#[cfg(test)]
mod tests {
use super::*;
const WIDE_BAND_HZ: f64 = 512.0 * REFERENCE_CHIP_RATE_HZ;
const HIGH_COMPONENT_WIDE_BAND_HZ: f64 = 2_048.0 * REFERENCE_CHIP_RATE_HZ;
const COSINE_WIDE_BAND_HZ: f64 = 16_384.0 * REFERENCE_CHIP_RATE_HZ;
const MULTIPATH_BAND_HZ: f64 = 64.0 * REFERENCE_CHIP_RATE_HZ;
#[test]
fn psd_closed_forms_match_pinned_values() {
let bpsk = SignalModulation::bpsk1();
let boc11 = SignalModulation::boc_sine(1.0, 1.0).unwrap();
let boc61 = SignalModulation::boc_sine(6.0, 1.0).unwrap();
let boc_cos_10_5 = SignalModulation::boc_cosine(10.0, 5.0).unwrap();
let cboc_plus = SignalModulation::cboc_6_1_1_over_11(CbocSign::Plus);
let cboc_minus = SignalModulation::cboc_6_1_1_over_11(CbocSign::Minus);
assert_close(
bpsk.psd_hz(0.0).unwrap(),
9.775171065493646e-7,
1.0e-21,
"BPSK(1) PSD at band center",
);
assert_close(
boc11.psd_hz(0.5 * REFERENCE_CHIP_RATE_HZ).unwrap(),
3.9617276106485926e-7,
1.0e-21,
"BOCsin(1,1) PSD at 0.5 chip-rate offset",
);
assert_close(
boc61.psd_hz(5.5 * REFERENCE_CHIP_RATE_HZ).unwrap(),
1.8890394898251426e-7,
1.0e-21,
"BOCsin(6,1) PSD at 5.5 chip-rate offset",
);
assert_close(
boc_cos_10_5.psd_hz(10.0 * REFERENCE_CHIP_RATE_HZ).unwrap(),
7.923455221297186e-8,
1.0e-22,
"BOCcos(10,5) PSD at 10.23 MHz offset",
);
assert_close(
cboc_plus.psd_hz(0.5 * REFERENCE_CHIP_RATE_HZ).unwrap(),
3.907695366836319e-7,
1.0e-20,
"CBOC plus PSD at 0.5 chip-rate offset",
);
assert_close(
cboc_minus.psd_hz(0.5 * REFERENCE_CHIP_RATE_HZ).unwrap(),
3.307930501412689e-7,
1.0e-21,
"CBOC minus PSD at 0.5 chip-rate offset",
);
}
#[test]
fn psds_have_unit_power_to_fixed_wide_band() {
let cases = [
(SignalModulation::bpsk1(), WIDE_BAND_HZ, 1.3e-3),
(
SignalModulation::boc_sine(1.0, 1.0).unwrap(),
WIDE_BAND_HZ,
1.3e-3,
),
(
SignalModulation::boc_cosine(10.0, 5.0).unwrap(),
COSINE_WIDE_BAND_HZ,
2.0e-3,
),
(
SignalModulation::mboc_6_1_1_over_11(),
HIGH_COMPONENT_WIDE_BAND_HZ,
1.0e-3,
),
(
SignalModulation::tmboc_6_1_4_over_33(),
HIGH_COMPONENT_WIDE_BAND_HZ,
1.0e-3,
),
(
SignalModulation::cboc_6_1_1_over_11(CbocSign::Plus),
HIGH_COMPONENT_WIDE_BAND_HZ,
1.0e-3,
),
];
for (modulation, bandwidth_hz, tolerance) in cases {
let power = power_in_band(&modulation, bandwidth_hz).unwrap();
assert!(
(power - 1.0).abs() < tolerance,
"{} power {power}",
modulation.label()
);
}
}
#[test]
fn mboc_and_tmboc_weights_are_pinned_to_public_signal_definitions() {
let f = 5.5 * REFERENCE_CHIP_RATE_HZ;
let boc11 = SignalModulation::boc_sine(1.0, 1.0).unwrap();
let boc61 = SignalModulation::boc_sine(6.0, 1.0).unwrap();
let mboc = SignalModulation::mboc_6_1_1_over_11();
let tmboc = SignalModulation::tmboc_6_1_4_over_33();
let mboc_expected =
10.0 / 11.0 * boc11.psd_hz(f).unwrap() + 1.0 / 11.0 * boc61.psd_hz(f).unwrap();
let tmboc_expected =
29.0 / 33.0 * boc11.psd_hz(f).unwrap() + 4.0 / 33.0 * boc61.psd_hz(f).unwrap();
assert_eq!(mboc.psd_hz(f).unwrap().to_bits(), mboc_expected.to_bits());
assert_eq!(tmboc.psd_hz(f).unwrap().to_bits(), tmboc_expected.to_bits());
}
#[test]
fn ssc_rows_match_betz_l1_table_values() {
let bpsk = SignalModulation::bpsk1();
let boc11 = SignalModulation::boc_sine(1.0, 1.0).unwrap();
let rows = [
(
"BPSK theoretical self, Betz Table 3 note",
&bpsk,
&bpsk,
-61.8,
0.08,
),
(
"C/A desired, BOC(1,1) interference",
&bpsk,
&boc11,
-67.8,
0.12,
),
(
"BOC(1,1) desired, C/A interference",
&boc11,
&bpsk,
-67.9,
0.12,
),
("BOC(1,1) self", &boc11, &boc11, -64.8, 0.12),
];
for (label, desired, interference, expected_db_hz, tolerance_db) in rows {
let got = spectral_separation_coefficient_db_hz(
desired,
interference,
BETZ_L1_RECEIVER_BANDWIDTH_HZ,
)
.unwrap();
assert!(
(got - expected_db_hz).abs() <= tolerance_db,
"{label}: got {got}, want {expected_db_hz}"
);
}
}
#[test]
fn dll_jitter_reduces_to_textbook_bpsk_case() {
let bpsk = SignalModulation::bpsk1();
let options = DllTrackingOptions {
cn0_db_hz: 45.0,
loop_bandwidth_hz: 1.0,
integration_time_s: 0.02,
correlator_spacing_chips: 0.5,
receiver_bandwidth_hz: WIDE_BAND_HZ,
};
let got = dll_thermal_noise_jitter(&bpsk, options, DllProcessing::Coherent).unwrap();
let cn0 = db_hz_to_hz(options.cn0_db_hz).unwrap();
let loop_factor = options.loop_bandwidth_hz
* (1.0 - 0.5 * options.loop_bandwidth_hz * options.integration_time_s);
let expected_chips =
libm::sqrt(loop_factor * options.correlator_spacing_chips / (2.0 * cn0));
assert_close(
got.chips,
expected_chips,
8.0e-5,
"BPSK coherent DLL jitter",
);
let noncoherent =
dll_thermal_noise_jitter(&bpsk, options, DllProcessing::NonCoherent).unwrap();
let expected_loss = 1.0
+ 1.0
/ (options.integration_time_s
* cn0
* (1.0 - 0.5 * options.correlator_spacing_chips));
assert_close(
noncoherent.squaring_loss,
expected_loss,
4.0e-4,
"BPSK noncoherent squaring loss",
);
}
#[test]
fn lower_bound_tracks_rms_bandwidth_definition() {
let boc11 = SignalModulation::boc_sine(1.0, 1.0).unwrap();
let options = DllTrackingOptions {
cn0_db_hz: 45.0,
loop_bandwidth_hz: 1.0,
integration_time_s: 0.02,
correlator_spacing_chips: 0.1,
receiver_bandwidth_hz: BETZ_L1_RECEIVER_BANDWIDTH_HZ,
};
let bound = dll_lower_bound(&boc11, options).unwrap();
let power = power_in_band(&boc11, options.receiver_bandwidth_hz).unwrap();
let beta = rms_bandwidth_hz(&boc11, options.receiver_bandwidth_hz).unwrap();
let cn0 = db_hz_to_hz(options.cn0_db_hz).unwrap();
let loop_factor = options.loop_bandwidth_hz
* (1.0 - 0.5 * options.loop_bandwidth_hz * options.integration_time_s);
let expected_s = libm::sqrt(loop_factor / ((TWO_PI * TWO_PI) * cn0 * beta * beta * power));
assert_close(bound.seconds, expected_s, 1.0e-18, "DLL lower bound");
}
#[test]
fn effective_cn0_degradation_matches_closed_form() {
let desired = SignalModulation::bpsk1();
let interference = SignalModulation::boc_sine(1.0, 1.0).unwrap();
let cn0_db_hz = 45.0;
let ratio = 10.0_f64.powf(-20.0 / 10.0);
let result = effective_cn0_degradation(
&desired,
cn0_db_hz,
BETZ_L1_RECEIVER_BANDWIDTH_HZ,
&[InterferenceTerm::new(interference.clone(), ratio)],
)
.unwrap();
let cn0 = db_hz_to_hz(cn0_db_hz).unwrap();
let signal_power = power_in_band(&desired, BETZ_L1_RECEIVER_BANDWIDTH_HZ).unwrap();
let ssc = spectral_separation_coefficient_hz(
&desired,
&interference,
BETZ_L1_RECEIVER_BANDWIDTH_HZ,
)
.unwrap();
let expected = 1.0 / (1.0 / cn0 + ratio * ssc / signal_power);
assert_close(result.effective_cn0_hz, expected, 1.0e-11, "effective C/N0");
}
#[test]
fn bpsk_multipath_envelope_matches_classic_canonical_case() {
let bpsk = SignalModulation::bpsk1();
let points = multipath_error_envelope(
&bpsk,
MultipathOptions {
multipath_to_direct_ratio: 0.5,
correlator_spacing_chips: 1.0,
receiver_bandwidth_hz: MULTIPATH_BAND_HZ,
},
&[0.0, 0.5, 1.0],
)
.unwrap();
assert_close(
points[1].in_phase_chips,
1.0 / 6.0,
2.5e-3,
"BPSK in-phase multipath envelope at 0.5 chip",
);
assert!(
points[1].anti_phase_chips < 0.0,
"anti-phase envelope should be negative for the canonical delay"
);
assert!(
points[2].running_average_chips > 0.0,
"running average envelope should accumulate"
);
}
#[test]
fn multipath_envelope_rejects_complete_cancellation_case() {
let bpsk = SignalModulation::bpsk1();
let got = multipath_error_envelope(
&bpsk,
MultipathOptions {
multipath_to_direct_ratio: 1.0,
correlator_spacing_chips: 1.0,
receiver_bandwidth_hz: MULTIPATH_BAND_HZ,
},
&[0.0],
);
assert_eq!(
got,
Err(invalid("multipath_to_direct_ratio", "out of range"))
);
}
fn assert_close(got: f64, expected: f64, tolerance: f64, label: &str) {
assert!(
(got - expected).abs() <= tolerance,
"{label}: got {got:e}, want {expected:e}, tolerance {tolerance:e}"
);
}
}