use comnum_core::{Error, Result, ensure};
use crate::detection::Criterion;
#[derive(Clone, Debug, PartialEq)]
pub struct Uplink {
pub antennas: usize,
pub large_scale: Vec<f64>,
pub snr_lin: f64,
pub pilots: Option<usize>,
}
impl Uplink {
pub fn new(antennas: usize, large_scale: Vec<f64>, snr_lin: f64) -> Result<Uplink> {
ensure(antennas >= 1 && !large_scale.is_empty(), || {
format!("the uplink needs antennas and users, got {antennas} and {}", large_scale.len())
})?;
ensure(large_scale.iter().all(|b| *b > 0.0 && b.is_finite()), || {
"the large-scale fading of every user must be positive and finite".into()
})?;
ensure(snr_lin > 0.0 && snr_lin.is_finite(), || format!("snr_lin must be positive, got {snr_lin}"))?;
Ok(Uplink { antennas, large_scale, snr_lin, pilots: None })
}
pub fn with_pilots(self, tau: usize) -> Result<Uplink> {
ensure(tau >= self.large_scale.len(), || {
format!("orthogonal pilots need tau >= K = {}, got {tau}", self.large_scale.len())
})?;
Ok(Uplink { pilots: Some(tau), ..self })
}
pub fn error_variance(&self) -> Vec<f64> {
match self.pilots {
None => vec![0.0; self.large_scale.len()],
Some(tau) => self.large_scale.iter().map(|b| b / (tau as f64 * self.snr_lin * b + 1.0)).collect(),
}
}
pub fn estimate_variance(&self) -> Vec<f64> {
self.large_scale.iter().zip(self.error_variance()).map(|(b, e)| b - e).collect()
}
pub fn effective_noise_variance(&self) -> f64 {
1.0 + self.snr_lin * self.error_variance().iter().sum::<f64>()
}
pub fn rate_bound(&self, receiver: Criterion, k: usize) -> Result<f64> {
let n_users = self.large_scale.len();
ensure(k < n_users, || format!("user {k} of {n_users}"))?;
let (m, p, bk) = (self.antennas as f64, self.snr_lin, self.large_scale[k]);
let others: f64 = self.large_scale.iter().enumerate().filter(|(i, _)| *i != k).map(|(_, b)| b).sum();
let sinr = match receiver {
Criterion::Mr => {
ensure(self.antennas >= 2, || "the MR bound needs M >= 2".into())?;
match self.pilots {
None => p * (m - 1.0) * bk / (p * others + 1.0),
Some(tau) => {
let t = tau as f64;
t * p * p * (m - 1.0) * bk * bk / (p * (t * p * bk + 1.0) * others + (t + 1.0) * p * bk + 1.0)
}
}
}
Criterion::Zf => {
ensure(self.antennas > n_users, || format!("the ZF bound needs M > K = {n_users}"))?;
let dof = m - n_users as f64;
match self.pilots {
None => p * dof * bk,
Some(tau) => {
let t = tau as f64;
let errors: f64 = self.large_scale.iter().map(|b| p * b / (t * p * b + 1.0)).sum();
t * p * p * dof * bk * bk / ((t * p * bk + 1.0) * errors + t * p * bk + 1.0)
}
}
}
Criterion::Mmse => {
return Err(Error::Config("the MMSE bound of Ngo et al. is an approximation: not implemented".into()));
}
};
Ok((1.0 + sinr).log2())
}
pub fn sum_rate_bound(&self, receiver: Criterion) -> Result<f64> {
(0..self.large_scale.len()).map(|k| self.rate_bound(receiver, k)).sum()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_bounds_of_one_user_with_perfect_csi_are_those_of_maximum_ratio_combining() {
let up = Uplink::new(16, vec![0.3], 4.0).unwrap();
let expected = (1.0 + 4.0 * 15.0 * 0.3f64).log2();
for r in [Criterion::Mr, Criterion::Zf] {
assert!((up.rate_bound(r, 0).unwrap() - expected).abs() < 1e-12);
}
}
#[test]
fn estimated_csi_splits_the_large_scale_fading() {
let up = Uplink::new(32, vec![1.0, 0.25], 2.0).unwrap().with_pilots(2).unwrap();
let (e, g) = (up.error_variance(), up.estimate_variance());
assert!((e[0] - 1.0 / 5.0).abs() < 1e-15 && (e[1] - 0.25 / 2.0).abs() < 1e-15);
assert!((g[0] + e[0] - 1.0).abs() < 1e-15 && (g[1] + e[1] - 0.25).abs() < 1e-15);
assert!((up.effective_noise_variance() - (1.0 + 2.0 * (0.2 + 0.125))).abs() < 1e-15);
for r in [Criterion::Mr, Criterion::Zf] {
let perfect = Uplink { pilots: None, ..up.clone() };
assert!(up.sum_rate_bound(r).unwrap() < perfect.sum_rate_bound(r).unwrap());
}
}
#[test]
fn the_power_scaling_laws_hold() {
let (eu, beta, k) = (10.0, vec![1.0, 0.5, 0.2], 3);
let m = 1_000_000usize;
let perfect = Uplink::new(m, beta.clone(), eu / m as f64).unwrap();
let m2 = 100_000_000usize;
let estimated = Uplink::new(m2, beta.clone(), eu / (m2 as f64).sqrt()).unwrap().with_pilots(k).unwrap();
for r in [Criterion::Mr, Criterion::Zf] {
for (i, b) in beta.iter().enumerate() {
let p = perfect.rate_bound(r, i).unwrap();
let e = estimated.rate_bound(r, i).unwrap();
assert!((p - (1.0 + b * eu).log2()).abs() < 1e-3, "{r:?} {i}: {p}");
assert!((e - (1.0 + k as f64 * b * b * eu * eu).log2()).abs() < 1e-2, "{r:?} {i}: {e}");
}
}
}
#[test]
fn invalid_uplinks_are_errors() {
assert!(Uplink::new(0, vec![1.0], 1.0).is_err());
assert!(Uplink::new(4, vec![], 1.0).is_err());
assert!(Uplink::new(4, vec![0.0], 1.0).is_err());
assert!(Uplink::new(4, vec![1.0], -1.0).is_err());
assert!(Uplink::new(4, vec![1.0, 1.0], 1.0).unwrap().with_pilots(1).is_err());
let up = Uplink::new(2, vec![1.0, 1.0], 1.0).unwrap();
assert!(up.rate_bound(Criterion::Zf, 0).is_err() && up.rate_bound(Criterion::Mmse, 0).is_err());
assert!(up.rate_bound(Criterion::Mr, 2).is_err() && up.rate_bound(Criterion::Mr, 0).is_ok());
}
}