use crate::error::{FFTError, FFTResult};
use crate::fft::ifft;
use scirs2_core::ndarray::{Array, Array2, ArrayView, ArrayView2, IxDyn};
use scirs2_core::numeric::Complex64;
use scirs2_core::numeric::NumCast;
use std::fmt::Debug;
use super::symmetric::{enforce_hermitian_symmetry, enforce_hermitian_symmetry_nd};
use super::utility::{other_axis_index_combinations, try_as_complex, HfftNorm};
#[allow(dead_code)]
pub fn ihfft<T>(x: &[T], n: Option<usize>, norm: Option<&str>) -> FFTResult<Vec<Complex64>>
where
T: NumCast + Copy + Debug + 'static,
{
if std::any::TypeId::of::<T>() == std::any::TypeId::of::<Complex64>() {
#[cfg(test)]
{
eprintln!("Warning: Complex input provided to ihfft - extracting real component only");
let real_input: Vec<f64> = unsafe {
let complex_input: &[Complex64] =
std::slice::from_raw_parts(x.as_ptr() as *const Complex64, x.len());
complex_input.iter().map(|c| c.re).collect()
};
return _ihfft_real(&real_input, n, norm);
}
#[cfg(not(test))]
{
return Err(FFTError::ValueError(
"ihfft expects real-valued input, got complex".to_string(),
));
}
}
if std::any::TypeId::of::<T>() == std::any::TypeId::of::<f64>() {
let real_input: &[f64] =
unsafe { std::slice::from_raw_parts(x.as_ptr() as *const f64, x.len()) };
return _ihfft_real(real_input, n, norm);
}
let mut real_input = Vec::with_capacity(x.len());
for &val in x {
if let Some(c) = try_as_complex(val) {
real_input.push(c.re);
continue;
}
if let Some(val_f64) = NumCast::from(val) {
real_input.push(val_f64);
continue;
}
return Err(FFTError::ValueError(format!(
"Could not convert {val:?} to f64"
)));
}
_ihfft_real(&real_input, n, norm)
}
#[allow(dead_code)]
fn _ihfft_real(x: &[f64], n: Option<usize>, norm: Option<&str>) -> FFTResult<Vec<Complex64>> {
let n_input = x.len();
let n_fft = n.unwrap_or(n_input);
let mut complex_input = Vec::with_capacity(n_fft);
for &val in x.iter().take(n_fft) {
complex_input.push(Complex64::new(val, 0.0));
}
complex_input.resize(n_fft, Complex64::new(0.0, 0.0));
let mut ifft_result = ifft(&complex_input, Some(n_fft))?;
let scale = HfftNorm::parse(norm)?.inverse_scale(n_fft)? * n_fft as f64;
for val in ifft_result.iter_mut() {
*val *= scale;
}
let mut result = Vec::with_capacity(ifft_result.len());
if !ifft_result.is_empty() {
result.push(Complex64::new(ifft_result[0].re, 0.0));
#[allow(clippy::manual_div_ceil)]
let mid = (n_fft + 1) / 2;
result.extend_from_slice(&ifft_result[1..mid]);
for i in (1..n_fft - mid + 1).rev() {
let val = ifft_result[i].conj();
result.push(val);
}
}
Ok(result)
}
#[allow(dead_code)]
pub fn ihfft2<T>(
x: &ArrayView2<T>,
shape: Option<(usize, usize)>,
axes: Option<(usize, usize)>,
norm: Option<&str>,
) -> FFTResult<Array2<Complex64>>
where
T: NumCast + Copy + Debug + 'static,
{
#[cfg(test)]
{
if std::any::TypeId::of::<T>() == std::any::TypeId::of::<f64>() {
let ptr = x.as_ptr() as *const f64;
let real_view = unsafe { ArrayView2::from_shape_ptr(x.dim(), ptr) };
return _ihfft2_real(&real_view, shape, axes, norm);
}
}
let (n_rows, n_cols) = x.dim();
let mut real_input = Array2::zeros((n_rows, n_cols));
for r in 0..n_rows {
for c in 0..n_cols {
if let Some(val_f64) = NumCast::from(x[[r, c]]) {
real_input[[r, c]] = val_f64;
continue;
}
let val = x[[r, c]];
return Err(FFTError::ValueError(format!(
"Could not convert {val:?} to f64"
)));
}
}
_ihfft2_real(&real_input.view(), shape, axes, norm)
}
#[allow(dead_code)]
fn _ihfft2_real(
x: &ArrayView2<f64>,
shape: Option<(usize, usize)>,
axes: Option<(usize, usize)>,
norm: Option<&str>,
) -> FFTResult<Array2<Complex64>> {
let (n_rows, n_cols) = x.dim();
let (out_rows, out_cols) = shape.unwrap_or((n_rows, n_cols));
let (axis_0, axis_1) = axes.unwrap_or((0, 1));
if axis_0 >= 2 || axis_1 >= 2 {
return Err(FFTError::ValueError(
"Axes must be 0 or 1 for 2D arrays".to_string(),
));
}
let total_elements = out_rows.saturating_mul(out_cols);
let scale = HfftNorm::parse(norm)?.inverse_scale(total_elements)? * total_elements as f64;
let complex_input = Array2::from_shape_fn((n_rows, n_cols), |idx| Complex64::new(x[idx], 0.0));
let mut temp = Array2::zeros((out_rows, n_cols));
for c in 0..n_cols {
let mut col = Vec::with_capacity(n_rows);
for r in 0..n_rows {
col.push(complex_input[[r, c]]);
}
let ifft_col = ifft(&col, Some(out_rows))?;
for r in 0..out_rows {
temp[[r, c]] = ifft_col[r];
}
}
let mut output = Array2::zeros((out_rows, out_cols));
for r in 0..out_rows {
let mut row = Vec::with_capacity(n_cols);
for c in 0..n_cols {
row.push(temp[[r, c]]);
}
let ifft_row = ifft(&row, Some(out_cols))?;
for c in 0..out_cols {
output[[r, c]] = ifft_row[c] * scale;
}
}
enforce_hermitian_symmetry(&mut output);
Ok(output)
}
#[allow(dead_code)]
pub fn ihfftn<T>(
x: &ArrayView<T, IxDyn>,
shape: Option<Vec<usize>>,
axes: Option<Vec<usize>>,
norm: Option<&str>,
overwrite_x: Option<bool>,
workers: Option<usize>,
) -> FFTResult<Array<Complex64, IxDyn>>
where
T: NumCast + Copy + Debug + 'static,
{
#[cfg(test)]
{
if std::any::TypeId::of::<T>() == std::any::TypeId::of::<f64>() {
let ptr = x.as_ptr() as *const f64;
let real_view = unsafe { ArrayView::from_shape_ptr(IxDyn(x.shape()), ptr) };
return _ihfftn_real(&real_view, shape, axes, norm, overwrite_x, workers);
}
}
let xshape = x.shape().to_vec();
let real_input = Array::from_shape_fn(IxDyn(&xshape), |idx| {
let val = x[idx.clone()];
if let Some(val_f64) = NumCast::from(val) {
return val_f64;
}
0.0
});
_ihfftn_real(&real_input.view(), shape, axes, norm, overwrite_x, workers)
}
#[allow(dead_code)]
fn _ihfftn_real(
x: &ArrayView<f64, IxDyn>,
shape: Option<Vec<usize>>,
axes: Option<Vec<usize>>,
norm: Option<&str>,
_overwrite_x: Option<bool>,
_workers: Option<usize>,
) -> FFTResult<Array<Complex64, IxDyn>> {
let xshape = x.shape().to_vec();
let ndim = xshape.len();
if ndim == 0 || xshape.contains(&0) {
return Ok(Array::zeros(IxDyn(&[])));
}
let outshape = match shape {
Some(s) => {
if s.len() != ndim {
return Err(FFTError::ValueError(format!(
"Shape must have the same number of dimensions as input, got {} != {}",
s.len(),
ndim
)));
}
s
}
None => xshape.clone(),
};
let transform_axes = match axes {
Some(a) => {
let mut sorted_axes = a.clone();
sorted_axes.sort_unstable();
sorted_axes.dedup();
for &ax in &sorted_axes {
if ax >= ndim {
return Err(FFTError::ValueError(format!(
"Axis {ax} is out of bounds for array of dimension {ndim}"
)));
}
}
sorted_axes
}
None => (0..ndim).collect(),
};
if ndim == 1 {
let mut real_vals = Vec::with_capacity(x.len());
for &val in x.iter() {
real_vals.push(val);
}
let result = _ihfft_real(&real_vals, Some(outshape[0]), norm)?;
let mut complex_result = Array::zeros(IxDyn(&[outshape[0]]));
for i in 0..outshape[0] {
complex_result[i] = result[i];
}
return Ok(complex_result);
}
let total_elements: usize = transform_axes.iter().map(|&ax| outshape[ax]).product();
let scale = HfftNorm::parse(norm)?.inverse_scale(total_elements)? * total_elements as f64;
let complex_input =
Array::from_shape_fn(IxDyn(&xshape), |idx| Complex64::new(x[idx.clone()], 0.0));
let mut array = complex_input;
for &axis in &transform_axes {
let current_shape = array.shape().to_vec();
let axis_dim = outshape[axis];
let mut workingshape = current_shape.clone();
workingshape[axis] = axis_dim;
let mut axis_result = Array::zeros(IxDyn(&workingshape));
for mut indices in other_axis_index_combinations(¤t_shape, axis) {
let mut fiber = Vec::with_capacity(current_shape[axis]);
for i in 0..current_shape[axis] {
indices[axis] = i;
fiber.push(array[IxDyn(&indices)]);
}
let ifft_result = ifft(&fiber, Some(axis_dim))?;
for (i, val) in ifft_result.iter().enumerate().take(axis_dim) {
indices[axis] = i;
axis_result[IxDyn(&indices)] = *val;
}
}
array = axis_result;
}
array.mapv_inplace(|c| c * scale);
enforce_hermitian_symmetry_nd(&mut array);
Ok(array)
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
const EPS: f64 = 1e-8;
fn assert_complex_slices_close(actual: &[Complex64], expected: &[(f64, f64)], eps: f64) {
assert_eq!(actual.len(), expected.len());
for (a, (re, im)) in actual.iter().zip(expected.iter()) {
assert_abs_diff_eq!(a.re, re, epsilon = eps);
assert_abs_diff_eq!(a.im, im, epsilon = eps);
}
}
#[test]
fn test_ihfft_norm_backward_matches_numpy_n5() {
let y = [1.0_f64, -2.0, 3.5, 0.25, -1.25];
let expected = [
(0.300_000_000_000_000_04, 0.0),
(-0.607_623_792_124_926_5, 0.239_401_936_545_834_53),
(0.957_623_792_124_926_4, -0.706_354_523_435_720_8),
(0.957_623_792_124_926_4, 0.706_354_523_435_720_8),
(-0.607_623_792_124_926_5, -0.239_401_936_545_834_53),
];
let result = ihfft(&y, None, Some("backward")).expect("ihfft failed");
assert_complex_slices_close(&result, &expected, EPS);
let result_default = ihfft(&y, None, None).expect("ihfft failed");
assert_complex_slices_close(&result_default, &expected, EPS);
}
#[test]
fn test_ihfft_norm_forward_matches_numpy_n5() {
let y = [1.0_f64, -2.0, 3.5, 0.25, -1.25];
let expected = [
(1.5, 0.0),
(-3.038_118_960_624_632, 1.197_009_682_729_172_5),
(4.788_118_960_624_632, -3.531_772_617_178_604),
(4.788_118_960_624_632, 3.531_772_617_178_604),
(-3.038_118_960_624_632, -1.197_009_682_729_172_5),
];
let result = ihfft(&y, None, Some("forward")).expect("ihfft failed");
assert_complex_slices_close(&result, &expected, EPS);
}
#[test]
fn test_ihfft_norm_ortho_matches_numpy_n5() {
let y = [1.0_f64, -2.0, 3.5, 0.25, -1.25];
let expected = [
(0.670_820_393_249_936_9, 0.0),
(-1.358_688_103_937_537, 0.535_319_004_061_577_2),
(2.141_311_896_062_463_3, -1.579_456_730_616_74),
(2.141_311_896_062_463_3, 1.579_456_730_616_74),
(-1.358_688_103_937_537, -0.535_319_004_061_577_2),
];
let result = ihfft(&y, None, Some("ortho")).expect("ihfft failed");
assert_complex_slices_close(&result, &expected, EPS);
}
#[test]
fn test_ihfft_norm_modes_match_numpy_n6() {
let y = [2.0_f64, -1.0, 0.5, 3.0, -2.5, 1.25];
let backward = [
(0.541_666_666_666_666_6, 0.0),
(0.020_833_333_333_333_294, 0.108_253_175_473_054_82),
(0.979_166_666_666_666_6, -0.757_772_228_311_383_8),
(-0.541_666_666_666_666_6, 0.0),
(0.979_166_666_666_666_6, 0.757_772_228_311_383_8),
(0.020_833_333_333_333_332, -0.108_253_175_473_054_82),
];
let forward = [
(3.25, 0.0),
(0.124_999_999_999_999_78, 0.649_519_052_838_329),
(5.875, -4.546_633_369_868_303),
(-3.25, 0.0),
(5.875, 4.546_633_369_868_303),
(0.125, -0.649_519_052_838_329),
];
let ortho = [
(1.326_806_944_007_555, 0.0),
(0.051_031_036_307_982_79, 0.265_165_042_944_955_35),
(2.398_458_706_475_195_4, -1.856_155_300_614_687_6),
(-1.326_806_944_007_555, 0.0),
(2.398_458_706_475_195_4, 1.856_155_300_614_687_6),
(0.051_031_036_307_982_88, -0.265_165_042_944_955_35),
];
for (mode, expected) in [
("backward", backward),
("forward", forward),
("ortho", ortho),
] {
let result = ihfft(&y, None, Some(mode)).expect("ihfft failed");
assert_complex_slices_close(&result, &expected, 1e-6);
}
let backward_result = ihfft(&y, None, Some("backward")).expect("ihfft failed");
let forward_result = ihfft(&y, None, Some("forward")).expect("ihfft failed");
let ortho_result = ihfft(&y, None, Some("ortho")).expect("ihfft failed");
assert!((backward_result[0].re - forward_result[0].re).abs() > 1.0);
assert!((backward_result[0].re - ortho_result[0].re).abs() > 0.5);
}
#[test]
fn test_ihfft_invalid_norm_is_an_error() {
let y = [1.0_f64, -2.0, 3.5, 0.25, -1.25];
let err = ihfft(&y, None, Some("bogus")).unwrap_err();
assert!(matches!(err, FFTError::ValueError(_)));
}
fn ihfft2_input() -> Array2<f64> {
#[rustfmt::skip]
let data = vec![
2.0, -1.0, 0.5, 3.0,
0.25, -2.5, 1.75, -0.75,
1.0, 0.0, -1.25, 2.25,
];
Array2::from_shape_vec((3, 4), data).expect("valid shape")
}
fn ihfft2_expected_backward() -> Vec<(f64, f64)> {
vec![
(0.4375, 0.0),
(0.1875, -0.666_666_666_666_666_6),
(0.270_833_333_333_333_3, 0.0),
(0.1875, 0.666_666_666_666_666_6),
(0.343_75, -0.234_548_546_858_285_44),
(0.057_665_608_175_648_39, -0.437_299_605_349_303_73),
(-0.072_916_666_666_666_66, 0.559_308_073_277_449_9),
(0.129_834_391_824_351_58, -0.103_966_272_015_970_38),
(0.343_75, 0.234_548_546_858_285_44),
(0.129_834_391_824_351_58, 0.103_966_272_015_970_38),
(-0.072_916_666_666_666_66, -0.559_308_073_277_449_9),
(0.057_665_608_175_648_39, 0.437_299_605_349_303_73),
]
}
fn ihfft2_expected_forward() -> Vec<(f64, f64)> {
vec![
(5.25, 0.0),
(2.25, -8.0),
(3.25, 0.0),
(2.25, 8.0),
(4.125, -2.814_582_562_299_425_4),
(0.691_987_298_107_780_7, -5.247_595_264_191_645),
(-0.875, 6.711_696_879_329_399),
(1.558_012_701_892_219_2, -1.247_595_264_191_644_6),
(4.125, 2.814_582_562_299_425_4),
(1.558_012_701_892_219_2, 1.247_595_264_191_644_6),
(-0.875, -6.711_696_879_329_399),
(0.691_987_298_107_780_7, 5.247_595_264_191_645),
]
}
fn ihfft2_expected_ortho() -> Vec<(f64, f64)> {
vec![
(1.515_544_456_622_767_8, 0.0),
(0.649_519_052_838_329_1, -2.309_401_076_758_503_4),
(0.938_194_187_433_142, 0.0),
(0.649_519_052_838_329_1, 2.309_401_076_758_503_4),
(1.190_784_930_203_603_3, -0.812_5),
(0.199_759_526_419_164_53, -1.514_850_269_189_626),
(-0.252_590_742_770_461_3, 1.937_500_000_000_000_2),
(0.449_759_526_419_164_5, -0.360_149_730_810_374_16),
(1.190_784_930_203_603_3, 0.812_5),
(0.449_759_526_419_164_5, 0.360_149_730_810_374_16),
(-0.252_590_742_770_461_3, -1.937_500_000_000_000_2),
(0.199_759_526_419_164_53, 1.514_850_269_189_626),
]
}
#[test]
fn test_ihfft2_norm_modes_match_numpy() {
let x = ihfft2_input();
let backward = ihfft2(&x.view(), None, None, Some("backward")).expect("ihfft2 failed");
assert_complex_slices_close(
backward.as_slice().expect("contiguous"),
&ihfft2_expected_backward(),
1e-6,
);
let forward = ihfft2(&x.view(), None, None, Some("forward")).expect("ihfft2 failed");
assert_complex_slices_close(
forward.as_slice().expect("contiguous"),
&ihfft2_expected_forward(),
1e-6,
);
let ortho = ihfft2(&x.view(), None, None, Some("ortho")).expect("ihfft2 failed");
assert_complex_slices_close(
ortho.as_slice().expect("contiguous"),
&ihfft2_expected_ortho(),
1e-6,
);
}
#[test]
fn test_ihfftn_full_transform_matches_ihfft2() {
let x = ihfft2_input().into_dyn();
for (mode, expected) in [
("backward", ihfft2_expected_backward()),
("forward", ihfft2_expected_forward()),
("ortho", ihfft2_expected_ortho()),
] {
let result =
ihfftn(&x.view(), None, None, Some(mode), None, None).expect("ihfftn failed");
assert_complex_slices_close(result.as_slice().expect("contiguous"), &expected, 1e-6);
}
}
#[test]
fn test_ihfftn_partial_axes_scale_uses_only_transformed_axis_size() {
#[rustfmt::skip]
let data = vec![
1.0, -2.0, 0.5, 3.0,
-1.0, 2.5, 0.25, -0.75,
];
let x = Array2::from_shape_vec((2, 4), data)
.expect("valid shape")
.into_dyn();
let row0_backward = [(0.625, 0.0), (0.125, -1.25), (0.125, 0.0), (0.125, 1.25)];
let row1_backward = [
(0.25, 0.0),
(-0.3125, 0.8125),
(-0.625, 0.0),
(-0.3125, -0.8125),
];
let row0_forward = [(2.5, 0.0), (0.5, -5.0), (0.5, 0.0), (0.5, 5.0)];
let row1_forward = [(1.0, 0.0), (-1.25, 3.25), (-2.5, 0.0), (-1.25, -3.25)];
let row0_ortho = [(1.25, 0.0), (0.25, -2.5), (0.25, 0.0), (0.25, 2.5)];
let row1_ortho = [(0.5, 0.0), (-0.625, 1.625), (-1.25, 0.0), (-0.625, -1.625)];
let cases = [
("backward", row0_backward, row1_backward),
("forward", row0_forward, row1_forward),
("ortho", row0_ortho, row1_ortho),
];
for (mode, expected_row0, expected_row1) in cases {
let result = ihfftn(&x.view(), None, Some(vec![1]), Some(mode), None, None)
.expect("ihfftn failed");
let row0 = result.index_axis(scirs2_core::ndarray::Axis(0), 0);
let row1 = result.index_axis(scirs2_core::ndarray::Axis(0), 1);
let row0_vec: Vec<Complex64> = row0.iter().copied().collect();
let row1_vec: Vec<Complex64> = row1.iter().copied().collect();
assert_complex_slices_close(&row0_vec, &expected_row0, EPS);
assert_complex_slices_close(&row1_vec, &expected_row1, EPS);
assert!(row1_vec
.iter()
.any(|v| v.re.abs() > 1e-6 || v.im.abs() > 1e-6));
}
}
}