use comnum_core::{Error, Result};
use comnum_math::{beta_inc, gauss_chebyshev, gauss_legendre};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct RandomDisc {
pub radius: f64,
pub alpha: f64,
}
impl RandomDisc {
fn check(&self) -> Result<()> {
if self.radius > 0.0 && self.alpha > 0.0 && self.radius.is_finite() && self.alpha.is_finite() {
Ok(())
} else {
Err(Error::Config(format!("a disc needs a positive radius and exponent, got {self:?}")))
}
}
pub fn gain_cdf(&self, y: f64) -> Result<f64> {
self.check()?;
let (t, w) = gauss_legendre(256);
let r = self.radius;
let s: f64 = t
.iter()
.zip(&w)
.map(|(&t, &w)| {
let z = r / 2.0 * (t + 1.0);
w * (-(-(1.0 + z.powf(self.alpha)) * y).exp_m1()) * z
})
.sum();
Ok(s * r / 2.0 * 2.0 / (r * r))
}
fn chebyshev(&self, n: usize) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
let (theta, w) = gauss_chebyshev(n);
let r = self.radius;
let z: Vec<f64> = theta.iter().map(|&t| r / 2.0 * t + r / 2.0).collect();
let c: Vec<f64> = z.iter().map(|&z| 1.0 + z.powf(self.alpha)).collect();
let a: Vec<f64> = theta.iter().zip(&w).zip(&z).map(|((&t, &w), &z)| w * (1.0 - t * t).sqrt() * z).collect();
let beta = a.iter().zip(&c).map(|(a, c)| a * c).collect();
(c, a, beta)
}
pub fn gain_cdf_chebyshev(&self, y: f64, n: usize) -> Result<f64> {
self.check()?;
if n == 0 {
return Err(Error::Config("Gauss-Chebyshev needs at least one node".into()));
}
let (c, a, _) = self.chebyshev(n);
Ok(c.iter().zip(&a).map(|(c, a)| a * (1.0 - (-c * y).exp())).sum::<f64>() / self.radius)
}
pub fn gain_slope(&self, n: usize) -> Result<f64> {
self.check()?;
if n == 0 {
return Err(Error::Config("Gauss-Chebyshev needs at least one node".into()));
}
Ok(self.chebyshev(n).2.iter().sum::<f64>() / self.radius)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct NomaDownlink {
pub powers: Vec<f64>,
pub rates: Vec<f64>,
}
impl NomaDownlink {
pub fn new(powers: Vec<f64>, rates: Vec<f64>) -> Result<NomaDownlink> {
if powers.is_empty() || powers.len() != rates.len() {
return Err(Error::Shape(format!("{} power coefficients for {} rates", powers.len(), rates.len())));
}
if powers.iter().chain(&rates).any(|v| !(v.is_finite() && *v >= 0.0)) {
return Err(Error::Config("power coefficients and rates must be non-negative".into()));
}
if (powers.iter().sum::<f64>() - 1.0).abs() > 1e-9 {
return Err(Error::Config(format!("the power coefficients must sum to 1, got {powers:?}")));
}
Ok(NomaDownlink { powers, rates })
}
pub fn thresholds(&self, snr: f64) -> Vec<f64> {
let m = self.powers.len();
let psi: Vec<f64> = (0..m)
.map(|j| {
let phi = 2f64.powf(self.rates[j]) - 1.0;
let rest: f64 = self.powers[j + 1..].iter().sum();
let margin = self.powers[j] - phi * rest;
if margin > 0.0 { phi / (snr * margin) } else { f64::INFINITY }
})
.collect();
psi.iter()
.scan(0.0f64, |acc, &p| {
*acc = acc.max(p);
Some(*acc)
})
.collect()
}
pub fn outage(&self, disc: &RandomDisc, snr: f64) -> Result<Vec<f64>> {
let m = self.powers.len();
self.thresholds(snr)
.iter()
.enumerate()
.map(|(i, &psi)| {
if psi.is_infinite() {
return Ok(1.0);
}
let f = disc.gain_cdf(psi)?;
Ok(beta_inc((i + 1) as f64, (m - i) as f64, f))
})
.collect()
}
pub fn outage_high_snr(&self, disc: &RandomDisc, snr: f64, n: usize) -> Result<Vec<f64>> {
let eta = disc.gain_slope(n)?;
let m = self.powers.len();
Ok(self
.thresholds(snr)
.iter()
.enumerate()
.map(|(i, &psi)| {
let k = i + 1;
let binom = (0..k).fold(1.0, |c, j| c * (m - j) as f64 / (j + 1) as f64);
(binom * (eta * psi).powi(k as i32)).min(1.0)
})
.collect())
}
}
pub fn orthogonal_outage(disc: &RandomDisc, snr: f64, rate: f64) -> Result<f64> {
disc.gain_cdf((2f64.powf(rate) - 1.0) / snr)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_gain_cdf_has_its_closed_form_for_alpha_2() {
let d = RandomDisc { radius: 3.0, alpha: 2.0 };
for y in [1e-3f64, 0.05, 0.5, 3.0] {
let r2 = 9.0;
let exact = 1.0 - (-y).exp() * (1.0 - (-r2 * y).exp()) / (r2 * y);
assert!((d.gain_cdf(y).unwrap() - exact).abs() < 1e-12, "{y}");
assert!((d.gain_cdf_chebyshev(y, 200).unwrap() - exact).abs() < 1e-4, "{y}");
}
assert!(d.gain_cdf(0.0).unwrap().abs() < 1e-15);
assert!(RandomDisc { radius: -1.0, alpha: 2.0 }.gain_cdf(1.0).is_err());
}
#[test]
fn the_outage_follows_condition_8_and_the_diversity_order() {
let d = RandomDisc { radius: 5.0, alpha: 3.0 };
let bad = NomaDownlink::new(vec![0.6, 0.4], vec![2.0, 1.0]).unwrap();
assert_eq!(bad.outage(&d, 1e6).unwrap(), vec![1.0, 1.0]);
let noma = NomaDownlink::new(vec![0.8, 0.2], vec![0.1, 0.5]).unwrap();
let (p3, p4) = (noma.outage(&d, 1e3).unwrap(), noma.outage(&d, 1e4).unwrap());
assert!(((p3[0] / p4[0]).log10() - 1.0).abs() < 0.1 && ((p3[1] / p4[1]).log10() - 2.0).abs() < 0.15);
assert!(NomaDownlink::new(vec![0.5, 0.4], vec![0.1, 0.1]).is_err());
assert!(NomaDownlink::new(vec![1.0], vec![0.1, 0.1]).is_err());
}
}