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, &) 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}