Skip to main content

sparse_ir/
polyfourier.rs

1//! Piecewise Legendre polynomial Fourier transform implementation for SparseIR
2//!
3//! This module provides Fourier transform functionality for piecewise Legendre
4//! polynomials, enabling evaluation in Matsubara frequency domain.
5
6use num_complex::Complex64;
7use std::f64::consts::PI;
8
9use crate::freq::MatsubaraFreq;
10use crate::poly::{PiecewiseLegendrePoly, PiecewiseLegendrePolyVector};
11use crate::special_functions::spherical_bessel_j;
12use crate::traits::{Bosonic, Fermionic, Statistics, StatisticsType};
13
14/// Power model for asymptotic behavior
15#[derive(Debug, Clone)]
16pub struct PowerModel {
17    pub moments: Vec<f64>,
18}
19
20impl PowerModel {
21    /// Create a new power model with given moments
22    pub fn new(moments: Vec<f64>) -> Self {
23        Self { moments }
24    }
25}
26
27/// Piecewise Legendre polynomial with Fourier transform capability
28///
29/// This represents a piecewise Legendre polynomial that can be evaluated
30/// in the Matsubara frequency domain using Fourier transform.
31#[derive(Debug, Clone)]
32pub struct PiecewiseLegendreFT<S: StatisticsType> {
33    /// The underlying piecewise Legendre polynomial
34    pub poly: PiecewiseLegendrePoly,
35    /// Asymptotic cutoff frequency index
36    pub n_asymp: f64,
37    /// Power model for asymptotic behavior
38    pub model: PowerModel,
39    _phantom: std::marker::PhantomData<S>,
40}
41
42// Type aliases for convenience
43pub type FermionicPiecewiseLegendreFT = PiecewiseLegendreFT<Fermionic>;
44pub type BosonicPiecewiseLegendreFT = PiecewiseLegendreFT<Bosonic>;
45
46impl<S: StatisticsType> PiecewiseLegendreFT<S> {
47    /// Create a new PiecewiseLegendreFT from a polynomial and statistics
48    ///
49    /// # Arguments
50    /// * `poly` - The underlying piecewise Legendre polynomial
51    /// * `stat` - Statistics type (Fermionic or Bosonic)
52    /// * `n_asymp` - Asymptotic cutoff frequency index (default: infinity)
53    ///
54    /// # Panics
55    /// Panics if the polynomial domain is not [-1, 1]
56    pub fn new(poly: PiecewiseLegendrePoly, _stat: S, n_asymp: Option<f64>) -> Self {
57        // Validate domain
58        if (poly.xmin - (-1.0)).abs() > 1e-12 || (poly.xmax - 1.0).abs() > 1e-12 {
59            panic!("Only interval [-1, 1] is supported for Fourier transform");
60        }
61
62        let n_asymp = n_asymp.unwrap_or(f64::INFINITY);
63        let model = Self::power_model(&poly);
64
65        Self {
66            poly,
67            n_asymp,
68            model,
69            _phantom: std::marker::PhantomData,
70        }
71    }
72
73    /// Get the asymptotic cutoff frequency index
74    pub fn get_n_asymp(&self) -> f64 {
75        self.n_asymp
76    }
77
78    /// Get the statistics type
79    pub fn get_statistics(&self) -> Statistics {
80        S::STATISTICS
81    }
82
83    /// Get the zeta value for this statistics type
84    pub fn zeta(&self) -> i64 {
85        match S::STATISTICS {
86            Statistics::Fermionic => 1,
87            Statistics::Bosonic => 0,
88        }
89    }
90
91    /// Get a reference to the underlying polynomial
92    pub fn get_poly(&self) -> &PiecewiseLegendrePoly {
93        &self.poly
94    }
95
96    /// Evaluate the Fourier transform at a Matsubara frequency
97    ///
98    /// # Arguments
99    /// * `omega` - Matsubara frequency
100    ///
101    /// # Returns
102    /// The complex Fourier transform value
103    pub fn evaluate(&self, omega: &MatsubaraFreq<S>) -> Complex64 {
104        let n = omega.get_n();
105        if (n as f64).abs() < self.n_asymp {
106            self.compute_unl_inner(&self.poly, n)
107        } else {
108            self.giw(n)
109        }
110    }
111
112    /// Evaluate at integer Matsubara index
113    pub fn evaluate_at_n(&self, n: i64) -> Complex64 {
114        match MatsubaraFreq::<S>::new(n) {
115            Ok(omega) => self.evaluate(&omega),
116            Err(_) => Complex64::new(0.0, 0.0), // Return zero for invalid frequencies
117        }
118    }
119
120    /// Evaluate at multiple Matsubara indices
121    pub fn evaluate_at_ns(&self, ns: &[i64]) -> Vec<Complex64> {
122        ns.iter().map(|&n| self.evaluate_at_n(n)).collect()
123    }
124
125    /// Create power model for asymptotic behavior
126    fn power_model(poly: &PiecewiseLegendrePoly) -> PowerModel {
127        let deriv_x1 = poly.derivs(1.0);
128        let moments = Self::power_moments(&deriv_x1, poly.l);
129        PowerModel::new(moments)
130    }
131
132    /// Compute power moments for asymptotic expansion
133    fn power_moments(deriv_x1: &[f64], l: i32) -> Vec<f64> {
134        let statsign = match S::STATISTICS {
135            Statistics::Fermionic => -1.0,
136            Statistics::Bosonic => 1.0,
137        };
138
139        let mut moments = deriv_x1.to_vec();
140        for (m, moment) in moments.iter_mut().enumerate() {
141            let m_f64 = (m + 1) as f64; // Julia uses 1-based indexing
142            *moment *=
143                -(statsign * (-1.0_f64).powi(m_f64 as i32) + (-1.0_f64).powi(l)) / 2.0_f64.sqrt();
144        }
145        moments
146    }
147
148    /// Compute the inner Fourier transform (for small frequencies)
149    fn compute_unl_inner(&self, poly: &PiecewiseLegendrePoly, wn: i64) -> Complex64 {
150        let wred = PI / 4.0 * wn as f64;
151        let phase_wi = Self::phase_stable(poly, wn);
152        let mut res = Complex64::new(0.0, 0.0);
153
154        let order_max = poly.polyorder;
155        let segment_count = poly.knots.len() - 1;
156
157        for order in 0..order_max {
158            for j in 0..segment_count {
159                let data_oj = poly.data[[order, j]];
160                let tnl = Self::get_tnl(order as i32, wred * poly.delta_x[j]);
161                res += data_oj * tnl * phase_wi[j] / poly.norms[j];
162            }
163        }
164
165        res / 2.0_f64.sqrt()
166    }
167
168    /// Compute asymptotic behavior (for large frequencies)
169    fn giw(&self, wn: i64) -> Complex64 {
170        let iw = Complex64::new(0.0, PI / 2.0 * wn as f64);
171        if wn == 0 {
172            return Complex64::new(0.0, 0.0);
173        }
174
175        let inv_iw = 1.0 / iw;
176
177        inv_iw * Self::evalpoly(inv_iw, &self.model.moments)
178    }
179
180    /// Evaluate polynomial at complex point (Horner's method)
181    fn evalpoly(x: Complex64, coeffs: &[f64]) -> Complex64 {
182        let mut result = Complex64::new(0.0, 0.0);
183        for i in (0..coeffs.len()).rev() {
184            result = result * x + Complex64::new(coeffs[i], 0.0);
185        }
186        result
187    }
188
189    /// Compute midpoint relative to nearest integer
190    ///
191    /// Returns (xmid_diff, extra_shift) where:
192    /// - xmid_diff: midpoint values for numerical stability
193    /// - extra_shift: nearest integer shift (-1, 0, or 1)
194    fn shift_xmid(knots: &[f64], delta_x: &[f64]) -> (Vec<f64>, Vec<i32>) {
195        let n_segments = delta_x.len();
196        let delta_x_half: Vec<f64> = delta_x.iter().map(|&dx| dx / 2.0).collect();
197
198        // xmid_m1: cumsum(Δx) - Δx_half
199        let mut xmid_m1 = Vec::with_capacity(n_segments);
200        let mut cumsum = 0.0;
201        for i in 0..n_segments {
202            cumsum += delta_x[i];
203            xmid_m1.push(cumsum - delta_x_half[i]);
204        }
205
206        // xmid_p1: -reverse(cumsum(reverse(Δx))) + Δx_half
207        let mut xmid_p1 = Vec::with_capacity(n_segments);
208        let mut cumsum_rev = 0.0;
209        for i in (0..n_segments).rev() {
210            cumsum_rev += delta_x[i];
211            xmid_p1.insert(0, -cumsum_rev + delta_x_half[i]);
212        }
213
214        // xmid_0: knots[1:] - Δx_half
215        let xmid_0: Vec<f64> = (0..n_segments)
216            .map(|i| knots[i + 1] - delta_x_half[i])
217            .collect();
218
219        // Determine shift and diff
220        let mut xmid_diff = Vec::with_capacity(n_segments);
221        let mut extra_shift = Vec::with_capacity(n_segments);
222
223        for i in 0..n_segments {
224            let shift = xmid_0[i].round() as i32;
225            extra_shift.push(shift);
226
227            // Choose appropriate xmid based on shift
228            let diff = match shift {
229                -1 => xmid_m1[i],
230                0 => xmid_0[i],
231                1 => xmid_p1[i],
232                _ => xmid_0[i], // Fallback
233            };
234            xmid_diff.push(diff);
235        }
236
237        (xmid_diff, extra_shift)
238    }
239
240    /// Compute stable phase factors
241    ///
242    /// Computes: im^mod(wn * (extra_shift + 1), 4) * cispi(wn * xmid_diff / 2)
243    /// where cispi(x) = exp(i*π*x)
244    fn phase_stable(poly: &PiecewiseLegendrePoly, wn: i64) -> Vec<Complex64> {
245        let (xmid_diff, extra_shift) = Self::shift_xmid(&poly.knots, &poly.delta_x);
246        let mut phase_wi = Vec::with_capacity(xmid_diff.len());
247
248        let im_unit = Complex64::new(0.0, 1.0);
249
250        for j in 0..xmid_diff.len() {
251            // Compute im^mod(wn * (extra_shift[j] + 1), 4)
252            let power = ((wn * (extra_shift[j] as i64 + 1)) % 4 + 4) % 4; // Ensure positive mod
253            let im_power = im_unit.powi(power as i32);
254
255            // Compute cispi(wn * xmid_diff[j] / 2) = exp(i*π*wn*xmid_diff/2)
256            let arg = PI * wn as f64 * xmid_diff[j] / 2.0;
257            let cispi = Complex64::new(arg.cos(), arg.sin());
258
259            phase_wi.push(im_power * cispi);
260        }
261
262        phase_wi
263    }
264
265    /// Find sign changes in the Fourier transform
266    ///
267    /// # Arguments
268    /// * `positive_only` - If true, only return positive frequency sign changes
269    ///
270    /// # Returns
271    /// Vector of Matsubara frequencies where sign changes occur
272    pub fn sign_changes(&self, positive_only: bool) -> Vec<MatsubaraFreq<S>> {
273        let f = Self::func_for_part(self);
274        let x0 = Self::find_all_roots(&f, DEFAULT_GRID);
275
276        // Transform grid indices to Matsubara frequencies
277        let mut matsubara_indices: Vec<i64> = x0.into_iter().map(|x| 2 * x + self.zeta()).collect();
278
279        if !positive_only {
280            symmetrize_matsubara_inplace(&mut matsubara_indices);
281        }
282
283        matsubara_indices
284            .into_iter()
285            .filter_map(|n| MatsubaraFreq::<S>::new(n).ok())
286            .collect()
287    }
288
289    /// Find extrema in the Fourier transform
290    ///
291    /// # Arguments
292    /// * `positive_only` - If true, only return positive frequency extrema
293    ///
294    /// # Returns
295    /// Vector of Matsubara frequencies where extrema occur
296    pub fn find_extrema(&self, positive_only: bool) -> Vec<MatsubaraFreq<S>> {
297        let f = Self::func_for_part(self);
298        let x0 = Self::discrete_extrema(&f, DEFAULT_GRID);
299
300        // Transform grid indices to Matsubara frequencies
301        let mut matsubara_indices: Vec<i64> = x0.into_iter().map(|x| 2 * x + self.zeta()).collect();
302
303        if !positive_only {
304            symmetrize_matsubara_inplace(&mut matsubara_indices);
305        }
306
307        matsubara_indices
308            .into_iter()
309            .filter_map(|n| MatsubaraFreq::<S>::new(n).ok())
310            .collect()
311    }
312
313    /// Create function for extracting real or imaginary part based on parity
314    ///
315    /// Takes a grid index and converts to the corresponding Matsubara frequency
316    /// index (2*n + zeta) before evaluating.
317    fn func_for_part(&self) -> impl Fn(i64) -> f64 + '_ {
318        let parity = self.poly.symm;
319        let poly_ft = self.clone();
320        let zeta = self.zeta();
321
322        move |n| {
323            // Convert grid index to Matsubara frequency index
324            let matsubara_n = 2 * n + zeta;
325            let omega = match MatsubaraFreq::<S>::new(matsubara_n) {
326                Ok(omega) => omega,
327                Err(_) => return 0.0,
328            };
329            let value = poly_ft.evaluate(&omega);
330
331            match parity {
332                1 => match S::STATISTICS {
333                    Statistics::Fermionic => value.im,
334                    Statistics::Bosonic => value.re,
335                },
336                -1 => match S::STATISTICS {
337                    Statistics::Fermionic => value.re,
338                    Statistics::Bosonic => value.im,
339                },
340                0 => {
341                    // For symm = 0, use real part for both statistics
342                    value.re
343                }
344                _ => panic!("Cannot detect parity for symm = {}", parity),
345            }
346        }
347    }
348
349    /// Find all roots using the same algorithm as the poly module
350    fn find_all_roots<F>(f: &F, xgrid: &[i64]) -> Vec<i64>
351    where
352        F: Fn(i64) -> f64,
353    {
354        if xgrid.is_empty() {
355            return Vec::new();
356        }
357
358        // Evaluate function at all grid points
359        let fx: Vec<f64> = xgrid.iter().map(|&x| f(x)).collect();
360
361        // Find exact zeros (direct hits)
362        let mut x_hit = Vec::new();
363        for i in 0..fx.len() {
364            if fx[i] == 0.0 {
365                x_hit.push(xgrid[i]);
366            }
367        }
368
369        // Find sign changes
370        let mut sign_change = Vec::new();
371        for i in 0..fx.len() - 1 {
372            let has_sign_change = fx[i].signum() != fx[i + 1].signum();
373            let not_hit = fx[i] != 0.0 && fx[i + 1] != 0.0;
374            let both_nonzero = fx[i].abs() > 1e-12 && fx[i + 1].abs() > 1e-12;
375            sign_change.push(has_sign_change && not_hit && both_nonzero);
376        }
377
378        // If no sign changes, return only direct hits
379        if sign_change.iter().all(|&sc| !sc) {
380            x_hit.sort();
381            return x_hit;
382        }
383
384        // Find intervals with sign changes
385        let mut a_intervals = Vec::new();
386        let mut b_intervals = Vec::new();
387        let mut fa_values = Vec::new();
388
389        for i in 0..sign_change.len() {
390            if sign_change[i] {
391                a_intervals.push(xgrid[i]);
392                b_intervals.push(xgrid[i + 1]);
393                fa_values.push(fx[i]);
394            }
395        }
396
397        // Use bisection for each interval with sign change
398        for i in 0..a_intervals.len() {
399            let root = Self::bisect(&f, a_intervals[i], b_intervals[i], fa_values[i]);
400            x_hit.push(root);
401        }
402
403        // Sort and return
404        x_hit.sort();
405        x_hit
406    }
407
408    /// Bisection method for integer grid
409    fn bisect<F>(f: &F, a: i64, b: i64, fa: f64) -> i64
410    where
411        F: Fn(i64) -> f64,
412    {
413        let mut a = a;
414        let mut b = b;
415        let mut fa = fa;
416
417        loop {
418            if (b - a).abs() <= 1 {
419                return a;
420            }
421
422            let mid = (a + b) / 2;
423            let fmid = f(mid);
424
425            if fa.signum() != fmid.signum() {
426                b = mid;
427            } else {
428                a = mid;
429                fa = fmid;
430            }
431        }
432    }
433
434    /// Find discrete extrema (matches Julia v1 / Python 1.x algorithm)
435    fn discrete_extrema<F>(f: &F, xgrid: &[i64]) -> Vec<i64>
436    where
437        F: Fn(i64) -> f64,
438    {
439        discrete_extrema(f, xgrid)
440    }
441
442    /// Get T_nl coefficient (special function)
443    ///
444    /// This implements the T_nl function which is related to spherical Bessel functions:
445    /// T_nl(w) = 2 * i^l * j_l(|w|) * (w < 0 ? conj : identity)
446    /// where j_l is the spherical Bessel function of the first kind.
447    pub fn get_tnl(l: i32, w: f64) -> Complex64 {
448        let abs_w = w.abs();
449
450        // Use the high-precision spherical Bessel function from special_functions
451        let sph_bessel = spherical_bessel_j(l, abs_w);
452
453        // Compute 2 * i^l
454        let im_unit = Complex64::new(0.0, 1.0);
455        let im_power = im_unit.powi(l);
456        let result = 2.0 * im_power * sph_bessel;
457
458        // Apply conjugation for negative w
459        if w < 0.0 { result.conj() } else { result }
460    }
461}
462
463/// Vector of PiecewiseLegendreFT polynomials
464#[derive(Debug, Clone)]
465pub struct PiecewiseLegendreFTVector<S: StatisticsType> {
466    pub polyvec: Vec<PiecewiseLegendreFT<S>>,
467    _phantom: std::marker::PhantomData<S>,
468}
469
470// Type aliases for convenience
471pub type FermionicPiecewiseLegendreFTVector = PiecewiseLegendreFTVector<Fermionic>;
472pub type BosonicPiecewiseLegendreFTVector = PiecewiseLegendreFTVector<Bosonic>;
473
474impl<S: StatisticsType> PiecewiseLegendreFTVector<S> {
475    /// Create an empty vector
476    pub fn new() -> Self {
477        Self {
478            polyvec: Vec::new(),
479            _phantom: std::marker::PhantomData,
480        }
481    }
482
483    /// Create from a vector of PiecewiseLegendreFT
484    pub fn from_vector(polyvec: Vec<PiecewiseLegendreFT<S>>) -> Self {
485        Self {
486            polyvec,
487            _phantom: std::marker::PhantomData,
488        }
489    }
490
491    /// Get the number of polynomials in the vector
492    pub fn len(&self) -> usize {
493        self.polyvec.len()
494    }
495
496    /// Check if the vector is empty
497    pub fn is_empty(&self) -> bool {
498        self.polyvec.is_empty()
499    }
500
501    /// Create from PiecewiseLegendrePolyVector and statistics
502    pub fn from_poly_vector(
503        polys: &PiecewiseLegendrePolyVector,
504        _stat: S,
505        n_asymp: Option<f64>,
506    ) -> Self {
507        let mut polyvec = Vec::with_capacity(polys.size());
508
509        for i in 0..polys.size() {
510            let poly = polys.get(i).unwrap().clone();
511            let ft_poly = PiecewiseLegendreFT::new(poly, _stat, n_asymp);
512            polyvec.push(ft_poly);
513        }
514
515        Self {
516            polyvec,
517            _phantom: std::marker::PhantomData,
518        }
519    }
520
521    /// Get the size of the vector
522    pub fn size(&self) -> usize {
523        self.polyvec.len()
524    }
525
526    /// Get element by index (immutable)
527    pub fn get(&self, index: usize) -> Option<&PiecewiseLegendreFT<S>> {
528        self.polyvec.get(index)
529    }
530
531    /// Get element by index (mutable)
532    pub fn get_mut(&mut self, index: usize) -> Option<&mut PiecewiseLegendreFT<S>> {
533        self.polyvec.get_mut(index)
534    }
535
536    /// Set element at index
537    pub fn set(&mut self, index: usize, poly: PiecewiseLegendreFT<S>) -> Result<(), String> {
538        if index >= self.polyvec.len() {
539            return Err(format!("Index {} out of range", index));
540        }
541        self.polyvec[index] = poly;
542        Ok(())
543    }
544
545    /// Create a similar empty vector
546    pub fn similar(&self) -> Self {
547        Self::new()
548    }
549
550    /// Get n_asymp from the first element (if any)
551    pub fn n_asymp(&self) -> f64 {
552        self.polyvec.first().map_or(f64::INFINITY, |p| p.n_asymp)
553    }
554
555    /// Evaluate all polynomials at a Matsubara frequency
556    pub fn evaluate_at(&self, omega: &MatsubaraFreq<S>) -> Vec<Complex64> {
557        self.polyvec
558            .iter()
559            .map(|poly| poly.evaluate(omega))
560            .collect()
561    }
562
563    /// Evaluate all polynomials at multiple Matsubara frequencies
564    pub fn evaluate_at_many(&self, omegas: &[MatsubaraFreq<S>]) -> Vec<Vec<Complex64>> {
565        omegas.iter().map(|omega| self.evaluate_at(omega)).collect()
566    }
567}
568
569// Indexing operators
570impl<S: StatisticsType> std::ops::Index<usize> for PiecewiseLegendreFTVector<S> {
571    type Output = PiecewiseLegendreFT<S>;
572
573    fn index(&self, index: usize) -> &Self::Output {
574        &self.polyvec[index]
575    }
576}
577
578impl<S: StatisticsType> std::ops::IndexMut<usize> for PiecewiseLegendreFTVector<S> {
579    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
580        &mut self.polyvec[index]
581    }
582}
583
584// Default implementations
585impl<S: StatisticsType> Default for PiecewiseLegendreFTVector<S> {
586    fn default() -> Self {
587        Self::new()
588    }
589}
590
591// ===== Matsubara sampling point selection =====
592
593/// Default grid for finding extrema/sign changes
594/// Matches C++ DEFAULT_GRID: [0:2^6-1] followed by exponential spacing up to 2^25
595/// Generated from Julia: [range(0; length=2^6); trunc.(Int, exp2.(range(6, 25; length=32 * (25 - 6) + 1)))]
596const DEFAULT_GRID: &[i64] = &[
597    0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
598    26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49,
599    50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 68, 69, 71, 72, 74, 76, 77,
600    79, 81, 82, 84, 86, 88, 90, 92, 94, 96, 98, 100, 103, 105, 107, 109, 112, 114, 117, 119, 122,
601    125, 128, 130, 133, 136, 139, 142, 145, 148, 152, 155, 158, 162, 165, 169, 173, 177, 181, 184,
602    189, 193, 197, 201, 206, 210, 215, 219, 224, 229, 234, 239, 245, 250, 256, 261, 267, 273, 279,
603    285, 291, 297, 304, 311, 317, 324, 331, 339, 346, 354, 362, 369, 378, 386, 394, 403, 412, 421,
604    430, 439, 449, 459, 469, 479, 490, 501, 512, 523, 534, 546, 558, 570, 583, 595, 608, 622, 635,
605    649, 663, 678, 693, 708, 724, 739, 756, 772, 789, 806, 824, 842, 861, 879, 899, 918, 939, 959,
606    980, 1002, 1024, 1046, 1069, 1092, 1116, 1141, 1166, 1191, 1217, 1244, 1271, 1299, 1327, 1357,
607    1386, 1417, 1448, 1479, 1512, 1545, 1579, 1613, 1649, 1685, 1722, 1759, 1798, 1837, 1878, 1919,
608    1961, 2004, 2048, 2092, 2138, 2185, 2233, 2282, 2332, 2383, 2435, 2488, 2543, 2599, 2655, 2714,
609    2773, 2834, 2896, 2959, 3024, 3090, 3158, 3227, 3298, 3370, 3444, 3519, 3596, 3675, 3756, 3838,
610    3922, 4008, 4096, 4185, 4277, 4371, 4466, 4564, 4664, 4766, 4870, 4977, 5086, 5198, 5311, 5428,
611    5547, 5668, 5792, 5919, 6049, 6181, 6316, 6455, 6596, 6741, 6888, 7039, 7193, 7351, 7512, 7676,
612    7844, 8016, 8192, 8371, 8554, 8742, 8933, 9129, 9328, 9533, 9741, 9955, 10173, 10396, 10623,
613    10856, 11094, 11336, 11585, 11838, 12098, 12363, 12633, 12910, 13193, 13482, 13777, 14078,
614    14387, 14702, 15024, 15353, 15689, 16032, 16384, 16742, 17109, 17484, 17866, 18258, 18657,
615    19066, 19483, 19910, 20346, 20792, 21247, 21712, 22188, 22673, 23170, 23677, 24196, 24726,
616    25267, 25820, 26386, 26964, 27554, 28157, 28774, 29404, 30048, 30706, 31378, 32065, 32768,
617    33485, 34218, 34968, 35733, 36516, 37315, 38132, 38967, 39821, 40693, 41584, 42494, 43425,
618    44376, 45347, 46340, 47355, 48392, 49452, 50535, 51641, 52772, 53928, 55108, 56315, 57548,
619    58809, 60096, 61412, 62757, 64131, 65536, 66971, 68437, 69936, 71467, 73032, 74631, 76265,
620    77935, 79642, 81386, 83168, 84989, 86850, 88752, 90695, 92681, 94711, 96785, 98904, 101070,
621    103283, 105545, 107856, 110217, 112631, 115097, 117618, 120193, 122825, 125514, 128263, 131072,
622    133942, 136875, 139872, 142935, 146064, 149263, 152531, 155871, 159284, 162772, 166337, 169979,
623    173701, 177504, 181391, 185363, 189422, 193570, 197809, 202140, 206566, 211090, 215712, 220435,
624    225262, 230195, 235236, 240387, 245650, 251029, 256526, 262144, 267884, 273750, 279744, 285870,
625    292129, 298526, 305063, 311743, 318569, 325545, 332674, 339958, 347402, 355009, 362783, 370727,
626    378845, 387141, 395618, 404281, 413133, 422180, 431424, 440871, 450525, 460390, 470472, 480774,
627    491301, 502059, 513053, 524288, 535768, 547500, 559488, 571740, 584259, 597053, 610126, 623487,
628    637139, 651091, 665348, 679917, 694805, 710019, 725567, 741455, 757690, 774282, 791236, 808562,
629    826267, 844360, 862849, 881743, 901051, 920781, 940944, 961548, 982603, 1004119, 1026107,
630    1048576, 1071536, 1095000, 1118977, 1143480, 1168519, 1194106, 1220253, 1246974, 1274279,
631    1302182, 1330696, 1359834, 1389611, 1420039, 1451134, 1482910, 1515381, 1548564, 1582473,
632    1617125, 1652535, 1688721, 1725699, 1763487, 1802102, 1841563, 1881888, 1923096, 1965207,
633    2008239, 2052214, 2097152, 2143073, 2190000, 2237955, 2286960, 2337038, 2388212, 2440507,
634    2493948, 2548558, 2604364, 2661392, 2719669, 2779222, 2840079, 2902269, 2965820, 3030763,
635    3097128, 3164947, 3234250, 3305071, 3377443, 3451399, 3526975, 3604205, 3683127, 3763777,
636    3846193, 3930414, 4016479, 4104428, 4194304, 4286147, 4380001, 4475911, 4573920, 4674076,
637    4776425, 4881015, 4987896, 5097116, 5208729, 5322785, 5439339, 5558445, 5680159, 5804538,
638    5931641, 6061527, 6194257, 6329894, 6468501, 6610142, 6754886, 6902798, 7053950, 7208411,
639    7366255, 7527555, 7692387, 7860828, 8032958, 8208857, 8388608, 8572294, 8760003, 8951822,
640    9147841, 9348153, 9552851, 9762031, 9975792, 10194233, 10417458, 10645571, 10878678, 11116890,
641    11360318, 11609077, 11863283, 12123055, 12388515, 12659788, 12937002, 13220285, 13509772,
642    13805597, 14107900, 14416823, 14732510, 15055110, 15384774, 15721657, 16065917, 16417714,
643    16777216, 17144589, 17520006, 17903645, 18295683, 18696307, 19105702, 19524063, 19951584,
644    20388467, 20834916, 21291142, 21757357, 22233781, 22720637, 23218155, 23726566, 24246110,
645    24777031, 25319577, 25874004, 26440571, 27019544, 27611195, 28215801, 28833647, 29465021,
646    30110221, 30769549, 31443315, 32131834, 32835429, 33554432,
647];
648
649/// Find sign changes of a Matsubara basis function
650///
651/// Returns Matsubara frequencies where the function changes sign.
652pub fn sign_changes<S: StatisticsType + 'static>(
653    u_hat: &PiecewiseLegendreFT<S>,
654    positive_only: bool,
655) -> Vec<MatsubaraFreq<S>> {
656    let f = func_for_part(u_hat);
657    let x0 = find_all(&f, DEFAULT_GRID);
658
659    // Convert to Matsubara indices: n = 2*x + zeta
660    let mut indices: Vec<i64> = x0.iter().map(|&x| 2 * x + u_hat.zeta()).collect();
661
662    if !positive_only {
663        symmetrize_matsubara_inplace(&mut indices);
664    }
665
666    indices
667        .iter()
668        .filter_map(|&n| MatsubaraFreq::<S>::new(n).ok())
669        .collect()
670}
671
672/// Find extrema of a Matsubara basis function
673///
674/// Returns Matsubara frequencies where the function has local extrema.
675pub fn find_extrema<S: StatisticsType + 'static>(
676    u_hat: &PiecewiseLegendreFT<S>,
677    positive_only: bool,
678) -> Vec<MatsubaraFreq<S>> {
679    let f = func_for_part(u_hat);
680    let x0 = discrete_extrema(&f, DEFAULT_GRID);
681
682    // Convert to Matsubara indices: n = 2*x + zeta
683    let mut indices: Vec<i64> = x0.iter().map(|&x| 2 * x + u_hat.zeta()).collect();
684
685    if !positive_only {
686        symmetrize_matsubara_inplace(&mut indices);
687    }
688
689    indices
690        .iter()
691        .filter_map(|&n| MatsubaraFreq::<S>::new(n).ok())
692        .collect()
693}
694
695/// Create a function that extracts the appropriate part (real/imag) based on parity
696fn func_for_part<S: StatisticsType + 'static>(
697    poly_ft: &PiecewiseLegendreFT<S>,
698) -> Box<dyn Fn(i64) -> f64> {
699    let parity = poly_ft.poly.symm();
700    let zeta = poly_ft.zeta();
701
702    // Clone what we need
703    let poly_ft_clone = poly_ft.clone();
704
705    Box::new(move |n: i64| {
706        let omega = MatsubaraFreq::<S>::new(2 * n + zeta).unwrap();
707        let value = poly_ft_clone.evaluate(&omega);
708
709        // Select real or imaginary part based on parity and statistics
710        if parity == 1 {
711            // Even parity
712            if S::STATISTICS == Statistics::Bosonic {
713                value.re
714            } else {
715                value.im
716            }
717        } else if parity == -1 {
718            // Odd parity
719            if S::STATISTICS == Statistics::Bosonic {
720                value.im
721            } else {
722                value.re
723            }
724        } else {
725            panic!("Cannot detect parity");
726        }
727    })
728}
729
730/// Integer bisection: find the zero crossing of f in [a, b] where f(a) and f(b)
731/// have different signs. Returns the largest integer x such that f(x) has the
732/// same sign as f(a). Matches Julia's `bisect` implementation.
733fn bisect(f: &dyn Fn(i64) -> f64, a: i64, b: i64, fa: f64) -> i64 {
734    let mut lo = a;
735    let mut hi = b;
736    let mut flo = fa;
737
738    while (hi - lo).abs() > 1 {
739        let mid = lo + (hi - lo) / 2;
740        let fmid = f(mid);
741        if flo.signum() != fmid.signum() {
742            hi = mid;
743        } else {
744            lo = mid;
745            flo = fmid;
746        }
747    }
748    lo
749}
750
751/// Integer bisection for extrema: find the discrete extremum of f in [a, b].
752/// Returns the integer x in [a, b] that maximizes |f(x)|.
753/// Matches Julia's `bisect_discr_extremum` implementation.
754fn bisect_discr_extremum(f: &dyn Fn(i64) -> f64, a: i64, b: i64) -> i64 {
755    let mut lo = a;
756    let mut hi = b;
757
758    while hi - lo > 2 {
759        let mid1 = lo + (hi - lo) / 3;
760        let mid2 = hi - (hi - lo) / 3;
761        if f(mid1).abs() < f(mid2).abs() {
762            lo = mid1;
763        } else {
764            hi = mid2;
765        }
766    }
767
768    // Check all remaining candidates
769    let mut best = lo;
770    let mut best_val = f(lo).abs();
771    for x in (lo + 1)..=hi {
772        let val = f(x).abs();
773        if val > best_val {
774            best = x;
775            best_val = val;
776        }
777    }
778    best
779}
780
781/// Find all sign changes of a function on a grid, using bisection to locate
782/// the precise integer position within each grid interval.
783/// Matches Julia's `find_all` with `bisect`.
784fn find_all(f: &dyn Fn(i64) -> f64, xgrid: &[i64]) -> Vec<i64> {
785    if xgrid.is_empty() {
786        return Vec::new();
787    }
788
789    let mut results = Vec::new();
790    let mut prev_val = f(xgrid[0]);
791
792    for i in 1..xgrid.len() {
793        let val = f(xgrid[i]);
794        // Detect sign change (both values must be nonzero)
795        if prev_val.signum() != val.signum() && prev_val != 0.0 && val != 0.0 {
796            // Bisect to find precise integer location of the zero
797            let root = bisect(f, xgrid[i - 1], xgrid[i], prev_val);
798            results.push(root);
799        }
800        prev_val = val;
801    }
802
803    results
804}
805
806/// Find discrete extrema of a function on a grid, using bisection to locate
807/// the precise integer position within each grid interval.
808/// Also checks boundary extrema. Matches Julia v1 / Python 1.x `discrete_extrema`.
809fn discrete_extrema(f: &dyn Fn(i64) -> f64, xgrid: &[i64]) -> Vec<i64> {
810    if xgrid.len() < 3 {
811        return Vec::new();
812    }
813
814    let fx: Vec<f64> = xgrid.iter().map(|&x| f(x)).collect();
815    let absfx: Vec<f64> = fx.iter().map(|v| v.abs()).collect();
816
817    // Forward differences: gx[i] = fx[i+1] - fx[i]
818    // signdfdx[i] = signbit(gx[i])
819    // A derivative sign change at index i means the extremum is strictly
820    // between xgrid[i] and xgrid[i+2].
821    let signdfdx: Vec<bool> = fx
822        .windows(2)
823        .map(|w| (w[1] - w[0]).is_sign_negative())
824        .collect();
825
826    let mut results = Vec::new();
827
828    for i in 0..signdfdx.len() - 1 {
829        if signdfdx[i] != signdfdx[i + 1] {
830            // Extremum between xgrid[i] and xgrid[i+2]
831            let refined = bisect_discr_extremum(f, xgrid[i], xgrid[i + 2]);
832            results.push(refined);
833        }
834    }
835
836    // Boundary: first point is extremum if |f| decreases or sign changes inwards
837    let sfx: Vec<bool> = fx.iter().map(|v| v.is_sign_negative()).collect();
838    if absfx[0] > absfx[1] || sfx[0] != sfx[1] {
839        results.insert(0, xgrid[0]);
840    }
841
842    // Boundary: last point is extremum if |f| decreases or sign changes inwards
843    let n = fx.len();
844    if absfx[n - 1] > absfx[n - 2] || sfx[n - 1] != sfx[n - 2] {
845        results.push(xgrid[n - 1]);
846    }
847
848    results
849}
850
851/// Symmetrize Matsubara indices by adding negative frequencies
852fn symmetrize_matsubara_inplace(xs: &mut Vec<i64>) {
853    if xs.is_empty() {
854        return;
855    }
856
857    // Remove zero if present
858    xs.retain(|&x| x != 0);
859
860    // Add negative frequencies
861    let positives: Vec<i64> = xs.iter().filter(|&&x| x > 0).copied().collect();
862    let mut negatives: Vec<i64> = positives.iter().map(|&x| -x).collect();
863
864    xs.append(&mut negatives);
865    xs.sort();
866    xs.dedup();
867}
868
869#[cfg(test)]
870#[path = "polyfourier_tests.rs"]
871mod polyfourier_tests;