Skip to main content

comnum_waveform/ofdm/
wiener.rs

1//! Two-dimensional Wiener interpolation of pilots in time and frequency (Hoeher,
2//! Kaiser & Robertson 1997).
3//!
4//! The channel $h_{l,k}$ of symbol $l$ and subcarrier $k$ is observed at the pilots
5//! $\mathcal{P}$, $\tilde{h}_{l',k'} = h_{l',k'} + n_{l',k'}$ (the LS estimates, noise of
6//! variance $\sigma^{2}/E_{p}$), and every position is estimated by a linear filter of
7//! the $N_{\mathrm{tap}}$ pilots $\mathcal{T}(l, k) \subset \mathcal{P}$ nearest to it,
8//! their eqs. (2) and (9),
9//!
10//! $$
11//! \hat{h}_{l,k} = \underline{w}^{T} \underline{\tilde{h}}, \qquad
12//! \underline{w}^{T} = \underline{\theta}^{T}\, \Phi^{-1}, \qquad
13//! J_{o}(l, k) = 1 - \underline{\theta}^{T} \Phi^{-1} \underline{\theta}^{*},
14//! $$
15//!
16//! with $\theta = E[h_{l,k} \tilde{h}^{*}]$ over the taps and $\Phi = E[\tilde{h}
17//! \tilde{h}^{H}] = R + \frac{\sigma^{2}}{E_{p}} I$ (eqs. (6)–(7), (12)): the linear MMSE
18//! of [`comnum_dsp::estimation::lmmse`] on the taps. For a WSSUS channel the correlation
19//! is separable (eq. (15)),
20//!
21//! $$
22//! R(\Delta l, \Delta k) = r_{t}(\Delta l)\, r_{f}(\Delta k),
23//! $$
24//!
25//! the time correlation $r_{t}$ of the Doppler spectrum at the symbol period $T_{s}$
26//! (uniform: $\mathrm{si}(2\pi f_{D} T_{s} \Delta l)$; Jakes: $J_{0}(2\pi f_{D} T_{s}
27//! \Delta l)$) and the frequency correlation $r_{f}$ of the delay power spectrum at the
28//! subcarrier spacing $\Delta F$ (uniform on $[-\tau_{\max}, \tau_{\max}]$:
29//! $\mathrm{si}(2\pi \tau_{\max} \Delta F \Delta k)$, or a [`PowerDelayProfile`]). The
30//! taps are the pilots nearest in the weighted distance $|\Delta l| f_{D} T_{s} +
31//! |\Delta k| \tau \Delta F$ (their Sec. 5, method B), ties broken by the smaller time
32//! distance then the pilot order. With pilots in one symbol only, the filter is the 1-D
33//! Wiener filter in frequency of their Fig. 2.
34//!
35//! The sampling theorem of the grid (their eq. (17)): pilots every $N_{t}$ symbols and
36//! $N_{f}$ subcarriers resolve the channel if $f_{D} T_{s} N_{t} \le 1/2$ and $\tau_{\max}
37//! \Delta F N_{f} \le 1/2$ (two-sided spreads), $1/4$ for a twofold oversampling.
38//!
39//! # Examples
40//! ```
41//! use comnum_waveform::ofdm::wiener::{Correlation2d, DelaySpectrum, DopplerSpectrum, Wiener2d};
42//! // pilots every 3 symbols and 6 subcarriers on a block of 12 x 24
43//! let pilots: Vec<(usize, usize)> =
44//!     (0..12).step_by(3).flat_map(|l| (0..24).step_by(6).map(move |k| (l, k))).collect();
45//! let corr = Correlation2d { fd_ts: 0.0832, doppler: DopplerSpectrum::Uniform, delay: DelaySpectrum::Uniform { tau_df: 0.0417 } };
46//! let w = Wiener2d::new(pilots, corr, 0.01, 10)?;
47//! let mse = w.mse(&[(1, 2), (0, 0)])?;
48//! assert!(mse[1] < 0.01 && mse[0] < 0.05);
49//! # Ok::<(), comnum_core::Error>(())
50//! ```
51//!
52//! # The robust estimator of Li, Cimini & Sollenberger (1998)
53//!
54//! An estimator designed for the worst channel of maximum Doppler $f_{\max}$ and delay
55//! spread $t_{\max}$, a flat band-limited Doppler spectrum and a uniform delay profile
56//! on the $K_{o} = \lfloor K t_{\max}/T_{s} \rfloor + 1$ taps of the DFT grid between 0 and
57//! $t_{\max}$ (their eq. (56) prints $\lceil K t_{\max}/T_{s} \rceil$, one tap fewer when
58//! $K t_{\max}/T_{s}$ is an integer; their figures follow the count of the taps), filters every kept tap in time with the temporal Wiener filter of the
59//! correlation $\mathrm{sinc}(n\omega_{d}/\pi)$, $\omega_{d} = 2\pi f_{\max} T_{f}$ (block
60//! period $T_{f}$). Its MSE, normalised to the channel power, is for an infinite filter
61//! (eq. (57))
62//!
63//! $$
64//! \overline{\mathrm{MMSE}} = \frac{K_{o}\rho}{K}\left(1 - \left[\frac{\pi K}{K_{o} \rho
65//! \omega_{d}} + 1\right]^{-\omega_{d}/\pi}\right),
66//! $$
67//!
68//! $\rho$ the noise to signal ratio, and for $L$ taps $\overline{\mathrm{MSE}} = K_{o} c_{0}
69//! \rho / K$ with $(c_{0}, \dots, c_{L-1})^{T} = (\bar{R}_{t} + \frac{K_{o}\rho}{K}
70//! I)^{-1} \bar{r}_{t}$ (eqs. (69)–(74)): [`RobustDesign::mse`] and [`RobustDesign::fir`].
71//! It bounds the MSE of every channel within the design (their eqs. (34)–(36)).
72//!
73//! # References
74//!
75//! 1. P. Hoeher, S. Kaiser and P. Robertson, "Two-dimensional pilot-symbol-aided channel
76//!    estimation by Wiener filtering," *Proc. IEEE ICASSP*, vol. 3, 1845–1848, 1997.
77//! 2. Y. Li, L. J. Cimini and N. R. Sollenberger, "Robust channel estimation for OFDM
78//!    systems with rapid dispersive fading channels," *IEEE Trans. Commun.*, 46(7),
79//!    902–915, 1998.
80
81use super::estimation::PowerDelayProfile;
82use comnum_core::{Result, ensure, ensure_shape};
83use comnum_dsp::estimation::{Lmmse, lmmse_mse};
84use comnum_math::linalg::CMat;
85use comnum_math::{C64, bessel_j0, sinc};
86use std::f64::consts::PI;
87
88/// The Doppler spectrum of every path, of maximum Doppler $f_{D}$.
89#[derive(Clone, Copy, Debug, PartialEq, Eq)]
90pub enum DopplerSpectrum {
91    /// Uniform on $[-f_{D}, f_{D}]$: $r_{t}(\Delta l) = \mathrm{si}(2\pi f_{D} T_{s} \Delta l)$
92    /// (Hoeher et al. 1997, eq. (15)).
93    Uniform,
94    /// Jakes (Clarke): $r_{t}(\Delta l) = J_{0}(2\pi f_{D} T_{s} \Delta l)$.
95    Jakes,
96}
97
98/// The delay power spectrum of the channel.
99#[derive(Clone, Debug, PartialEq)]
100pub enum DelaySpectrum {
101    /// Uniform on $[-\tau_{\max}, \tau_{\max}]$, `tau_df` = $\tau_{\max} \Delta F$:
102    /// $r_{f}(\Delta k) = \mathrm{si}(2\pi \tau_{\max} \Delta F \Delta k)$ (eq. (15)).
103    Uniform {
104        /// The one-sided maximum delay over the useful duration, $\tau_{\max} \Delta F$.
105        tau_df: f64,
106    },
107    /// A delay profile on the sampling grid of the symbol
108    /// ([`PowerDelayProfile::correlation`]).
109    Profile(PowerDelayProfile),
110}
111
112/// The separable time-frequency correlation $r_{t}(\Delta l)\, r_{f}(\Delta k)$ of a
113/// WSSUS channel of unit power (see the module).
114#[derive(Clone, Debug, PartialEq)]
115pub struct Correlation2d {
116    /// The maximum Doppler over the symbol rate, $f_{D} T_{s}$.
117    pub fd_ts: f64,
118    /// The Doppler spectrum.
119    pub doppler: DopplerSpectrum,
120    /// The delay power spectrum.
121    pub delay: DelaySpectrum,
122}
123
124impl Correlation2d {
125    /// $E[h_{l + \Delta l, k + \Delta k}\, h_{l,k}^{*}]$.
126    pub fn at(&self, dl: isize, dk: isize) -> C64 {
127        let x = 2.0 * self.fd_ts * dl as f64;
128        let rt = match self.doppler {
129            DopplerSpectrum::Uniform => sinc(x),
130            DopplerSpectrum::Jakes => bessel_j0(PI * x),
131        };
132        let rf = match &self.delay {
133            DelaySpectrum::Uniform { tau_df } => C64::new(sinc(2.0 * tau_df * dk as f64), 0.0),
134            DelaySpectrum::Profile(p) => p.correlation(dk as f64),
135        };
136        rf * rt
137    }
138
139    /// The weight of a subcarrier in the distance of the tap selection: $\tau_{\max}
140    /// \Delta F$, or the largest delay of a profile over its FFT size.
141    fn frequency_weight(&self) -> f64 {
142        match &self.delay {
143            DelaySpectrum::Uniform { tau_df } => *tau_df,
144            DelaySpectrum::Profile(p) => p.delays().iter().fold(0.0f64, |m, &d| m.max(d)) / p.n_fft() as f64,
145        }
146    }
147}
148
149/// The 2-D Wiener interpolator of a set of pilots (see the module).
150#[derive(Clone, Debug, PartialEq)]
151pub struct Wiener2d {
152    pilots: Vec<(usize, usize)>,
153    corr: Correlation2d,
154    noise_to_pilot: f64,
155    n_taps: usize,
156}
157
158impl Wiener2d {
159    /// The interpolator of the pilots at `(symbol, subcarrier)` positions, for the
160    /// correlation `corr`, the noise of the LS estimates `noise_to_pilot` =
161    /// $\sigma^{2}/E_{p}$ and $N_{\mathrm{tap}}$ = `n_taps` pilots per position (at most
162    /// the number of pilots).
163    ///
164    /// # Errors
165    /// [`comnum_core::Error::Config`] without pilots, for no tap or more taps than
166    /// pilots, a negative noise or Doppler.
167    pub fn new(
168        pilots: Vec<(usize, usize)>,
169        corr: Correlation2d,
170        noise_to_pilot: f64,
171        n_taps: usize,
172    ) -> Result<Wiener2d> {
173        ensure(!pilots.is_empty(), || "the interpolator needs pilots".into())?;
174        ensure((1..=pilots.len()).contains(&n_taps), || {
175            format!("{n_taps} taps for {} pilots: between 1 and the number of pilots", pilots.len())
176        })?;
177        ensure(noise_to_pilot >= 0.0 && corr.fd_ts >= 0.0, || "the noise and the Doppler must be non-negative".into())?;
178        Ok(Wiener2d { pilots, corr, noise_to_pilot, n_taps })
179    }
180
181    /// The pilots, `(symbol, subcarrier)`.
182    pub fn pilots(&self) -> &[(usize, usize)] {
183        &self.pilots
184    }
185
186    /// The pilots per estimated position, $N_{\mathrm{tap}}$.
187    pub fn n_taps(&self) -> usize {
188        self.n_taps
189    }
190
191    /// The pilots (indices into the pilot list) used for the position `(l, k)`: the
192    /// $N_{\mathrm{tap}}$ nearest in $|\Delta l| f_{D} T_{s} + |\Delta k| \tau \Delta F$.
193    pub fn taps(&self, l: usize, k: usize) -> Vec<usize> {
194        let (wt, wf) = (self.corr.fd_ts, self.corr.frequency_weight());
195        let mut order: Vec<(f64, usize, usize)> = self
196            .pilots
197            .iter()
198            .enumerate()
199            .map(|(i, &(pl, pk))| {
200                let (dl, dk) = (pl.abs_diff(l), pk.abs_diff(k));
201                (dl as f64 * wt + dk as f64 * wf, dl, i)
202            })
203            .collect();
204        order.sort_by(|a, b| a.0.total_cmp(&b.0).then(a.1.cmp(&b.1)).then(a.2.cmp(&b.2)));
205        // the selected pilots in their own order: positions with the same taps share a
206        // factorisation ([`Wiener2d::plan`])
207        let mut taps: Vec<usize> = order.into_iter().take(self.n_taps).map(|(_, _, i)| i).collect();
208        taps.sort_unstable();
209        taps
210    }
211
212    /// The positions of `targets` grouped by their taps: `(taps, indices into targets)`,
213    /// in the order of their first position.
214    fn groups(&self, targets: &[(usize, usize)]) -> Vec<(Vec<usize>, Vec<usize>)> {
215        let mut groups: Vec<(Vec<usize>, Vec<usize>)> = Vec::new();
216        let mut index: std::collections::HashMap<Vec<usize>, usize> = std::collections::HashMap::new();
217        for (t, &(l, k)) in targets.iter().enumerate() {
218            let taps = self.taps(l, k);
219            if let Some(&g) = index.get(&taps) {
220                groups[g].1.push(t);
221            } else {
222                index.insert(taps.clone(), groups.len());
223                groups.push((taps, vec![t]));
224            }
225        }
226        groups
227    }
228
229    /// The correlations of the positions `members` of `targets` with the taps (one row
230    /// each) and of the taps.
231    fn group_system(&self, targets: &[(usize, usize)], members: &[usize], taps: &[usize]) -> (CMat, CMat) {
232        let d = |a: usize, b: usize| a as isize - b as isize;
233        let theta = CMat::from_shape_fn((members.len(), taps.len()), |(i, j)| {
234            let ((l, k), (pl, pk)) = (targets[members[i]], self.pilots[taps[j]]);
235            self.corr.at(d(l, pl), d(k, pk))
236        });
237        let r = CMat::from_shape_fn((taps.len(), taps.len()), |(i, j)| {
238            let ((il, ik), (jl, jk)) = (self.pilots[taps[i]], self.pilots[taps[j]]);
239            self.corr.at(d(il, jl), d(ik, jk))
240        });
241        (theta, r)
242    }
243
244    /// The estimator of the positions `targets`, factored once per set of taps: apply it
245    /// to the LS estimates of every symbol that shares the pilots and the correlation.
246    ///
247    /// # Errors
248    /// [`comnum_core::Error::Numerical`] for a singular system.
249    pub fn plan(&self, targets: &[(usize, usize)]) -> Result<WienerPlan> {
250        let groups = self
251            .groups(targets)
252            .into_iter()
253            .map(|(taps, members)| {
254                let (theta, r) = self.group_system(targets, &members, &taps);
255                Ok((taps, members, Lmmse::new(theta, &r, self.noise_to_pilot)?))
256            })
257            .collect::<Result<_>>()?;
258        Ok(WienerPlan { n_pilots: self.pilots.len(), n_targets: targets.len(), groups })
259    }
260
261    /// The estimates at the positions `targets` from the LS estimates `h_ls` at the
262    /// pilots (in their order).
263    ///
264    /// # Errors
265    /// [`comnum_core::Error::Shape`] unless `h_ls` has one value per pilot,
266    /// [`comnum_core::Error::Numerical`] for a singular system.
267    pub fn estimate(&self, h_ls: &[C64], targets: &[(usize, usize)]) -> Result<Vec<C64>> {
268        self.plan(targets)?.apply(h_ls)
269    }
270
271    /// The mean square error $J_{o}$ of the estimate at every position of `targets`
272    /// (eq. (12)), for a channel of the assumed correlation.
273    ///
274    /// # Errors
275    /// [`comnum_core::Error::Numerical`] for a singular system.
276    pub fn mse(&self, targets: &[(usize, usize)]) -> Result<Vec<f64>> {
277        let mut out = vec![0.0; targets.len()];
278        for (taps, members) in self.groups(targets) {
279            let (theta, r) = self.group_system(targets, &members, &taps);
280            let mse = lmmse_mse(&theta, &r, self.noise_to_pilot, &vec![1.0; members.len()])?;
281            for (m, v) in members.iter().zip(mse) {
282                out[*m] = v;
283            }
284        }
285        Ok(out)
286    }
287}
288
289/// The [`Wiener2d`] estimator of a set of positions, one factorisation per set of taps
290/// ([`Wiener2d::plan`]).
291#[derive(Clone, Debug)]
292pub struct WienerPlan {
293    n_pilots: usize,
294    n_targets: usize,
295    groups: Vec<(Vec<usize>, Vec<usize>, Lmmse)>,
296}
297
298impl WienerPlan {
299    /// The estimates at the positions of the plan from the LS estimates `h_ls` at the
300    /// pilots (in their order).
301    ///
302    /// # Errors
303    /// [`comnum_core::Error::Shape`] unless `h_ls` has one value per pilot.
304    pub fn apply(&self, h_ls: &[C64]) -> Result<Vec<C64>> {
305        ensure_shape(h_ls.len() == self.n_pilots, || {
306            format!("{} LS estimates for {} pilots", h_ls.len(), self.n_pilots)
307        })?;
308        let mut out = vec![C64::new(0.0, 0.0); self.n_targets];
309        for (taps, members, lmmse) in &self.groups {
310            let obs: Vec<C64> = taps.iter().map(|&t| h_ls[t]).collect();
311            for (m, v) in members.iter().zip(lmmse.apply(&obs)?) {
312                out[*m] = v;
313            }
314        }
315        Ok(out)
316    }
317}
318
319/// The robust estimator of Li, Cimini & Sollenberger (1998) for $K$ tones (see the
320/// module): the design delay spread spans $K_{o}$ taps of the DFT grid, the design
321/// Doppler is $\omega_{d} = 2\pi f_{\max} T_{f}$.
322///
323/// # Examples
324/// ```
325/// use comnum_waveform::ofdm::wiener::RobustDesign;
326/// // 120 tones of 160 us, blocks of 200 us, 20 us and 40 Hz: the taps 0 to 15 (their Fig. 4)
327/// let d = RobustDesign::new(120, 20e-6 / 160e-6, 40.0 * 200e-6)?;
328/// assert_eq!(d.k_o, 16);
329/// let mse_db = 10.0 * d.mse(0.1).log10();                 // SNR 10 dB
330/// assert!((mse_db + 27.7).abs() < 0.1);
331/// let (c, mse50) = d.fir(50, 0.1)?;
332/// assert_eq!(c.len(), 50);
333/// assert!(mse50 > d.mse(0.1));                           // a finite filter does worse
334/// # Ok::<(), comnum_core::Error>(())
335/// ```
336#[derive(Clone, Copy, Debug, PartialEq)]
337pub struct RobustDesign {
338    /// Tones $K$.
339    pub k: usize,
340    /// Taps of the design delay spread on the DFT grid, $K_{o}$.
341    pub k_o: usize,
342    /// The design Doppler $\omega_{d} = 2\pi f_{\max} T_{f}$ (rad per block).
343    pub omega_d: f64,
344}
345
346impl RobustDesign {
347    /// The design of `k` tones for a delay spread `tmax_ts` (over the useful duration
348    /// $T_{s}$: the $K_{o} = \lfloor K t_{\max}/T_{s} \rfloor + 1$ taps of the grid from 0 to
349    /// $t_{\max}$, at most $K$) and a maximum Doppler
350    /// `fmax_tf` (over the block rate, $f_{\max} T_{f}$).
351    ///
352    /// # Errors
353    /// [`comnum_core::Error::Config`] without tones, for a negative spread or a Doppler
354    /// outside $[0, 1/2]$.
355    pub fn new(k: usize, tmax_ts: f64, fmax_tf: f64) -> Result<RobustDesign> {
356        ensure(k > 0, || "the design needs tones".into())?;
357        ensure(tmax_ts >= 0.0 && (0.0..=0.5).contains(&fmax_tf), || {
358            format!("the delay spread must be non-negative and f_max T_f in [0, 1/2], got {tmax_ts} and {fmax_tf}")
359        })?;
360        let k_o = ((k as f64 * tmax_ts).floor() as usize + 1).min(k);
361        Ok(RobustDesign { k, k_o, omega_d: 2.0 * PI * fmax_tf })
362    }
363
364    /// The MSE of the infinite-length estimator at the noise to signal ratio `rho`,
365    /// eq. (57) (normalised to the power of the channel).
366    pub fn mse(&self, rho: f64) -> f64 {
367        let a = self.k_o as f64 * rho / self.k as f64;
368        if self.omega_d == 0.0 {
369            return 0.0;
370        }
371        a * (1.0 - (PI / (a * self.omega_d) + 1.0).powf(-self.omega_d / PI))
372    }
373
374    /// The coefficients $c_{0}, \dots, c_{L-1}$ of the $L$-tap temporal filter and its MSE
375    /// $K_{o} c_{0} \rho / K$ at the noise to signal ratio `rho` (eqs. (69)–(74)).
376    ///
377    /// # Errors
378    /// [`comnum_core::Error::Config`] for no tap, [`comnum_core::Error::Numerical`] for a
379    /// singular system.
380    pub fn fir(&self, taps: usize, rho: f64) -> Result<(Vec<f64>, f64)> {
381        ensure(taps > 0, || "the filter needs at least one tap".into())?;
382        let a = self.k_o as f64 * rho / self.k as f64;
383        let r = |n: usize| sinc(n as f64 * self.omega_d / PI);
384        let m =
385            CMat::from_shape_fn((taps, taps), |(i, j)| C64::new(r(i.abs_diff(j)) + if i == j { a } else { 0.0 }, 0.0));
386        let b = CMat::from_shape_fn((taps, 1), |(i, _)| C64::new(r(i), 0.0));
387        let c: Vec<f64> =
388            comnum_math::linalg::solve_hpd(&m.view(), &b.view())?.column(0).iter().map(|v| v.re).collect();
389        let mse = a * c[0];
390        Ok((c, mse))
391    }
392}
393
394#[cfg(test)]
395mod tests {
396    use super::*;
397
398    fn corr() -> Correlation2d {
399        Correlation2d { fd_ts: 0.05, doppler: DopplerSpectrum::Uniform, delay: DelaySpectrum::Uniform { tau_df: 0.04 } }
400    }
401
402    #[test]
403    fn a_pilot_alone_is_estimated_with_the_mmse_of_its_own_noise() {
404        let w = Wiener2d::new(vec![(0, 0)], corr(), 0.1, 1).unwrap();
405        // one observation of variance 1 + 0.1: J = 1 - 1/1.1
406        assert!((w.mse(&[(0, 0)]).unwrap()[0] - (1.0 - 1.0 / 1.1)).abs() < 1e-12);
407        let h = w.estimate(&[C64::new(1.1, 0.0)], &[(0, 0)]).unwrap();
408        assert!((h[0] - C64::new(1.0, 0.0)).norm() < 1e-12);
409    }
410
411    #[test]
412    fn more_taps_never_increase_the_error_and_the_taps_are_the_nearest() {
413        let pilots: Vec<(usize, usize)> =
414            (0..9).step_by(3).flat_map(|l| (0..30).step_by(6).map(move |k| (l, k))).collect();
415        let mut last = f64::INFINITY;
416        for n in [1, 4, 9, 15] {
417            let j = Wiener2d::new(pilots.clone(), corr(), 0.01, n).unwrap().mse(&[(4, 9)]).unwrap()[0];
418            assert!(j <= last + 1e-12, "{n} taps: {j} after {last}");
419            last = j;
420        }
421        let w = Wiener2d::new(pilots.clone(), corr(), 0.01, 2).unwrap();
422        let near: Vec<(usize, usize)> = w.taps(3, 7).into_iter().map(|i| pilots[i]).collect();
423        // |dl| 0.05 + |dk| 0.04: (3, 6) at 0.04, (0, 6) at 0.19, before (3, 12) at 0.20;
424        // the taps in the order of the pilots
425        assert_eq!(near, vec![(0, 6), (3, 6)]);
426    }
427
428    #[test]
429    fn the_robust_fir_tends_to_eq_57_and_one_tap_is_the_static_mmse() {
430        let d = RobustDesign::new(120, 20e-6 / 160e-6, 0.008).unwrap();
431        let rho = 0.1;
432        let a = d.k_o as f64 * rho / 120.0;
433        assert!((d.fir(1, rho).unwrap().1 - a / (1.0 + a)).abs() < 1e-15);
434        // the filter converges slowly to eq. (57): 0.8 dB short at 100 taps in their Fig. 4
435        let long = d.fir(400, rho).unwrap().1;
436        assert!(long > d.mse(rho) && long < 1.1 * d.mse(rho), "{long} {}", d.mse(rho));
437        assert!(RobustDesign::new(0, 0.1, 0.01).is_err() && RobustDesign::new(8, 0.1, 0.6).is_err());
438        assert!(d.fir(0, rho).is_err());
439    }
440
441    #[test]
442    fn invalid_configurations_are_errors() {
443        assert!(Wiener2d::new(vec![], corr(), 0.1, 1).is_err());
444        assert!(Wiener2d::new(vec![(0, 0)], corr(), 0.1, 2).is_err());
445        assert!(Wiener2d::new(vec![(0, 0)], corr(), -0.1, 1).is_err());
446        let w = Wiener2d::new(vec![(0, 0), (0, 6)], corr(), 0.1, 1).unwrap();
447        assert!(w.estimate(&[C64::new(1.0, 0.0)], &[(0, 1)]).is_err());
448    }
449}