sparse_ir/
basis.rs

1//! Finite temperature basis for SparseIR
2//!
3//! This module provides the `FiniteTempBasis` type which represents the
4//! intermediate representation (IR) basis for a given temperature.
5
6use std::sync::Arc;
7
8use crate::kernel::{CentrosymmKernel, KernelProperties, LogisticKernel};
9use crate::poly::{PiecewiseLegendrePolyVector, default_sampling_points};
10use crate::polyfourier::PiecewiseLegendreFTVector;
11use crate::sve::{SVEResult, TworkType, compute_sve};
12use crate::traits::{Bosonic, Fermionic, StatisticsType};
13
14// Re-export Statistics enum for C-API
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum Statistics {
17    Fermionic,
18    Bosonic,
19}
20
21/// Finite temperature basis for imaginary time/frequency Green's functions
22///
23/// For a continuation kernel `K` from real frequencies `ω ∈ [-ωmax, ωmax]` to
24/// imaginary time `τ ∈ [0, β]`, this type stores the truncated singular
25/// value expansion or IR basis:
26///
27/// ```text
28/// K(τ, ω) ≈ sum(u[l](τ) * s[l] * v[l](ω) for l in 1:L)
29/// ```
30///
31/// This basis is inferred from a reduced form by appropriate scaling of
32/// the variables.
33///
34/// # Type Parameters
35///
36/// * `K` - Kernel type implementing `KernelProperties + CentrosymmKernel`
37/// * `S` - Statistics type (`Fermionic` or `Bosonic`)
38#[derive(Clone)]
39pub struct FiniteTempBasis<K, S>
40where
41    K: KernelProperties + CentrosymmKernel + Clone + 'static,
42    S: StatisticsType,
43{
44    /// The kernel used to construct this basis
45    kernel: K,
46
47    /// The SVE result (in scaled variables)
48    sve_result: Arc<SVEResult>,
49
50    /// Accuracy of the basis (relative error)
51    accuracy: f64,
52
53    /// Inverse temperature β
54    beta: f64,
55
56    /// Left singular functions on imaginary time axis τ ∈ [0, β]
57    /// Arc for efficient sharing (large immutable data)
58    u: Arc<PiecewiseLegendrePolyVector>,
59
60    /// Right singular functions on real frequency axis ω ∈ [-ωmax, ωmax]
61    /// Arc for efficient sharing (large immutable data)
62    v: Arc<PiecewiseLegendrePolyVector>,
63
64    /// Singular values
65    s: Vec<f64>,
66
67    /// Left singular functions on Matsubara frequency axis (Fourier transform of u)
68    /// Arc for efficient sharing (large immutable data)
69    uhat: Arc<PiecewiseLegendreFTVector<S>>,
70
71    /// Full uhat (before truncation to basis size)
72    /// Arc for efficient sharing (large immutable data, used for Matsubara sampling)
73    uhat_full: Arc<PiecewiseLegendreFTVector<S>>,
74
75    _phantom: std::marker::PhantomData<S>,
76}
77
78impl<K, S> FiniteTempBasis<K, S>
79where
80    K: KernelProperties + CentrosymmKernel + Clone + 'static,
81    S: StatisticsType,
82{
83    // ========== Getters ==========
84
85    /// Get a reference to the kernel
86    pub fn kernel(&self) -> &K {
87        &self.kernel
88    }
89
90    /// Get the SVE result
91    pub fn sve_result(&self) -> &Arc<SVEResult> {
92        &self.sve_result
93    }
94
95    /// Get the accuracy of the basis
96    pub fn accuracy(&self) -> f64 {
97        self.accuracy
98    }
99
100    /// Get the inverse temperature β
101    pub fn beta(&self) -> f64 {
102        self.beta
103    }
104
105    /// Get the left singular functions (u) on imaginary time axis
106    pub fn u(&self) -> &Arc<PiecewiseLegendrePolyVector> {
107        &self.u
108    }
109
110    /// Get the right singular functions (v) on real frequency axis
111    pub fn v(&self) -> &Arc<PiecewiseLegendrePolyVector> {
112        &self.v
113    }
114
115    /// Get the singular values
116    pub fn s(&self) -> &[f64] {
117        &self.s
118    }
119
120    /// Get the left singular functions on Matsubara frequency axis (uhat)
121    pub fn uhat(&self) -> &Arc<PiecewiseLegendreFTVector<S>> {
122        &self.uhat
123    }
124
125    /// Get the full uhat (before truncation)
126    pub fn uhat_full(&self) -> &Arc<PiecewiseLegendreFTVector<S>> {
127        &self.uhat_full
128    }
129
130    // ========== Other methods ==========
131
132    /// Get the frequency cutoff ωmax
133    pub fn wmax(&self) -> f64 {
134        self.kernel.lambda() / self.beta
135    }
136
137    /// Get default Matsubara sampling points as i64 indices (for C-API)
138    pub fn default_matsubara_sampling_points_i64(&self, positive_only: bool) -> Vec<i64>
139    where
140        S: 'static,
141    {
142        let freqs = self.default_matsubara_sampling_points(positive_only);
143        freqs.into_iter().map(|f| f.n()).collect()
144    }
145
146    /// Get default Matsubara sampling points as i64 indices with mitigate parameter (for C-API)
147    ///
148    /// # Panics
149    /// Panics if the kernel is not centrosymmetric. This method relies on
150    /// centrosymmetry to generate sampling points.
151    pub fn default_matsubara_sampling_points_i64_with_mitigate(
152        &self,
153        positive_only: bool,
154        mitigate: bool,
155        n_points: usize,
156    ) -> Vec<i64>
157    where
158        S: 'static,
159    {
160        if !self.kernel().is_centrosymmetric() {
161            panic!(
162                "default_matsubara_sampling_points_i64_with_mitigate is not supported for non-centrosymmetric kernels. \
163                 The current implementation relies on centrosymmetry to generate sampling points."
164            );
165        }
166        let fence = mitigate;
167        let freqs = Self::default_matsubara_sampling_points_impl(
168            &self.uhat_full,
169            n_points,
170            fence,
171            positive_only,
172        );
173        freqs.into_iter().map(|f| f.n()).collect()
174    }
175
176    /// Create a new FiniteTempBasis
177    ///
178    /// # Arguments
179    ///
180    /// * `kernel` - Kernel implementing `KernelProperties + CentrosymmKernel`
181    /// * `beta` - Inverse temperature (β > 0)
182    /// * `epsilon` - Accuracy parameter (optional, defaults to NaN for auto)
183    /// * `max_size` - Maximum number of basis functions (optional)
184    ///
185    /// # Returns
186    ///
187    /// A new FiniteTempBasis
188    pub fn new(kernel: K, beta: f64, epsilon: Option<f64>, max_size: Option<usize>) -> Self {
189        // Validate inputs
190        if beta <= 0.0 {
191            panic!("Inverse temperature beta must be positive, got {}", beta);
192        }
193
194        // Compute SVE
195        let epsilon_value = epsilon.unwrap_or(f64::NAN);
196        let sve_result = compute_sve(
197            kernel.clone(),
198            epsilon_value,
199            None, // cutoff
200            max_size,
201            TworkType::Auto,
202        );
203
204        Self::from_sve_result(kernel, beta, sve_result, epsilon, max_size)
205    }
206
207    /// Create basis from existing SVE result
208    ///
209    /// This is useful when you want to reuse the same SVE computation
210    /// for both fermionic and bosonic bases.
211    pub fn from_sve_result(
212        kernel: K,
213        beta: f64,
214        sve_result: SVEResult,
215        epsilon: Option<f64>,
216        max_size: Option<usize>,
217    ) -> Self {
218        // Get truncated u, s, v from SVE result
219        let (u_sve, s_sve, v_sve) = sve_result.part(epsilon, max_size);
220
221        // Calculate accuracy
222        let accuracy = if sve_result.s.len() > s_sve.len() {
223            sve_result.s[s_sve.len()] / sve_result.s[0]
224        } else {
225            sve_result.s[sve_result.s.len() - 1] / sve_result.s[0]
226        };
227
228        // Get kernel parameters
229        let lambda = kernel.lambda();
230        let omega_max = lambda / beta;
231
232        // Scale polynomials to new variables
233        // tau = β/2 * (x + 1), w = ωmax * y
234
235        // Transform u: x ∈ [-1, 1] → τ ∈ [0, β]
236        let u_knots: Vec<f64> = u_sve.get_polys()[0]
237            .knots
238            .iter()
239            .map(|&x| beta / 2.0 * (x + 1.0))
240            .collect();
241        let u_delta_x: Vec<f64> = u_sve.get_polys()[0]
242            .delta_x
243            .iter()
244            .map(|&dx| beta / 2.0 * dx)
245            .collect();
246        let u_symm: Vec<i32> = u_sve.get_polys().iter().map(|p| p.symm).collect();
247
248        let u = u_sve.rescale_domain(u_knots, Some(u_delta_x), Some(u_symm));
249
250        // Transform v: y ∈ [-1, 1] → ω ∈ [-ωmax, ωmax]
251        let v_knots: Vec<f64> = v_sve.get_polys()[0]
252            .knots
253            .iter()
254            .map(|&y| omega_max * y)
255            .collect();
256        let v_delta_x: Vec<f64> = v_sve.get_polys()[0]
257            .delta_x
258            .iter()
259            .map(|&dy| omega_max * dy)
260            .collect();
261        let v_symm: Vec<i32> = v_sve.get_polys().iter().map(|p| p.symm).collect();
262
263        let v = v_sve.rescale_domain(v_knots, Some(v_delta_x), Some(v_symm));
264
265        // Scale singular values
266        // s_scaled = sqrt(β/2 * ωmax) * ωmax^(-ypower) * s_sve
267        let ypower = kernel.ypower();
268        let scale_factor = (beta / 2.0 * omega_max).sqrt() * omega_max.powi(-ypower);
269        let s: Vec<f64> = s_sve.iter().map(|&x| scale_factor * x).collect();
270
271        // Construct uhat (Fourier transform of u)
272        // HACK: Fourier transforms only work on unit interval, so we scale the data
273        let uhat_base_full = sve_result.u.scale_data(beta.sqrt());
274        let conv_rad = kernel.conv_radius();
275
276        // Create statistics marker instance using Default trait
277        // S is a zero-sized type (ZST) like Fermionic or Bosonic
278        let stat_marker = S::default();
279
280        let uhat_full = PiecewiseLegendreFTVector::<S>::from_poly_vector(
281            &uhat_base_full,
282            stat_marker,
283            Some(conv_rad),
284        );
285
286        // Truncate uhat to basis size
287        let uhat_polyvec: Vec<_> = uhat_full.polyvec.iter().take(s.len()).cloned().collect();
288        let uhat = PiecewiseLegendreFTVector::from_vector(uhat_polyvec);
289
290        Self {
291            kernel,
292            sve_result: Arc::new(sve_result),
293            accuracy,
294            beta,
295            u: Arc::new(u),
296            v: Arc::new(v),
297            s,
298            uhat: Arc::new(uhat),
299            uhat_full: Arc::new(uhat_full),
300            _phantom: std::marker::PhantomData,
301        }
302    }
303
304    /// Get the size of the basis (number of basis functions)
305    pub fn size(&self) -> usize {
306        self.s.len()
307    }
308
309    /// Get the cutoff parameter Λ = β * ωmax
310    pub fn lambda(&self) -> f64 {
311        self.kernel.lambda()
312    }
313
314    /// Get the frequency cutoff ωmax
315    pub fn omega_max(&self) -> f64 {
316        self.lambda() / self.beta
317    }
318
319    /// Get significance of each singular value (s[i] / s[0])
320    pub fn significance(&self) -> Vec<f64> {
321        let s0 = self.s[0];
322        self.s.iter().map(|&s| s / s0).collect()
323    }
324
325    /// Get default tau sampling points
326    ///
327    /// C++ implementation: libsparseir/include/sparseir/basis.hpp:229-270
328    ///
329    /// Returns sampling points in imaginary time τ ∈ [-β/2, β/2].
330    ///
331    /// # Panics
332    /// Panics if the kernel is not centrosymmetric. This method relies on
333    /// centrosymmetry to generate symmetric sampling points.
334    pub fn default_tau_sampling_points(&self) -> Vec<f64> {
335        if !self.kernel().is_centrosymmetric() {
336            panic!(
337                "default_tau_sampling_points is not supported for non-centrosymmetric kernels. \
338                 The current implementation relies on centrosymmetry to generate symmetric sampling points."
339            );
340        }
341        let points = self.default_tau_sampling_points_size_requested(self.size());
342        let basis_size = self.size();
343        if points.len() < basis_size {
344            eprintln!(
345                "Warning: Number of tau sampling points ({}) is less than basis size ({}). \
346                 Basis parameters: beta={}, wmax={}, epsilon={:.2e}",
347                points.len(),
348                basis_size,
349                self.beta,
350                self.wmax(),
351                self.accuracy()
352            );
353        }
354        points
355    }
356
357    /// Get default tau sampling points with a requested size
358    ///
359    /// # Panics
360    /// Panics if the kernel is not centrosymmetric. This method relies on
361    /// centrosymmetry to generate symmetric sampling points.
362    pub fn default_tau_sampling_points_size_requested(&self, size_requested: usize) -> Vec<f64> {
363        if !self.kernel().is_centrosymmetric() {
364            panic!(
365                "default_tau_sampling_points_size_requested is not supported for non-centrosymmetric kernels. \
366                 The current implementation relies on centrosymmetry to generate symmetric sampling points."
367            );
368        }
369        // C++: Eigen::VectorXd x = default_sampling_points(*(this->sve_result->u), sz);
370        let x = default_sampling_points(&self.sve_result.u, size_requested);
371        // C++: Extract unique half of sampling points
372        let mut unique_x = Vec::new();
373        if x.len() % 2 == 0 {
374            // C++: for (auto i = 0; i < x.size() / 2; ++i)
375            for i in 0..(x.len() / 2) {
376                unique_x.push(x[i]);
377            }
378        } else {
379            // C++: for (auto i = 0; i < x.size() / 2; ++i)
380            for i in 0..(x.len() / 2) {
381                unique_x.push(x[i]);
382            }
383            // C++: auto x_new = 0.5 * (unique_x.back() + 0.5);
384            let x_new = 0.5 * (unique_x.last().unwrap() + 0.5);
385            unique_x.push(x_new);
386        }
387
388        // C++: Generate symmetric points
389        //      Eigen::VectorXd smpl_taus(2 * unique_x.size());
390        //      for (auto i = 0; i < unique_x.size(); ++i) {
391        //          smpl_taus(i) = (this->beta / 2.0) * (unique_x[i] + 1.0);
392        //          smpl_taus(unique_x.size() + i) = -smpl_taus(i);
393        //      }
394        let mut smpl_taus = Vec::with_capacity(2 * unique_x.len());
395        for &ux in &unique_x {
396            smpl_taus.push((self.beta / 2.0) * (ux + 1.0));
397        }
398        for i in 0..unique_x.len() {
399            smpl_taus.push(-smpl_taus[i]);
400        }
401
402        // C++: std::sort(smpl_taus.data(), smpl_taus.data() + smpl_taus.size());
403        smpl_taus.sort_by(|a, b| a.partial_cmp(b).unwrap());
404
405        // C++: Check if the number of sampling points is even
406        if smpl_taus.len() % 2 != 0 {
407            panic!("The number of tau sampling points is odd!");
408        }
409
410        // C++: Check if tau = 0 is not in the sampling points
411        for &tau in &smpl_taus {
412            if tau.abs() < 1e-10 {
413                eprintln!(
414                    "Warning: tau = 0 is in the sampling points (absolute error: {})",
415                    tau.abs()
416                );
417            }
418        }
419
420        // C++ implementation returns tau in [-beta/2, beta/2] (does NOT convert to [0, beta])
421        // This matches the natural range for centrosymmetric kernels
422        smpl_taus
423    }
424
425    /// Get default Matsubara frequency sampling points
426    ///
427    /// Returns sampling points as MatsubaraFreq objects based on extrema
428    /// of the Matsubara basis functions (same algorithm as C++/Julia).
429    ///
430    /// # Arguments
431    /// * `positive_only` - If true, returns only non-negative frequencies
432    ///
433    /// # Returns
434    /// Vector of Matsubara frequency sampling points
435    ///
436    /// # Panics
437    /// Panics if the kernel is not centrosymmetric. This method relies on
438    /// centrosymmetry to generate sampling points.
439    pub fn default_matsubara_sampling_points(
440        &self,
441        positive_only: bool,
442    ) -> Vec<crate::freq::MatsubaraFreq<S>>
443    where
444        S: 'static,
445    {
446        if !self.kernel().is_centrosymmetric() {
447            panic!(
448                "default_matsubara_sampling_points is not supported for non-centrosymmetric kernels. \
449                 The current implementation relies on centrosymmetry to generate sampling points."
450            );
451        }
452        let fence = false;
453        let points = Self::default_matsubara_sampling_points_impl(
454            &self.uhat_full,
455            self.size(),
456            fence,
457            positive_only,
458        );
459        let basis_size = self.size();
460        // For positive_only=true, we need 2*n_sampling_points >= basis_size
461        // For positive_only=false, we need n_sampling_points >= basis_size
462        let effective_points = if positive_only {
463            2 * points.len()
464        } else {
465            points.len()
466        };
467        if effective_points < basis_size {
468            eprintln!(
469                "Warning: Number of Matsubara sampling points ({}{}) is less than basis size ({}). \
470                 Basis parameters: beta={}, wmax={}, epsilon={:.2e}",
471                points.len(),
472                if positive_only { " × 2" } else { "" },
473                basis_size,
474                self.beta,
475                self.wmax(),
476                self.accuracy()
477            );
478        }
479        points
480    }
481
482    /// Fence Matsubara sampling points to improve conditioning
483    ///
484    /// This function adds additional sampling points near the outer frequencies
485    /// to improve the conditioning of the sampling matrix. This is particularly
486    /// important for Matsubara sampling where we cannot freely choose sampling points.
487    ///
488    /// Implementation matches C++ version in `basis.hpp` (lines 407-452).
489    fn fence_matsubara_sampling(
490        omega_n: &mut Vec<crate::freq::MatsubaraFreq<S>>,
491        positive_only: bool,
492    ) where
493        S: StatisticsType + 'static,
494    {
495        use crate::freq::{BosonicFreq, MatsubaraFreq};
496
497        if omega_n.is_empty() {
498            return;
499        }
500
501        // Collect outer frequencies
502        let mut outer_frequencies = Vec::new();
503        if positive_only {
504            outer_frequencies.push(omega_n[omega_n.len() - 1]);
505        } else {
506            outer_frequencies.push(omega_n[0]);
507            outer_frequencies.push(omega_n[omega_n.len() - 1]);
508        }
509
510        for wn_outer in outer_frequencies {
511            let outer_val = wn_outer.n();
512            // In SparseIR.jl-v1, ωn_diff is always created as BosonicFreq
513            // This ensures diff_val is always even (valid for Bosonic)
514            let mut diff_val = 2 * (0.025 * outer_val as f64).round() as i64;
515
516            // Handle edge case: if diff_val is 0, set it to 2 (minimum even value for Bosonic)
517            if diff_val == 0 {
518                diff_val = 2;
519            }
520
521            // Get the n value from BosonicFreq (same as diff_val since it's even)
522            let wn_diff = BosonicFreq::new(diff_val).unwrap().n();
523
524            // Sign function: returns +1 if n > 0, -1 if n < 0, 0 if n == 0
525            // Matches C++ implementation: (a.get_n() > 0) - (a.get_n() < 0)
526            let sign_val = if outer_val > 0 {
527                1
528            } else if outer_val < 0 {
529                -1
530            } else {
531                0
532            };
533
534            // Check original size before adding (C++ checks wn.size() before each push)
535            let original_size = omega_n.len();
536            if original_size >= 20 {
537                // For Fermionic: wn_outer.n is odd, wn_diff is even, so wn_outer.n ± wn_diff is odd (valid)
538                // For Bosonic: wn_outer.n is even, wn_diff is even, so wn_outer.n ± wn_diff is even (valid)
539                let new_n = outer_val - sign_val * wn_diff;
540                if let Ok(new_freq) = MatsubaraFreq::<S>::new(new_n) {
541                    omega_n.push(new_freq);
542                }
543            }
544            if original_size >= 42 {
545                let new_n = outer_val + sign_val * wn_diff;
546                if let Ok(new_freq) = MatsubaraFreq::<S>::new(new_n) {
547                    omega_n.push(new_freq);
548                }
549            }
550        }
551
552        // Sort and remove duplicates using BTreeSet
553        let omega_n_set: std::collections::BTreeSet<MatsubaraFreq<S>> = omega_n.drain(..).collect();
554        *omega_n = omega_n_set.into_iter().collect();
555    }
556
557    pub fn default_matsubara_sampling_points_impl(
558        uhat_full: &PiecewiseLegendreFTVector<S>,
559        l: usize,
560        fence: bool,
561        positive_only: bool,
562    ) -> Vec<crate::freq::MatsubaraFreq<S>>
563    where
564        S: StatisticsType + 'static,
565    {
566        use crate::freq::MatsubaraFreq;
567        use crate::polyfourier::{find_extrema, sign_changes};
568        use std::collections::BTreeSet;
569
570        let mut l_requested = l;
571
572        // Adjust l_requested based on statistics (same as C++)
573        if S::STATISTICS == crate::traits::Statistics::Fermionic && l_requested % 2 != 0 {
574            l_requested += 1;
575        } else if S::STATISTICS == crate::traits::Statistics::Bosonic && l_requested % 2 == 0 {
576            l_requested += 1;
577        }
578
579        // Choose sign_changes or find_extrema based on l_requested
580        let mut omega_n = if l_requested < uhat_full.len() {
581            sign_changes(&uhat_full[l_requested], positive_only)
582        } else {
583            find_extrema(&uhat_full[uhat_full.len() - 1], positive_only)
584        };
585
586        // For bosons, include zero frequency explicitly to prevent conditioning issues
587        if S::STATISTICS == crate::traits::Statistics::Bosonic {
588            omega_n.push(MatsubaraFreq::<S>::new(0).unwrap());
589        }
590
591        // Sort and remove duplicates using BTreeSet
592        let omega_n_set: BTreeSet<MatsubaraFreq<S>> = omega_n.into_iter().collect();
593        let mut omega_n: Vec<MatsubaraFreq<S>> = omega_n_set.into_iter().collect();
594
595        // Check expected size
596        let expected_size = if positive_only {
597            l_requested.div_ceil(2)
598        } else {
599            l_requested
600        };
601
602        if omega_n.len() != expected_size {
603            eprintln!(
604                "Warning: Requested {} sampling frequencies for basis size L = {}, but got {}.",
605                expected_size,
606                l,
607                omega_n.len()
608            );
609        }
610
611        // Apply fencing if requested (same as C++ implementation)
612        if fence {
613            Self::fence_matsubara_sampling(&mut omega_n, positive_only);
614        }
615
616        omega_n
617    }
618    /// Get default omega (real frequency) sampling points
619    ///
620    /// Returns sampling points on the real-frequency axis ω ∈ [-ωmax, ωmax].
621    /// These are used as pole locations for the Discrete Lehmann Representation (DLR).
622    ///
623    /// The sampling points are chosen as the roots of the L-th basis function
624    /// in the spectral domain (v), which provides near-optimal conditioning.
625    ///
626    /// # Returns
627    /// Vector of real-frequency sampling points in [-ωmax, ωmax]
628    pub fn default_omega_sampling_points(&self) -> Vec<f64> {
629        let sz = self.size();
630
631        // Use UNTRUNCATED sve_result.v (same as C++)
632        // C++: default_sampling_points(*(sve_result->v), sz)
633        let y = default_sampling_points(&self.sve_result.v, sz);
634
635        // Scale to [-ωmax, ωmax]
636        let wmax = self.kernel.lambda() / self.beta;
637        let omega_points: Vec<f64> = y.into_iter().map(|yi| wmax * yi).collect();
638
639        omega_points
640    }
641}
642
643// ============================================================================
644// Trait implementations
645// ============================================================================
646
647impl<K, S> crate::basis_trait::Basis<S> for FiniteTempBasis<K, S>
648where
649    K: KernelProperties + CentrosymmKernel + Clone + 'static,
650    S: StatisticsType + 'static,
651{
652    type Kernel = K;
653
654    fn kernel(&self) -> &Self::Kernel {
655        &self.kernel
656    }
657
658    fn beta(&self) -> f64 {
659        self.beta
660    }
661
662    fn wmax(&self) -> f64 {
663        self.kernel.lambda() / self.beta
664    }
665
666    fn lambda(&self) -> f64 {
667        self.kernel.lambda()
668    }
669
670    fn size(&self) -> usize {
671        self.size()
672    }
673
674    fn accuracy(&self) -> f64 {
675        self.accuracy
676    }
677
678    fn significance(&self) -> Vec<f64> {
679        if let Some(&first_s) = self.s.first() {
680            self.s.iter().map(|&s| s / first_s).collect()
681        } else {
682            vec![]
683        }
684    }
685
686    fn svals(&self) -> Vec<f64> {
687        self.s.clone()
688    }
689
690    fn default_tau_sampling_points(&self) -> Vec<f64> {
691        self.default_tau_sampling_points()
692    }
693
694    fn default_matsubara_sampling_points(
695        &self,
696        positive_only: bool,
697    ) -> Vec<crate::freq::MatsubaraFreq<S>> {
698        self.default_matsubara_sampling_points(positive_only)
699    }
700
701    fn evaluate_tau(&self, tau: &[f64]) -> mdarray::DTensor<f64, 2> {
702        use crate::taufuncs::normalize_tau;
703        use mdarray::DTensor;
704
705        let n_points = tau.len();
706        let basis_size = self.size();
707
708        // Evaluate each basis function at all tau points
709        // Result: matrix[i, l] = u_l(tau[i])
710        // Note: tau can be in [-beta, beta] and will be normalized to [0, beta]
711        // self.u polynomials are already scaled to tau ∈ [0, beta] domain
712        DTensor::<f64, 2>::from_fn([n_points, basis_size], |idx| {
713            let i = idx[0]; // tau index
714            let l = idx[1]; // basis function index
715
716            // Normalize tau to [0, beta] with statistics-dependent sign
717            let (tau_norm, sign) = normalize_tau::<S>(tau[i], self.beta);
718
719            // Evaluate basis function directly (u polynomials are in tau domain)
720            sign * self.u[l].evaluate(tau_norm)
721        })
722    }
723
724    fn evaluate_matsubara(
725        &self,
726        freqs: &[crate::freq::MatsubaraFreq<S>],
727    ) -> mdarray::DTensor<num_complex::Complex<f64>, 2> {
728        use mdarray::DTensor;
729        use num_complex::Complex;
730
731        let n_points = freqs.len();
732        let basis_size = self.size();
733
734        // Evaluate each basis function at all Matsubara frequencies
735        // Result: matrix[i, l] = uhat_l(iωn[i])
736        DTensor::<Complex<f64>, 2>::from_fn([n_points, basis_size], |idx| {
737            let i = idx[0]; // frequency index
738            let l = idx[1]; // basis function index
739            self.uhat[l].evaluate(&freqs[i])
740        })
741    }
742
743    fn evaluate_omega(&self, omega: &[f64]) -> mdarray::DTensor<f64, 2> {
744        use mdarray::DTensor;
745
746        let n_points = omega.len();
747        let basis_size = self.size();
748
749        // Evaluate each spectral basis function at all omega points
750        // Result: matrix[i, l] = V_l(omega[i])
751        DTensor::<f64, 2>::from_fn([n_points, basis_size], |idx| {
752            let i = idx[0]; // omega index
753            let l = idx[1]; // basis function index
754            self.v[l].evaluate(omega[i])
755        })
756    }
757
758    fn default_omega_sampling_points(&self) -> Vec<f64> {
759        self.default_omega_sampling_points()
760    }
761}
762
763// ============================================================================
764// Type aliases
765// ============================================================================
766
767/// Type alias for fermionic basis with LogisticKernel
768pub type FermionicBasis = FiniteTempBasis<LogisticKernel, Fermionic>;
769
770/// Type alias for bosonic basis with LogisticKernel
771pub type BosonicBasis = FiniteTempBasis<LogisticKernel, Bosonic>;
772
773#[cfg(test)]
774#[path = "basis_tests.rs"]
775mod basis_tests;