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}