use super::schema::ParamType;
use super::{Block, Extension, Frame};
use comnum_core::rng::Rng;
use comnum_core::{Error, Result, ensure};
use comnum_mapping::constellation::Constellation;
use comnum_waveform::cpm::{Cpm, CpmConfig, FrequencyPulse, ModulationIndex, msk_bits, msk_precode};
use serde::Deserialize;
use serde_json::Value as Json;
pub(super) fn blocks() -> Vec<Extension> {
vec![
builtin!(
"cpm_tx",
"Continuous-phase modulator of the symbols of a real constellation (CPFSK, MSK, GMSK, raised-cosine CPM): a unit-envelope signal at sps samples per symbol.",
CpmSpec,
needs [Samples, Constellation],
gives [],
|q, _, _| Ok(Box::new(CpmTx(q))),
),
builtin!(
"cpm_rx",
"CPM detector (maximum-likelihood sequence detection over the phase trellis, or one-symbol differential detection) back to the symbols.",
CpmRxSpec,
needs [Samples, Constellation],
gives [],
|q, _, _| Ok(Box::new(CpmRx::new(q))),
),
]
}
block_params! {
struct CpmSpec {
h: String ["1/2"], "modulation index p/q, as a fraction (1/2 with a rectangular pulse of length 1 is MSK)";
pulse: String ["rec"] one_of ["rec", "rc", "gmsk"],
"frequency pulse: rectangular (1REC is CPFSK), raised cosine, or Gaussian (GMSK, with bt)";
length: usize [1] >= 1, "length L of the pulse in symbols";
bt: f64 [0.3] > 0, "bandwidth–time product of the Gaussian pulse";
sps: usize [4] >= 2, "samples per symbol";
precode: bool [false],
"MSK precoding (binary, h 1/2, rec, length 1): the bits are the boundary signs of the equivalent OQPSK signal, and cpm_rx reads them from the phase states of the ML path, with the BER of BPSK";
}
}
impl CpmSpec {
fn cpm(&self, order: usize) -> Result<Cpm> {
let h: ModulationIndex = self.h.parse().map_err(|e: Error| e.context("cpm"))?;
CpmConfig {
order,
h_num: h.num,
h_den: h.den,
pulse: FrequencyPulse::from_name(&self.pulse, self.bt)?,
length: self.length,
sps: self.sps,
}
.build()
}
fn checked(&self, order: usize, precode: bool, block: &str) -> Result<Cpm> {
let cpm = self.cpm(order)?;
let c = cpm.config();
let msk = order == 2 && c.h_num == 1 && c.h_den == 2 && c.pulse == FrequencyPulse::Rectangular && c.length == 1;
ensure(!precode || msk, || format!("{block}: precode needs MSK (BPSK, h: 1/2, pulse: rec, length: 1)"))?;
Ok(cpm)
}
}
fn levels(c: &Constellation, block: &str) -> Result<(Vec<usize>, Vec<usize>)> {
let points = c.points();
ensure(points.iter().all(|p| p.im.abs() < 1e-9), || {
format!("{block}: the constellation must be real (PAM or BPSK), got {:?}", c.kind())
})?;
let mut order: Vec<usize> = (0..points.len()).collect();
order.sort_by(|&a, &b| points[a].re.total_cmp(&points[b].re));
let mut rank = vec![0; points.len()];
for (r, &i) in order.iter().enumerate() {
rank[i] = r;
}
Ok((rank, order))
}
struct CpmTx(CpmSpec);
impl Block for CpmTx {
fn apply(&self, s: &mut Frame, _: &mut Rng) -> Result<()> {
s.require_one_sps("cpm_tx")?;
let c = s.constellation("cpm_tx")?;
let (rank, _) = levels(c, "cpm_tx")?;
let cpm = self.0.checked(c.order(), self.0.precode, "cpm_tx")?;
let mut symbols: Vec<usize> = c.decide(&s.samples).into_iter().map(|i| rank[i]).collect();
if self.0.precode {
let bits: Vec<u8> = symbols.iter().map(|&r| r as u8).collect();
symbols = msk_precode(&bits);
}
s.samples = cpm.modulate(&symbols)?;
s.sps = self.0.sps as f64;
Ok(())
}
}
block_params! {
struct CpmRxSpec {
h: String ["1/2"], "as cpm_tx";
pulse: String ["rec"] one_of ["rec", "rc", "gmsk"], "as cpm_tx";
length: usize [1] >= 1, "as cpm_tx";
bt: f64 [0.3] > 0, "as cpm_tx";
sps: usize [4] >= 2, "as cpm_tx";
detector: CpmDetector ["ml"] one_of ["ml", "differential"],
"ml (Viterbi over the phase trellis, coherent) or differential (sign of the one-symbol phase increment, after a Gaussian low-pass of rx_bt)";
rx_bt: Option<f64> [absent] > 0, "bandwidth–time product of the receive low-pass of the differential detector (none: no filter)";
precode: bool [false], "as cpm_tx (with the ml detector)";
}
}
#[derive(Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
enum CpmDetector {
Ml,
Differential,
}
impl ParamType for CpmDetector {
fn schema() -> Json {
String::schema()
}
}
struct CpmRx {
spec: CpmRxSpec,
differential: bool,
}
impl CpmRx {
fn new(spec: CpmRxSpec) -> CpmRx {
let differential = spec.detector == CpmDetector::Differential;
CpmRx { spec, differential }
}
fn precode(&self) -> bool {
self.spec.precode && !self.differential
}
}
impl Block for CpmRx {
fn apply(&self, s: &mut Frame, _: &mut Rng) -> Result<()> {
let c = s.constellation("cpm_rx")?;
let (_, index_of) = levels(c, "cpm_rx")?;
ensure(s.sps == self.spec.sps as f64, || {
format!("cpm_rx: expects {} samples per symbol (as cpm_tx), the signal has {}", self.spec.sps, s.sps)
})?;
let tx = CpmSpec {
h: self.spec.h.clone(),
pulse: self.spec.pulse.clone(),
length: self.spec.length,
bt: self.spec.bt,
sps: self.spec.sps,
precode: self.spec.precode,
};
let cpm = tx.checked(c.order(), self.precode(), "cpm_rx")?;
let decided = if self.differential {
cpm.differential_detect(&s.samples, self.spec.rx_bt)?
} else if self.precode() {
msk_bits(&cpm.ml_detect_path(&s.samples)?.phase_states).into_iter().map(usize::from).collect()
} else {
cpm.ml_detect(&s.samples)?
};
s.samples = decided.iter().map(|&r| c.points()[index_of[r]]).collect();
s.sps = 1.0;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::super::{BuildContext, build};
use comnum_core::rng::Rng;
use serde_json::json;
#[test]
fn msk_round_trip_is_exact_without_noise() {
let ctx = BuildContext::default();
let mut rng = Rng::new(3);
let mut s = super::Frame::default();
let source = build(&json!({"type": "source", "constellation": "BPSK", "n_symbols": 500}), &ctx).unwrap();
source.apply(&mut s, &mut rng).unwrap();
let tx_symbols = s.samples.clone();
build(&json!({"type": "cpm_tx", "sps": 4}), &ctx).unwrap().apply(&mut s, &mut rng).unwrap();
assert_eq!(s.samples.len(), 2000);
assert!(s.samples.iter().all(|v| (v.norm() - 1.0).abs() < 1e-12), "constant envelope");
build(&json!({"type": "cpm_rx", "sps": 4}), &ctx).unwrap().apply(&mut s, &mut rng).unwrap();
assert_eq!(s.samples, tx_symbols);
}
#[test]
fn a_complex_constellation_is_refused() {
let ctx = BuildContext::default();
let mut rng = Rng::new(3);
let mut s = super::Frame::default();
build(&json!({"type": "source", "constellation": "QPSK", "n_symbols": 100}), &ctx)
.unwrap()
.apply(&mut s, &mut rng)
.unwrap();
let e = build(&json!({"type": "cpm_tx"}), &ctx).unwrap().apply(&mut s, &mut rng).unwrap_err();
assert!(e.to_string().contains("real"), "{e}");
assert!(build(&json!({"type": "cpm_tx", "h": "half"}), &ctx).unwrap().apply(&mut s, &mut rng).is_err());
}
}