use crate::ofdm::{Ofdm, PaprReduction};
use comnum_core::rng::Rng;
use comnum_core::{Error, Result, ensure};
use comnum_dsp::nonlinear::limit_envelope;
use comnum_dsp::signal::map_envelope;
use comnum_mapping::constellation::Constellation;
use comnum_math::{C64, cis, db2amp, db2lin, papr_db};
use std::f64::consts::PI;
pub fn ccdf_nyquist(n: usize, papr_db: f64) -> f64 {
let g = db2lin(papr_db);
1.0 - (1.0 - (-g).exp()).powi(n as i32)
}
pub fn ccdf_oversampled(n: usize, papr_db: f64) -> f64 {
let g = db2lin(papr_db);
(1.0 - (-(n as f64) * (PI * g / 3.0).sqrt() * (-g).exp()).exp()).clamp(0.0, 1.0)
}
pub fn ccdf_slm(n: usize, candidates: usize, papr_db: f64) -> f64 {
ccdf_oversampled(n, papr_db).powi(candidates as i32)
}
pub fn ccdf_slm_nyquist(n: usize, candidates: usize, papr_db: f64) -> f64 {
ccdf_nyquist(n, papr_db).powi(candidates as i32)
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Clip {
pub ratio_db: f64,
}
impl PaprReduction for Clip {
fn reduce(&self, ofdm: &Ofdm, data: &[C64]) -> Result<(Vec<C64>, usize)> {
let mut x = ofdm.to_time(&ofdm.spread(data))?;
limit_envelope(&mut x, db2amp(self.ratio_db));
Ok((x, 0))
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ClipFilter {
pub ratio_db: f64,
pub iterations: usize,
}
impl PaprReduction for ClipFilter {
fn reduce(&self, ofdm: &Ofdm, data: &[C64]) -> Result<(Vec<C64>, usize)> {
let mut x = ofdm.to_time(&ofdm.spread(data))?;
let a = db2amp(self.ratio_db);
for _ in 0..self.iterations {
limit_envelope(&mut x, a);
x = ofdm.filter(&x)?;
}
Ok((x, 0))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Slm {
pub candidates: usize,
pub seed: u64,
}
impl Slm {
pub fn phases(&self, u: usize, k: usize) -> Vec<C64> {
if u == 0 {
return vec![C64::new(1.0, 0.0); k];
}
let mut r = comnum_core::rng::member(self.seed, u as u64);
(0..k).map(|_| cis(PI / 2.0 * r.below(4) as f64)).collect()
}
}
impl PaprReduction for Slm {
fn reduce(&self, ofdm: &Ofdm, data: &[C64]) -> Result<(Vec<C64>, usize)> {
ensure(self.candidates >= 1, || "SLM needs at least one candidate".into())?;
let active = ofdm.spread(data);
let mut best = (f64::INFINITY, Vec::new(), 0);
for u in 0..self.candidates {
let p = self.phases(u, active.len());
let x = ofdm.to_time(&active.iter().zip(&p).map(|(a, b)| a * b).collect::<Vec<_>>())?;
let v = papr_db(&x);
if v < best.0 {
best = (v, x, u);
}
}
Ok((best.1, best.2))
}
fn recover(&self, ofdm: &Ofdm, active: Vec<C64>, side: usize) -> Result<Vec<C64>> {
ensure(side < self.candidates, || format!("SLM side information {side} out of {}", self.candidates))?;
let p = self.phases(side, active.len());
Ok(ofdm.despread(&active.iter().zip(&p).map(|(a, b)| a * b.conj()).collect::<Vec<_>>()))
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Partition {
Adjacent,
Interleaved,
Pseudorandom(u64),
}
impl Partition {
pub fn from_name(name: &str, seed: u64) -> Result<Partition> {
match name.to_ascii_lowercase().as_str() {
"adjacent" => Ok(Partition::Adjacent),
"interleaved" => Ok(Partition::Interleaved),
"pseudorandom" => Ok(Partition::Pseudorandom(seed)),
other => Err(Error::Config(format!("unknown partition {other:?}: adjacent, interleaved or pseudorandom"))),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Pts {
pub subblocks: usize,
pub phases: usize,
pub partition: Partition,
}
impl Pts {
pub fn assignment(&self, k: usize) -> Vec<usize> {
let v = self.subblocks.max(1);
match self.partition {
Partition::Adjacent => (0..k).map(|i| i * v / k).collect(),
Partition::Interleaved => (0..k).map(|i| i % v).collect(),
Partition::Pseudorandom(seed) => {
let mut a: Vec<usize> = (0..k).map(|i| i % v).collect();
Rng::new(seed).shuffle(&mut a);
a
}
}
}
pub fn factors(&self, index: usize) -> Vec<C64> {
let mut f = vec![C64::new(1.0, 0.0)];
let mut rest = index;
for _ in 1..self.subblocks {
f.push(cis(2.0 * PI * (rest % self.phases) as f64 / self.phases as f64));
rest /= self.phases;
}
f
}
fn combinations(&self) -> Result<usize> {
ensure(self.subblocks >= 1 && self.phases >= 1, || "PTS needs subblocks and phases".into())?;
let n = (1..self.subblocks).try_fold(1usize, |acc, _| acc.checked_mul(self.phases));
n.filter(|n| *n <= 1 << 20).ok_or_else(|| Error::Config("too many PTS combinations".into()))
}
}
impl PaprReduction for Pts {
fn reduce(&self, ofdm: &Ofdm, data: &[C64]) -> Result<(Vec<C64>, usize)> {
let combinations = self.combinations()?;
let active = ofdm.spread(data);
let assign = self.assignment(active.len());
let parts = (0..self.subblocks)
.map(|v| {
let masked: Vec<C64> =
active.iter().zip(&assign).map(|(a, &b)| if b == v { *a } else { C64::new(0.0, 0.0) }).collect();
ofdm.to_time(&masked)
})
.collect::<Result<Vec<_>>>()?;
let mut best = (f64::INFINITY, 0usize);
let mut x = vec![C64::new(0.0, 0.0); ofdm.fft_len()];
for index in 0..combinations {
let f = self.factors(index);
for (n, v) in x.iter_mut().enumerate() {
*v = parts.iter().zip(&f).map(|(p, b)| p[n] * b).sum();
}
let p = papr_db(&x);
if p < best.0 {
best = (p, index);
}
}
let f = self.factors(best.1);
let x = (0..ofdm.fft_len()).map(|n| parts.iter().zip(&f).map(|(p, b)| p[n] * b).sum()).collect();
Ok((x, best.1))
}
fn recover(&self, ofdm: &Ofdm, active: Vec<C64>, side: usize) -> Result<Vec<C64>> {
ensure(side < self.combinations()?, || format!("PTS side information {side} out of range"))?;
let f = self.factors(side);
let assign = self.assignment(active.len());
Ok(ofdm.despread(&active.iter().zip(&assign).map(|(a, &v)| a * f[v].conj()).collect::<Vec<_>>()))
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct ToneReservation {
pub reserved: Vec<usize>,
pub ratio_db: f64,
pub iterations: usize,
pub step: f64,
}
impl ToneReservation {
pub fn random(n_active: usize, count: usize, seed: u64, ratio_db: f64) -> ToneReservation {
let mut idx: Vec<usize> = (0..n_active).collect();
Rng::new(seed).shuffle(&mut idx);
let mut reserved: Vec<usize> = idx.into_iter().take(count.min(n_active)).collect();
reserved.sort_unstable();
ToneReservation { reserved, ratio_db, iterations: 20, step: 1.0 }
}
fn mask(&self, ofdm: &Ofdm) -> Result<Vec<bool>> {
ensure(!ofdm.dft_spread, || "tone reservation needs plain OFDM".into())?;
let mut m = vec![false; ofdm.n_active];
for &i in &self.reserved {
ensure(i < ofdm.n_active, || format!("reserved tone {i} is not an active subcarrier"))?;
m[i] = true;
}
Ok(m)
}
}
impl PaprReduction for ToneReservation {
fn data_len(&self, ofdm: &Ofdm) -> usize {
ofdm.n_active.saturating_sub(self.reserved.len())
}
fn reduce(&self, ofdm: &Ofdm, data: &[C64]) -> Result<(Vec<C64>, usize)> {
let mask = self.mask(ofdm)?;
let mut it = data.iter();
let active: Vec<C64> = mask
.iter()
.map(|&r| if r { C64::new(0.0, 0.0) } else { *it.next().unwrap_or(&C64::new(0.0, 0.0)) })
.collect();
let mut x = ofdm.to_time(&active)?;
let a = db2amp(self.ratio_db);
for _ in 0..self.iterations {
let mut clipped = x.clone();
limit_envelope(&mut clipped, a);
if clipped.iter().zip(&x).all(|(c, v)| c == v) {
break;
}
let noise: Vec<C64> = clipped.iter().zip(&x).map(|(c, v)| c - v).collect();
let projected: Vec<C64> = ofdm
.to_active(&noise)?
.into_iter()
.zip(&mask)
.map(|(v, &r)| if r { v } else { C64::new(0.0, 0.0) })
.collect();
for (v, c) in x.iter_mut().zip(ofdm.to_time(&projected)?) {
*v += c * self.step;
}
}
Ok((x, 0))
}
fn recover(&self, ofdm: &Ofdm, active: Vec<C64>, _side: usize) -> Result<Vec<C64>> {
let mask = self.mask(ofdm)?;
Ok(active.into_iter().zip(&mask).filter(|(_, r)| !**r).map(|(v, _)| v).collect())
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Ace {
pub constellation: Constellation,
pub ratio_db: f64,
pub iterations: usize,
pub step: f64,
}
impl PaprReduction for Ace {
fn reduce(&self, ofdm: &Ofdm, data: &[C64]) -> Result<(Vec<C64>, usize)> {
ensure(!ofdm.dft_spread, || "active constellation extension needs plain OFDM".into())?;
let pts = self.constellation.points();
let max_re = pts.iter().map(|p| p.re.abs()).fold(0.0, f64::max);
let max_im = pts.iter().map(|p| p.im.abs()).fold(0.0, f64::max);
let tol = 1e-9 * max_re.max(max_im).max(1e-300);
let original: Vec<C64> = data.iter().map(|d| pts[self.constellation.nearest(*d)]).collect();
let mut active = data.to_vec();
let mut x = ofdm.to_time(&active)?;
let a = db2amp(self.ratio_db);
for _ in 0..self.iterations {
let mut clipped = x.clone();
limit_envelope(&mut clipped, a);
let noise: Vec<C64> = clipped.iter().zip(&x).map(|(c, v)| c - v).collect();
if noise.iter().all(|v| v.norm_sqr() == 0.0) {
break;
}
for ((v, c), p) in active.iter_mut().zip(ofdm.to_active(&noise)?).zip(&original) {
let re = if p.re.abs() >= max_re - tol && c.re * p.re > 0.0 { c.re } else { 0.0 };
let im = if p.im.abs() >= max_im - tol && c.im * p.im > 0.0 { c.im } else { 0.0 };
*v += C64::new(re, im) * self.step;
}
x = ofdm.to_time(&active)?;
}
Ok((x, 0))
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Companding {
MuLaw {
mu: f64,
peak_db: f64,
},
Exponential {
degree: f64,
},
}
fn mu_law_nodes() -> &'static (Vec<f64>, Vec<f64>) {
static NODES: std::sync::OnceLock<(Vec<f64>, Vec<f64>)> = std::sync::OnceLock::new();
NODES.get_or_init(|| comnum_math::gauss_legendre(64))
}
#[derive(Clone, Copy, Debug)]
enum Law {
MuLaw { mu: f64, v: f64, gain: f64 },
Exponential { degree: f64, alpha: f64 },
}
impl Law {
fn compress(&self, r: f64) -> f64 {
match *self {
Law::MuLaw { mu, v, gain } => gain * v * (1.0 + mu * r / v).ln() / (1.0 + mu).ln(),
Law::Exponential { degree: d, alpha } => (alpha * (1.0 - (-r * r).exp())).powf(1.0 / d),
}
}
fn expand(&self, r: f64) -> f64 {
match *self {
Law::MuLaw { mu, v, gain } => {
let c = r / gain;
v / mu * ((1.0 + mu).powf(c / v) - 1.0)
}
Law::Exponential { degree: d, alpha } => {
let u = (1.0 - r.powf(d) / alpha).max(1e-16);
(-u.ln()).sqrt()
}
}
}
}
impl Companding {
fn mu_law_gain(mu: f64, v: f64) -> f64 {
let a = mu / v;
let half = (a * 7.0).ln_1p() / 2.0;
let (x, w) = mu_law_nodes();
let integral = x
.iter()
.zip(w)
.map(|(x, w)| {
let s = (x + 1.0) * half;
let r = s.exp_m1() / a;
w * 2.0 * r * (-r * r).exp() * s * s * s.exp() / a
})
.sum::<f64>()
* half;
let k = v / (1.0 + mu).ln();
1.0 / (k * k * integral).sqrt()
}
fn law(&self) -> Law {
match *self {
Companding::MuLaw { mu, peak_db } => {
let v = db2amp(peak_db);
Law::MuLaw { mu, v, gain: Companding::mu_law_gain(mu, v) }
}
Companding::Exponential { degree } => {
Law::Exponential { degree, alpha: ((degree + 2.0) / degree).powf(degree / 2.0) }
}
}
}
pub fn compress(&self, r: f64) -> f64 {
self.law().compress(r)
}
pub fn expand(&self, r: f64) -> f64 {
self.law().expand(r)
}
fn apply(x: &mut [C64], f: impl Fn(f64) -> f64) {
map_envelope(x, |r| C64::new(f(r) / r, 0.0));
}
}
impl PaprReduction for Companding {
fn reduce(&self, ofdm: &Ofdm, data: &[C64]) -> Result<(Vec<C64>, usize)> {
if let Companding::MuLaw { mu, .. } = self {
ensure(*mu > 0.0, || "μ must be positive".into())?;
}
if let Companding::Exponential { degree } = self {
ensure(*degree > 0.0, || "the degree must be positive".into())?;
}
let mut x = ofdm.to_time(&ofdm.spread(data))?;
let law = self.law();
Companding::apply(&mut x, |r| law.compress(r));
Ok((x, 0))
}
fn restore_time(&self, block: &mut [C64]) {
let law = self.law();
Companding::apply(block, |r| law.expand(r));
}
}
#[cfg(test)]
mod tests {
use super::*;
use comnum_math::power;
fn qam_data(c: &Constellation, n: usize, seed: u64) -> Vec<C64> {
let mut r = Rng::new(seed);
(0..n).map(|_| c.points()[r.below(c.order())]).collect()
}
fn papr_95(ofdm: &Ofdm, red: &dyn PaprReduction, data: &[C64]) -> f64 {
let k = red.data_len(ofdm);
let blocks = data.len() / k;
let (x, _) = ofdm.modulate_with(&data[..blocks * k], red).unwrap();
let mut p: Vec<f64> = x.chunks(ofdm.block_len()).map(papr_db).collect();
p.sort_by(f64::total_cmp);
p[(0.95 * p.len() as f64) as usize]
}
#[test]
fn the_ccdf_of_ofdm_follows_ochiai_and_imai() {
let ofdm = Ofdm::new(256, 256).unwrap();
let c = Constellation::qam(4).unwrap().normalized();
let blocks = 4000;
let x = ofdm.modulate(&qam_data(&c, 256 * blocks, 1)).unwrap();
let p: Vec<f64> = x.chunks(ofdm.block_len()).map(papr_db).collect();
for g in [8.0, 9.0, 10.0] {
let emp = p.iter().filter(|v| **v > g).count() as f64 / p.len() as f64;
let th = ccdf_oversampled(256, g);
assert!((emp / th - 1.0).abs() < 0.25, "{g} dB: {emp} vs {th}");
assert!(ccdf_nyquist(256, g) < th);
}
}
#[test]
fn bussgang_matches_a_monte_carlo() {
let mut r = Rng::new(2);
let x: Vec<C64> = (0..400_000).map(|_| r.complex_gaussian()).collect();
for cr in [0.0, 3.0, 6.0] {
let mut y = x.clone();
limit_envelope(&mut y, db2amp(cr));
let alpha: C64 = y.iter().zip(&x).map(|(a, b)| a * b.conj()).sum::<C64>() / power(&x) / x.len() as f64;
let th = comnum_dsp::nonlinear::clipping_bussgang(cr);
assert!((alpha - th.gain).norm() < 3e-3, "{cr} dB: {alpha} vs {}", th.gain);
assert!((power(&y) - th.output_power).abs() < 3e-3);
let d: f64 = y.iter().zip(&x).map(|(a, b)| (a - b * th.gain).norm_sqr()).sum::<f64>() / x.len() as f64;
assert!((d / th.distortion - 1.0).abs() < 0.03);
}
}
#[test]
fn every_technique_reduces_the_papr_and_lossless_ones_recover_the_data() {
let ofdm = Ofdm::new(128, 128).unwrap();
let c = Constellation::qam(16).unwrap().normalized();
let data = qam_data(&c, 128 * 200, 3);
let plain = papr_95(&ofdm, &crate::ofdm::NoReduction, &data);
let techniques: Vec<(&str, Box<dyn PaprReduction>, bool)> = vec![
("clip", Box::new(Clip { ratio_db: 4.0 }), false),
("icf", Box::new(ClipFilter { ratio_db: 4.0, iterations: 4 }), false),
("slm", Box::new(Slm { candidates: 8, seed: 4 }), true),
("pts", Box::new(Pts { subblocks: 4, phases: 4, partition: Partition::Pseudorandom(5) }), true),
("tr", Box::new(ToneReservation::random(128, 12, 6, 4.0)), true),
("ace", Box::new(Ace { constellation: c.clone(), ratio_db: 4.0, iterations: 10, step: 1.0 }), false),
("exp", Box::new(Companding::Exponential { degree: 2.0 }), true),
("mu", Box::new(Companding::MuLaw { mu: 4.0, peak_db: 9.0 }), true),
];
for (name, t, lossless) in &techniques {
let k = t.data_len(&ofdm);
let d = &data[..(data.len() / k) * k];
let reduced = papr_95(&ofdm, t.as_ref(), d);
assert!(reduced < plain - 1.0, "{name}: {reduced} vs {plain}");
let (x, side) = ofdm.modulate_with(d, t.as_ref()).unwrap();
let back = ofdm.demodulate_with(&x, t.as_ref(), &side).unwrap();
if *lossless {
assert!(back.iter().zip(d).all(|(a, b)| (a - b).norm() < 1e-6), "{name}");
} else {
assert!(
back.iter().zip(d).filter(|(a, b)| c.nearest(**a) != c.nearest(**b)).count() < d.len() / 50,
"{name}"
);
}
}
let (x, _) = ofdm.modulate_with(&data, &Companding::Exponential { degree: 2.0 }).unwrap();
assert!(papr_db(&x) <= comnum_math::lin2db(2.0) + 0.3);
}
fn mu_law_compress_simpson(mu: f64, peak_db: f64, r: f64) -> f64 {
let v = db2amp(peak_db);
let c = |t: f64| v * (1.0 + mu * t.sqrt() / v).ln() / (1.0 + mu).ln();
let gain = 1.0 / comnum_math::simpson(|t| c(t).powi(2) * (-t).exp(), 0.0, 60.0, 6000).sqrt();
gain * v * (1.0 + mu * r / v).ln() / (1.0 + mu).ln()
}
#[test]
fn the_mu_law_gain_is_the_integral_and_follows_the_former_simpson_rule() {
for (mu, peak_db, power, simpson_error) in [
(4.0, 9.0, 2.089_224_506_453_305, 3.5e-7),
(255.0, 0.0, 0.912_706_126_294_248_1, 2.6e-4),
(255.0, 12.0, 8.026_021_096_858_328, 1.1e-4),
(100.0, 3.0, 1.531_119_965_389_956, 1.2e-4),
(10.0, 6.0, 1.910_143_981_626_004_6, 3e-6),
] {
let gain = Companding::mu_law_gain(mu, db2amp(peak_db));
let e = 1.0 / (gain * gain);
assert!((e / power - 1.0).abs() < 1e-13, "μ {mu}, {peak_db} dB: {e} vs {power}");
let law = Companding::MuLaw { mu, peak_db };
for r in [0.01, 0.3, 1.0, 2.0, 4.5] {
let (new, old) = (law.compress(r), mu_law_compress_simpson(mu, peak_db, r));
assert!((new / old - 1.0).abs() < simpson_error, "μ {mu}, {peak_db} dB, r {r}: {new} vs {old}");
assert!((law.expand(new) / r - 1.0).abs() < 1e-12);
}
}
let law = Companding::MuLaw { mu: 255.0, peak_db: 0.0 }.law();
let mut rng = Rng::new(11);
let n = 400_000;
let p = (0..n).map(|_| law.compress(rng.complex_gaussian().norm()).powi(2)).sum::<f64>() / f64::from(n);
assert!((p - 1.0).abs() < 5e-3, "{p}");
}
#[test]
fn ace_never_moves_a_point_across_a_decision_boundary() {
let ofdm = Ofdm::new(64, 64).unwrap();
let c = Constellation::qam(16).unwrap().normalized();
let data = qam_data(&c, 64 * 50, 7);
let ace = Ace { constellation: c.clone(), ratio_db: 3.0, iterations: 20, step: 1.0 };
let (x, side) = ofdm.modulate_with(&data, &ace).unwrap();
let back = ofdm.demodulate_with(&x, &ace, &side).unwrap();
assert_eq!(c.decide(&back), c.decide(&data));
}
#[test]
fn dft_spread_ofdm_has_a_lower_papr() {
let c = Constellation::qam(4).unwrap().normalized();
let data = qam_data(&c, 64 * 300, 8);
let ofdm = Ofdm::new(256, 64).unwrap();
let sc = Ofdm { dft_spread: true, ..ofdm.clone() };
let p0 = papr_95(&ofdm, &crate::ofdm::NoReduction, &data);
let p1 = papr_95(&sc, &crate::ofdm::NoReduction, &data);
assert!(p1 < p0 - 2.0, "{p1} vs {p0}");
}
}
#[cfg(test)]
mod literature {
use super::*;
#[test]
fn ochiai_2001_eqs_11_and_27() {
let lambda = db2lin(10.0);
let nyquist = 1.0 - (1.0 - (-lambda).exp()).powi(256);
assert!((ccdf_nyquist(256, 10.0) / nyquist - 1.0).abs() < 1e-12);
let r = lambda.sqrt();
let crest = (-(PI / 3.0).sqrt() * 256.0 * r * (-r * r).exp()).exp();
assert!((ccdf_oversampled(256, 10.0) / (1.0 - crest) - 1.0).abs() < 1e-12);
let p15 = ccdf_oversampled(100_000, 15.0);
assert!(p15 > 3e-9 && p15 < 3e-8, "{p15:e}");
}
}