Skip to main content

oxicuda_cs/measurement/
coded_diffraction.rs

1//! Coded Diffraction Patterns (CDP) and Wirtinger-Flow phase retrieval (Candès, Li & Soltanolkotabi 2015).
2//!
3//! In ptychography / coherent diffraction imaging one measures squared magnitudes of a
4//! *modulated* Fourier transform of the unknown (complex) signal `x ∈ ℂⁿ`:
5//!
6//! ```text
7//! y_{l} = | F (d_l ⊙ x) |² ,   l = 1, …, L
8//! ```
9//!
10//! where `d_l ∈ ℂⁿ` is a random *coded diffraction mask* (a per-pixel modulation) and
11//! `F` is the unnormalised DFT. Stacking the `L` patterns gives `m = L·n` real,
12//! phaseless measurements — enough (for `L ≳ 4`) to recover `x` up to a global phase.
13//!
14//! This module provides:
15//!
16//! - [`CodedDiffraction`] — a set of random complex masks defining the forward operator.
17//! - [`CodedDiffraction::forward`] — the phaseless measurement `y = |F(d_l ⊙ x)|²`.
18//! - [`CodedDiffraction::wirtinger_flow`] — recovery by Wirtinger Flow: spectral
19//!   initialisation followed by gradient descent on the intensity-domain loss
20//!   `½ Σ_l ‖ |A_l x|² − y_l ‖²` using the *Wirtinger* gradient.
21//!
22//! Signals are stored as interleaved real/imaginary `f64` pairs: `x[2k]` is `Re(x_k)`,
23//! `x[2k+1]` is `Im(x_k)`. All transforms are implemented from scratch (an O(n²) DFT),
24//! so the module depends only on `std`.
25//!
26//! # References
27//!
28//! - E. J. Candès, X. Li & M. Soltanolkotabi (2015), "Phase Retrieval from Coded
29//!   Diffraction Patterns", Applied and Computational Harmonic Analysis 39(2):277-299.
30//! - E. J. Candès, X. Li & M. Soltanolkotabi (2015), "Phase Retrieval via Wirtinger
31//!   Flow: Theory and Algorithms", IEEE Trans. Information Theory 61(4):1985-2007.
32
33use crate::error::{CsError, CsResult};
34use crate::handle::LcgRng;
35
36// ---------------------------------------------------------------------------
37// Lightweight complex helpers on interleaved [re, im] slices
38// ---------------------------------------------------------------------------
39
40#[inline]
41fn cmul(ar: f64, ai: f64, br: f64, bi: f64) -> (f64, f64) {
42    (ar * br - ai * bi, ar * bi + ai * br)
43}
44
45/// Unnormalised DFT of an interleaved complex vector `x` (length `2n`).
46///
47/// Returns interleaved `X_k = Σ_j x_j exp(−2πi jk/n)`.
48fn dft(x: &[f64], n: usize) -> Vec<f64> {
49    let mut out = vec![0.0_f64; 2 * n];
50    let two_pi = std::f64::consts::TAU;
51    for k in 0..n {
52        let mut sr = 0.0_f64;
53        let mut si = 0.0_f64;
54        for j in 0..n {
55            let angle = -two_pi * (j as f64) * (k as f64) / (n as f64);
56            let (wr, wi) = (angle.cos(), angle.sin());
57            let xr = x[2 * j];
58            let xi = x[2 * j + 1];
59            let (pr, pi) = cmul(xr, xi, wr, wi);
60            sr += pr;
61            si += pi;
62        }
63        out[2 * k] = sr;
64        out[2 * k + 1] = si;
65    }
66    out
67}
68
69/// Inverse (unnormalised by `1/n`) DFT of an interleaved complex vector.
70fn idft(x: &[f64], n: usize) -> Vec<f64> {
71    let mut out = vec![0.0_f64; 2 * n];
72    let two_pi = std::f64::consts::TAU;
73    let inv_n = 1.0 / (n as f64);
74    for k in 0..n {
75        let mut sr = 0.0_f64;
76        let mut si = 0.0_f64;
77        for j in 0..n {
78            let angle = two_pi * (j as f64) * (k as f64) / (n as f64);
79            let (wr, wi) = (angle.cos(), angle.sin());
80            let xr = x[2 * j];
81            let xi = x[2 * j + 1];
82            let (pr, pi) = cmul(xr, xi, wr, wi);
83            sr += pr;
84            si += pi;
85        }
86        out[2 * k] = sr * inv_n;
87        out[2 * k + 1] = si * inv_n;
88    }
89    out
90}
91
92// ---------------------------------------------------------------------------
93// Coded diffraction operator
94// ---------------------------------------------------------------------------
95
96/// A coded-diffraction forward operator: `L` random complex masks of length `n`.
97#[derive(Debug, Clone)]
98pub struct CodedDiffraction {
99    /// Signal length `n`.
100    pub n: usize,
101    /// Number of coded patterns `L`.
102    pub n_masks: usize,
103    /// Masks, interleaved complex, length `n_masks · 2n` (mask `l` occupies
104    /// `[l·2n .. (l+1)·2n]`).
105    masks: Vec<f64>,
106}
107
108/// Distribution from which coded-diffraction mask entries are drawn.
109#[derive(Debug, Clone, Copy)]
110pub enum MaskKind {
111    /// Octanary masks of Candès et al.: `d = b1 · b2` with `b1 ∈ {1, −1, i, −i}` and
112    /// `b2 ∈ {√2/2, √3}` (used in the CDP phase-retrieval guarantees).
113    Octanary,
114    /// Uniform-phase unit-modulus masks `d = exp(iθ)`, `θ ∼ U[0, 2π)`.
115    UniformPhase,
116    /// Real Rademacher masks `d ∈ {1, −1}` (no imaginary part).
117    Rademacher,
118}
119
120impl CodedDiffraction {
121    /// Construct `n_masks` random masks of length `n`.
122    ///
123    /// # Errors
124    /// [`CsError::InvalidParameter`] for `n == 0` or `n_masks == 0`.
125    pub fn new(n: usize, n_masks: usize, kind: MaskKind, rng: &mut LcgRng) -> CsResult<Self> {
126        if n == 0 || n_masks == 0 {
127            return Err(CsError::InvalidParameter(
128                "coded diffraction: n and n_masks must be > 0".into(),
129            ));
130        }
131        let mut masks = vec![0.0_f64; n_masks * 2 * n];
132        for l in 0..n_masks {
133            for j in 0..n {
134                let (re, im) = sample_mask_entry(kind, rng);
135                masks[l * 2 * n + 2 * j] = re;
136                masks[l * 2 * n + 2 * j + 1] = im;
137            }
138        }
139        Ok(Self { n, n_masks, masks })
140    }
141
142    /// Total number of real measurements `m = L · n`.
143    #[must_use]
144    pub fn n_measurements(&self) -> usize {
145        self.n_masks * self.n
146    }
147
148    /// Mask `l` as an interleaved complex slice of length `2n`.
149    fn mask(&self, l: usize) -> &[f64] {
150        &self.masks[l * 2 * self.n..(l + 1) * 2 * self.n]
151    }
152
153    /// Apply the `l`-th linear measurement operator `A_l x = F (d_l ⊙ x)` (complex).
154    ///
155    /// `x` is interleaved complex of length `2n`. Returns interleaved complex `2n`.
156    fn apply(&self, l: usize, x: &[f64]) -> Vec<f64> {
157        let d = self.mask(l);
158        let mut mod_x = vec![0.0_f64; 2 * self.n];
159        for j in 0..self.n {
160            let (pr, pi) = cmul(d[2 * j], d[2 * j + 1], x[2 * j], x[2 * j + 1]);
161            mod_x[2 * j] = pr;
162            mod_x[2 * j + 1] = pi;
163        }
164        dft(&mod_x, self.n)
165    }
166
167    /// Apply the adjoint `A_lᴴ z = d̄_l ⊙ F⁻¹(z)` (with the `1/n`-scaled inverse DFT
168    /// so that `A_lᴴ A_l` is well-conditioned). `z` interleaved complex length `2n`.
169    fn apply_adjoint(&self, l: usize, z: &[f64]) -> Vec<f64> {
170        let d = self.mask(l);
171        let inv = idft(z, self.n);
172        let mut out = vec![0.0_f64; 2 * self.n];
173        for j in 0..self.n {
174            // d̄ ⊙ inv: conjugate the mask.
175            let (pr, pi) = cmul(d[2 * j], -d[2 * j + 1], inv[2 * j], inv[2 * j + 1]);
176            out[2 * j] = pr;
177            out[2 * j + 1] = pi;
178        }
179        out
180    }
181
182    /// Phaseless forward measurement `y = |A_l x|²` stacked over all masks.
183    ///
184    /// `x` is interleaved complex (length `2n`). Output is real, length `m = L·n`,
185    /// laid out mask-major: `y[l·n + k] = |(A_l x)_k|²`.
186    ///
187    /// # Errors
188    /// [`CsError::DimensionMismatch`] if `x.len() != 2n`.
189    pub fn forward(&self, x: &[f64]) -> CsResult<Vec<f64>> {
190        if x.len() != 2 * self.n {
191            return Err(CsError::DimensionMismatch {
192                a: x.len(),
193                b: 2 * self.n,
194            });
195        }
196        let mut y = vec![0.0_f64; self.n_measurements()];
197        for l in 0..self.n_masks {
198            let ax = self.apply(l, x);
199            for k in 0..self.n {
200                let re = ax[2 * k];
201                let im = ax[2 * k + 1];
202                y[l * self.n + k] = re * re + im * im;
203            }
204        }
205        Ok(y)
206    }
207
208    /// Recover `x` (up to a global phase) from phaseless measurements `y` by Wirtinger Flow.
209    ///
210    /// * `y`        — measurements from [`forward`](Self::forward), length `m = L·n`.
211    /// * `cfg`      — algorithm configuration.
212    /// * `rng`      — RNG for the spectral-initialisation power iteration.
213    ///
214    /// Returns the recovered interleaved-complex signal (length `2n`).
215    ///
216    /// # Errors
217    /// * [`CsError::DimensionMismatch`] if `y.len() != m`.
218    /// * [`CsError::NumericalInstability`] if the spectral initialiser degenerates.
219    pub fn wirtinger_flow(
220        &self,
221        y: &[f64],
222        cfg: &WirtingerConfig,
223        rng: &mut LcgRng,
224    ) -> CsResult<Vec<f64>> {
225        let m = self.n_measurements();
226        if y.len() != m {
227            return Err(CsError::DimensionMismatch { a: y.len(), b: m });
228        }
229
230        // Normalisation constant λ² = (1/m) Σ y_l ·  (mean intensity scaled by n).
231        // Candès WF uses λ² = n · Σ y / Σ ‖a‖²; here ‖a_l‖² aggregates to L·n per pixel.
232        let sum_y: f64 = y.iter().sum();
233        let lambda_sq = (self.n as f64) * sum_y / (m as f64);
234        let lambda = lambda_sq.max(1e-30).sqrt();
235
236        // ── Spectral initialisation: leading eigenvector of  Y = (1/m) Σ y_r a_r a_rᴴ. ──
237        // Power iteration using only matrix-vector products via the operators.
238        let mut z = random_unit_complex(self.n, rng);
239        for _ in 0..cfg.power_iters {
240            let yz = self.apply_y(y, &z, m);
241            let nrm = cnorm(&yz);
242            if nrm < 1e-300 {
243                return Err(CsError::NumericalInstability(
244                    "WF spectral init: degenerate leading eigenvector".into(),
245                ));
246            }
247            for v in z.iter_mut() {
248                *v = 0.0;
249            }
250            for j in 0..2 * self.n {
251                z[j] = yz[j] / nrm;
252            }
253        }
254        // Scale the initial guess to the correct magnitude.
255        for v in z.iter_mut() {
256            *v *= lambda;
257        }
258
259        // ── Wirtinger gradient descent. ──
260        // Loss f(x) = (1/2m) Σ_r ( |a_rᴴ x|² − y_r )².
261        // Wirtinger gradient: ∇f = (1/m) Σ_r ( |a_rᴴ x|² − y_r ) (a_rᴴ x) a_r.
262        let step = cfg.step_size / lambda_sq.max(1e-30);
263        for _ in 0..cfg.max_iter {
264            let grad = self.wf_gradient(y, &z, m);
265            for j in 0..2 * self.n {
266                z[j] -= step * grad[j];
267            }
268        }
269
270        Ok(z)
271    }
272
273    /// Compute `Y z = (1/m) Σ_r y_r a_r (a_rᴴ z)` for the spectral initialiser.
274    fn apply_y(&self, y: &[f64], z: &[f64], m: usize) -> Vec<f64> {
275        let inv_m = 1.0 / (m as f64);
276        let mut acc = vec![0.0_f64; 2 * self.n];
277        for l in 0..self.n_masks {
278            // a_rᴴ z for every pixel r in this mask = (A_l z)*  ... we need per-pixel.
279            let az = self.apply(l, z); // (A_l z)_k for all k
280            // weight each pixel by y and form contribution back through adjoint.
281            let mut w = vec![0.0_f64; 2 * self.n];
282            for k in 0..self.n {
283                let yr = y[l * self.n + k];
284                // (a_rᴴ z) is the k-th entry of A_l z; multiply by y_r.
285                w[2 * k] = yr * az[2 * k];
286                w[2 * k + 1] = yr * az[2 * k + 1];
287            }
288            let contrib = self.apply_adjoint(l, &w);
289            for j in 0..2 * self.n {
290                acc[j] += contrib[j];
291            }
292        }
293        for v in acc.iter_mut() {
294            *v *= inv_m;
295        }
296        acc
297    }
298
299    /// Wirtinger gradient of the intensity loss at `z`.
300    fn wf_gradient(&self, y: &[f64], z: &[f64], m: usize) -> Vec<f64> {
301        let inv_m = 1.0 / (m as f64);
302        let mut grad = vec![0.0_f64; 2 * self.n];
303        for l in 0..self.n_masks {
304            let az = self.apply(l, z);
305            let mut resid = vec![0.0_f64; 2 * self.n];
306            for k in 0..self.n {
307                let re = az[2 * k];
308                let im = az[2 * k + 1];
309                let mag_sq = re * re + im * im;
310                let factor = mag_sq - y[l * self.n + k]; // (|a^H x|² − y_r)
311                resid[2 * k] = factor * re;
312                resid[2 * k + 1] = factor * im;
313            }
314            let contrib = self.apply_adjoint(l, &resid);
315            for j in 0..2 * self.n {
316                grad[j] += contrib[j];
317            }
318        }
319        for v in grad.iter_mut() {
320            *v *= inv_m;
321        }
322        grad
323    }
324}
325
326/// Configuration for Wirtinger Flow recovery.
327#[derive(Debug, Clone)]
328pub struct WirtingerConfig {
329    /// Number of power-iteration steps for spectral initialisation (default `50`).
330    pub power_iters: usize,
331    /// Number of gradient-descent iterations (default `400`).
332    pub max_iter: usize,
333    /// Base step size (scaled internally by `1/λ²`) (default `0.2`).
334    pub step_size: f64,
335}
336
337impl Default for WirtingerConfig {
338    fn default() -> Self {
339        Self {
340            power_iters: 50,
341            max_iter: 400,
342            step_size: 0.2,
343        }
344    }
345}
346
347// ---------------------------------------------------------------------------
348// Free helpers
349// ---------------------------------------------------------------------------
350
351/// Frobenius norm of an interleaved complex vector.
352fn cnorm(x: &[f64]) -> f64 {
353    x.iter().map(|v| v * v).sum::<f64>().sqrt()
354}
355
356/// Random unit-norm interleaved complex vector of `n` entries.
357fn random_unit_complex(n: usize, rng: &mut LcgRng) -> Vec<f64> {
358    let mut z = vec![0.0_f64; 2 * n];
359    for v in z.iter_mut() {
360        *v = rng.next_normal();
361    }
362    let nrm = cnorm(&z).max(1e-300);
363    for v in z.iter_mut() {
364        *v /= nrm;
365    }
366    z
367}
368
369/// Sample one coded-diffraction mask entry `(re, im)` from `kind`.
370fn sample_mask_entry(kind: MaskKind, rng: &mut LcgRng) -> (f64, f64) {
371    match kind {
372        MaskKind::Octanary => {
373            // b1 ∈ {1, −1, i, −i} (uniform), b2 ∈ {√2/2 w.p. 4/5, √3 w.p. 1/5}.
374            let phase = rng.next_usize(4);
375            let (pr, pi) = match phase {
376                0 => (1.0, 0.0),
377                1 => (-1.0, 0.0),
378                2 => (0.0, 1.0),
379                _ => (0.0, -1.0),
380            };
381            let b2 = if rng.next_f64() < 0.8 {
382                std::f64::consts::FRAC_1_SQRT_2
383            } else {
384                3.0_f64.sqrt()
385            };
386            (pr * b2, pi * b2)
387        }
388        MaskKind::UniformPhase => {
389            let theta = std::f64::consts::TAU * rng.next_f64();
390            (theta.cos(), theta.sin())
391        }
392        MaskKind::Rademacher => {
393            if rng.next_bool() {
394                (1.0, 0.0)
395            } else {
396                (-1.0, 0.0)
397            }
398        }
399    }
400}
401
402/// Align two interleaved-complex vectors up to a global phase and return the relative
403/// reconstruction error `‖x̂ e^{iφ} − x‖ / ‖x‖` minimised over the global phase `φ`.
404///
405/// Useful for tests since phase retrieval recovers `x` only up to `e^{iφ}`.
406///
407/// # Errors
408/// [`CsError::DimensionMismatch`] if the two vectors differ in length.
409pub fn phase_aligned_error(x_hat: &[f64], x: &[f64]) -> CsResult<f64> {
410    if x_hat.len() != x.len() {
411        return Err(CsError::DimensionMismatch {
412            a: x_hat.len(),
413            b: x.len(),
414        });
415    }
416    // Optimal phase: φ = arg(⟨x_hat, x⟩) where ⟨·,·⟩ = Σ conj(x_hat) x.
417    let mut ipr = 0.0_f64;
418    let mut ipi = 0.0_f64;
419    let n = x.len() / 2;
420    for k in 0..n {
421        let (ar, ai) = (x_hat[2 * k], x_hat[2 * k + 1]);
422        let (br, bi) = (x[2 * k], x[2 * k + 1]);
423        // conj(a) * b = (ar − i ai)(br + i bi).
424        ipr += ar * br + ai * bi;
425        ipi += ar * bi - ai * br;
426    }
427    let mag = (ipr * ipr + ipi * ipi).sqrt();
428    let (cr, ci) = if mag > 1e-300 {
429        (ipr / mag, ipi / mag)
430    } else {
431        (1.0, 0.0)
432    };
433    // x_hat · e^{iφ} with e^{iφ} = (cr, ci).
434    let mut err_sq = 0.0_f64;
435    let mut x_sq = 0.0_f64;
436    for k in 0..n {
437        let (ar, ai) = (x_hat[2 * k], x_hat[2 * k + 1]);
438        let (rr, ri) = cmul(ar, ai, cr, ci);
439        let dr = rr - x[2 * k];
440        let di = ri - x[2 * k + 1];
441        err_sq += dr * dr + di * di;
442        x_sq += x[2 * k] * x[2 * k] + x[2 * k + 1] * x[2 * k + 1];
443    }
444    Ok((err_sq / x_sq.max(1e-300)).sqrt())
445}
446
447#[cfg(test)]
448mod tests {
449    use super::*;
450
451    fn complex_signal(reals: &[f64], imags: &[f64]) -> Vec<f64> {
452        let mut x = Vec::with_capacity(reals.len() * 2);
453        for (r, i) in reals.iter().zip(imags.iter()) {
454            x.push(*r);
455            x.push(*i);
456        }
457        x
458    }
459
460    #[test]
461    fn dft_idft_round_trip() {
462        let n = 6;
463        let x = complex_signal(
464            &[1.0, -2.0, 3.0, 0.5, -1.0, 2.0],
465            &[0.0, 1.0, -1.0, 0.0, 2.0, -0.5],
466        );
467        let back = idft(&dft(&x, n), n);
468        for k in 0..2 * n {
469            assert!(
470                (back[k] - x[k]).abs() < 1e-9,
471                "k={k}: {} vs {}",
472                back[k],
473                x[k]
474            );
475        }
476    }
477
478    #[test]
479    fn constructor_rejects_zero_dims() {
480        let mut rng = LcgRng::new(1);
481        assert!(CodedDiffraction::new(0, 4, MaskKind::Octanary, &mut rng).is_err());
482        assert!(CodedDiffraction::new(8, 0, MaskKind::Octanary, &mut rng).is_err());
483    }
484
485    #[test]
486    fn forward_shapes() {
487        let mut rng = LcgRng::new(2);
488        let cdp = CodedDiffraction::new(8, 5, MaskKind::UniformPhase, &mut rng).expect("ok");
489        assert_eq!(cdp.n_measurements(), 40);
490        let x = vec![0.1_f64; 16];
491        let y = cdp.forward(&x).expect("ok");
492        assert_eq!(y.len(), 40);
493        assert!(y.iter().all(|&v| v >= 0.0), "intensities non-negative");
494    }
495
496    #[test]
497    fn forward_dimension_mismatch() {
498        let mut rng = LcgRng::new(3);
499        let cdp = CodedDiffraction::new(8, 4, MaskKind::Octanary, &mut rng).expect("ok");
500        assert!(matches!(
501            cdp.forward(&[0.0; 10]),
502            Err(CsError::DimensionMismatch { .. })
503        ));
504    }
505
506    #[test]
507    fn forward_phase_invariant_intensity() {
508        // |A (e^{iφ} x)|² = |A x|²: the global phase must not change the measurements.
509        let mut rng = LcgRng::new(4);
510        let cdp = CodedDiffraction::new(6, 4, MaskKind::UniformPhase, &mut rng).expect("ok");
511        let x = complex_signal(
512            &[1.0, 2.0, -1.0, 0.5, 0.0, 3.0],
513            &[0.0, -1.0, 1.0, 0.0, 2.0, 0.0],
514        );
515        let y0 = cdp.forward(&x).expect("ok");
516        // Rotate x by φ = π/3.
517        let (cr, ci) = (
518            std::f64::consts::FRAC_PI_3.cos(),
519            std::f64::consts::FRAC_PI_3.sin(),
520        );
521        let mut x_rot = vec![0.0_f64; x.len()];
522        for k in 0..6 {
523            let (rr, ri) = cmul(x[2 * k], x[2 * k + 1], cr, ci);
524            x_rot[2 * k] = rr;
525            x_rot[2 * k + 1] = ri;
526        }
527        let y1 = cdp.forward(&x_rot).expect("ok");
528        for (a, b) in y0.iter().zip(y1.iter()) {
529            assert!((a - b).abs() < 1e-8, "{a} vs {b}");
530        }
531    }
532
533    #[test]
534    fn phase_aligned_error_zero_for_rotation() {
535        let x = complex_signal(&[1.0, -2.0, 0.5], &[0.5, 1.0, -1.0]);
536        let (cr, ci) = (0.6_f64, 0.8_f64); // unit modulus
537        let mut x_rot = vec![0.0_f64; x.len()];
538        for k in 0..3 {
539            let (rr, ri) = cmul(x[2 * k], x[2 * k + 1], cr, ci);
540            x_rot[2 * k] = rr;
541            x_rot[2 * k + 1] = ri;
542        }
543        let err = phase_aligned_error(&x_rot, &x).expect("ok");
544        assert!(err < 1e-9, "err = {err}");
545    }
546
547    #[test]
548    fn phase_aligned_error_dim_mismatch() {
549        assert!(matches!(
550            phase_aligned_error(&[1.0, 0.0], &[1.0, 0.0, 0.0, 0.0]),
551            Err(CsError::DimensionMismatch { .. })
552        ));
553    }
554
555    #[test]
556    fn wirtinger_flow_recovers_real_signal() {
557        // Small real signal, octanary masks, oversampled (L = 10) ⇒ recovery up to
558        // global phase. Coded-diffraction phase retrieval needs enough patterns;
559        // more masks widen the basin of attraction of the spectral initialiser.
560        let n = 5usize;
561        let mut rng = LcgRng::new(20);
562        let cdp = CodedDiffraction::new(n, 10, MaskKind::Octanary, &mut rng).expect("ok");
563        let x = complex_signal(&[1.0, -1.0, 0.5, 2.0, -0.5], &[0.0; 5]);
564        let y = cdp.forward(&x).expect("ok");
565        let cfg = WirtingerConfig {
566            power_iters: 120,
567            max_iter: 2500,
568            step_size: 0.15,
569        };
570        let mut rng2 = LcgRng::new(99);
571        let x_hat = cdp.wirtinger_flow(&y, &cfg, &mut rng2).expect("ok");
572        let err = phase_aligned_error(&x_hat, &x).expect("ok");
573        assert!(err < 0.15, "relative error = {err}");
574    }
575
576    #[test]
577    fn wirtinger_flow_recovers_complex_signal() {
578        let n = 4usize;
579        let mut rng = LcgRng::new(31);
580        let cdp = CodedDiffraction::new(n, 8, MaskKind::Octanary, &mut rng).expect("ok");
581        let x = complex_signal(&[1.0, 0.0, -1.0, 0.5], &[0.5, 1.0, 0.0, -1.0]);
582        let y = cdp.forward(&x).expect("ok");
583        let cfg = WirtingerConfig {
584            power_iters: 100,
585            max_iter: 2000,
586            step_size: 0.15,
587        };
588        let mut rng2 = LcgRng::new(7);
589        let x_hat = cdp.wirtinger_flow(&y, &cfg, &mut rng2).expect("ok");
590        let err = phase_aligned_error(&x_hat, &x).expect("ok");
591        assert!(err < 0.2, "relative error = {err}");
592    }
593
594    #[test]
595    fn wirtinger_flow_dimension_mismatch() {
596        let mut rng = LcgRng::new(5);
597        let cdp = CodedDiffraction::new(8, 4, MaskKind::Octanary, &mut rng).expect("ok");
598        let mut rng2 = LcgRng::new(6);
599        assert!(matches!(
600            cdp.wirtinger_flow(&[0.0; 10], &WirtingerConfig::default(), &mut rng2),
601            Err(CsError::DimensionMismatch { .. })
602        ));
603    }
604
605    #[test]
606    fn recovered_signal_reproduces_measurements() {
607        // The intensity of the WF estimate should closely match y.
608        let n = 4usize;
609        let mut rng = LcgRng::new(40);
610        let cdp = CodedDiffraction::new(n, 7, MaskKind::Octanary, &mut rng).expect("ok");
611        let x = complex_signal(&[2.0, -1.0, 0.0, 1.0], &[0.0, 0.5, -0.5, 0.0]);
612        let y = cdp.forward(&x).expect("ok");
613        let cfg = WirtingerConfig {
614            power_iters: 100,
615            max_iter: 2000,
616            step_size: 0.15,
617        };
618        let mut rng2 = LcgRng::new(8);
619        let x_hat = cdp.wirtinger_flow(&y, &cfg, &mut rng2).expect("ok");
620        let y_hat = cdp.forward(&x_hat).expect("ok");
621        let mut num = 0.0_f64;
622        let mut den = 0.0_f64;
623        for (a, b) in y_hat.iter().zip(y.iter()) {
624            num += (a - b) * (a - b);
625            den += b * b;
626        }
627        let rel = (num / den.max(1e-30)).sqrt();
628        assert!(rel < 0.2, "measurement reproduction error = {rel}");
629    }
630
631    #[test]
632    fn rademacher_masks_are_real() {
633        let mut rng = LcgRng::new(50);
634        let cdp = CodedDiffraction::new(6, 3, MaskKind::Rademacher, &mut rng).expect("ok");
635        for l in 0..3 {
636            let m = cdp.mask(l);
637            for j in 0..6 {
638                assert_eq!(m[2 * j + 1], 0.0, "imag part must be zero");
639                assert!((m[2 * j].abs() - 1.0).abs() < 1e-12, "must be ±1");
640            }
641        }
642    }
643
644    #[test]
645    fn zero_signal_zero_measurements() {
646        let mut rng = LcgRng::new(60);
647        let cdp = CodedDiffraction::new(5, 4, MaskKind::Octanary, &mut rng).expect("ok");
648        let y = cdp.forward(&vec![0.0_f64; 10]).expect("ok");
649        assert!(y.iter().all(|&v| v == 0.0));
650    }
651}