1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
// SPDX-License-Identifier: AGPL-3.0-only
//! The receiver side of the `jamming` kind (CAP-06 (c)): the optional `[acquisition]` section
//! (acquisition-versus-tracking hysteresis) and the optional `[front_end]` section (the AGC
//! reading and the ADC's quantiser loss), pre-registered in
//! `docs/design/evidence/jamming-acq-agc/PREREGISTRATION.md`.
//!
//! **Acquisition.** A visible satellite that is not acquired becomes acquired at an epoch
//! where its effective C/Nâ‚€ is at or above `acquisition_threshold_dbhz` and it has been not
//! acquired for at least `reacquisition_delay_s` (a satellite never acquired counts from
//! t = 0). An acquired satellite stays acquired while it is visible and at or above the
//! tracking threshold. The threshold is the whole acquisition model: no search, dwell or
//! false alarm.
//!
//! **Front end.** The AGC holds the ADC input at the loss-minimising level for a Gaussian
//! input ([`crate::iq::frontend::quant::optimum_step_sigma`]). The AGC turns its gain down by
//! `10·log10(1 + J/N)` for a jammer of received power `J` over the noise `kT·B`, and every link
//! loses the quantiser's correlation loss. That loss is exact for noise plus a noise-like
//! (`broadband`) jammer and not valid for CW, narrowband or swept jammers, which the run flags.
use crate::portable_math::PortableFloat;
use serde::{Deserialize, Serialize};
/// The `[acquisition]` section of a `jamming` scenario.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct AcquisitionCfg {
/// Effective C/Nâ‚€ a satellite needs to be (re)acquired (dB-Hz); at or above the tracking
/// threshold.
pub acquisition_threshold_dbhz: f64,
/// Time a satellite must have been not acquired before it can be re-acquired (s).
#[serde(default)]
pub reacquisition_delay_s: f64,
}
impl AcquisitionCfg {
/// Check the section against the scenario's tracking threshold.
pub fn check(&self, tracking_threshold_dbhz: f64) -> Result<(), String> {
if !(self.acquisition_threshold_dbhz.is_finite()
&& self.acquisition_threshold_dbhz >= tracking_threshold_dbhz)
{
return Err(format!(
"acquisition.acquisition_threshold_dbhz ({}) must be finite and at or above \
tracking_threshold_dbhz ({tracking_threshold_dbhz})",
self.acquisition_threshold_dbhz
));
}
if !(self.reacquisition_delay_s.is_finite() && self.reacquisition_delay_s >= 0.0) {
return Err(format!(
"acquisition.reacquisition_delay_s must be finite and non-negative (got {})",
self.reacquisition_delay_s
));
}
Ok(())
}
}
/// One satellite's acquisition state across epochs.
#[derive(Clone, Copy, Debug)]
pub struct AcqState {
acquired: bool,
/// Time of the first epoch of the current not-acquired stretch (s).
not_acquired_since: f64,
}
impl Default for AcqState {
/// Not acquired since t = 0.
fn default() -> Self {
Self {
acquired: false,
not_acquired_since: 0.0,
}
}
}
impl AcqState {
/// Advance to epoch `t` where the satellite is visible with effective C/Nâ‚€ `cn0_dbhz`;
/// returns whether it is acquired at `t`.
pub fn visible(
&mut self,
cfg: &AcquisitionCfg,
tracking_threshold_dbhz: f64,
t: f64,
cn0_dbhz: f64,
) -> bool {
if self.acquired {
if cn0_dbhz < tracking_threshold_dbhz {
self.acquired = false;
self.not_acquired_since = t;
}
} else if cn0_dbhz >= cfg.acquisition_threshold_dbhz
&& t - self.not_acquired_since >= cfg.reacquisition_delay_s
{
self.acquired = true;
}
self.acquired
}
/// Advance to epoch `t` where the satellite is not visible.
pub fn not_visible(&mut self, t: f64) {
if self.acquired {
self.acquired = false;
self.not_acquired_since = t;
}
}
}
/// The `[front_end]` section of a `jamming` scenario.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct FrontEndCfg {
/// ADC resolution (bits), 1 to 16.
pub adc_bits: u32,
/// Front-end noise bandwidth for the AGC's J/N (MHz); default twice the chip rate.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bandwidth_mhz: Option<f64>,
}
impl FrontEndCfg {
/// Check the section's numbers.
pub fn check(&self) -> Result<(), String> {
if !(1..=16).contains(&self.adc_bits) {
return Err(format!(
"front_end.adc_bits must be 1 to 16 (got {})",
self.adc_bits
));
}
if let Some(b) = self.bandwidth_mhz {
if !(b.is_finite() && b > 0.0) {
return Err(format!(
"front_end.bandwidth_mhz must be positive (got {b})"
));
}
}
Ok(())
}
}
/// The `front_end` block of a run with a `[front_end]` section.
#[derive(Clone, Debug, Serialize)]
pub struct FrontEnd {
/// How far the AGC turns its gain down because of the jammer, `10·log10(1 + J/N)` (dB).
pub agc_gain_reduction_db: f64,
/// The ADC's minimum correlation loss for a Gaussian input (dB), taken off every link's
/// effective C/Nâ‚€.
pub quantiser_loss_db: f64,
/// `"gaussian-input"` with no jammer or a `broadband` one;
/// `"gaussian-input-not-valid-for-this-jammer"` otherwise.
pub loss_model: String,
}
/// The front-end block for `cfg`, a jammer of received power `jammer_dbw` (dBW, `None` with no
/// jammer) and type `jammer_type`, thermal noise density `n0_dbw_per_hz` and chip rate
/// `chip_rate_hz`.
pub fn front_end(
cfg: &FrontEndCfg,
jammer: Option<(f64, &str)>,
n0_dbw_per_hz: f64,
chip_rate_hz: f64,
) -> FrontEnd {
let b_hz = cfg.bandwidth_mhz.map_or(2.0 * chip_rate_hz, |b| b * 1e6);
let noise_dbw = n0_dbw_per_hz + 10.0 * b_hz.plog10();
let agc_gain_reduction_db = jammer.map_or(0.0, |(j_dbw, _)| {
10.0 * (1.0 + 10f64.ppowf((j_dbw - noise_dbw) / 10.0)).plog10()
});
let gaussian = jammer.is_none_or(|(_, kind)| kind == "broadband");
FrontEnd {
agc_gain_reduction_db,
quantiser_loss_db: crate::iq::frontend::quant::optimum_step_sigma(cfg.adc_bits).1,
loss_model: if gaussian {
"gaussian-input"
} else {
"gaussian-input-not-valid-for-this-jammer"
}
.into(),
}
}
/// Units of the `[acquisition]` outputs, emitted only with that section.
pub const ACQUISITION_UNITS: &[crate::field_schema::FieldUnit] = {
use crate::field_schema::{FieldUnit, ProvenanceClass::*};
&[
FieldUnit {
path: "epochs[].acquired",
unit: "count",
provenance: Computed,
definition: "visible satellites acquired at this epoch under the acquisition \
section's hysteresis (acquired at or above the acquisition threshold \
after the re-acquisition delay, held while at or above the tracking \
threshold)",
},
FieldUnit {
path: "fom.availability_acquired",
unit: "1",
provenance: Computed,
definition: "fraction of epochs with at least four acquired satellites",
},
]
};
/// Units of the `front_end` block, emitted only with a `[front_end]` section.
pub const FRONT_END_UNITS: &[crate::field_schema::FieldUnit] = {
use crate::field_schema::{FieldUnit, ProvenanceClass::*};
&[
FieldUnit {
path: "front_end.agc_gain_reduction_db",
unit: "dB",
provenance: Computed,
definition: "how far the AGC turns its gain down because of the jammer, \
10 log10(1 + J/N) with J the jammer's received power and N the \
thermal noise power in the front-end bandwidth; 0 with no jammer",
},
FieldUnit {
path: "front_end.quantiser_loss_db",
unit: "dB",
provenance: Computed,
definition: "minimum correlation loss of the ADC for a Gaussian input, taken off \
every link's cn0_effective_dbhz; not valid for a non-Gaussian jammer \
(see loss_model)",
},
]
};
#[cfg(test)]
mod tests {
use super::*;
fn cfg(acq: f64, delay: f64) -> AcquisitionCfg {
AcquisitionCfg {
acquisition_threshold_dbhz: acq,
reacquisition_delay_s: delay,
}
}
#[test]
fn a_satellite_is_held_between_the_thresholds_and_reacquired_after_the_delay() {
let c = cfg(32.0, 60.0);
let mut s = AcqState::default();
// Strong at t = 0 but the delay counts from t = 0: not yet.
assert!(!s.visible(&c, 25.0, 0.0, 40.0));
assert!(s.visible(&c, 25.0, 60.0, 40.0));
// Held between the thresholds, lost below the tracking one.
assert!(s.visible(&c, 25.0, 90.0, 28.0));
assert!(!s.visible(&c, 25.0, 120.0, 24.0));
// Strong again, but only 30 s and then 59 s after the loss.
assert!(!s.visible(&c, 25.0, 150.0, 40.0));
assert!(!s.visible(&c, 25.0, 179.0, 40.0));
assert!(s.visible(&c, 25.0, 180.0, 40.0));
s.not_visible(210.0);
assert!(!s.visible(&c, 25.0, 240.0, 40.0));
}
#[test]
fn a_broadband_jammer_keeps_the_gaussian_loss_model() {
let f = FrontEndCfg {
adc_bits: 2,
bandwidth_mhz: None,
};
let n0 = -204.0;
let noise = n0 + 10.0 * (2.046e6f64).plog10();
let fe = front_end(&f, Some((noise, "broadband")), n0, 1.023e6);
assert!((fe.agc_gain_reduction_db - 10.0 * 2f64.plog10()).abs() < 1e-12);
assert_eq!(fe.loss_model, "gaussian-input");
assert_eq!(
front_end(&f, Some((noise, "cw")), n0, 1.023e6).loss_model,
"gaussian-input-not-valid-for-this-jammer"
);
assert_eq!(front_end(&f, None, n0, 1.023e6).agc_gain_reduction_db, 0.0);
}
}