scirs2_fft/hfft/
symmetric.rs

1//! Hermitian Symmetry Utilities
2//!
3//! This module contains utility functions for working with Hermitian-symmetric arrays.
4//! Hermitian symmetry is a property where a complex array satisfies a[i] = conj(a[-i])
5//! for all indices i. This property is important for algorithms like HFFT and IHFFT.
6
7use scirs2_core::ndarray::{Array, Array2, Dimension, IxDyn};
8use scirs2_core::numeric::Complex64;
9use std::ops::Not;
10
11/// Enforce Hermitian symmetry on a 2D complex array.
12///
13/// This function modifies the input array to ensure it satisfies Hermitian symmetry properties.
14/// Specifically, it ensures that array[i, j] = conj(array[rows-i, cols-j]) for key components
15/// of the array.
16///
17/// # Arguments
18///
19/// * `array` - Mutable reference to a 2D complex array
20#[allow(dead_code)]
21pub fn enforce_hermitian_symmetry(array: &mut Array2<Complex64>) {
22    let (rows, cols) = array.dim();
23
24    // Make DC component real
25    if rows > 0 && cols > 0 {
26        array[[0, 0]] = Complex64::new(array[[0, 0]].re, 0.0);
27    }
28
29    // For a truly Hermitian-symmetric result, we need to:
30    // 1. Ensure that array[i, j] = conj(_array[rows-i, cols-j]) for all i, j
31    // 2. Ensure that array[i, 0] and array[0, j] have special properties
32
33    // Ensure first row and column elements satisfy Hermitian constraints
34    if rows > 1 && cols > 1 {
35        // First row, special case
36        for j in 1..cols / 2 + (cols % 2).not() {
37            let conj_val = array[[0, cols - j]].conj();
38            array[[0, j]] = conj_val;
39        }
40
41        // First column, special case
42        for i in 1..rows / 2 + (rows % 2).not() {
43            let conj_val = array[[rows - i, 0]].conj();
44            array[[i, 0]] = conj_val;
45        }
46
47        // Handle the corners and mid-points for even-sized dimensions
48        if rows % 2 == 0 && rows > 0 {
49            array[[rows / 2, 0]] = Complex64::new(array[[rows / 2, 0]].re, 0.0);
50        }
51
52        if cols % 2 == 0 && cols > 0 {
53            array[[0, cols / 2]] = Complex64::new(array[[0, cols / 2]].re, 0.0);
54        }
55    }
56}
57
58/// Enforce Hermitian symmetry on an N-dimensional complex array.
59///
60/// This function is a generalized version of `enforce_hermitian_symmetry` for
61/// N-dimensional arrays. It ensures that key symmetry properties are satisfied.
62///
63/// # Arguments
64///
65/// * `array` - Mutable reference to an N-dimensional complex array
66#[allow(dead_code)]
67pub fn enforce_hermitian_symmetry_nd(array: &mut Array<Complex64, IxDyn>) {
68    let shape = array.shape().to_vec();
69    let ndim = shape.len();
70
71    if ndim == 0 || array.is_empty() {
72        return;
73    }
74
75    // Make DC component real
76    // Use direct _array access instead of indices
77    if let Some(slice) = array.as_slice_mut() {
78        if !slice.is_empty() {
79            slice[0] = Complex64::new(slice[0].re, 0.0);
80        }
81    }
82
83    // For higher dimensions (1D+), handle special cases
84    match ndim {
85        1 => {
86            // 1D case - simpler to handle directly
87            if let Some(slice) = array.as_slice_mut() {
88                let n = slice.len();
89
90                // Make elements Hermitian-symmetric
91                for i in 1..n / 2 + 1 {
92                    if i < n && (n - i) < n {
93                        let avg = (slice[i] + slice[n - i].conj()) * 0.5;
94                        slice[i] = avg;
95                        slice[n - i] = avg.conj();
96                    }
97                }
98
99                // If n is even, make the Nyquist frequency component real
100                if n % 2 == 0 && n >= 2 {
101                    slice[n / 2] = Complex64::new(slice[n / 2].re, 0.0);
102                }
103            }
104        }
105        2 => {
106            // Convert to 2D view for easier access
107            if let Ok(mut array2) = array
108                .clone()
109                .into_dimensionality::<scirs2_core::ndarray::Ix2>()
110            {
111                // Apply 2D symmetry (same as in enforce_hermitian_symmetry)
112                enforce_hermitian_symmetry(&mut array2);
113
114                // Copy back to the original _array
115                let view2 = array2.view();
116                let flat = view2.as_slice().unwrap();
117                if let Some(target) = array.as_slice_mut() {
118                    target.copy_from_slice(flat);
119                }
120            }
121        }
122        _ => {
123            // For 3+ dimensions, we do a simplified enforcement
124            // This doesn't handle all possible Hermitian symmetry properties
125            // but covers the most important ones
126
127            // For each 2D plane along the first two dimensions, apply 2D symmetry
128            if let Ok(mut view) = array
129                .view_mut()
130                .into_dimensionality::<scirs2_core::ndarray::Ix3>()
131            {
132                let (dim1, dim2, _) = view.dim();
133
134                for k in 0..view.dim().2 {
135                    let mut slice = view.slice_mut(scirs2_core::ndarray::s![.., .., k]);
136                    let mut array2 = Array2::zeros((dim1, dim2));
137
138                    // Copy data to a temporary 2D _array
139                    for i in 0..dim1 {
140                        for j in 0..dim2 {
141                            array2[[i, j]] = slice[[i, j]];
142                        }
143                    }
144
145                    // Apply 2D symmetry
146                    enforce_hermitian_symmetry(&mut array2);
147
148                    // Copy back
149                    for i in 0..dim1 {
150                        for j in 0..dim2 {
151                            slice[[i, j]] = array2[[i, j]];
152                        }
153                    }
154                }
155            }
156        }
157    }
158}
159
160/// Check if an array is approximately Hermitian-symmetric.
161///
162/// This function verifies whether a complex array satisfies Hermitian symmetry properties
163/// within a specified tolerance.
164///
165/// # Arguments
166///
167/// * `array` - Reference to a complex array
168/// * `tolerance` - Maximum allowed deviation from Hermitian symmetry (default: 1e-10)
169///
170/// # Returns
171///
172/// * `true` if the array is approximately Hermitian-symmetric, `false` otherwise
173#[allow(dead_code)]
174pub fn is_hermitian_symmetric<D>(array: &Array<Complex64, D>, tolerance: Option<f64>) -> bool
175where
176    D: Dimension,
177{
178    let tol = tolerance.unwrap_or(1e-10);
179    let shape = array.shape();
180
181    // For multi-dimensional arrays, check symmetry across all dimensions
182    // This is a simplified version that focuses on key symmetry points
183
184    // Check DC component is real (or close to it)
185    if !shape.is_empty() && !array.is_empty() {
186        let dc_val = &array.as_slice().unwrap()[0];
187        if dc_val.im.abs() > tol {
188            return false;
189        }
190    }
191
192    // For a full check, we would need to verify that a[i] = conj(a[-i]) for all indices
193    // This could be computationally expensive for large arrays
194    // Here we check a sample of key points
195
196    // Simple implementation for 1D arrays
197    if shape.len() == 1 && shape[0] > 1 {
198        let n = shape[0];
199        let data = array.as_slice().unwrap();
200        for i in 1..n / 2 + 1 {
201            if i < n && (n - i) < n {
202                let a = &data[i];
203                let b = data[n - i].conj();
204
205                if (a.re - b.re).abs() > tol || (a.im - b.im).abs() > tol {
206                    return false;
207                }
208            }
209        }
210        return true;
211    }
212
213    // Simple implementation for 2D arrays
214    if shape.len() == 2 {
215        let (rows, cols) = (shape[0], shape[1]);
216
217        // Check the _array using direct indexing for 2D
218        let array2 = array
219            .to_owned()
220            .into_dimensionality::<scirs2_core::ndarray::Ix2>()
221            .unwrap();
222
223        // Check first row
224        for j in 1..cols / 2 + 1 {
225            if j < cols && (cols - j) < cols {
226                let a = &array2[[0, j]];
227                let b = array2[[0, cols - j]].conj();
228
229                if (a.re - b.re).abs() > tol || (a.im - b.im).abs() > tol {
230                    return false;
231                }
232            }
233        }
234
235        // Check first column
236        for i in 1..rows / 2 + 1 {
237            if i < rows && (rows - i) < rows {
238                let a = &array2[[i, 0]];
239                let b = array2[[rows - i, 0]].conj();
240
241                if (a.re - b.re).abs() > tol || (a.im - b.im).abs() > tol {
242                    return false;
243                }
244            }
245        }
246    }
247
248    // For higher dimensions, we would need a more sophisticated approach
249    // This is a simplified check that may not catch all deviations from Hermitian symmetry
250    true
251}
252
253/// Create a Hermitian-symmetric array from a real-valued amplitude spectrum.
254///
255/// This function builds a complex array with Hermitian symmetry, where the amplitudes
256/// are specified by the input array and phases are generated to ensure symmetry.
257///
258/// # Arguments
259///
260/// * `amplitudes` - Real-valued amplitudes for the frequency components
261/// * `randomize_phases` - Whether to use random phases (if true) or zero phases (if false)
262///
263/// # Returns
264///
265/// * A complex array with Hermitian symmetry
266#[allow(dead_code)]
267pub fn create_hermitian_symmetric_signal(
268    amplitudes: &[f64],
269    randomize_phases: bool,
270) -> Vec<Complex64> {
271    use scirs2_core::random::Rng;
272
273    let n = amplitudes.len();
274    let mut result = Vec::with_capacity(n);
275
276    // DC component is always real
277    result.push(Complex64::new(amplitudes[0], 0.0));
278
279    let mut rng = scirs2_core::random::rng();
280
281    // For each frequency component (excluding DC and Nyquist)
282    for (_i, &amp) in amplitudes.iter().enumerate().skip(1).take(n / 2 - 1) {
283        // Generate a phase (either random or zero)
284        let phase = if randomize_phases {
285            2.0 * std::f64::consts::PI * rng.random::<f64>()
286        } else {
287            0.0
288        };
289
290        // Create complex number with given amplitude and phase
291        let value = Complex64::from_polar(amp, phase);
292        result.push(value);
293
294        // Add conjugate for negative frequency
295        result.push(value.conj());
296    }
297
298    // If n is even, add Nyquist frequency component (must be real)
299    if n.is_multiple_of(2) && n > 0 {
300        result.push(Complex64::new(amplitudes[n / 2], 0.0));
301    }
302
303    result
304}