1use crate::{check_input, check_rate};
52use comnum_core::signal::{Signal, Symbols};
53use comnum_core::traits::{Demodulate, Modulate};
54use comnum_core::units::Hertz;
55use comnum_core::{Result, ensure, ensure_shape};
56use comnum_dsp::filter::{convolve_full, normalize_energy, response_at};
57use comnum_dsp::window::chebyshev;
58use comnum_math::{C64, cis, fft};
59
60#[derive(Clone, Debug, PartialEq)]
64pub struct Ufmc {
65 pub n_fft: usize,
67 pub n_subbands: usize,
69 pub subband_size: usize,
71 pub filter_len: usize,
73 pub sidelobe_db: f64,
75 pub sample_rate: Hertz,
78}
79
80impl Default for Ufmc {
81 fn default() -> Ufmc {
85 Ufmc { n_fft: 64, n_subbands: 4, subband_size: 12, filter_len: 16, sidelobe_db: 40.0, sample_rate: Hertz(1.0) }
86 }
87}
88
89impl Ufmc {
90 pub fn check(&self) -> Result<()> {
97 let (b, q, n) = (self.n_subbands, self.subband_size, self.n_fft);
98 ensure(b >= 1 && q >= 1 && b * q <= n, || format!("{b} sub-bands of {q} do not fit a grid of {n}"))?;
99 ensure(self.filter_len >= 1 && self.filter_len <= n + 1, || {
100 format!("a filter of {} taps for a grid of {n}: between 1 and N + 1", self.filter_len)
101 })?;
102 ensure(self.sidelobe_db.is_finite(), || "the sidelobe attenuation must be finite".into())?;
103 check_rate(self.sample_rate)
104 }
105
106 pub fn n_active(&self) -> usize {
108 self.n_subbands * self.subband_size
109 }
110
111 pub fn block_len(&self) -> usize {
113 self.n_fft + self.filter_len - 1
114 }
115
116 pub fn subcarrier(&self, i: usize) -> isize {
118 i as isize - (self.n_active() / 2) as isize
119 }
120
121 fn window(&self) -> Vec<f64> {
123 let mut w = chebyshev(self.filter_len, self.sidelobe_db);
124 normalize_energy(&mut w);
125 w
126 }
127
128 pub fn subband_taps(&self, b: usize) -> Vec<C64> {
131 self.shifted(&self.window(), b)
132 }
133
134 fn shifted(&self, window: &[f64], b: usize) -> Vec<C64> {
135 let first = self.subcarrier(b * self.subband_size) as f64;
136 let centre = first + (self.subband_size as f64 - 1.0) / 2.0;
137 let mid = (window.len() as f64 - 1.0) / 2.0;
139 window
140 .iter()
141 .enumerate()
142 .map(|(l, w)| cis(std::f64::consts::TAU * centre * (l as f64 - mid) / self.n_fft as f64) * *w)
143 .collect()
144 }
145
146 pub fn response(&self, i: usize) -> C64 {
149 self.response_of(&self.subband_taps(i / self.subband_size), i)
150 }
151
152 fn response_of(&self, taps: &[C64], i: usize) -> C64 {
153 response_at(taps, self.subcarrier(i) as f64, self.n_fft as f64)
154 }
155
156 pub fn modulate(&self, data: &[C64]) -> Result<Vec<C64>> {
162 self.check()?;
163 let (n, k) = (self.n_fft, self.n_active());
164 ensure_shape(data.len() % k == 0, || format!("{} symbols for {k} subcarriers", data.len()))?;
165 let scale = n as f64 / (k as f64).sqrt();
166 let window = self.window();
167 let filters: Vec<Vec<C64>> = (0..self.n_subbands).map(|b| self.shifted(&window, b)).collect();
168 let mut out = Vec::with_capacity(data.len() / k * self.block_len());
169 for symbol in data.chunks_exact(k) {
170 let mut x = vec![C64::new(0.0, 0.0); self.block_len()];
171 for (b, f) in filters.iter().enumerate() {
172 let mut grid = vec![C64::new(0.0, 0.0); n];
173 for q in 0..self.subband_size {
174 let i = b * self.subband_size + q;
175 grid[fft::centred_bin(i, self.n_active(), n)] = symbol[i] * scale;
176 }
177 for (xt, v) in x.iter_mut().zip(convolve_full(f, &fft::ifft(&grid))) {
178 *xt += v;
179 }
180 }
181 out.extend(x);
182 }
183 Ok(out)
184 }
185
186 pub fn demodulate(&self, samples: &[C64]) -> Result<Vec<C64>> {
193 self.check()?;
194 let (n, len) = (self.n_fft, self.block_len());
195 ensure_shape(samples.len() % len == 0, || format!("{} samples for symbols of {len}", samples.len()))?;
196 let scale = n as f64 / (self.n_active() as f64).sqrt();
197 let window = self.window();
198 let filters: Vec<Vec<C64>> = (0..self.n_subbands).map(|b| self.shifted(&window, b)).collect();
199 let gains: Vec<C64> =
200 (0..self.n_active()).map(|i| self.response_of(&filters[i / self.subband_size], i) * scale).collect();
201 let mut out = Vec::with_capacity(samples.len() / len * self.n_active());
202 for block in samples.chunks_exact(len) {
203 let mut buf = block.to_vec();
204 buf.resize(2 * n, C64::new(0.0, 0.0));
205 let spec = fft::fft(&buf);
206 for (i, g) in gains.iter().enumerate() {
207 let bin = 2 * fft::centred_bin(i, self.n_active(), n);
209 out.push(spec[bin] / g);
210 }
211 }
212 Ok(out)
213 }
214}
215
216impl Modulate for Ufmc {
219 fn modulate_into(&self, x: &Symbols, out: &mut Signal) -> Result<()> {
220 out.set_stream(&Ufmc::modulate(self, crate::one_stream(x, "UFMC")?)?, self.sample_rate);
221 Ok(())
222 }
223}
224
225impl Demodulate for Ufmc {
227 fn demodulate_into(&mut self, y: &Signal, out: &mut Symbols) -> Result<()> {
231 check_input(y, self.sample_rate, "UFMC")?;
232 out.set_stream(&Ufmc::demodulate(self, y.stream(0))?);
233 Ok(())
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240 use comnum_core::rng::Rng;
241
242 #[test]
246 fn the_shared_filter_helpers_are_the_former_code() {
247 let u = Ufmc::default();
248 let w = chebyshev(16, 40.0);
249 let norm = w.iter().map(|v| v * v).sum::<f64>().sqrt();
250 let old: Vec<f64> = w.into_iter().map(|v| v / norm).collect();
251 assert_eq!(u.window(), old);
252 for i in 0..u.n_active() {
253 let taps = u.subband_taps(i / u.subband_size);
254 let k = u.subcarrier(i) as f64;
255 let former: C64 =
256 taps.iter().enumerate().map(|(l, f)| f * cis(-std::f64::consts::TAU * k * l as f64 / 64.0)).sum();
257 assert_eq!(u.response(i), former);
258 }
259 }
260
261 #[test]
262 fn the_sub_bands_are_orthogonal_and_the_filters_unit_energy() {
263 let u = Ufmc { n_fft: 128, n_subbands: 6, filter_len: 32, sidelobe_db: 50.0, ..Ufmc::default() };
264 let e: f64 = u.subband_taps(0).iter().map(C64::norm_sqr).sum();
265 assert!((e - 1.0).abs() < 1e-12);
266 let mut d = vec![C64::new(0.0, 0.0); u.n_active()];
268 for v in &mut d[24..36] {
269 *v = C64::new(1.0, -1.0);
270 }
271 let y = u.demodulate(&u.modulate(&d).unwrap()).unwrap();
272 assert!(y.iter().zip(&d).all(|(a, b)| (a - b).norm() < 1e-10));
273 }
274
275 #[test]
276 fn the_sidelobes_fall_faster_than_those_of_ofdm() {
277 let mut r = Rng::new(2);
281 let mut level = |u: &Ufmc| {
282 let n = u.n_fft;
283 let mut p = vec![0.0; 8 * n];
284 for _ in 0..200 {
285 let mut d = vec![C64::new(0.0, 0.0); u.n_active()];
286 for v in &mut d[..12] {
287 *v = C64::new(if r.below(2) == 0 { 1.0 } else { -1.0 }, if r.below(2) == 0 { 1.0 } else { -1.0 });
288 }
289 let mut x = u.modulate(&d).unwrap();
290 x.resize(8 * n, C64::new(0.0, 0.0));
291 for (a, b) in p.iter_mut().zip(fft::fft(&x)) {
292 *a += b.norm_sqr();
293 }
294 }
295 let at = |k: isize| p[(8 * k + 4).rem_euclid(8 * n as isize) as usize];
297 let first = u.subcarrier(0);
298 comnum_math::lin2db(at(first - 8) / at(first + 6))
299 };
300 let ufmc = level(&Ufmc { n_fft: 128, filter_len: 33, sidelobe_db: 60.0, ..Ufmc::default() });
301 let ofdm = level(&Ufmc { n_fft: 128, filter_len: 1, sidelobe_db: 60.0, ..Ufmc::default() });
302 assert!(ufmc < ofdm - 20.0, "UFMC {ufmc} dB, OFDM {ofdm} dB");
303 }
304
305 #[test]
306 fn filters_longer_than_the_receiver_dft_are_rejected() {
307 assert!(Ufmc { filter_len: 65, ..Ufmc::default() }.check().is_ok());
308 assert!(Ufmc { filter_len: 66, ..Ufmc::default() }.check().is_err());
309 assert!(Ufmc { filter_len: 0, ..Ufmc::default() }.check().is_err());
310 assert!(Ufmc { n_subbands: 6, ..Ufmc::default() }.check().is_err());
311 }
312}