Skip to main content

comnum_waveform/cpm/
detection.rs

1//! The receivers of CPM: the maximum-likelihood sequence detector, the quadrature
2//! detector of MSK and its precoder, the differential detector, and the minimum
3//! distance.
4
5use super::{Cpm, FrequencyPulse};
6use comnum_core::{Error, Result, ensure, ensure_shape};
7use comnum_dsp::filter::{LowPass, fir_filter, low_pass_fir};
8use comnum_math::{C64, cis};
9use std::f64::consts::{PI, TAU};
10
11/// The survivor path of the maximum-likelihood sequence detector.
12#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct MlPath {
14    /// The symbol indices $i_{n}$ ($a_{n} = 2 i_{n} - (M - 1)$).
15    pub symbols: Vec<usize>,
16    /// The terminal phase states $\theta_{n}$ in units of $\pi/q$, modulo $2q$, at the
17    /// start of every interval and after the last one (`symbols.len() + 1` values).
18    pub phase_states: Vec<usize>,
19}
20
21impl Cpm {
22    /// The largest number of difference sequences [`Cpm::min_distance2`] visits.
23    pub const MAX_DIFFERENCE_SEQUENCES: usize = 1 << 20;
24
25    /// The number of whole symbols of a received signal.
26    fn n_symbols(&self, r: &[C64]) -> Result<usize> {
27        let sps = self.cfg.sps;
28        ensure_shape(r.len() >= sps && r.len() % sps == 0, || {
29            format!("{} samples is not a positive multiple of {sps} samples per symbol", r.len())
30        })?;
31        Ok(r.len() / sps)
32    }
33
34    /// The conjugated partial-response waveforms of every combination of a window
35    /// (`combo = i_new + M · past`).
36    fn branch_waveforms(&self) -> Vec<Vec<C64>> {
37        let (m, l, sps) = (self.cfg.order, self.cfg.length, self.cfg.sps);
38        let mut window = vec![0usize; l];
39        (0..m * self.past_combos)
40            .map(|combo| {
41                let mut c = combo;
42                for w in &mut window {
43                    *w = c % m;
44                    c /= m;
45                }
46                (0..sps).map(|t| cis(-self.partial_phase(&window, t))).collect()
47            })
48            .collect()
49    }
50
51    /// The coherent maximum-likelihood sequence detector (Viterbi algorithm over the
52    /// phase trellis): the symbol indices of the most likely sequence, starting from the
53    /// initial state of the modulator and ending in any state.
54    ///
55    /// # Errors
56    /// [`comnum_core::Error::Shape`] unless `r` holds a positive whole number of symbols.
57    ///
58    /// # Examples
59    /// ```
60    /// use comnum_waveform::cpm::{CpmConfig, FrequencyPulse};
61    /// let cpm = CpmConfig { order: 4, h_num: 1, h_den: 4, pulse: FrequencyPulse::RaisedCosine, length: 2, sps: 4 }.build()?;
62    /// let a = vec![0, 3, 1, 2, 2, 0, 1, 3, 3, 0];
63    /// assert_eq!(cpm.ml_detect(&cpm.modulate(&a)?)?, a);
64    /// # Ok::<(), comnum_core::Error>(())
65    /// ```
66    pub fn ml_detect(&self, r: &[C64]) -> Result<Vec<usize>> {
67        Ok(self.ml_detect_path(r)?.symbols)
68    }
69
70    /// The maximum-likelihood sequence with its terminal phase states (see [`MlPath`]).
71    ///
72    /// # Errors
73    /// As [`Cpm::ml_detect`].
74    pub fn ml_detect_path(&self, r: &[C64]) -> Result<MlPath> {
75        let n_sym = self.n_symbols(r)?;
76        let (m, l, sps) = (self.cfg.order, self.cfg.length, self.cfg.sps);
77        let n_phase = self.n_phase_states();
78        let n_states = self.n_states();
79        let pc = self.past_combos;
80        let waves = self.branch_waveforms();
81        let rot: Vec<C64> = (0..n_phase).map(|s| cis(-PI * s as f64 / f64::from(self.cfg.h_den))).collect();
82        // the initial state: phase 0, past symbols +1
83        let past0 = (0..l.saturating_sub(1)).fold(0usize, |acc, _| acc * m + self.one());
84        let mut metric = vec![f64::NEG_INFINITY; n_states];
85        metric[past0] = 0.0;
86        let mut next = vec![f64::NEG_INFINITY; n_states];
87        let mut prev = vec![0u32; n_sym * n_states];
88        let mut sym = vec![0u8; n_sym * n_states];
89        let mut corr = vec![C64::new(0.0, 0.0); waves.len()];
90        for n in 0..n_sym {
91            let seg = &r[n * sps..(n + 1) * sps];
92            for (c, w) in corr.iter_mut().zip(&waves) {
93                *c = seg.iter().zip(w).map(|(a, b)| a * b).sum();
94            }
95            next.fill(f64::NEG_INFINITY);
96            for state in 0..n_states {
97                let acc = metric[state];
98                if acc == f64::NEG_INFINITY {
99                    continue;
100                }
101                let (s, past) = (state / pc, state % pc);
102                for i in 0..m {
103                    let combo = i + m * past;
104                    let branch = acc + (corr[combo] * rot[s]).re;
105                    let leaving = if l == 1 { i } else { past / m.pow((l - 2) as u32) };
106                    let s_next = self.next_phase(s, leaving);
107                    let past_next = if l == 1 { 0 } else { (i + m * past) % pc };
108                    let to = s_next * pc + past_next;
109                    if branch > next[to] {
110                        next[to] = branch;
111                        prev[n * n_states + to] = state as u32;
112                        sym[n * n_states + to] = i as u8;
113                    }
114                }
115            }
116            // keep the metrics bounded over long sequences
117            let best = next.iter().copied().fold(f64::NEG_INFINITY, f64::max);
118            for v in &mut next {
119                *v -= best;
120            }
121            std::mem::swap(&mut metric, &mut next);
122        }
123        let mut state = (0..n_states).max_by(|&a, &b| metric[a].total_cmp(&metric[b])).unwrap_or(0);
124        let mut symbols = vec![0usize; n_sym];
125        let mut phase_states = vec![0usize; n_sym + 1];
126        phase_states[n_sym] = state / pc;
127        for n in (0..n_sym).rev() {
128            symbols[n] = sym[n * n_states + state] as usize;
129            state = prev[n * n_states + state] as usize;
130            phase_states[n] = state / pc;
131        }
132        Ok(MlPath { symbols, phase_states })
133    }
134
135    /// Whether the signal is MSK (binary 1REC with $h = 1/2$).
136    fn ensure_msk(&self) -> Result<()> {
137        let c = &self.cfg;
138        ensure(
139            c.order == 2 && c.length == 1 && c.pulse == FrequencyPulse::Rectangular && c.h_num == 1 && c.h_den == 2,
140            || "the quadrature detector needs MSK (binary 1REC, h = 1/2)".into(),
141        )
142    }
143
144    /// The quadrature detector of MSK: the boundary bits $d_{n}$, $n = 1 \dots N$
145    /// ($d_{n} = +1$ is the bit 1), decided from
146    /// $\operatorname{Re}\left[(-j)^{n} \int_{(n-1)T}^{(n+1)T} r(t) \cos \frac{\pi (t - nT)}{2T} \\, dt\right]$
147    /// (the last window is truncated to the signal). Over AWGN, each decision has the
148    /// error probability $Q(\sqrt{2 E_{b}/N_{0}})$ of antipodal signalling.
149    ///
150    /// # Errors
151    /// [`comnum_core::Error::Config`] unless the signal is MSK, [`comnum_core::Error::Shape`]
152    /// unless it holds a whole number of symbols.
153    pub fn msk_detect(&self, r: &[C64]) -> Result<Vec<u8>> {
154        self.ensure_msk()?;
155        let n_sym = self.n_symbols(r)?;
156        let sps = self.cfg.sps;
157        let (mut rotate, step) = (C64::new(1.0, 0.0), C64::new(0.0, -1.0));
158        let mut bits = Vec::with_capacity(n_sym);
159        for n in 1..=n_sym {
160            rotate *= step;
161            let centre = n * sps;
162            let lo = centre - sps;
163            let hi = (centre + sps).min(r.len());
164            let z: C64 = (lo..hi).map(|t| r[t] * (PI * (t as f64 - centre as f64) / (2.0 * sps as f64)).cos()).sum();
165            bits.push(u8::from((z * rotate).re > 0.0));
166        }
167        Ok(bits)
168    }
169
170    /// Whether the signal is binary (the differential detector).
171    fn ensure_binary(&self) -> Result<()> {
172        ensure(self.cfg.order == 2, || "the differential detector needs a binary signal".into())
173    }
174
175    /// The one-symbol differential phase detector of binary CPM: after an optional
176    /// low-pass predetection filter of normalised 3 dB bandwidth `rx_bt` (`None` for no
177    /// filter; the band-pass bandwidth $B_{i}$ of Simon & Wang, 1984, is twice this
178    /// value, and about $0.5/T$ is the best choice for MSK), the
179    /// signal is sampled every symbol at $t_{n} = (n + (L + 1)/2) T$, where the phase
180    /// increment $\varphi(t_{n}) - \varphi(t_{n-1})$ depends most on $a_{n}$ (for an even
181    /// $L$ and an odd `sps` the instant is rounded down to a sample), and
182    /// $\hat{a}_{n} = \operatorname{sign} \operatorname{Im} (r_{n} r_{n-1}^{\ast})$. A
183    /// symbol whose instant falls after the end of the signal uses its last sample.
184    ///
185    /// For MSK without filter the increment is exactly $\pi a_{n}/2$; the partial
186    /// response of GMSK closes the eye, so the detector degrades with $BT$.
187    ///
188    /// # Errors
189    /// [`comnum_core::Error::Config`] unless the signal is binary or if `rx_bt` is not
190    /// positive, [`comnum_core::Error::Shape`] unless it holds a whole number of symbols.
191    pub fn differential_detect(&self, r: &[C64], rx_bt: Option<f64>) -> Result<Vec<usize>> {
192        self.ensure_binary()?;
193        let n_sym = self.n_symbols(r)?;
194        let (l, sps) = (self.cfg.length, self.cfg.sps);
195        let filtered;
196        let r = match rx_bt {
197            Some(bt) => {
198                ensure(bt > 0.0 && bt.is_finite(), || format!("the receive bandwidth must be positive, got {bt}"))?;
199                let taps = low_pass_fir(bt, sps as f64, 4 * sps + 1, LowPass::Gauss);
200                filtered = fir_filter(&taps, r);
201                &filtered[..]
202            }
203            None => r,
204        };
205        let offset = (l + 1) * sps / 2;
206        let at = |k: usize| r[k.min(r.len() - 1)];
207        let sample = |n: usize| at(n * sps + offset);
208        let mut prev = at(offset - sps);
209        let mut out = Vec::with_capacity(n_sym);
210        for n in 0..n_sym {
211            let cur = sample(n);
212            out.push(usize::from((cur * prev.conj()).im > 0.0));
213            prev = cur;
214        }
215        Ok(out)
216    }
217
218    /// The normalised minimum squared Euclidean distance over the pairs of sequences
219    /// that diverge at $t = 0$ and merge again within `max_len` symbols (the upper bound
220    /// $d_{B}^{2}$ of Anderson, Aulin & Sundberg, ch. 3, which is the true $d_{\min}^{2}$
221    /// when the shortest merge is the closest one, as for MSK):
222    ///
223    /// $$
224    /// d^{2} = \frac{\log_{2} M}{T} \int_{0}^{(N + L - 1) T} \left[1 - \cos \Delta \varphi(t)\right] dt,
225    /// \qquad \Delta \varphi(t) = 2 \pi h \sum_{k=0}^{N-1} \gamma_{k} \\, q(t - kT),
226    /// $$
227    ///
228    /// over the difference sequences $\gamma_{k} = a_{k} - a_{k}^{\prime} \in \\{0, \pm 2,
229    /// \dots, \pm 2(M-1)\\}$ with $\gamma_{0} \ne 0$ and $\pi h \sum_{k} \gamma_{k}
230    /// \equiv 0 \pmod{2 \pi}$ (the merge). The integral is evaluated by Simpson's rule
231    /// with 256 intervals per symbol (within $10^{-9}$ of the closed form $2(1 -
232    /// \operatorname{sinc} 2h)$ of binary CPFSK); it is infinite if no sequence merges
233    /// within `max_len` symbols. With this normalisation antipodal signalling has
234    /// $d^{2} = 2$. The search visits $(2M - 1)^{N}$ difference sequences for `max_len`
235    /// $= N$, at most [`Cpm::MAX_DIFFERENCE_SEQUENCES`]; with `max_len` $= 0$ it visits
236    /// none and returns infinity.
237    ///
238    /// # Errors
239    /// [`Error::Config`] if $(2M - 1)^{N}$ exceeds [`Cpm::MAX_DIFFERENCE_SEQUENCES`] (at
240    /// `max_len` = 20, binary CPM would visit $3.5 \cdot 10^{9}$ sequences).
241    ///
242    /// # Examples
243    /// ```
244    /// use comnum_waveform::cpm::CpmConfig;
245    /// let msk = CpmConfig::msk(4).build()?;
246    /// assert!((msk.min_distance2(3)? - 2.0).abs() < 1e-3);
247    /// assert!(msk.min_distance2(20).is_err());
248    /// # Ok::<(), comnum_core::Error>(())
249    /// ```
250    pub fn min_distance2(&self, max_len: usize) -> Result<f64> {
251        let (m, l) = (self.cfg.order, self.cfg.length);
252        let n_diff = 2 * m - 1;
253        let res = 256usize;
254        let h = self.h();
255        let unit = i64::from(self.cfg.h_den) * 2;
256        let mut best = f64::INFINITY;
257        if max_len == 0 {
258            return Ok(best);
259        }
260        let n_codes = u32::try_from(max_len)
261            .ok()
262            .and_then(|e| n_diff.checked_pow(e))
263            .filter(|&n| n <= Cpm::MAX_DIFFERENCE_SEQUENCES)
264            .ok_or_else(|| {
265                Error::Config(format!(
266                    "{n_diff}^{max_len} difference sequences exceed the {} of the search",
267                    Cpm::MAX_DIFFERENCE_SEQUENCES
268                ))
269            })?;
270        let mut gamma = vec![0i64; max_len];
271        for code in 0..n_codes {
272            let mut c = code;
273            for g in &mut gamma {
274                *g = (c % n_diff) as i64 - (m as i64 - 1);
275                c /= n_diff;
276            }
277            if gamma[0] == 0 {
278                continue;
279            }
280            // the merge: p Σ γ ≡ 0 (mod 2q), γ in units of 2 (the differences are even)
281            let total: i64 = gamma.iter().sum();
282            if (i64::from(self.cfg.h_num) * 2 * total).rem_euclid(unit) != 0 {
283                continue;
284            }
285            let span = max_len + l - 1;
286            let integral = comnum_math::simpson(
287                |t| {
288                    let dphi: f64 = gamma
289                        .iter()
290                        .enumerate()
291                        .map(|(i, &g)| 2.0 * g as f64 * self.cfg.pulse.phase_response(t - i as f64, l))
292                        .sum::<f64>()
293                        * TAU
294                        * h;
295                    1.0 - dphi.cos()
296                },
297                0.0,
298                span as f64,
299                span * res,
300            );
301            best = best.min((m as f64).log2() * integral);
302        }
303        Ok(best)
304    }
305}
306
307/// The differential precoder of MSK: the information bits $b_{1} \dots b_{N}$ give the
308/// boundary signs $d_{n} = 2 b_{n} - 1$ with $d_{0} = +1$, and the CPM symbols
309/// $a_{n} = d_{n} d_{n+1}$ (as indices, $i_{n} = (a_{n} + 1)/2$), so that the signal
310/// equals OQPSK with half-sinusoid pulses carrying $d_{n}$ on the in-phase component at
311/// even $n$ and on the quadrature component at odd $n$ (Proakis & Salehi, §3.3-2).
312///
313/// # Examples
314/// ```
315/// use comnum_waveform::cpm::msk_precode;
316/// assert_eq!(msk_precode(&[1, 1, 0, 0, 1]), vec![1, 1, 0, 1, 0]);
317/// ```
318pub fn msk_precode(bits: &[u8]) -> Vec<usize> {
319    let mut d_prev = 1i8;
320    bits.iter()
321        .map(|&b| {
322            let d = if b & 1 == 1 { 1i8 } else { -1 };
323            let a = d_prev * d;
324            d_prev = d;
325            usize::from(a > 0)
326        })
327        .collect()
328}
329
330/// The information bits of MSK from the terminal phase states of a survivor path
331/// ([`MlPath::phase_states`], in units of $\pi/2$): $d_{n} = \operatorname{Re}
332/// (e^{j \theta_{n}} (-j)^{n})$ for $n = 1 \dots N$, the inverse of [`msk_precode`].
333///
334/// # Examples
335/// ```
336/// use comnum_waveform::cpm::{CpmConfig, msk_bits, msk_precode};
337/// let msk = CpmConfig::msk(4).build()?;
338/// let bits = [1u8, 0, 1, 1, 0, 0, 0, 1];
339/// let path = msk.ml_detect_path(&msk.modulate(&msk_precode(&bits))?)?;
340/// assert_eq!(msk_bits(&path.phase_states), bits);
341/// # Ok::<(), comnum_core::Error>(())
342/// ```
343pub fn msk_bits(phase_states: &[usize]) -> Vec<u8> {
344    phase_states.iter().enumerate().skip(1).map(|(n, &s)| u8::from((s + 4 - n % 4) % 4 == 0)).collect()
345}