1use crate::ofdm::{Ofdm, PaprReduction};
92use comnum_core::rng::Rng;
93use comnum_core::{Error, Result, ensure};
94use comnum_dsp::nonlinear::limit_envelope;
95use comnum_dsp::signal::map_envelope;
96use comnum_mapping::constellation::Constellation;
97use comnum_math::{C64, cis, db2amp, db2lin, papr_db};
98use std::f64::consts::PI;
99
100pub fn ccdf_nyquist(n: usize, papr_db: f64) -> f64 {
105 let g = db2lin(papr_db);
106 1.0 - (1.0 - (-g).exp()).powi(n as i32)
107}
108
109pub fn ccdf_oversampled(n: usize, papr_db: f64) -> f64 {
114 let g = db2lin(papr_db);
115 (1.0 - (-(n as f64) * (PI * g / 3.0).sqrt() * (-g).exp()).exp()).clamp(0.0, 1.0)
116}
117
118pub fn ccdf_slm(n: usize, candidates: usize, papr_db: f64) -> f64 {
121 ccdf_oversampled(n, papr_db).powi(candidates as i32)
122}
123
124pub fn ccdf_slm_nyquist(n: usize, candidates: usize, papr_db: f64) -> f64 {
134 ccdf_nyquist(n, papr_db).powi(candidates as i32)
135}
136
137#[derive(Clone, Copy, Debug, PartialEq)]
142pub struct Clip {
143 pub ratio_db: f64,
145}
146
147impl PaprReduction for Clip {
148 fn reduce(&self, ofdm: &Ofdm, data: &[C64]) -> Result<(Vec<C64>, usize)> {
149 let mut x = ofdm.to_time(&ofdm.spread(data))?;
150 limit_envelope(&mut x, db2amp(self.ratio_db));
151 Ok((x, 0))
152 }
153}
154
155#[derive(Clone, Copy, Debug, PartialEq)]
159pub struct ClipFilter {
160 pub ratio_db: f64,
162 pub iterations: usize,
164}
165
166impl PaprReduction for ClipFilter {
167 fn reduce(&self, ofdm: &Ofdm, data: &[C64]) -> Result<(Vec<C64>, usize)> {
168 let mut x = ofdm.to_time(&ofdm.spread(data))?;
169 let a = db2amp(self.ratio_db);
170 for _ in 0..self.iterations {
171 limit_envelope(&mut x, a);
172 x = ofdm.filter(&x)?;
173 }
174 Ok((x, 0))
175 }
176}
177
178#[derive(Clone, Copy, Debug, PartialEq, Eq)]
184pub struct Slm {
185 pub candidates: usize,
187 pub seed: u64,
189}
190
191impl Slm {
192 pub fn phases(&self, u: usize, k: usize) -> Vec<C64> {
194 if u == 0 {
195 return vec![C64::new(1.0, 0.0); k];
196 }
197 let mut r = comnum_core::rng::member(self.seed, u as u64);
198 (0..k).map(|_| cis(PI / 2.0 * r.below(4) as f64)).collect()
199 }
200}
201
202impl PaprReduction for Slm {
203 fn reduce(&self, ofdm: &Ofdm, data: &[C64]) -> Result<(Vec<C64>, usize)> {
204 ensure(self.candidates >= 1, || "SLM needs at least one candidate".into())?;
205 let active = ofdm.spread(data);
206 let mut best = (f64::INFINITY, Vec::new(), 0);
207 for u in 0..self.candidates {
208 let p = self.phases(u, active.len());
209 let x = ofdm.to_time(&active.iter().zip(&p).map(|(a, b)| a * b).collect::<Vec<_>>())?;
210 let v = papr_db(&x);
211 if v < best.0 {
212 best = (v, x, u);
213 }
214 }
215 Ok((best.1, best.2))
216 }
217
218 fn recover(&self, ofdm: &Ofdm, active: Vec<C64>, side: usize) -> Result<Vec<C64>> {
219 ensure(side < self.candidates, || format!("SLM side information {side} out of {}", self.candidates))?;
220 let p = self.phases(side, active.len());
221 Ok(ofdm.despread(&active.iter().zip(&p).map(|(a, b)| a * b.conj()).collect::<Vec<_>>()))
222 }
223}
224
225#[non_exhaustive]
229#[derive(Clone, Copy, Debug, PartialEq, Eq)]
230pub enum Partition {
231 Adjacent,
233 Interleaved,
235 Pseudorandom(u64),
237}
238
239impl Partition {
240 pub fn from_name(name: &str, seed: u64) -> Result<Partition> {
254 match name.to_ascii_lowercase().as_str() {
255 "adjacent" => Ok(Partition::Adjacent),
256 "interleaved" => Ok(Partition::Interleaved),
257 "pseudorandom" => Ok(Partition::Pseudorandom(seed)),
258 other => Err(Error::Config(format!("unknown partition {other:?}: adjacent, interleaved or pseudorandom"))),
259 }
260 }
261}
262
263#[derive(Clone, Copy, Debug, PartialEq, Eq)]
269pub struct Pts {
270 pub subblocks: usize,
272 pub phases: usize,
274 pub partition: Partition,
276}
277
278impl Pts {
279 pub fn assignment(&self, k: usize) -> Vec<usize> {
281 let v = self.subblocks.max(1);
282 match self.partition {
283 Partition::Adjacent => (0..k).map(|i| i * v / k).collect(),
284 Partition::Interleaved => (0..k).map(|i| i % v).collect(),
285 Partition::Pseudorandom(seed) => {
286 let mut a: Vec<usize> = (0..k).map(|i| i % v).collect();
287 Rng::new(seed).shuffle(&mut a);
288 a
289 }
290 }
291 }
292
293 pub fn factors(&self, index: usize) -> Vec<C64> {
295 let mut f = vec![C64::new(1.0, 0.0)];
296 let mut rest = index;
297 for _ in 1..self.subblocks {
298 f.push(cis(2.0 * PI * (rest % self.phases) as f64 / self.phases as f64));
299 rest /= self.phases;
300 }
301 f
302 }
303
304 fn combinations(&self) -> Result<usize> {
305 ensure(self.subblocks >= 1 && self.phases >= 1, || "PTS needs subblocks and phases".into())?;
306 let n = (1..self.subblocks).try_fold(1usize, |acc, _| acc.checked_mul(self.phases));
307 n.filter(|n| *n <= 1 << 20).ok_or_else(|| Error::Config("too many PTS combinations".into()))
308 }
309}
310
311impl PaprReduction for Pts {
312 fn reduce(&self, ofdm: &Ofdm, data: &[C64]) -> Result<(Vec<C64>, usize)> {
313 let combinations = self.combinations()?;
314 let active = ofdm.spread(data);
315 let assign = self.assignment(active.len());
316 let parts = (0..self.subblocks)
317 .map(|v| {
318 let masked: Vec<C64> =
319 active.iter().zip(&assign).map(|(a, &b)| if b == v { *a } else { C64::new(0.0, 0.0) }).collect();
320 ofdm.to_time(&masked)
321 })
322 .collect::<Result<Vec<_>>>()?;
323 let mut best = (f64::INFINITY, 0usize);
324 let mut x = vec![C64::new(0.0, 0.0); ofdm.fft_len()];
325 for index in 0..combinations {
326 let f = self.factors(index);
327 for (n, v) in x.iter_mut().enumerate() {
328 *v = parts.iter().zip(&f).map(|(p, b)| p[n] * b).sum();
329 }
330 let p = papr_db(&x);
331 if p < best.0 {
332 best = (p, index);
333 }
334 }
335 let f = self.factors(best.1);
336 let x = (0..ofdm.fft_len()).map(|n| parts.iter().zip(&f).map(|(p, b)| p[n] * b).sum()).collect();
337 Ok((x, best.1))
338 }
339
340 fn recover(&self, ofdm: &Ofdm, active: Vec<C64>, side: usize) -> Result<Vec<C64>> {
341 ensure(side < self.combinations()?, || format!("PTS side information {side} out of range"))?;
342 let f = self.factors(side);
343 let assign = self.assignment(active.len());
344 Ok(ofdm.despread(&active.iter().zip(&assign).map(|(a, &v)| a * f[v].conj()).collect::<Vec<_>>()))
345 }
346}
347
348#[derive(Clone, Debug, PartialEq)]
356pub struct ToneReservation {
357 pub reserved: Vec<usize>,
359 pub ratio_db: f64,
361 pub iterations: usize,
363 pub step: f64,
365}
366
367impl ToneReservation {
368 pub fn random(n_active: usize, count: usize, seed: u64, ratio_db: f64) -> ToneReservation {
371 let mut idx: Vec<usize> = (0..n_active).collect();
372 Rng::new(seed).shuffle(&mut idx);
373 let mut reserved: Vec<usize> = idx.into_iter().take(count.min(n_active)).collect();
374 reserved.sort_unstable();
375 ToneReservation { reserved, ratio_db, iterations: 20, step: 1.0 }
376 }
377
378 fn mask(&self, ofdm: &Ofdm) -> Result<Vec<bool>> {
379 ensure(!ofdm.dft_spread, || "tone reservation needs plain OFDM".into())?;
380 let mut m = vec![false; ofdm.n_active];
381 for &i in &self.reserved {
382 ensure(i < ofdm.n_active, || format!("reserved tone {i} is not an active subcarrier"))?;
383 m[i] = true;
384 }
385 Ok(m)
386 }
387}
388
389impl PaprReduction for ToneReservation {
390 fn data_len(&self, ofdm: &Ofdm) -> usize {
391 ofdm.n_active.saturating_sub(self.reserved.len())
392 }
393
394 fn reduce(&self, ofdm: &Ofdm, data: &[C64]) -> Result<(Vec<C64>, usize)> {
395 let mask = self.mask(ofdm)?;
396 let mut it = data.iter();
397 let active: Vec<C64> = mask
398 .iter()
399 .map(|&r| if r { C64::new(0.0, 0.0) } else { *it.next().unwrap_or(&C64::new(0.0, 0.0)) })
400 .collect();
401 let mut x = ofdm.to_time(&active)?;
402 let a = db2amp(self.ratio_db);
403 for _ in 0..self.iterations {
404 let mut clipped = x.clone();
405 limit_envelope(&mut clipped, a);
406 if clipped.iter().zip(&x).all(|(c, v)| c == v) {
407 break;
408 }
409 let noise: Vec<C64> = clipped.iter().zip(&x).map(|(c, v)| c - v).collect();
410 let projected: Vec<C64> = ofdm
411 .to_active(&noise)?
412 .into_iter()
413 .zip(&mask)
414 .map(|(v, &r)| if r { v } else { C64::new(0.0, 0.0) })
415 .collect();
416 for (v, c) in x.iter_mut().zip(ofdm.to_time(&projected)?) {
417 *v += c * self.step;
418 }
419 }
420 Ok((x, 0))
421 }
422
423 fn recover(&self, ofdm: &Ofdm, active: Vec<C64>, _side: usize) -> Result<Vec<C64>> {
424 let mask = self.mask(ofdm)?;
425 Ok(active.into_iter().zip(&mask).filter(|(_, r)| !**r).map(|(v, _)| v).collect())
426 }
427}
428
429#[derive(Clone, Debug, PartialEq)]
436pub struct Ace {
437 pub constellation: Constellation,
439 pub ratio_db: f64,
441 pub iterations: usize,
443 pub step: f64,
445}
446
447impl PaprReduction for Ace {
448 fn reduce(&self, ofdm: &Ofdm, data: &[C64]) -> Result<(Vec<C64>, usize)> {
449 ensure(!ofdm.dft_spread, || "active constellation extension needs plain OFDM".into())?;
450 let pts = self.constellation.points();
451 let max_re = pts.iter().map(|p| p.re.abs()).fold(0.0, f64::max);
452 let max_im = pts.iter().map(|p| p.im.abs()).fold(0.0, f64::max);
453 let tol = 1e-9 * max_re.max(max_im).max(1e-300);
454 let original: Vec<C64> = data.iter().map(|d| pts[self.constellation.nearest(*d)]).collect();
455 let mut active = data.to_vec();
456 let mut x = ofdm.to_time(&active)?;
457 let a = db2amp(self.ratio_db);
458 for _ in 0..self.iterations {
459 let mut clipped = x.clone();
460 limit_envelope(&mut clipped, a);
461 let noise: Vec<C64> = clipped.iter().zip(&x).map(|(c, v)| c - v).collect();
462 if noise.iter().all(|v| v.norm_sqr() == 0.0) {
463 break;
464 }
465 for ((v, c), p) in active.iter_mut().zip(ofdm.to_active(&noise)?).zip(&original) {
466 let re = if p.re.abs() >= max_re - tol && c.re * p.re > 0.0 { c.re } else { 0.0 };
467 let im = if p.im.abs() >= max_im - tol && c.im * p.im > 0.0 { c.im } else { 0.0 };
468 *v += C64::new(re, im) * self.step;
469 }
470 x = ofdm.to_time(&active)?;
471 }
472 Ok((x, 0))
473 }
474}
475
476#[non_exhaustive]
487#[derive(Clone, Copy, Debug, PartialEq)]
488pub enum Companding {
489 MuLaw {
495 mu: f64,
497 peak_db: f64,
499 },
500 Exponential {
505 degree: f64,
507 },
508}
509
510fn mu_law_nodes() -> &'static (Vec<f64>, Vec<f64>) {
513 static NODES: std::sync::OnceLock<(Vec<f64>, Vec<f64>)> = std::sync::OnceLock::new();
514 NODES.get_or_init(|| comnum_math::gauss_legendre(64))
515}
516
517#[derive(Clone, Copy, Debug)]
521enum Law {
522 MuLaw { mu: f64, v: f64, gain: f64 },
523 Exponential { degree: f64, alpha: f64 },
524}
525
526impl Law {
527 fn compress(&self, r: f64) -> f64 {
528 match *self {
529 Law::MuLaw { mu, v, gain } => gain * v * (1.0 + mu * r / v).ln() / (1.0 + mu).ln(),
530 Law::Exponential { degree: d, alpha } => (alpha * (1.0 - (-r * r).exp())).powf(1.0 / d),
531 }
532 }
533
534 fn expand(&self, r: f64) -> f64 {
535 match *self {
536 Law::MuLaw { mu, v, gain } => {
537 let c = r / gain;
538 v / mu * ((1.0 + mu).powf(c / v) - 1.0)
539 }
540 Law::Exponential { degree: d, alpha } => {
541 let u = (1.0 - r.powf(d) / alpha).max(1e-16);
542 (-u.ln()).sqrt()
543 }
544 }
545 }
546}
547
548impl Companding {
549 fn mu_law_gain(mu: f64, v: f64) -> f64 {
563 let a = mu / v;
564 let half = (a * 7.0).ln_1p() / 2.0;
565 let (x, w) = mu_law_nodes();
566 let integral = x
567 .iter()
568 .zip(w)
569 .map(|(x, w)| {
570 let s = (x + 1.0) * half;
571 let r = s.exp_m1() / a;
572 w * 2.0 * r * (-r * r).exp() * s * s * s.exp() / a
573 })
574 .sum::<f64>()
575 * half;
576 let k = v / (1.0 + mu).ln();
577 1.0 / (k * k * integral).sqrt()
578 }
579
580 fn law(&self) -> Law {
582 match *self {
583 Companding::MuLaw { mu, peak_db } => {
584 let v = db2amp(peak_db);
585 Law::MuLaw { mu, v, gain: Companding::mu_law_gain(mu, v) }
586 }
587 Companding::Exponential { degree } => {
588 Law::Exponential { degree, alpha: ((degree + 2.0) / degree).powf(degree / 2.0) }
589 }
590 }
591 }
592
593 pub fn compress(&self, r: f64) -> f64 {
596 self.law().compress(r)
597 }
598
599 pub fn expand(&self, r: f64) -> f64 {
601 self.law().expand(r)
602 }
603
604 fn apply(x: &mut [C64], f: impl Fn(f64) -> f64) {
605 map_envelope(x, |r| C64::new(f(r) / r, 0.0));
606 }
607}
608
609impl PaprReduction for Companding {
610 fn reduce(&self, ofdm: &Ofdm, data: &[C64]) -> Result<(Vec<C64>, usize)> {
611 if let Companding::MuLaw { mu, .. } = self {
612 ensure(*mu > 0.0, || "μ must be positive".into())?;
613 }
614 if let Companding::Exponential { degree } = self {
615 ensure(*degree > 0.0, || "the degree must be positive".into())?;
616 }
617 let mut x = ofdm.to_time(&ofdm.spread(data))?;
618 let law = self.law();
619 Companding::apply(&mut x, |r| law.compress(r));
620 Ok((x, 0))
621 }
622
623 fn restore_time(&self, block: &mut [C64]) {
624 let law = self.law();
625 Companding::apply(block, |r| law.expand(r));
626 }
627}
628
629#[cfg(test)]
630mod tests {
631 use super::*;
632 use comnum_math::power;
633
634 fn qam_data(c: &Constellation, n: usize, seed: u64) -> Vec<C64> {
635 let mut r = Rng::new(seed);
636 (0..n).map(|_| c.points()[r.below(c.order())]).collect()
637 }
638
639 fn papr_95(ofdm: &Ofdm, red: &dyn PaprReduction, data: &[C64]) -> f64 {
641 let k = red.data_len(ofdm);
642 let blocks = data.len() / k;
643 let (x, _) = ofdm.modulate_with(&data[..blocks * k], red).unwrap();
644 let mut p: Vec<f64> = x.chunks(ofdm.block_len()).map(papr_db).collect();
645 p.sort_by(f64::total_cmp);
646 p[(0.95 * p.len() as f64) as usize]
647 }
648
649 #[test]
650 fn the_ccdf_of_ofdm_follows_ochiai_and_imai() {
651 let ofdm = Ofdm::new(256, 256).unwrap();
652 let c = Constellation::qam(4).unwrap().normalized();
653 let blocks = 4000;
654 let x = ofdm.modulate(&qam_data(&c, 256 * blocks, 1)).unwrap();
655 let p: Vec<f64> = x.chunks(ofdm.block_len()).map(papr_db).collect();
656 for g in [8.0, 9.0, 10.0] {
657 let emp = p.iter().filter(|v| **v > g).count() as f64 / p.len() as f64;
658 let th = ccdf_oversampled(256, g);
659 assert!((emp / th - 1.0).abs() < 0.25, "{g} dB: {emp} vs {th}");
660 assert!(ccdf_nyquist(256, g) < th);
662 }
663 }
664
665 #[test]
666 fn bussgang_matches_a_monte_carlo() {
667 let mut r = Rng::new(2);
668 let x: Vec<C64> = (0..400_000).map(|_| r.complex_gaussian()).collect();
669 for cr in [0.0, 3.0, 6.0] {
670 let mut y = x.clone();
671 limit_envelope(&mut y, db2amp(cr));
672 let alpha: C64 = y.iter().zip(&x).map(|(a, b)| a * b.conj()).sum::<C64>() / power(&x) / x.len() as f64;
673 let th = comnum_dsp::nonlinear::clipping_bussgang(cr);
674 assert!((alpha - th.gain).norm() < 3e-3, "{cr} dB: {alpha} vs {}", th.gain);
675 assert!((power(&y) - th.output_power).abs() < 3e-3);
676 let d: f64 = y.iter().zip(&x).map(|(a, b)| (a - b * th.gain).norm_sqr()).sum::<f64>() / x.len() as f64;
677 assert!((d / th.distortion - 1.0).abs() < 0.03);
678 }
679 }
680
681 #[test]
682 fn every_technique_reduces_the_papr_and_lossless_ones_recover_the_data() {
683 let ofdm = Ofdm::new(128, 128).unwrap();
684 let c = Constellation::qam(16).unwrap().normalized();
685 let data = qam_data(&c, 128 * 200, 3);
686 let plain = papr_95(&ofdm, &crate::ofdm::NoReduction, &data);
687 let techniques: Vec<(&str, Box<dyn PaprReduction>, bool)> = vec![
688 ("clip", Box::new(Clip { ratio_db: 4.0 }), false),
689 ("icf", Box::new(ClipFilter { ratio_db: 4.0, iterations: 4 }), false),
690 ("slm", Box::new(Slm { candidates: 8, seed: 4 }), true),
691 ("pts", Box::new(Pts { subblocks: 4, phases: 4, partition: Partition::Pseudorandom(5) }), true),
692 ("tr", Box::new(ToneReservation::random(128, 12, 6, 4.0)), true),
693 ("ace", Box::new(Ace { constellation: c.clone(), ratio_db: 4.0, iterations: 10, step: 1.0 }), false),
694 ("exp", Box::new(Companding::Exponential { degree: 2.0 }), true),
695 ("mu", Box::new(Companding::MuLaw { mu: 4.0, peak_db: 9.0 }), true),
696 ];
697 for (name, t, lossless) in &techniques {
698 let k = t.data_len(&ofdm);
699 let d = &data[..(data.len() / k) * k];
700 let reduced = papr_95(&ofdm, t.as_ref(), d);
701 assert!(reduced < plain - 1.0, "{name}: {reduced} vs {plain}");
702 let (x, side) = ofdm.modulate_with(d, t.as_ref()).unwrap();
703 let back = ofdm.demodulate_with(&x, t.as_ref(), &side).unwrap();
704 if *lossless {
705 assert!(back.iter().zip(d).all(|(a, b)| (a - b).norm() < 1e-6), "{name}");
706 } else {
707 assert!(
709 back.iter().zip(d).filter(|(a, b)| c.nearest(**a) != c.nearest(**b)).count() < d.len() / 50,
710 "{name}"
711 );
712 }
713 }
714 let (x, _) = ofdm.modulate_with(&data, &Companding::Exponential { degree: 2.0 }).unwrap();
716 assert!(papr_db(&x) <= comnum_math::lin2db(2.0) + 0.3);
717 }
718
719 fn mu_law_compress_simpson(mu: f64, peak_db: f64, r: f64) -> f64 {
722 let v = db2amp(peak_db);
723 let c = |t: f64| v * (1.0 + mu * t.sqrt() / v).ln() / (1.0 + mu).ln();
724 let gain = 1.0 / comnum_math::simpson(|t| c(t).powi(2) * (-t).exp(), 0.0, 60.0, 6000).sqrt();
725 gain * v * (1.0 + mu * r / v).ln() / (1.0 + mu).ln()
726 }
727
728 #[test]
729 fn the_mu_law_gain_is_the_integral_and_follows_the_former_simpson_rule() {
730 for (mu, peak_db, power, simpson_error) in [
733 (4.0, 9.0, 2.089_224_506_453_305, 3.5e-7),
734 (255.0, 0.0, 0.912_706_126_294_248_1, 2.6e-4),
735 (255.0, 12.0, 8.026_021_096_858_328, 1.1e-4),
736 (100.0, 3.0, 1.531_119_965_389_956, 1.2e-4),
737 (10.0, 6.0, 1.910_143_981_626_004_6, 3e-6),
738 ] {
739 let gain = Companding::mu_law_gain(mu, db2amp(peak_db));
741 let e = 1.0 / (gain * gain);
742 assert!((e / power - 1.0).abs() < 1e-13, "μ {mu}, {peak_db} dB: {e} vs {power}");
743 let law = Companding::MuLaw { mu, peak_db };
744 for r in [0.01, 0.3, 1.0, 2.0, 4.5] {
745 let (new, old) = (law.compress(r), mu_law_compress_simpson(mu, peak_db, r));
746 assert!((new / old - 1.0).abs() < simpson_error, "μ {mu}, {peak_db} dB, r {r}: {new} vs {old}");
747 assert!((law.expand(new) / r - 1.0).abs() < 1e-12);
748 }
749 }
750 let law = Companding::MuLaw { mu: 255.0, peak_db: 0.0 }.law();
752 let mut rng = Rng::new(11);
753 let n = 400_000;
754 let p = (0..n).map(|_| law.compress(rng.complex_gaussian().norm()).powi(2)).sum::<f64>() / f64::from(n);
755 assert!((p - 1.0).abs() < 5e-3, "{p}");
757 }
758
759 #[test]
760 fn ace_never_moves_a_point_across_a_decision_boundary() {
761 let ofdm = Ofdm::new(64, 64).unwrap();
762 let c = Constellation::qam(16).unwrap().normalized();
763 let data = qam_data(&c, 64 * 50, 7);
764 let ace = Ace { constellation: c.clone(), ratio_db: 3.0, iterations: 20, step: 1.0 };
765 let (x, side) = ofdm.modulate_with(&data, &ace).unwrap();
766 let back = ofdm.demodulate_with(&x, &ace, &side).unwrap();
767 assert_eq!(c.decide(&back), c.decide(&data));
768 }
769
770 #[test]
771 fn dft_spread_ofdm_has_a_lower_papr() {
772 let c = Constellation::qam(4).unwrap().normalized();
773 let data = qam_data(&c, 64 * 300, 8);
774 let ofdm = Ofdm::new(256, 64).unwrap();
775 let sc = Ofdm { dft_spread: true, ..ofdm.clone() };
776 let p0 = papr_95(&ofdm, &crate::ofdm::NoReduction, &data);
777 let p1 = papr_95(&sc, &crate::ofdm::NoReduction, &data);
778 assert!(p1 < p0 - 2.0, "{p1} vs {p0}");
779 }
780}
781
782#[cfg(test)]
784mod literature {
785 use super::*;
786
787 #[test]
794 fn ochiai_2001_eqs_11_and_27() {
795 let lambda = db2lin(10.0);
796 let nyquist = 1.0 - (1.0 - (-lambda).exp()).powi(256);
799 assert!((ccdf_nyquist(256, 10.0) / nyquist - 1.0).abs() < 1e-12);
800 let r = lambda.sqrt();
801 let crest = (-(PI / 3.0).sqrt() * 256.0 * r * (-r * r).exp()).exp();
802 assert!((ccdf_oversampled(256, 10.0) / (1.0 - crest) - 1.0).abs() < 1e-12);
803 let p15 = ccdf_oversampled(100_000, 15.0);
804 assert!(p15 > 3e-9 && p15 < 3e-8, "{p15:e}");
805 }
806}