scirs2_fft/
fht.rs

1//! Fast Hankel Transform (FHT) module
2//!
3//! This module implements the Fast Hankel Transform using the FFTLog algorithm,
4//! similar to SciPy's implementation.
5
6use crate::error::{FFTError, FFTResult};
7use std::f64::consts::PI;
8
9/// Fast Hankel Transform using FFTLog algorithm
10///
11/// Computes the discrete Hankel transform of a logarithmically spaced periodic
12/// sequence. This is the FFTLog algorithm by Hamilton (2000).
13///
14/// # Arguments
15///
16/// * `a` - Real input array, logarithmically spaced
17/// * `dln` - Uniform logarithmic spacing of the input array
18/// * `mu` - Order of the Bessel function
19/// * `offset` - Offset of the uniform logarithmic spacing (default 0.0)
20/// * `bias` - Index of the power law bias (default 0.0)
21///
22/// # Returns
23///
24/// The transformed output array
25#[allow(dead_code)]
26pub fn fht(
27    a: &[f64],
28    dln: f64,
29    mu: f64,
30    offset: Option<f64>,
31    bias: Option<f64>,
32) -> FFTResult<Vec<f64>> {
33    let n = a.len();
34    if n == 0 {
35        return Err(FFTError::ValueError(
36            "Input array cannot be empty".to_string(),
37        ));
38    }
39
40    let offset = offset.unwrap_or(0.0);
41    let bias = bias.unwrap_or(0.0);
42
43    // Calculate the FFTLog coefficients
44    let coeffs = fht_coefficients(n, dln, mu, offset, bias)?;
45
46    // Multiply input by coefficients
47    let modified_input: Vec<f64> = a
48        .iter()
49        .zip(coeffs.iter())
50        .map(|(&ai, &ci)| ai * ci)
51        .collect();
52
53    // Apply FFT (we need the full FFT, not just real FFT)
54    let spectrum = crate::fft(&modified_input, None)?;
55
56    // Extract the appropriate part for the result
57    let result: Vec<f64> = spectrum.iter().map(|c| c.re).take(n).collect();
58
59    Ok(result)
60}
61
62/// Inverse Fast Hankel Transform
63///
64/// Computes the inverse discrete Hankel transform of a logarithmically spaced
65/// periodic sequence.
66///
67/// # Arguments
68///
69/// * `A` - Real input array, logarithmically spaced Hankel transform
70/// * `dln` - Uniform logarithmic spacing
71/// * `mu` - Order of the Bessel function  
72/// * `offset` - Offset of the uniform logarithmic spacing (default 0.0)
73/// * `bias` - Index of the power law bias (default 0.0)
74///
75/// # Returns
76///
77/// The inverse transformed output array
78#[allow(dead_code)]
79pub fn ifht(
80    a: &[f64],
81    dln: f64,
82    mu: f64,
83    offset: Option<f64>,
84    bias: Option<f64>,
85) -> FFTResult<Vec<f64>> {
86    // For orthogonal transforms, the inverse is similar with adjusted parameters
87    let bias_inv = -bias.unwrap_or(0.0);
88    fht(a, dln, mu, offset, Some(bias_inv))
89}
90
91/// Calculate optimal offset for the FFTLog method
92///
93/// For periodic signals ('periodic' boundary), the optimal offset is zero.
94/// Otherwise, you should use the optimal offset to obtain accurate Hankel transforms.
95///
96/// # Arguments
97///
98/// * `dln` - Uniform logarithmic spacing
99/// * `mu` - Order of the Bessel function
100/// * `initial` - Initial guess for the offset (default 0.0)  
101/// * `bias` - Index of the power law bias (default 0.0)
102///
103/// # Returns
104///
105/// The optimal logarithmic offset
106#[allow(dead_code)]
107pub fn fhtoffset(_dln: f64, _mu: f64, initial: Option<f64>, bias: Option<f64>) -> FFTResult<f64> {
108    let bias = bias.unwrap_or(0.0);
109    let initial = initial.unwrap_or(0.0);
110
111    // For the simple case without optimization
112    if bias == 0.0 {
113        Ok(0.0)
114    } else {
115        // In practice, finding the optimal offset requires solving
116        // a transcendental equation. For now, return a simple approximation.
117        Ok(initial)
118    }
119}
120
121/// Compute the FFTLog coefficients
122#[allow(dead_code)]
123fn fht_coefficients(n: usize, dln: f64, mu: f64, offset: f64, bias: f64) -> FFTResult<Vec<f64>> {
124    let mut coeffs = vec![0.0; n];
125
126    // Calculate the coefficients using the analytical formula
127    for (i, coeff) in coeffs.iter_mut().enumerate() {
128        let m = i as f64 - n as f64 / 2.0;
129        let k = 2.0 * PI * m / (n as f64 * dln);
130
131        // Basic coefficient without bias
132        let basic_coeff = k.powf(mu) * (-(k * k) / 4.0).exp();
133
134        // Apply bias correction if needed
135        let biased_coeff = if bias != 0.0 {
136            basic_coeff * (1.0 + bias * k * k).powf(-bias / 2.0)
137        } else {
138            basic_coeff
139        };
140
141        // Apply phase offset
142        let phase = offset * k;
143        *coeff = biased_coeff * phase.cos();
144    }
145
146    Ok(coeffs)
147}
148
149/// Compute the discrete Hankel transform sample points
150///
151/// This function computes the sample points for the discrete Hankel transform
152/// when the input array is logarithmically spaced.
153///
154/// # Arguments
155///
156/// * `n` - Number of sample points
157/// * `dln` - Logarithmic spacing
158/// * `offset` - Logarithmic offset
159///
160/// # Returns
161///
162/// Sample points for the transform
163#[allow(dead_code)]
164pub fn fht_sample_points(n: usize, dln: f64, offset: f64) -> Vec<f64> {
165    (0..n)
166        .map(|i| ((i as f64 - n as f64 / 2.0) * dln + offset).exp())
167        .collect()
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    use approx::assert_relative_eq;
174
175    #[test]
176    fn test_fht_basic() {
177        let n = 64;
178        let dln = 0.1;
179        let mu = 0.0;
180
181        // Create a simple test signal
182        let x: Vec<f64> = (0..n)
183            .map(|i| ((i as f64 - n as f64 / 2.0) * dln).exp())
184            .collect();
185
186        // Test forward transform
187        let y = fht(&x, dln, mu, None, None).unwrap();
188        assert_eq!(y.len(), n);
189
190        // Test inverse transform
191        let x_recovered = ifht(&y, dln, mu, None, None).unwrap();
192        assert_eq!(x_recovered.len(), n);
193    }
194
195    #[test]
196    fn test_fhtoffset() {
197        let dln = 0.1;
198        let mu = 0.5;
199
200        // Test with zero bias
201        let offset1 = fhtoffset(dln, mu, None, Some(0.0)).unwrap();
202        assert_relative_eq!(offset1, 0.0, epsilon = 1e-10);
203
204        // Test with non-zero bias and initial guess
205        let offset2 = fhtoffset(dln, mu, Some(0.5), Some(1.0)).unwrap();
206        assert_relative_eq!(offset2, 0.5, epsilon = 1e-10);
207    }
208
209    #[test]
210    fn test_sample_points() {
211        let n = 8;
212        let dln = 0.5;
213        let offset = 1.0;
214
215        let points = fht_sample_points(n, dln, offset);
216        assert_eq!(points.len(), n);
217
218        // Check that points are logarithmically spaced
219        for i in 1..n {
220            let ratio = points[i] / points[i - 1];
221            assert_relative_eq!(ratio.ln(), dln, epsilon = 1e-10);
222        }
223    }
224}