1use crate::precoding::water_filling_powers;
97use comnum_core::{Error, Result, ensure, ensure_shape};
98use comnum_math::C64;
99use comnum_math::linalg::{CMat, hermitian, identity, inverse_hpd, matmul, solve_hpd, svd};
100use ndarray::ArrayView2;
101use std::f64::consts::PI;
102
103#[derive(Clone, Copy, Debug, PartialEq)]
105pub enum TracePrior {
106 Known(f64),
108 FromLs,
111}
112
113#[derive(Clone, Debug, PartialEq)]
115pub enum ChannelEstimator {
116 Ls,
118 ScaledLs(TracePrior),
120 Mmse(CMat),
122 RelaxedMmse(TracePrior),
124}
125
126#[derive(Clone, Debug, PartialEq)]
128pub struct Training {
129 p: CMat,
130 gram_inv: Option<CMat>,
132}
133
134impl Training {
135 pub fn new(p: CMat) -> Result<Training> {
142 let (t, n) = p.dim();
143 ensure(t >= 1 && n >= t, || format!("the training must be t x N with N >= t >= 1, got {t} x {n}"))?;
144 ensure(p.iter().all(|z| z.re.is_finite() && z.im.is_finite()), || "the training must be finite".into())?;
145 let gram = matmul(&p.view(), &hermitian(&p.view()).view());
146 let gram_inv = inverse_hpd(&gram.view()).ok();
147 Ok(Training { p, gram_inv })
148 }
149
150 pub fn orthogonal(n_tx: usize, n: usize, total_power: f64) -> Result<Training> {
156 ensure(n_tx >= 1 && n >= n_tx, || format!("the training needs N >= t >= 1, got t = {n_tx}, N = {n}"))?;
157 ensure(total_power > 0.0 && total_power.is_finite(), || {
158 format!("the training power must be positive, got {total_power}")
159 })?;
160 let a = (total_power / (n * n_tx) as f64).sqrt();
161 let p = CMat::from_shape_fn((n_tx, n), |(k, m)| C64::from_polar(a, 2.0 * PI * ((k * m) % n) as f64 / n as f64));
162 Training::new(p)
163 }
164
165 pub fn mmse_optimal(
173 r_h: &ArrayView2<'_, C64>,
174 noise_var: f64,
175 n_rx: usize,
176 total_power: f64,
177 n: usize,
178 ) -> Result<Training> {
179 let t = check_correlation(r_h)?;
180 ensure(n >= t, || format!("the training needs N >= t, got t = {t}, N = {n}"))?;
181 ensure(n_rx >= 1, || "at least one receive antenna".into())?;
182 ensure(noise_var > 0.0 && noise_var.is_finite(), || {
183 format!("the noise variance must be positive, got {noise_var}")
184 })?;
185 let scale = noise_var * n_rx as f64;
186 let (q, lambda, _) = svd(r_h);
187 let powers = water_filling_powers(&lambda, total_power / scale)?;
188 let mut p = CMat::zeros((t, n));
189 for (i, pw) in powers.iter().enumerate() {
190 let g = (scale * pw).sqrt();
191 for k in 0..t {
192 p[(k, i)] = q[(k, i)] * g;
193 }
194 }
195 Training::new(p)
196 }
197
198 pub fn symbols(&self) -> &CMat {
200 &self.p
201 }
202
203 pub fn n_tx(&self) -> usize {
205 self.p.nrows()
206 }
207
208 pub fn n_symbols(&self) -> usize {
210 self.p.ncols()
211 }
212
213 pub fn total_power(&self) -> f64 {
215 self.p.iter().map(C64::norm_sqr).sum()
216 }
217
218 pub fn estimate(&self, s: &ArrayView2<'_, C64>, estimator: &ChannelEstimator, noise_var: f64) -> Result<CMat> {
229 ensure_shape(s.ncols() == self.n_symbols(), || {
230 format!("the received training has {} columns, the training {}", s.ncols(), self.n_symbols())
231 })?;
232 ensure(s.nrows() >= 1, || "at least one receive antenna".into())?;
233 let from_ls = || -> Result<f64> {
234 let h_ls = matmul(s, &self.ls_filter()?.view());
235 Ok(h_ls.iter().map(C64::norm_sqr).sum())
236 };
237 let a = self.filter(estimator, noise_var, s.nrows(), from_ls)?;
238 Ok(matmul(s, &a.view()))
239 }
240
241 pub fn mse(
251 &self,
252 estimator: &ChannelEstimator,
253 r_h: &ArrayView2<'_, C64>,
254 noise_var: f64,
255 n_rx: usize,
256 ) -> Result<f64> {
257 ensure_shape(r_h.dim() == (self.n_tx(), self.n_tx()), || {
258 format!("the correlation is {:?}, the training has {} rows", r_h.dim(), self.n_tx())
259 })?;
260 ensure(n_rx >= 1, || "at least one receive antenna".into())?;
261 let a = self.filter(estimator, noise_var, n_rx, || {
262 Err(Error::Config("LS-SLS and LS-RMMSE have no closed-form MSE (estimate it by Monte Carlo)".into()))
263 })?;
264 let b = identity(self.n_tx()) - matmul(&self.p.view(), &a.view());
265 let bias = matmul(&hermitian(&b.view()).view(), &matmul(r_h, &b.view()).view());
266 let bias: f64 = (0..self.n_tx()).map(|i| bias[(i, i)].re).sum();
267 let noise: f64 = a.iter().map(C64::norm_sqr).sum();
268 Ok(bias + noise_var * n_rx as f64 * noise)
269 }
270
271 fn ls_filter(&self) -> Result<CMat> {
273 let g = self.gram_inv.as_ref().ok_or_else(|| {
274 Error::Config(format!("LS needs a training of rank t = {} (P P^H invertible)", self.n_tx()))
275 })?;
276 Ok(matmul(&hermitian(&self.p.view()).view(), &g.view()))
277 }
278
279 fn lmmse_filter(&self, r: &ArrayView2<'_, C64>, noise_var: f64, n_rx: usize) -> Result<CMat> {
281 let ph_r = matmul(&hermitian(&self.p.view()).view(), r);
282 let m = matmul(&ph_r.view(), &self.p.view()) + identity(self.n_symbols()) * C64::from(noise_var * n_rx as f64);
283 solve_hpd(&m.view(), &ph_r.view())
284 }
285
286 fn filter(
289 &self,
290 estimator: &ChannelEstimator,
291 noise_var: f64,
292 n_rx: usize,
293 from_ls: impl FnOnce() -> Result<f64>,
294 ) -> Result<CMat> {
295 ensure(noise_var >= 0.0 && noise_var.is_finite(), || {
296 format!("the noise variance must be non-negative, got {noise_var}")
297 })?;
298 let trace = |prior: &TracePrior| -> Result<f64> {
299 let tr = match prior {
300 TracePrior::Known(tr) => *tr,
301 TracePrior::FromLs => from_ls()?,
302 };
303 ensure(tr > 0.0 && tr.is_finite(), || format!("the trace of R_H must be positive, got {tr}"))?;
304 Ok(tr)
305 };
306 match estimator {
307 ChannelEstimator::Ls => self.ls_filter(),
308 ChannelEstimator::ScaledLs(prior) => {
309 let a = self.ls_filter()?;
310 let g = self.gram_inv.as_ref().map_or(0.0, |g| (0..self.n_tx()).map(|i| g[(i, i)].re).sum());
311 let tr = trace(prior)?;
312 let j_ls = noise_var * n_rx as f64 * g;
313 Ok(a * C64::from(tr / (j_ls + tr)))
314 }
315 ChannelEstimator::Mmse(r) => {
316 check_correlation(&r.view())?;
317 ensure_shape(r.nrows() == self.n_tx(), || {
318 format!("the correlation is {:?}, the training has {} rows", r.dim(), self.n_tx())
319 })?;
320 self.lmmse_filter(&r.view(), noise_var, n_rx)
321 }
322 ChannelEstimator::RelaxedMmse(prior) => {
323 let alpha = trace(prior)? / self.n_tx() as f64;
324 self.lmmse_filter(&(identity(self.n_tx()) * C64::from(alpha)).view(), noise_var, n_rx)
325 }
326 }
327 }
328}
329
330fn check_correlation(r: &ArrayView2<'_, C64>) -> Result<usize> {
332 let (t, c) = r.dim();
333 ensure(t >= 1 && t == c, || format!("the correlation must be square and non-empty, got {t} x {c}"))?;
334 ensure(r.iter().all(|z| z.re.is_finite() && z.im.is_finite()), || "the correlation must be finite".into())?;
335 Ok(t)
336}
337
338fn inverse_kappas(trainings: &[Training]) -> Result<Vec<f64>> {
340 ensure(!trainings.is_empty(), || "at least one training period".into())?;
341 let t = trainings[0].n_tx();
342 trainings
343 .iter()
344 .map(|tr| {
345 ensure_shape(tr.n_tx() == t, || "every period must train the same transmit antennas".into())?;
346 let g = tr.gram_inv.as_ref().ok_or_else(|| Error::Config("BLUE needs trainings of rank t".into()))?;
347 Ok(1.0 / (0..t).map(|i| g[(i, i)].re).sum::<f64>())
348 })
349 .collect()
350}
351
352pub fn blue_weights(trainings: &[Training]) -> Result<Vec<f64>> {
360 let inv = inverse_kappas(trainings)?;
361 let sum: f64 = inv.iter().sum();
362 Ok(inv.iter().map(|k| k / sum).collect())
363}
364
365pub fn blue_combine(estimates: &[CMat], weights: &[f64]) -> Result<CMat> {
372 ensure_shape(!estimates.is_empty() && estimates.len() == weights.len(), || {
373 format!("{} estimates and {} weights", estimates.len(), weights.len())
374 })?;
375 let dim = estimates[0].dim();
376 ensure_shape(estimates.iter().all(|h| h.dim() == dim), || "the estimates must have the same shape".into())?;
377 let mut out = CMat::zeros(dim);
378 for (h, w) in estimates.iter().zip(weights) {
379 out.scaled_add(C64::from(*w), h);
380 }
381 Ok(out)
382}
383
384pub fn blue_mse(trainings: &[Training], noise_var: f64, n_rx: usize) -> Result<f64> {
393 ensure(noise_var >= 0.0 && n_rx >= 1, || {
394 format!("a non-negative noise variance and a receive antenna, got {noise_var} and {n_rx}")
395 })?;
396 let inv = inverse_kappas(trainings)?;
397 Ok(noise_var * n_rx as f64 / inv.iter().sum::<f64>())
398}
399
400#[cfg(test)]
401mod tests {
402 #![allow(clippy::unwrap_used, reason = "test code: a failure is a test failure")]
403 use super::*;
404
405 fn exponential(t: usize, eps: f64, r: usize) -> CMat {
406 CMat::from_shape_fn((t, t), |(i, j)| C64::from(r as f64 * eps.powi(i.abs_diff(j) as i32)))
407 }
408
409 fn close(a: f64, b: f64) -> bool {
410 (a - b).abs() <= 1e-10 * b.abs().max(1.0)
411 }
412
413 #[test]
414 fn the_dft_training_has_orthogonal_rows_of_the_power() {
415 let p = Training::orthogonal(3, 5, 6.0).unwrap();
416 let g = matmul(&p.symbols().view(), &hermitian(&p.symbols().view()).view());
417 for i in 0..3 {
418 for j in 0..3 {
419 let want = if i == j { 2.0 } else { 0.0 };
420 assert!((g[(i, j)] - C64::from(want)).norm() < 1e-12);
421 }
422 }
423 assert!(close(p.total_power(), 6.0));
424 }
425
426 #[test]
427 fn the_mse_of_orthogonal_training_follows_eqs_12_20_49() {
428 let (t, r, power, noise) = (4, 3, 20.0, 0.7);
429 let p = Training::orthogonal(t, t, power).unwrap();
430 let rh = exponential(t, 0.6, r);
431 let tr = (r * t) as f64;
432 let j_ls = noise * (t * t * r) as f64 / power;
433 assert!(close(p.mse(&ChannelEstimator::Ls, &rh.view(), noise, r).unwrap(), j_ls));
434 let j_sls = noise * (t * t * r) as f64 * tr / (noise * (t * t * r) as f64 + power * tr);
435 let sls = ChannelEstimator::ScaledLs(TracePrior::Known(tr));
436 let rmmse = ChannelEstimator::RelaxedMmse(TracePrior::Known(tr));
437 assert!(close(p.mse(&sls, &rh.view(), noise, r).unwrap(), j_sls));
438 assert!(close(p.mse(&rmmse, &rh.view(), noise, r).unwrap(), j_sls));
439 assert!(j_sls < j_ls);
440 }
441
442 #[test]
443 fn the_lmmse_mse_is_eq_28_and_eq_32_at_its_optimal_training() {
444 let (t, r, noise) = (3, 2, 0.5);
445 let rh = exponential(t, 0.8, r);
446 let mmse = ChannelEstimator::Mmse(rh.clone());
447 let p = Training::orthogonal(t, t, 9.0).unwrap();
449 let inner = inverse_hpd(&rh.view()).unwrap()
450 + matmul(&p.symbols().view(), &hermitian(&p.symbols().view()).view()) * C64::from(1.0 / (noise * r as f64));
451 let e = inverse_hpd(&inner.view()).unwrap();
452 let j28: f64 = (0..t).map(|i| e[(i, i)].re).sum();
453 assert!(close(p.mse(&mmse, &rh.view(), noise, r).unwrap(), j28));
454 let power = 200.0;
456 let opt = Training::mmse_optimal(&rh.view(), noise, r, power, t).unwrap();
457 assert!(close(opt.total_power(), power));
458 let tr_inv: f64 = {
459 let i = inverse_hpd(&rh.view()).unwrap();
460 (0..t).map(|k| i[(k, k)].re).sum()
461 };
462 let j32 = (t * t) as f64 / (tr_inv + power / (noise * r as f64));
463 assert!(close(opt.mse(&mmse, &rh.view(), noise, r).unwrap(), j32));
464 let orth = Training::orthogonal(t, t, power).unwrap();
465 assert!(opt.mse(&mmse, &rh.view(), noise, r).unwrap() < orth.mse(&mmse, &rh.view(), noise, r).unwrap());
466 }
467
468 #[test]
469 fn the_water_filled_mse_is_active_modes_over_the_level_plus_the_inactive_eigenvalues() {
470 let (t, r, noise, power) = (4, 2, 1.0, 0.4);
472 let rh = exponential(t, 0.9, r);
473 let opt = Training::mmse_optimal(&rh.view(), noise, r, power, t).unwrap();
474 let (_, lambda, _) = svd(&rh.view());
475 let pw = water_filling_powers(&lambda, power / (noise * r as f64)).unwrap();
476 let active: Vec<usize> = (0..t).filter(|&i| pw[i] > 0.0).collect();
477 assert!(active.len() < t);
478 let mu = pw[active[0]] + 1.0 / lambda[active[0]];
479 let want = active.len() as f64 / mu + (0..t).filter(|i| pw[*i] == 0.0).map(|i| lambda[i]).sum::<f64>();
480 let got = opt.mse(&ChannelEstimator::Mmse(rh.clone()), &rh.view(), noise, r).unwrap();
481 assert!(close(got, want), "{got} {want}");
482 assert!(opt.mse(&ChannelEstimator::Ls, &rh.view(), noise, r).is_err());
484 }
485
486 #[test]
487 fn the_estimators_are_exact_without_noise_and_scaled_ls_shrinks_ls() {
488 let (t, r) = (2, 3);
489 let p = Training::orthogonal(t, 4, 8.0).unwrap();
490 let h = CMat::from_shape_fn((r, t), |(i, j)| C64::new(i as f64 - 1.0, j as f64 + 0.5));
491 let s = matmul(&h.view(), &p.symbols().view());
492 for est in [ChannelEstimator::Ls, ChannelEstimator::ScaledLs(TracePrior::FromLs)] {
493 let e = p.estimate(&s.view(), &est, 0.0).unwrap();
494 assert!((&e - &h).iter().all(|z| z.norm() < 1e-12), "{est:?}");
495 }
496 let ls = p.estimate(&s.view(), &ChannelEstimator::Ls, 1.0).unwrap();
497 let sls = p.estimate(&s.view(), &ChannelEstimator::ScaledLs(TracePrior::Known(6.0)), 1.0).unwrap();
498 let gamma = 6.0 / (3.0 * 2.0 * 2.0 / 8.0 + 6.0);
500 assert!((&sls - &(ls * C64::from(gamma))).iter().all(|z| z.norm() < 1e-12));
501 }
502
503 #[test]
504 fn the_blue_weights_are_the_power_shares_for_orthogonal_trainings() {
505 let trainings: Vec<Training> =
506 [1.0, 3.0, 4.0].iter().map(|&pw| Training::orthogonal(2, 2, pw).unwrap()).collect();
507 let w = blue_weights(&trainings).unwrap();
508 for (wk, pk) in w.iter().zip([1.0, 3.0, 4.0]) {
509 assert!(close(*wk, pk / 8.0));
510 }
511 assert!(close(blue_mse(&trainings, 0.5, 3).unwrap(), 0.5 * 4.0 * 3.0 / 8.0));
513 let hs = vec![
514 CMat::from_elem((1, 2), C64::from(1.0)),
515 CMat::from_elem((1, 2), C64::from(3.0)),
516 CMat::from_elem((1, 2), C64::from(2.0)),
517 ];
518 let c = blue_combine(&hs, &w).unwrap();
519 assert!((c[(0, 1)] - C64::from((1.0 + 9.0 + 8.0) / 8.0)).norm() < 1e-12);
520 }
521
522 #[test]
523 fn invalid_inputs_are_errors() {
524 assert!(Training::orthogonal(3, 2, 1.0).is_err());
525 assert!(Training::orthogonal(2, 2, 0.0).is_err());
526 let p = Training::orthogonal(2, 2, 1.0).unwrap();
527 let s = CMat::zeros((2, 3));
528 assert!(matches!(p.estimate(&s.view(), &ChannelEstimator::Ls, 0.1), Err(Error::Shape(_))));
529 let rh = identity(2);
530 assert!(p.mse(&ChannelEstimator::ScaledLs(TracePrior::FromLs), &rh.view(), 0.1, 1).is_err());
531 assert!(p.mse(&ChannelEstimator::ScaledLs(TracePrior::Known(-1.0)), &rh.view(), 0.1, 1).is_err());
532 assert!(p.mse(&ChannelEstimator::Mmse(identity(3)), &rh.view(), 0.1, 1).is_err());
533 assert!(blue_weights(&[]).is_err());
534 assert!(blue_combine(&[CMat::zeros((1, 2))], &[0.5, 0.5]).is_err());
535 }
536}