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}