1use super::{CeOfdm, TAU};
45use comnum_core::{Result, ensure, ensure_shape};
46use comnum_mapping::constellation::Constellation;
47use comnum_math::C64;
48use comnum_mimo::message_passing::{Domain, Prior};
49
50#[derive(Clone, Debug, PartialEq)]
52pub struct Gamp {
53 pub iterations: usize,
55 pub damping: f64,
57 pub points: usize,
59}
60
61impl Default for Gamp {
62 fn default() -> Gamp {
64 Gamp { iterations: 50, damping: 0.5, points: 64 }
65 }
66}
67
68fn axis_levels(c: &Constellation) -> Result<(Prior, Prior)> {
71 let unique = |values: Vec<f64>| {
72 let mut v = values;
73 v.sort_by(f64::total_cmp);
74 v.dedup_by(|a, b| (*a - *b).abs() < 1e-9);
75 v
76 };
77 let re = unique(c.points().iter().map(|p| p.re).collect());
78 let im = unique(c.points().iter().map(|p| p.im).collect());
79 let near = |levels: &[f64], v: f64| levels.iter().any(|l| (l - v).abs() < 1e-9);
80 let product = re.len() * im.len() == c.order() && c.points().iter().all(|p| near(&re, p.re) && near(&im, p.im));
81 ensure(product, || {
82 "GAMP needs a constellation whose real and imaginary parts are independent (square QAM, PAM, BPSK)".into()
83 })?;
84 let prior = |levels: Vec<f64>| {
85 let m = levels.len();
86 Prior::Discrete {
87 points: levels.into_iter().map(|l| C64::new(l, 0.0)).collect(),
88 probs: vec![1.0 / m as f64; m],
89 }
90 };
91 Ok((prior(re), prior(im)))
92}
93
94fn input_moments(prior: &Prior, r: f64, tr: f64) -> (f64, f64) {
97 let (m, v) = prior.denoise(C64::new(r, 0.0), tr, Domain::Real);
98 (m.re, v)
99}
100
101const LAPLACE_CONCENTRATION: f64 = 25.0;
104
105const LAPLACE_SPREAD: f64 = 8.0;
108
109const QUADRATURE_HALF_WIDTH: f64 = 7.0;
112
113const POINTS_PER_BRANCH: f64 = 4.0;
115
116const MAX_QUADRATURE_POINTS: usize = 4096;
118
119fn output_moments(p: f64, tp: f64, c: f64, psi: f64, kappa: f64, points: usize) -> (f64, f64) {
122 let sd = tp.sqrt();
123 if c <= 0.0 {
124 return (p, tp);
125 }
126 let wl = 1.0 / (kappa * c.sqrt());
128 if c >= LAPLACE_CONCENTRATION {
129 let v = wl * wl;
131 let spread = LAPLACE_SPREAD * (tp + v).sqrt();
132 let m_lo = ((kappa * (p - spread) - psi) / TAU).floor() as i64;
133 let m_hi = ((kappa * (p + spread) - psi) / TAU).ceil() as i64;
134 let (mut s0, mut s1, mut s2) = (0.0, 0.0, 0.0);
135 let mut terms = Vec::with_capacity((m_hi - m_lo + 1) as usize);
136 for m in m_lo..=m_hi {
137 let mu = (psi + TAU * m as f64) / kappa;
138 terms.push((mu, -(mu - p).powi(2) / (2.0 * (tp + v))));
139 }
140 let top = terms.iter().map(|t| t.1).fold(f64::NEG_INFINITY, f64::max);
141 let var = tp * v / (tp + v);
142 for (mu, lw) in terms {
143 let w = (lw - top).exp();
144 let mean = (mu * tp + p * v) / (tp + v);
145 s0 += w;
146 s1 += w * mean;
147 s2 += w * (var + mean * mean);
148 }
149 let mean = s1 / s0;
150 return (mean, (s2 / s0 - mean * mean).max(0.0));
151 }
152 let span = 2.0 * QUADRATURE_HALF_WIDTH * sd;
155 let step = (span / points as f64).min(wl / POINTS_PER_BRANCH);
156 let n = ((span / step).ceil() as usize).clamp(points, MAX_QUADRATURE_POINTS);
157 let h = span / n as f64;
158 let z0 = p - QUADRATURE_HALF_WIDTH * sd;
159 let logw = |z: f64| -(z - p).powi(2) / (2.0 * tp) + c * (kappa * z - psi).cos();
160 let top = (0..n).map(|i| logw(z0 + (i as f64 + 0.5) * h)).fold(f64::NEG_INFINITY, f64::max);
161 let (mut s0, mut s1, mut s2) = (0.0, 0.0, 0.0);
162 for i in 0..n {
163 let z = z0 + (i as f64 + 0.5) * h;
164 let w = (logw(z) - top).exp();
165 s0 += w;
166 s1 += w * z;
167 s2 += w * z * z;
168 }
169 let mean = s1 / s0;
170 (mean, (s2 / s0 - mean * mean).max(0.0))
171}
172
173impl Gamp {
174 pub fn detect(&self, ce: &CeOfdm, r: &[C64], c: &Constellation, noise_var: f64) -> Result<Vec<C64>> {
197 ce.check()?;
198 ensure(self.iterations >= 1 && self.points >= 2, || "GAMP needs one iteration and two points".into())?;
199 ensure(self.damping > 0.0 && self.damping <= 1.0, || {
200 format!("the damping must lie in (0, 1], got {}", self.damping)
201 })?;
202 ensure(noise_var > 0.0, || format!("the noise variance must be positive, got {noise_var}"))?;
203 let (k, l) = (ce.n_subcarriers, ce.block_len());
204 ensure_shape(r.len() % l == 0, || format!("{} samples is not a multiple of N + N_cp = {l}", r.len()))?;
205 let (re, im) = axis_levels(c)?;
206 let mut out = Vec::with_capacity(r.len() / l * k);
207 for block in r.chunks(l) {
208 out.extend(self.block(ce, comnum_dsp::signal::strip_prefix(block, ce.n_cp)?, &re, &im, noise_var));
209 }
210 Ok(out)
211 }
212
213 fn block(&self, ce: &CeOfdm, r: &[C64], re: &Prior, im: &Prior, noise_var: f64) -> Vec<C64> {
214 let (k, n) = (ce.n_subcarriers, ce.n_fft());
215 let kappa = TAU * ce.h;
216 let a = 2.0 / (2.0 * k as f64).sqrt();
217 let beta = self.damping;
218 let forward = |u: &[C64]| ce.synthesize(u);
220 let adjoint = |y: &[f64]| -> Vec<C64> { ce.data_bins(y).into_iter().map(|v| v * a).collect() };
221 let mut u_hat = vec![C64::new(re.mean().re, im.mean().re); k];
222 let mut u_bar = u_hat.clone();
223 let prior_var = re.variance() + im.variance();
224 let mut tu = prior_var / 2.0;
225 let mut s_hat = vec![0.0; n];
226 let mut ts = 0.0;
227 let likelihood: Vec<(f64, f64)> = r.iter().map(|v| (2.0 * v.norm() / noise_var, v.arg())).collect();
228 for t in 0..self.iterations {
229 let tp = 2.0 * tu;
231 let au = forward(&u_hat);
232 let mut ts_new = 0.0;
233 for (i, (&(cn, psi), x)) in likelihood.iter().zip(&au).enumerate() {
234 let p = x - tp * s_hat[i];
235 let (z, tz) = output_moments(p, tp, cn, psi, kappa, self.points);
236 let s = (z - p) / tp;
237 s_hat[i] = if t == 0 { s } else { (1.0 - beta) * s_hat[i] + beta * s };
238 ts_new += (1.0 - tz / tp).max(0.0) / tp;
240 }
241 ts_new /= n as f64;
242 ts = if t == 0 { ts_new } else { (1.0 - beta) * ts + beta * ts_new };
243 for (b, h) in u_bar.iter_mut().zip(&u_hat) {
245 *b = if t == 0 { *h } else { (1.0 - beta) * *b + beta * h };
246 }
247 let tr = k as f64 / (n as f64 * ts.max(1e-300));
248 let back = adjoint(&s_hat);
249 let mut next = Vec::with_capacity(k);
250 let mut tu_new = 0.0;
251 for (b, g) in u_bar.iter().zip(&back) {
252 let rv = b + g * tr;
253 let (mr, vr) = input_moments(re, rv.re, tr);
254 let (mi, vi) = input_moments(im, rv.im, tr);
255 next.push(C64::new(mr, mi));
256 tu_new += vr + vi;
257 }
258 if next.iter().any(|v| !v.is_finite()) {
259 break;
260 }
261 u_hat = next;
262 tu = tu_new / (2 * k) as f64;
263 if tu < 1e-9 * prior_var {
265 break;
266 }
267 }
268 u_hat
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275 use crate::ce_ofdm::{Receiver, Unwrap};
276 use comnum_core::rng::Rng;
277
278 #[test]
279 fn the_output_step_matches_the_quadrature_and_the_input_step_the_levels() {
280 for psi in [0.5, 2.0, 3.0, -3.1] {
282 let (m1, v1) = output_moments(0.3, 0.8, 30.0, psi, 1.4, 64);
283 let (sd, n) = (0.8f64.sqrt(), 400_000);
284 let h = 16.0 * sd / n as f64;
285 let (mut s0, mut s1, mut s2) = (0.0, 0.0, 0.0);
286 for i in 0..n {
287 let z = 0.3 - 8.0 * sd + (i as f64 + 0.5) * h;
288 let w = (-(z - 0.3).powi(2) / 1.6 + 30.0 * ((1.4 * z - psi).cos() - 1.0)).exp();
289 (s0, s1, s2) = (s0 + w, s1 + w * z, s2 + w * z * z);
290 }
291 let (m2, v2) = (s1 / s0, s2 / s0 - (s1 / s0).powi(2));
292 assert!((m1 - m2).abs() < 0.05 && (v1 - v2).abs() < 0.05 * v2.max(0.01), "{psi}: {m1} {v1} vs {m2} {v2}");
294 }
295 assert_eq!(output_moments(0.2, 0.5, 0.0, 1.0, 1.0, 64), (0.2, 0.5));
297 let (bpsk, _) = axis_levels(&Constellation::psk(2).unwrap()).unwrap();
298 assert!(bpsk.mean().norm() < 1e-15 && (bpsk.variance() - 1.0).abs() < 1e-15);
299 let (m, v) = input_moments(&bpsk, 0.9, 0.01);
300 assert!((m - 1.0).abs() < 1e-6 && v < 1e-6);
301 }
302
303 #[test]
304 fn gamp_detects_where_the_phase_demodulator_fails() {
305 let c = Constellation::qam(4).unwrap().normalized();
308 let ce = CeOfdm::new(63, 4, 1.4 / TAU).unwrap();
309 let mut rng = Rng::new(3);
310 let (_, d) = comnum_mapping::source::symbols(&c, 63 * 8, None, &mut rng).unwrap();
311 for ebn0 in [8.0, 30.0] {
312 let sigma2 = ce.noise_variance(ebn0 + comnum_math::lin2db(2.0));
313 let r: Vec<C64> =
314 ce.modulate(&d).unwrap().into_iter().map(|v| v + rng.complex_gaussian() * sigma2.sqrt()).collect();
315 let ser = |e: &[C64]| {
316 c.decide(e).iter().zip(c.decide(&d)).filter(|(a, b)| *a != b).count() as f64 / d.len() as f64
317 };
318 let gamp = Gamp::default().detect(&ce, &r, &c, sigma2).unwrap();
319 let atan = ce.demodulate_with_receiver(&r, Receiver::Arctangent(Unwrap::None)).unwrap();
320 assert!(ser(&gamp) == 0.0 && ser(&atan) > 0.1, "{ebn0} dB: GAMP {} arctangent {}", ser(&gamp), ser(&atan));
321 }
322 let psk8 = Constellation::psk(8).unwrap();
324 assert!(Gamp::default().detect(&ce, &vec![C64::new(1.0, 0.0); ce.block_len()], &psk8, 0.1).is_err());
325 }
326}