Skip to main content

sparse_ir_basis/
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::error::{
9    Error, require_accuracy, require_nonzero_size, require_positive_finite, require_threshold,
10};
11use crate::kernel::{CentrosymmKernel, KernelProperties, LogisticKernel};
12use crate::poly::{PiecewiseLegendrePoly, PiecewiseLegendrePolyVector, default_sampling_points};
13use crate::polyfourier::PiecewiseLegendreFTVector;
14use crate::sve::{SVEResult, TworkType, compute_sve};
15use crate::traits::{Bosonic, Fermionic, StatisticsType};
16
17// Re-export Statistics enum for C-API
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum Statistics {
20    Fermionic,
21    Bosonic,
22}
23
24/// Finite temperature basis for imaginary time/frequency Green's functions
25///
26/// For a continuation kernel `K` from real frequencies `ω ∈ [-ωmax, ωmax]` to
27/// imaginary time `τ ∈ [0, β]`, this type stores the truncated singular
28/// value expansion or IR basis:
29///
30/// ```text
31/// K(τ, ω) ≈ sum(u[l](τ) * s[l] * v[l](ω) for l in 0..L)
32/// ```
33///
34/// This basis is inferred from a reduced form by appropriate scaling of
35/// the variables.
36///
37/// # Type Parameters
38///
39/// * `K` - Kernel type implementing `KernelProperties + CentrosymmKernel`
40/// * `S` - Statistics type (`Fermionic` or `Bosonic`)
41#[derive(Clone)]
42pub struct FiniteTempBasis<K, S>
43where
44    K: KernelProperties + CentrosymmKernel + Clone + 'static,
45    S: StatisticsType,
46{
47    /// The kernel used to construct this basis
48    kernel: K,
49
50    /// The SVE result (in scaled variables)
51    sve_result: Arc<SVEResult>,
52
53    /// Accuracy of the basis (relative error)
54    accuracy: f64,
55
56    /// Inverse temperature β
57    beta: f64,
58
59    /// Left singular functions on imaginary time axis τ ∈ [0, β]
60    /// Arc for efficient sharing (large immutable data)
61    u: Arc<PiecewiseLegendrePolyVector>,
62
63    /// Right singular functions on real frequency axis ω ∈ [-ωmax, ωmax]
64    /// Arc for efficient sharing (large immutable data)
65    v: Arc<PiecewiseLegendrePolyVector>,
66
67    /// Singular values
68    s: Vec<f64>,
69
70    /// Left singular functions on Matsubara frequency axis (Fourier transform of u)
71    /// Arc for efficient sharing (large immutable data)
72    uhat: Arc<PiecewiseLegendreFTVector<S>>,
73
74    /// Full uhat (before truncation to basis size)
75    /// Arc for efficient sharing (large immutable data, used for Matsubara sampling)
76    uhat_full: Arc<PiecewiseLegendreFTVector<S>>,
77
78    _phantom: std::marker::PhantomData<S>,
79}
80
81/// Check that `sve_result` is an SVE on [-1, 1] × [-1, 1], the domain of
82/// the kernels in the scaled variables x = 2τ/β - 1 and y = ω/ωmax
83///
84/// The tolerance is the one of the Fourier transform of the basis functions
85/// (`PiecewiseLegendreFT::new`), which requires the interval [-1, 1].
86fn check_unit_domain(sve_result: &SVEResult) -> Result<(), Error> {
87    let domain =
88        |funcs: &PiecewiseLegendrePolyVector| funcs.get_polys().first().map(|p| (p.xmin, p.xmax));
89    let (Some(u), Some(v)) = (domain(&sve_result.u), domain(&sve_result.v)) else {
90        return Err(Error::EmptyInput { name: "sve_result" });
91    };
92    let is_unit = |(lo, hi): (f64, f64)| (lo + 1.0).abs() <= 1e-12 && (hi - 1.0).abs() <= 1e-12;
93    if is_unit(u) && is_unit(v) {
94        Ok(())
95    } else {
96        Err(Error::InvalidParameter {
97            name: "sve_result",
98            value: format!("an SVE on [{:?}, {:?}] × [{:?}, {:?}]", u.0, u.1, v.0, v.1),
99            reason: "must be an SVE on [-1, 1] × [-1, 1]".to_string(),
100        })
101    }
102}
103
104/// Knots and widths of `poly` with the first and last knot set exactly to
105/// -1 and 1
106///
107/// `check_unit_domain` accepts an SVE whose domain differs from [-1, 1] by up
108/// to 1e-12 (e.g. from `spir_sve_result_from_matrix`). Scaled to τ and ω,
109/// such knots end inside [0, β] and [-ωmax, ωmax], where evaluating at the
110/// ends panics. The width of an end segment is recomputed only if its knot
111/// moved, so an SVE with exact ends keeps its knots and widths bit for bit.
112fn unit_knots_and_widths(poly: &PiecewiseLegendrePoly) -> (Vec<f64>, Vec<f64>) {
113    let mut knots = poly.knots.clone();
114    let mut widths = poly.delta_x.clone();
115    let last = knots.len() - 1;
116    if knots[0] != -1.0 {
117        knots[0] = -1.0;
118        widths[0] = knots[1] - knots[0];
119    }
120    if knots[last] != 1.0 {
121        knots[last] = 1.0;
122        widths[last - 1] = knots[last] - knots[last - 1];
123    }
124    (knots, widths)
125}
126
127impl<K, S> FiniteTempBasis<K, S>
128where
129    K: KernelProperties + CentrosymmKernel + Clone + 'static,
130    S: StatisticsType,
131{
132    // ========== Getters ==========
133
134    /// Get a reference to the kernel
135    pub fn kernel(&self) -> &K {
136        &self.kernel
137    }
138
139    /// Get the SVE result the basis was built from
140    ///
141    /// It is not truncated to the basis size: a basis limited by `max_size`
142    /// or `epsilon` keeps all singular functions of the SVE.
143    pub fn sve_result(&self) -> &Arc<SVEResult> {
144        &self.sve_result
145    }
146
147    /// Get the accuracy of the basis
148    pub fn accuracy(&self) -> f64 {
149        self.accuracy
150    }
151
152    /// Get the inverse temperature β
153    pub fn beta(&self) -> f64 {
154        self.beta
155    }
156
157    /// Get the left singular functions (u) on imaginary time axis
158    pub fn u(&self) -> &Arc<PiecewiseLegendrePolyVector> {
159        &self.u
160    }
161
162    /// Get the right singular functions (v) on real frequency axis
163    pub fn v(&self) -> &Arc<PiecewiseLegendrePolyVector> {
164        &self.v
165    }
166
167    /// Get the singular values
168    pub fn s(&self) -> &[f64] {
169        &self.s
170    }
171
172    /// Get the left singular functions on Matsubara frequency axis (uhat)
173    pub fn uhat(&self) -> &Arc<PiecewiseLegendreFTVector<S>> {
174        &self.uhat
175    }
176
177    /// Get the full uhat (before truncation)
178    ///
179    /// Holds the Matsubara transforms of all singular functions of
180    /// [`sve_result`](Self::sve_result), not only of the `size()` basis
181    /// functions; the default Matsubara sampling points use them.
182    pub fn uhat_full(&self) -> &Arc<PiecewiseLegendreFTVector<S>> {
183        &self.uhat_full
184    }
185
186    // ========== Other methods ==========
187
188    /// Get the frequency cutoff ωmax
189    pub fn wmax(&self) -> f64 {
190        self.kernel.lambda() / self.beta
191    }
192
193    /// Get default Matsubara sampling points as i64 indices (for C-API)
194    ///
195    /// # Errors
196    ///
197    /// [`Error::NotSupported`] if the basis functions have no definite parity
198    /// (an SVE that is not centrosymmetric, e.g. from `compute_sve_general`;
199    /// #183)
200    pub fn default_matsubara_sampling_points_i64(
201        &self,
202        positive_only: bool,
203    ) -> Result<Vec<i64>, Error>
204    where
205        S: 'static,
206    {
207        Ok(self
208            .default_matsubara_sampling_points(positive_only)?
209            .into_iter()
210            .map(|f| f.n())
211            .collect())
212    }
213
214    /// Get default Matsubara sampling points as i64 indices with mitigate parameter (for C-API)
215    ///
216    /// # Errors
217    ///
218    /// [`Error::NotSupported`] if the basis functions have no definite parity
219    /// (an SVE that is not centrosymmetric, e.g. from `compute_sve_general`;
220    /// #183)
221    pub fn default_matsubara_sampling_points_i64_with_mitigate(
222        &self,
223        positive_only: bool,
224        mitigate: bool,
225        n_points: usize,
226    ) -> Result<Vec<i64>, Error>
227    where
228        S: 'static,
229    {
230        if !self.kernel().is_centrosymmetric() {
231            return Err(Error::NotSupported {
232                what: "default Matsubara sampling points of a basis whose kernel is not \
233                       centrosymmetric: they rely on the parity of the basis functions"
234                    .to_string(),
235            });
236        }
237        let fence = mitigate;
238        let freqs = Self::default_matsubara_sampling_points_impl(
239            &self.uhat_full,
240            n_points,
241            fence,
242            positive_only,
243        )?;
244        Ok(freqs.into_iter().map(|f| f.n()).collect())
245    }
246
247    /// Create a new FiniteTempBasis
248    ///
249    /// # Arguments
250    ///
251    /// * `kernel` - Kernel implementing `KernelProperties + CentrosymmKernel`
252    /// * `beta` - Inverse temperature (β > 0)
253    /// * `epsilon` - Accuracy of the basis, in (0, 1). `None` selects the best
254    ///   accuracy of the working precision (about 1.6e-16).
255    /// * `max_size` - Maximum number of basis functions (optional). It limits
256    ///   the basis, not the SVE: the SVE is computed and kept in full, as in
257    ///   [`from_sve_result`](Self::from_sve_result) with an untruncated SVE.
258    ///   The default sampling points and [`accuracy`](Self::accuracy) of the
259    ///   basis use the singular functions beyond it.
260    ///
261    /// # Returns
262    ///
263    /// A new FiniteTempBasis
264    ///
265    /// # Errors
266    ///
267    /// * [`Error::InvalidParameter`] if `beta` is not positive and finite,
268    ///   `epsilon` is not in (0, 1), or `max_size` is `Some(0)`. These are
269    ///   checked before the SVE is computed.
270    /// * The errors of [`compute_sve`]: [`Error::NonFiniteInput`] if the
271    ///   discretized kernel has a non-finite entry, [`Error::DecompositionFailed`]
272    ///   if an SVD of the SVE fails
273    /// * The errors of [`from_sve_result`](Self::from_sve_result)
274    pub fn new(
275        kernel: K,
276        beta: f64,
277        epsilon: Option<f64>,
278        max_size: Option<usize>,
279    ) -> Result<Self, Error> {
280        // Validate before the (expensive) SVE
281        require_positive_finite("beta", beta)?;
282        require_accuracy("epsilon", epsilon)?;
283        require_nonzero_size("max_size", max_size)?;
284
285        // Compute the SVE without a size limit; `from_sve_result` truncates
286        // only the basis to `max_size`. The default sampling points of a basis
287        // of size L are the roots of u_L (tau) and v_L (omega) and the sign
288        // changes of uhat_L or uhat_{L+1} (Matsubara), and its accuracy is
289        // s_L / s_0. An SVE truncated to `max_size` functions lacks them, and
290        // the point selection would fall back to the extrema of the last
291        // function (issue #285).
292        //
293        // Convention-matched with SparseIR.jl 1.1.4 (src/basis.jl,
294        // `FiniteTempBasis(statistics, β, ωmax, ε; max_size, kernel,
295        // sve_result=SVEResult(kernel; ε))`): its default SVE takes no `lmax`,
296        // and `part(sve_result; ε, max_size)` truncates the basis only. No
297        // code was ported.
298        let sve_result = compute_sve(
299            kernel.clone(),
300            epsilon,
301            None, // cutoff
302            None, // no limit on the number of singular values
303            TworkType::Auto,
304        )?;
305
306        Self::from_sve_result(kernel, beta, sve_result, epsilon, max_size)
307    }
308
309    /// Create basis from existing SVE result
310    ///
311    /// This is useful when you want to reuse the same SVE computation
312    /// for both fermionic and bosonic bases.
313    ///
314    /// `max_size` (and `epsilon`) truncate the basis functions and singular
315    /// values only. `sve_result` is kept as given: the default sampling points
316    /// and [`accuracy`](Self::accuracy) use its singular functions beyond the
317    /// basis, so pass an untruncated SVE to get the points of SparseIR.jl.
318    ///
319    /// # Errors
320    ///
321    /// * [`Error::InvalidParameter`] if `beta` is not positive and finite,
322    ///   `epsilon` is not in [0, 1) (0 keeps every singular value), `max_size`
323    ///   is `Some(0)`, or `sve_result` is not an SVE on [-1, 1] × [-1, 1]
324    /// * [`Error::EmptyInput`] if `sve_result` has no singular functions
325    /// * The errors of [`SVEResult::part`] (for an `SVEResult` whose public
326    ///   fields break its invariants)
327    pub fn from_sve_result(
328        kernel: K,
329        beta: f64,
330        sve_result: SVEResult,
331        epsilon: Option<f64>,
332        max_size: Option<usize>,
333    ) -> Result<Self, Error> {
334        require_positive_finite("beta", beta)?;
335        require_threshold("epsilon", epsilon)?;
336        require_nonzero_size("max_size", max_size)?;
337        check_unit_domain(&sve_result)?;
338
339        // Get truncated u, s, v from SVE result
340        let (u_sve, s_sve, v_sve) = sve_result.part(epsilon, max_size)?;
341
342        // Calculate accuracy
343        let accuracy = if sve_result.s.len() > s_sve.len() {
344            sve_result.s[s_sve.len()] / sve_result.s[0]
345        } else {
346            sve_result.s[sve_result.s.len() - 1] / sve_result.s[0]
347        };
348
349        // Get kernel parameters
350        let lambda = kernel.lambda();
351        let omega_max = lambda / beta;
352
353        // Scale polynomials to new variables
354        // tau = β/2 * (x + 1), w = ωmax * y
355
356        // Transform u: x ∈ [-1, 1] → τ ∈ [0, β]
357        let (x_knots, x_widths) = unit_knots_and_widths(&u_sve.get_polys()[0]);
358        let u_knots: Vec<f64> = x_knots.iter().map(|&x| beta / 2.0 * (x + 1.0)).collect();
359        let u_delta_x: Vec<f64> = x_widths.iter().map(|&dx| beta / 2.0 * dx).collect();
360        let u_symm: Vec<i32> = u_sve.get_polys().iter().map(|p| p.symm).collect();
361
362        let u = u_sve.rescale_domain(u_knots, Some(u_delta_x), Some(u_symm))?;
363
364        // Transform v: y ∈ [-1, 1] → ω ∈ [-ωmax, ωmax]
365        let (y_knots, y_widths) = unit_knots_and_widths(&v_sve.get_polys()[0]);
366        let v_knots: Vec<f64> = y_knots.iter().map(|&y| omega_max * y).collect();
367        let v_delta_x: Vec<f64> = y_widths.iter().map(|&dy| omega_max * dy).collect();
368        let v_symm: Vec<i32> = v_sve.get_polys().iter().map(|p| p.symm).collect();
369
370        let v = v_sve.rescale_domain(v_knots, Some(v_delta_x), Some(v_symm))?;
371
372        // Scale singular values to τ = β(x + 1)/2 and ω = ωmax y. A kernel with
373        // `ypower` carries that power of y = ω/ωmax, so its physical form is
374        // K(τ, ω) = ωmax^ypower K(x, y) and
375        // s_scaled = sqrt(β/2 * ωmax) * ωmax^ypower * s_sve,
376        // e.g. S_l = sqrt(β ωmax³/2) s_l for RegularizedBoseKernel (irbasis
377        // paper, Chikano et al., CPC 240, 181 (2019), arXiv:1807.05237, Eq. (25)).
378        let ypower = kernel.ypower();
379        let scale_factor = (beta / 2.0 * omega_max).sqrt() * omega_max.powi(ypower);
380        let s: Vec<f64> = s_sve.iter().map(|&x| scale_factor * x).collect();
381
382        // Construct uhat (Fourier transform of u)
383        // HACK: Fourier transforms only work on unit interval, so we scale the data
384        let uhat_base_full = sve_result.u.scale_data(beta.sqrt());
385        let conv_rad = kernel.conv_radius();
386
387        // Create statistics marker instance using Default trait
388        // S is a zero-sized type (ZST) like Fermionic or Bosonic
389        let stat_marker = S::default();
390
391        let uhat_full = PiecewiseLegendreFTVector::<S>::from_poly_vector(
392            &uhat_base_full,
393            stat_marker,
394            Some(conv_rad),
395        )?;
396
397        // Truncate uhat to basis size
398        let uhat_polyvec: Vec<_> = uhat_full.polyvec.iter().take(s.len()).cloned().collect();
399        let uhat = PiecewiseLegendreFTVector::from_vector(uhat_polyvec);
400
401        Ok(Self {
402            kernel,
403            sve_result: Arc::new(sve_result),
404            accuracy,
405            beta,
406            u: Arc::new(u),
407            v: Arc::new(v),
408            s,
409            uhat: Arc::new(uhat),
410            uhat_full: Arc::new(uhat_full),
411            _phantom: std::marker::PhantomData,
412        })
413    }
414
415    /// Get the size of the basis (number of basis functions)
416    pub fn size(&self) -> usize {
417        self.s.len()
418    }
419
420    /// Get the cutoff parameter Λ = β * ωmax
421    pub fn lambda(&self) -> f64 {
422        self.kernel.lambda()
423    }
424
425    /// Get the frequency cutoff ωmax
426    pub fn omega_max(&self) -> f64 {
427        self.lambda() / self.beta
428    }
429
430    /// Get significance of each singular value (s[i] / s[0])
431    pub fn significance(&self) -> Vec<f64> {
432        let s0 = self.s[0];
433        self.s.iter().map(|&s| s / s0).collect()
434    }
435
436    /// Get default tau sampling points
437    ///
438    /// Returns sampling points in imaginary time τ ∈ [-β/2, β/2].
439    ///
440    /// Roots are found with symmetry exploitation (matching Python 1.x / Julia v1),
441    /// then mapped to [-β/2, β/2] by folding τ_physical ∈ [0, β] around β/2.
442    ///
443    /// # Errors
444    ///
445    /// * [`Error::NotSupported`] if the default points are not defined for
446    ///   this basis: its SVE has so few singular functions that the last one
447    ///   has no extrema (e.g. `compute_sve` with `max_num_svals = Some(2)`)
448    pub fn default_tau_sampling_points(&self) -> Result<Vec<f64>, Error> {
449        let points = self.default_tau_sampling_points_size_requested(self.size())?;
450        let basis_size = self.size();
451        if points.len() < basis_size {
452            debug_warn!(
453                "Number of tau sampling points ({}) is less than basis size ({}). \
454                 Basis parameters: beta={}, wmax={}, epsilon={:.2e}",
455                points.len(),
456                basis_size,
457                self.beta,
458                self.wmax(),
459                self.accuracy()
460            );
461        }
462        Ok(points)
463    }
464
465    /// Get default tau sampling points with a requested size
466    ///
467    /// Returns sampling points in τ ∈ [-β/2, β/2].
468    ///
469    /// # Errors
470    ///
471    /// * [`Error::NotSupported`] if the default points are not defined for
472    ///   this basis: its SVE has so few singular functions that the last one
473    ///   has no extrema (e.g. `compute_sve` with `max_num_svals = Some(2)`)
474    pub fn default_tau_sampling_points_size_requested(
475        &self,
476        size_requested: usize,
477    ) -> Result<Vec<f64>, Error> {
478        let x = default_sampling_points(&self.sve_result.u, "u", size_requested)?;
479        let half_beta = self.beta / 2.0;
480        // Map roots to physical tau ∈ [0, β], then fold to [-β/2, β/2]
481        let mut smpl_taus: Vec<f64> = x
482            .iter()
483            .map(|&xi| {
484                let tau = half_beta * (xi + 1.0);
485                if tau <= half_beta {
486                    tau
487                } else {
488                    tau - self.beta
489                }
490            })
491            .collect();
492        smpl_taus.sort_by(|a, b| a.partial_cmp(b).unwrap());
493        Ok(smpl_taus)
494    }
495
496    /// Get default Matsubara frequency sampling points
497    ///
498    /// Returns sampling points as MatsubaraFreq objects: the sign changes of the
499    /// first discarded Matsubara basis function (its extrema when that function
500    /// is not available); bosonic sets always include n = 0.
501    ///
502    /// # Arguments
503    /// * `positive_only` - If true, returns only non-negative frequencies
504    ///
505    /// # Returns
506    /// Vector of Matsubara frequency sampling points
507    ///
508    /// # Errors
509    ///
510    /// [`Error::NotSupported`] if the basis functions have no definite parity
511    /// (an SVE that is not centrosymmetric, e.g. from `compute_sve_general`;
512    /// #183)
513    pub fn default_matsubara_sampling_points(
514        &self,
515        positive_only: bool,
516    ) -> Result<Vec<crate::freq::MatsubaraFreq<S>>, Error>
517    where
518        S: 'static,
519    {
520        if !self.kernel().is_centrosymmetric() {
521            return Err(Error::NotSupported {
522                what: "default Matsubara sampling points of a basis whose kernel is not \
523                       centrosymmetric: they rely on the parity of the basis functions"
524                    .to_string(),
525            });
526        }
527        let fence = false;
528        let points = Self::default_matsubara_sampling_points_impl(
529            &self.uhat_full,
530            self.size(),
531            fence,
532            positive_only,
533        )?;
534        let basis_size = self.size();
535        // For positive_only=true, we need 2*n_sampling_points >= basis_size
536        // For positive_only=false, we need n_sampling_points >= basis_size
537        let effective_points = if positive_only {
538            2 * points.len()
539        } else {
540            points.len()
541        };
542        if effective_points < basis_size {
543            debug_warn!(
544                "Number of Matsubara sampling points ({}{}) is less than basis size ({}). \
545                 Basis parameters: beta={}, wmax={}, epsilon={:.2e}",
546                points.len(),
547                if positive_only { " × 2" } else { "" },
548                basis_size,
549                self.beta,
550                self.wmax(),
551                self.accuracy()
552            );
553        }
554        Ok(points)
555    }
556
557    /// Fence Matsubara sampling points to improve conditioning
558    ///
559    /// This function adds additional sampling points near the outer frequencies
560    /// to improve the conditioning of the sampling matrix. This is particularly
561    /// important for Matsubara sampling where we cannot freely choose sampling points.
562    ///
563    /// Implementation matches C++ version in `basis.hpp` (lines 407-452).
564    fn fence_matsubara_sampling(
565        omega_n: &mut Vec<crate::freq::MatsubaraFreq<S>>,
566        positive_only: bool,
567    ) where
568        S: StatisticsType + 'static,
569    {
570        use crate::freq::{BosonicFreq, MatsubaraFreq};
571
572        if omega_n.is_empty() {
573            return;
574        }
575
576        // Collect outer frequencies
577        let mut outer_frequencies = Vec::new();
578        if positive_only {
579            outer_frequencies.push(omega_n[omega_n.len() - 1]);
580        } else {
581            outer_frequencies.push(omega_n[0]);
582            outer_frequencies.push(omega_n[omega_n.len() - 1]);
583        }
584
585        for wn_outer in outer_frequencies {
586            let outer_val = wn_outer.n();
587            // In SparseIR.jl-v1, ωn_diff is always created as BosonicFreq
588            // This ensures diff_val is always even (valid for Bosonic)
589            let mut diff_val = 2 * (0.025 * outer_val as f64).round() as i64;
590
591            // Handle edge case: if diff_val is 0, set it to 2 (minimum even value for Bosonic)
592            if diff_val == 0 {
593                diff_val = 2;
594            }
595
596            // Get the n value from BosonicFreq (same as diff_val since it's even)
597            let wn_diff = BosonicFreq::new(diff_val).unwrap().n();
598
599            // Sign function: returns +1 if n > 0, -1 if n < 0, 0 if n == 0
600            // Matches C++ implementation: (a.get_n() > 0) - (a.get_n() < 0)
601            let sign_val = if outer_val > 0 {
602                1
603            } else if outer_val < 0 {
604                -1
605            } else {
606                0
607            };
608
609            // Check original size before adding (C++ checks wn.size() before each push)
610            let original_size = omega_n.len();
611            if original_size >= 20 {
612                // For Fermionic: wn_outer.n is odd, wn_diff is even, so wn_outer.n ± wn_diff is odd (valid)
613                // For Bosonic: wn_outer.n is even, wn_diff is even, so wn_outer.n ± wn_diff is even (valid)
614                let new_n = outer_val - sign_val * wn_diff;
615                if let Ok(new_freq) = MatsubaraFreq::<S>::new(new_n) {
616                    omega_n.push(new_freq);
617                }
618            }
619            if original_size >= 42 {
620                let new_n = outer_val + sign_val * wn_diff;
621                if let Ok(new_freq) = MatsubaraFreq::<S>::new(new_n) {
622                    omega_n.push(new_freq);
623                }
624            }
625        }
626
627        // Sort and remove duplicates using BTreeSet
628        let omega_n_set: std::collections::BTreeSet<MatsubaraFreq<S>> = omega_n.drain(..).collect();
629        *omega_n = omega_n_set.into_iter().collect();
630    }
631
632    /// Default Matsubara sampling points for a basis of size `l` from the
633    /// Matsubara basis functions `uhat_full`: the sign changes of
634    /// `uhat_full[l]` (after the parity adjustment of `l`), or the extrema of
635    /// the last function when `uhat_full` has no function `l`; bosonic sets
636    /// always include n = 0. `fence` adds points near the outer frequencies.
637    ///
638    /// # Errors
639    ///
640    /// * [`Error::EmptyInput`] if `uhat_full` is empty
641    /// * [`Error::NotSupported`] if the functions have no definite parity
642    ///   (symm = 0, as from an SVE that is not centrosymmetric, #183)
643    pub(crate) fn default_matsubara_sampling_points_impl(
644        uhat_full: &PiecewiseLegendreFTVector<S>,
645        l: usize,
646        fence: bool,
647        positive_only: bool,
648    ) -> Result<Vec<crate::freq::MatsubaraFreq<S>>, Error>
649    where
650        S: StatisticsType + 'static,
651    {
652        use crate::freq::MatsubaraFreq;
653        use crate::polyfourier::{find_extrema, sign_changes};
654        use std::collections::BTreeSet;
655
656        if uhat_full.is_empty() {
657            return Err(Error::EmptyInput { name: "uhat_full" });
658        }
659
660        let mut l_requested = l;
661
662        // Adjust l_requested based on statistics (same as C++)
663        if S::STATISTICS == crate::traits::Statistics::Fermionic && l_requested % 2 != 0 {
664            l_requested += 1;
665        } else if S::STATISTICS == crate::traits::Statistics::Bosonic && l_requested % 2 == 0 {
666            l_requested += 1;
667        }
668
669        // Choose sign_changes or find_extrema based on l_requested
670        let mut omega_n = if l_requested < uhat_full.len() {
671            sign_changes(&uhat_full[l_requested], positive_only)?
672        } else {
673            find_extrema(&uhat_full[uhat_full.len() - 1], positive_only)?
674        };
675
676        // For bosons, include zero frequency explicitly to prevent conditioning issues
677        if S::STATISTICS == crate::traits::Statistics::Bosonic {
678            omega_n.push(MatsubaraFreq::<S>::new(0).unwrap());
679        }
680
681        // Sort and remove duplicates using BTreeSet
682        let omega_n_set: BTreeSet<MatsubaraFreq<S>> = omega_n.into_iter().collect();
683        let mut omega_n: Vec<MatsubaraFreq<S>> = omega_n_set.into_iter().collect();
684
685        // Check expected size
686        let expected_size = if positive_only {
687            l_requested.div_ceil(2)
688        } else {
689            l_requested
690        };
691
692        if omega_n.len() != expected_size {
693            debug_warn!(
694                "Requested {} sampling frequencies for basis size L = {}, but got {}.",
695                expected_size,
696                l,
697                omega_n.len()
698            );
699        }
700
701        // Apply fencing if requested (same as C++ implementation)
702        if fence {
703            Self::fence_matsubara_sampling(&mut omega_n, positive_only);
704        }
705
706        Ok(omega_n)
707    }
708    /// Get default omega (real frequency) sampling points
709    ///
710    /// Returns sampling points on the real-frequency axis ω ∈ [-ωmax, ωmax].
711    /// These are used as pole locations for the Discrete Lehmann Representation (DLR).
712    ///
713    /// The sampling points are chosen as the roots of the L-th basis function
714    /// in the spectral domain (v), which provides near-optimal conditioning.
715    ///
716    /// # Returns
717    /// Vector of real-frequency sampling points in [-ωmax, ωmax]
718    ///
719    /// # Errors
720    ///
721    /// * [`Error::NotSupported`] if the default points are not defined for
722    ///   this basis: its SVE has so few singular functions that the last one
723    ///   has no extrema (e.g. `compute_sve` with `max_num_svals = Some(2)`)
724    pub fn default_omega_sampling_points(&self) -> Result<Vec<f64>, Error> {
725        let sz = self.size();
726
727        // Use UNTRUNCATED sve_result.v (same as C++)
728        // C++: default_sampling_points(*(sve_result->v), sz)
729        let y = default_sampling_points(&self.sve_result.v, "v", sz)?;
730
731        // Scale to [-ωmax, ωmax]
732        let wmax = self.kernel.lambda() / self.beta;
733        let omega_points: Vec<f64> = y.into_iter().map(|yi| wmax * yi).collect();
734
735        Ok(omega_points)
736    }
737}
738
739// ============================================================================
740// Trait implementations
741// ============================================================================
742
743impl<K, S> crate::basis_trait::Basis<S> for FiniteTempBasis<K, S>
744where
745    K: KernelProperties + CentrosymmKernel + Clone + 'static,
746    S: StatisticsType + 'static,
747{
748    fn beta(&self) -> f64 {
749        self.beta
750    }
751
752    fn wmax(&self) -> f64 {
753        self.kernel.lambda() / self.beta
754    }
755
756    fn lambda(&self) -> f64 {
757        self.kernel.lambda()
758    }
759
760    fn size(&self) -> usize {
761        self.size()
762    }
763
764    fn accuracy(&self) -> f64 {
765        self.accuracy
766    }
767
768    fn significance(&self) -> Vec<f64> {
769        if let Some(&first_s) = self.s.first() {
770            self.s.iter().map(|&s| s / first_s).collect()
771        } else {
772            vec![]
773        }
774    }
775
776    fn svals(&self) -> Vec<f64> {
777        self.s.clone()
778    }
779
780    fn default_tau_sampling_points(&self) -> Result<Vec<f64>, Error> {
781        self.default_tau_sampling_points()
782    }
783
784    fn default_matsubara_sampling_points(
785        &self,
786        positive_only: bool,
787    ) -> Result<Vec<crate::freq::MatsubaraFreq<S>>, Error> {
788        self.default_matsubara_sampling_points(positive_only)
789    }
790
791    fn evaluate_tau(&self, tau: &[f64]) -> Result<crate::Matrix<f64>, Error> {
792        use crate::matrix::Mat;
793        use crate::taufuncs::normalize_tau;
794
795        let basis_size = self.size();
796
797        // Normalize every τ ∈ [-β, β] to [0, β] with the statistics sign
798        // first; this rejects a τ outside [-β, β] and NaN. The u polynomials
799        // are on [0, β] exactly (from_sve_result sets their ends), so
800        // evaluating them cannot fail.
801        let normalized = tau
802            .iter()
803            .map(|&t| normalize_tau::<S>(t, self.beta))
804            .collect::<Result<Vec<(f64, f64)>, Error>>()?;
805        if normalized.is_empty() {
806            // An empty set of points gives an empty matrix.
807            return Ok(Mat::<f64>::from_elem([0, basis_size], 0.0).into_typed());
808        }
809
810        // Result: matrix[i, l] = u_l(tau[i])
811        Ok(Mat::<f64>::from_fn([normalized.len(), basis_size], |idx| {
812            let (tau_norm, sign) = normalized[idx[0]];
813            sign * self.u[idx[1]].evaluate(tau_norm)
814        })
815        .into_typed())
816    }
817
818    fn evaluate_matsubara(
819        &self,
820        freqs: &[crate::freq::MatsubaraFreq<S>],
821    ) -> Result<crate::Matrix<num_complex::Complex<f64>>, Error> {
822        use crate::matrix::Mat;
823        use num_complex::Complex;
824
825        let n_points = freqs.len();
826        let basis_size = self.size();
827        if n_points == 0 {
828            // See evaluate_tau.
829            return Ok(
830                Mat::<Complex<f64>>::from_elem([0, basis_size], Complex::new(0.0, 0.0))
831                    .into_typed(),
832            );
833        }
834
835        // Evaluate each basis function at all Matsubara frequencies
836        // Result: matrix[i, l] = uhat_l(iν[i])
837        Ok(Mat::<Complex<f64>>::from_fn([n_points, basis_size], |idx| {
838            let i = idx[0]; // frequency index
839            let l = idx[1]; // basis function index
840            self.uhat[l].evaluate(&freqs[i])
841        })
842        .into_typed())
843    }
844
845    fn evaluate_omega(&self, omega: &[f64]) -> Result<crate::Matrix<f64>, Error> {
846        use crate::matrix::Mat;
847
848        let basis_size = self.size();
849        // The v polynomials are on [-ωmax, ωmax] exactly (from_sve_result
850        // sets their ends).
851        let domain = (self.v.xmin(), self.v.xmax());
852        if let Some(&w) = omega.iter().find(|&&w| !(w >= domain.0 && w <= domain.1)) {
853            return Err(Error::OutOfDomain {
854                name: "omega",
855                value: w,
856                domain,
857            });
858        }
859        if omega.is_empty() {
860            // See evaluate_tau.
861            return Ok(Mat::<f64>::from_elem([0, basis_size], 0.0).into_typed());
862        }
863
864        // Result: matrix[i, l] = V_l(omega[i])
865        Ok(Mat::<f64>::from_fn([omega.len(), basis_size], |idx| {
866            self.v[idx[1]].evaluate(omega[idx[0]])
867        })
868        .into_typed())
869    }
870
871    fn default_omega_sampling_points(&self) -> Result<Vec<f64>, Error> {
872        self.default_omega_sampling_points()
873    }
874}
875
876// ============================================================================
877// Type aliases
878// ============================================================================
879
880/// Type alias for fermionic basis with LogisticKernel
881pub type FermionicBasis = FiniteTempBasis<LogisticKernel, Fermionic>;
882
883/// Type alias for bosonic basis with LogisticKernel
884pub type BosonicBasis = FiniteTempBasis<LogisticKernel, Bosonic>;
885
886/// Default Matsubara sampling points for a basis of size `l`, from the
887/// Matsubara basis functions `uhat_full` alone
888///
889/// The points are the sign changes of `uhat_full[l]` (after the parity
890/// adjustment of `l`), or the extrema of the last function when `uhat_full`
891/// has no function `l`; bosonic sets always include n = 0. `fence` adds
892/// points near the outer frequencies. This is what
893/// [`FiniteTempBasis::default_matsubara_sampling_points`] computes, for a
894/// caller that holds the functions but not the basis.
895///
896/// # Errors
897///
898/// * [`Error::EmptyInput`] if `uhat_full` is empty
899/// * [`Error::NotSupported`] if the functions have no definite parity
900///   (symm = 0, as from an SVE that is not centrosymmetric, #183)
901pub fn default_matsubara_sampling_points_from_uhat<S>(
902    uhat_full: &PiecewiseLegendreFTVector<S>,
903    l: usize,
904    fence: bool,
905    positive_only: bool,
906) -> Result<Vec<crate::freq::MatsubaraFreq<S>>, Error>
907where
908    S: StatisticsType + 'static,
909{
910    FiniteTempBasis::<LogisticKernel, S>::default_matsubara_sampling_points_impl(
911        uhat_full,
912        l,
913        fence,
914        positive_only,
915    )
916}
917
918#[cfg(test)]
919#[path = "basis_tests.rs"]
920mod basis_tests;