Skip to main content

sparse_ir/
dlr.rs

1//! Discrete Lehmann Representation (DLR)
2//!
3//! This module provides the Discrete Lehmann Representation (DLR) basis,
4//! which represents Green's functions as a linear combination of poles on the
5//! real-frequency axis.
6
7use crate::fitters::RealMatrixFitter;
8use crate::freq::MatsubaraFreq;
9use crate::gemm::GemmBackendHandle;
10use crate::traits::{Statistics, StatisticsType};
11use mdarray::DTensor;
12use num_complex::Complex;
13use std::marker::PhantomData;
14
15/// Generic single-pole Green's function at imaginary time τ
16///
17/// Computes G(τ) for either fermionic or bosonic statistics based on the type parameter S.
18///
19/// # Type Parameters
20/// * `S` - Statistics type (Fermionic or Bosonic)
21///
22/// # Arguments
23/// * `tau` - Imaginary time (can be outside [0, β))
24/// * `omega` - Pole position (real frequency)
25/// * `beta` - Inverse temperature
26///
27/// # Returns
28/// Real-valued Green's function G(τ)
29///
30/// # Example
31/// ```ignore
32/// use sparse_ir::traits::Fermionic;
33/// let g_f = gtau_single_pole::<Fermionic>(0.5, 5.0, 1.0);
34///
35/// use sparse_ir::traits::Bosonic;
36/// let g_b = gtau_single_pole::<Bosonic>(0.5, 5.0, 1.0);
37/// ```
38pub fn gtau_single_pole<S: StatisticsType>(tau: f64, omega: f64, beta: f64) -> f64 {
39    match S::STATISTICS {
40        Statistics::Fermionic => fermionic_single_pole(tau, omega, beta),
41        Statistics::Bosonic => bosonic_single_pole(tau, omega, beta),
42    }
43}
44
45/// Compute fermionic single-pole Green's function at imaginary time τ
46///
47/// Evaluates G(τ) = -exp(-ω×τ) / (1 + exp(-β×ω)) for a single pole at frequency ω.
48///
49/// Supports extended τ ranges with anti-periodic boundary conditions:
50/// - G(τ + β) = -G(τ) (fermionic anti-periodicity)
51/// - Valid for τ ∈ (-β, 2β)
52///
53/// # Arguments
54/// * `tau` - Imaginary time (can be outside [0, β))
55/// * `omega` - Pole position (real frequency)
56/// * `beta` - Inverse temperature
57///
58/// # Returns
59/// Real-valued Green's function G(τ)
60///
61/// # Example
62/// ```ignore
63/// let beta = 1.0;
64/// let omega = 5.0;
65/// let tau = 0.5 * beta;
66/// let g = fermionic_single_pole(tau, omega, beta);
67/// ```
68pub fn fermionic_single_pole(tau: f64, omega: f64, beta: f64) -> f64 {
69    use crate::taufuncs::normalize_tau;
70    use crate::traits::Fermionic;
71
72    // Normalize τ to [0, β] and track sign from anti-periodicity
73    // G(τ + β) = -G(τ) for fermions
74    let (tau_normalized, sign) = normalize_tau::<Fermionic>(tau, beta);
75
76    // Avoid overflow for large negative ω by factoring out exp(βω).
77    // Both branches keep the exponent non-positive.
78    let value = if omega >= 0.0 {
79        -(-omega * tau_normalized).exp() / (1.0 + (-beta * omega).exp())
80    } else {
81        -(omega * (beta - tau_normalized)).exp() / (1.0 + (beta * omega).exp())
82    };
83
84    sign * value
85}
86
87/// Compute bosonic single-pole Green's function at imaginary time τ
88///
89/// Evaluates G(τ) = exp(-ω×τ) / (1 - exp(-β×ω)) for a single pole at frequency ω.
90///
91/// Supports extended τ ranges with periodic boundary conditions:
92/// - G(τ + β) = G(τ) (bosonic periodicity)
93/// - Valid for τ ∈ (-β, 2β)
94///
95/// # Arguments
96/// * `tau` - Imaginary time (can be outside [0, β))
97/// * `omega` - Pole position (real frequency)
98/// * `beta` - Inverse temperature
99///
100/// # Returns
101/// Real-valued Green's function G(τ)
102///
103/// # Example
104/// ```ignore
105/// let beta = 1.0;
106/// let omega = 5.0;
107/// let tau = 0.5 * beta;
108/// let g = bosonic_single_pole(tau, omega, beta);
109/// ```
110pub fn bosonic_single_pole(tau: f64, omega: f64, beta: f64) -> f64 {
111    use crate::taufuncs::normalize_tau;
112    use crate::traits::Bosonic;
113
114    // Normalize τ to [0, β] using periodicity
115    // G(τ + β) = G(τ) for bosons
116    let tau_normalized = normalize_tau::<Bosonic>(tau, beta).0;
117
118    if omega >= 0.0 {
119        (-omega * tau_normalized).exp() / (1.0 - (-beta * omega).exp())
120    } else {
121        -(omega * (beta - tau_normalized)).exp() / (1.0 - (beta * omega).exp())
122    }
123}
124
125/// Generic single-pole Green's function at Matsubara frequency
126///
127/// Computes G(iωn) = 1/(iωn - ω) for a single pole at frequency ω.
128///
129/// # Type Parameters
130/// * `S` - Statistics type (Fermionic or Bosonic)
131///
132/// # Arguments
133/// * `matsubara_freq` - Matsubara frequency
134/// * `omega` - Pole position (real frequency)
135/// * `beta` - Inverse temperature
136///
137/// # Returns
138/// Complex-valued Green's function G(iωn)
139pub fn giwn_single_pole<S: StatisticsType>(
140    matsubara_freq: &MatsubaraFreq<S>,
141    omega: f64,
142    beta: f64,
143) -> Complex<f64> {
144    // G(iωn) = 1/(iωn - ω)
145    let wn = matsubara_freq.value(beta);
146    let denominator = Complex::new(0.0, 1.0) * wn - Complex::new(omega, 0.0);
147    Complex::new(1.0, 0.0) / denominator
148}
149
150// ============================================================================
151// Discrete Lehmann Representation
152// ============================================================================
153
154/// Discrete Lehmann Representation (DLR)
155///
156/// The DLR is a variant of the IR basis based on a "sketching" of the analytic
157/// continuation kernel K. Instead of using singular value expansion, it represents
158/// Green's functions as a linear combination of poles on the real-frequency axis:
159///
160/// ```text
161/// G(iν) = Σ_i a[i] * reg[i] / (iν - ω[i])
162/// ```
163///
164/// where:
165/// - `ω[i]` are pole positions on the real axis
166/// - `a[i]` are expansion coefficients
167/// - `reg[i]` are kernel-dependent pole weights on the physical ω grid
168///
169/// The public `regularizers` field stores the raw kernel regularizer
170/// `w(β, ω_i)`. Internally, DLR evaluations use `pole_weights`, which include
171/// the ω-domain normalization carried by `FiniteTempBasis`.
172///
173/// # Type Parameters
174/// * `S` - Statistics type (Fermionic or Bosonic)
175pub struct DiscreteLehmannRepresentation<S>
176where
177    S: StatisticsType,
178{
179    /// Pole positions on the real-frequency axis ω ∈ [-ωmax, ωmax]
180    pub poles: Vec<f64>,
181
182    /// Inverse temperature β
183    pub beta: f64,
184
185    /// Maximum frequency ωmax
186    pub wmax: f64,
187
188    /// LogisticKernel reference basis used for Basis trait compatibility
189    kernel: crate::kernel::LogisticKernel,
190
191    /// Power with which the source kernel scales the spectral variable.
192    kernel_ypower: i32,
193
194    /// Accuracy of the representation
195    pub accuracy: f64,
196
197    /// Regularizers for each pole: regularizer[i] = w(β, ω_i)
198    /// These are computed from the source IR basis kernel.
199    pub regularizers: Vec<f64>,
200
201    /// Pole weights used in tau and Matsubara evaluations.
202    ///
203    /// `FiniteTempBasis` rescales the ω-domain singular values by `wmax^-ypower`.
204    /// Combined with the dimensionless kernel regularizer `y^ypower =
205    /// (ω / wmax)^ypower`, the physical pole basis carries an additional
206    /// factor `wmax^(-2 * ypower)`.
207    pole_weights: Vec<f64>,
208
209    /// Fitting matrix from IR: fitmat = -s · V(poles)
210    /// Used for to_IR transformation
211    fitmat: DTensor<f64, 2>,
212
213    /// Fitter for from_IR transformation (uses SVD of fitmat)
214    fitter: RealMatrixFitter,
215
216    /// Marker for statistics type
217    _phantom: PhantomData<S>,
218}
219
220impl<S> DiscreteLehmannRepresentation<S>
221where
222    S: StatisticsType,
223{
224    pub fn kernel_ypower(&self) -> i32 {
225        self.kernel_ypower
226    }
227
228    pub fn pole_weights(&self) -> &[f64] {
229        &self.pole_weights
230    }
231
232    /// Create DLR from IR basis with custom poles
233    ///
234    /// The tau-domain pole basis is built from the logistic representation, while
235    /// kernel-specific regularizers are preserved for compatible kernels.
236    ///
237    /// # Arguments
238    /// * `basis` - The IR basis to construct DLR from
239    /// * `poles` - Pole positions on the real-frequency axis
240    ///
241    /// # Returns
242    /// A new DLR representation
243    pub fn with_poles<K>(
244        basis: &impl crate::basis_trait::Basis<S, Kernel = K>,
245        poles: Vec<f64>,
246    ) -> Self
247    where
248        S: 'static,
249        K: crate::kernel::KernelProperties + Clone,
250    {
251        use crate::kernel::LogisticKernel;
252
253        let beta = basis.beta();
254        let wmax = basis.wmax();
255        let accuracy = basis.accuracy();
256        let kernel_ypower = basis.kernel().ypower();
257
258        // Compute fitting matrix: fitmat = -s · V(poles)
259        // This transforms DLR coefficients to IR coefficients
260        let v_at_poles = basis.evaluate_omega(&poles); // shape: [n_poles, basis_size]
261        let s = basis.svals(); // Non-normalized singular values (same as C++)
262
263        let basis_size = basis.size();
264        let n_poles = poles.len();
265
266        // fitmat[l, i] = -s[l] * V_l(pole[i])
267        // C++: fitmat = (-A_array * s_array.replicate(1, A.cols())).matrix()
268        let fitmat = DTensor::<f64, 2>::from_fn([basis_size, n_poles], |idx| {
269            let l = idx[0];
270            let i = idx[1];
271            -s[l] * v_at_poles[[i, l]]
272        });
273
274        // Create fitter for from_IR (inverse operation)
275        let fitter = RealMatrixFitter::new(fitmat.clone());
276
277        let lambda = beta * wmax;
278        let logistic_kernel = LogisticKernel::new(lambda);
279        let regularizers: Vec<f64> = poles
280            .iter()
281            .map(|&pole| basis.kernel().regularizer::<S>(beta, pole))
282            .collect();
283        let pole_weight_scale = wmax.powi(2 * kernel_ypower);
284        let pole_weights: Vec<f64> = regularizers
285            .iter()
286            .map(|&regularizer| regularizer / pole_weight_scale)
287            .collect();
288
289        Self {
290            poles,
291            beta,
292            wmax,
293            kernel: logistic_kernel,
294            kernel_ypower,
295            accuracy,
296            regularizers,
297            pole_weights,
298            fitmat,
299            fitter,
300            _phantom: PhantomData,
301        }
302    }
303
304    fn zero_pole_tau_limit(&self) -> f64 {
305        match self.kernel_ypower {
306            0 => -0.5,
307            1 => -1.0 / (self.beta * self.wmax * self.wmax),
308            _ => panic!(
309                "DLR tau evaluation does not support kernel ypower = {}",
310                self.kernel_ypower
311            ),
312        }
313    }
314
315    fn zero_pole_matsubara_limit(&self) -> f64 {
316        match self.kernel_ypower {
317            0 => -0.5 * self.beta,
318            1 => -1.0 / (self.wmax * self.wmax),
319            _ => panic!(
320                "DLR Matsubara evaluation does not support kernel ypower = {}",
321                self.kernel_ypower
322            ),
323        }
324    }
325
326    /// Create DLR from IR basis with default pole locations
327    ///
328    /// Uses the default omega sampling points from the basis.
329    ///
330    /// # Arguments
331    /// * `basis` - The IR basis to construct DLR from
332    ///
333    /// # Returns
334    /// A new DLR representation with default poles
335    ///
336    /// # Panics
337    /// Panics if the number of default poles is less than the basis size.
338    /// This can happen with certain kernel types (e.g., RegularizedBoseKernel)
339    /// due to numerical precision limitations in root finding.
340    pub fn new<K>(basis: &impl crate::basis_trait::Basis<S, Kernel = K>) -> Self
341    where
342        S: 'static,
343        K: crate::kernel::KernelProperties + Clone,
344    {
345        let poles = basis.default_omega_sampling_points();
346        let basis_size = basis.size();
347        if basis_size > poles.len() {
348            eprintln!(
349                "Warning: Number of default poles ({}) is less than basis size ({}). \
350                 This may happen if not enough precision is left in the polynomial.",
351                poles.len(),
352                basis_size
353            );
354        }
355        assert!(
356            basis_size <= poles.len(),
357            "The number of poles must be greater than or equal to the basis size"
358        );
359        Self::with_poles(basis, poles)
360    }
361
362    // ========================================================================
363    // Public API (generic, user-friendly)
364    // ========================================================================
365
366    /// Convert IR coefficients to DLR (N-dimensional, generic over real/complex)
367    ///
368    /// # Type Parameters
369    /// * `T` - Element type (f64 or Complex<f64>)
370    ///
371    /// # Arguments
372    /// * `gl` - IR coefficients as N-D tensor
373    /// * `dim` - Dimension along which to transform
374    ///
375    /// # Returns
376    /// DLR coefficients as N-D tensor
377    pub fn from_ir_nd<T>(
378        &self,
379        backend: Option<&GemmBackendHandle>,
380        gl: &mdarray::Tensor<T, mdarray::DynRank>,
381        dim: usize,
382    ) -> mdarray::Tensor<T, mdarray::DynRank>
383    where
384        T: num_complex::ComplexFloat
385            + faer_traits::ComplexField
386            + From<f64>
387            + Copy
388            + Default
389            + 'static,
390    {
391        use mdarray::{DTensor, Shape};
392
393        let mut gl_shape = vec![];
394        gl.shape().with_dims(|dims| {
395            gl_shape.extend_from_slice(dims);
396        });
397
398        let basis_size = gl_shape[dim];
399        assert_eq!(
400            basis_size,
401            self.fitmat.shape().0,
402            "IR basis size mismatch: expected {}, got {}",
403            self.fitmat.shape().0,
404            basis_size
405        );
406
407        // Move target dimension to position 0
408        let gl_dim0 = crate::sampling::movedim(gl, dim, 0);
409
410        // Reshape to 2D
411        let extra_size = gl_dim0.len() / basis_size;
412        let gl_2d_dyn = gl_dim0.reshape(&[basis_size, extra_size][..]).to_tensor();
413
414        let gl_2d = DTensor::<T, 2>::from_fn([basis_size, extra_size], |idx| {
415            gl_2d_dyn[&[idx[0], idx[1]][..]]
416        });
417
418        // Fit using fitter's generic 2D method
419        let g_dlr_2d = self.fitter.fit_2d_generic::<T>(backend, &gl_2d);
420
421        // Reshape back
422        let n_poles = self.poles.len();
423        let mut g_dlr_shape = vec![n_poles];
424        gl_dim0.shape().with_dims(|dims| {
425            for i in 1..dims.len() {
426                g_dlr_shape.push(dims[i]);
427            }
428        });
429
430        let g_dlr_dim0 = g_dlr_2d.into_dyn().reshape(&g_dlr_shape[..]).to_tensor();
431        crate::sampling::movedim(&g_dlr_dim0, 0, dim)
432    }
433
434    /// Convert DLR coefficients to IR (N-dimensional, generic over real/complex)
435    ///
436    /// # Type Parameters
437    /// * `T` - Element type (f64 or Complex<f64>)
438    ///
439    /// # Arguments
440    /// * `g_dlr` - DLR coefficients as N-D tensor
441    /// * `dim` - Dimension along which to transform
442    ///
443    /// # Returns
444    /// IR coefficients as N-D tensor
445    pub fn to_ir_nd<T>(
446        &self,
447        backend: Option<&GemmBackendHandle>,
448        g_dlr: &mdarray::Tensor<T, mdarray::DynRank>,
449        dim: usize,
450    ) -> mdarray::Tensor<T, mdarray::DynRank>
451    where
452        T: num_complex::ComplexFloat
453            + faer_traits::ComplexField
454            + From<f64>
455            + Copy
456            + Default
457            + 'static,
458    {
459        use mdarray::{DTensor, Shape};
460
461        let mut g_dlr_shape = vec![];
462        g_dlr.shape().with_dims(|dims| {
463            g_dlr_shape.extend_from_slice(dims);
464        });
465
466        let n_poles = g_dlr_shape[dim];
467        assert_eq!(
468            n_poles,
469            self.poles.len(),
470            "DLR size mismatch: expected {}, got {}",
471            self.poles.len(),
472            n_poles
473        );
474
475        // Move target dimension to position 0
476        let g_dlr_dim0 = crate::sampling::movedim(g_dlr, dim, 0);
477
478        // Reshape to 2D
479        let extra_size = g_dlr_dim0.len() / n_poles;
480        let g_dlr_2d_dyn = g_dlr_dim0.reshape(&[n_poles, extra_size][..]).to_tensor();
481
482        let g_dlr_2d = DTensor::<T, 2>::from_fn([n_poles, extra_size], |idx| {
483            g_dlr_2d_dyn[&[idx[0], idx[1]][..]]
484        });
485
486        // Evaluate using fitter's generic 2D method
487        let gl_2d = self.fitter.evaluate_2d_generic::<T>(backend, &g_dlr_2d);
488
489        // Reshape back
490        let basis_size = self.fitmat.shape().0;
491        let mut gl_shape = vec![basis_size];
492        g_dlr_dim0.shape().with_dims(|dims| {
493            for i in 1..dims.len() {
494                gl_shape.push(dims[i]);
495            }
496        });
497
498        let gl_dim0 = gl_2d.into_dyn().reshape(&gl_shape[..]).to_tensor();
499        crate::sampling::movedim(&gl_dim0, 0, dim)
500    }
501}
502
503// ============================================================================
504// Basis trait implementation for DLR
505// ============================================================================
506
507impl<S> crate::basis_trait::Basis<S> for DiscreteLehmannRepresentation<S>
508where
509    S: StatisticsType + 'static,
510{
511    type Kernel = crate::kernel::LogisticKernel;
512
513    fn kernel(&self) -> &Self::Kernel {
514        // DLR always uses LogisticKernel for weight computations
515        &self.kernel
516    }
517
518    fn beta(&self) -> f64 {
519        self.beta
520    }
521
522    fn wmax(&self) -> f64 {
523        self.wmax
524    }
525
526    fn lambda(&self) -> f64 {
527        self.beta * self.wmax
528    }
529
530    fn size(&self) -> usize {
531        self.poles.len()
532    }
533
534    fn accuracy(&self) -> f64 {
535        self.accuracy
536    }
537
538    fn significance(&self) -> Vec<f64> {
539        // All poles are equally significant in DLR
540        vec![1.0; self.poles.len()]
541    }
542
543    fn svals(&self) -> Vec<f64> {
544        // All poles are equally significant in DLR (no singular value concept)
545        vec![1.0; self.poles.len()]
546    }
547
548    fn default_tau_sampling_points(&self) -> Vec<f64> {
549        // DLR does not own the underlying IR basis, so it cannot delegate.
550        // Callers should obtain tau sampling points from the IR basis that
551        // was used to construct this DLR, e.g. `ir_basis.default_tau_sampling_points()`.
552        unimplemented!(
553            "DLR does not directly support default tau sampling points; \
554             use the underlying IR basis"
555        )
556    }
557
558    fn default_matsubara_sampling_points(
559        &self,
560        _positive_only: bool,
561    ) -> Vec<crate::freq::MatsubaraFreq<S>> {
562        // DLR does not own the underlying IR basis, so it cannot delegate.
563        // Callers should obtain Matsubara sampling points from the IR basis
564        // that was used to construct this DLR, e.g.
565        // `ir_basis.default_matsubara_sampling_points(positive_only)`.
566        unimplemented!(
567            "DLR does not directly support default Matsubara sampling points; \
568             use the underlying IR basis"
569        )
570    }
571
572    fn evaluate_tau(&self, tau: &[f64]) -> mdarray::DTensor<f64, 2> {
573        use crate::taufuncs::normalize_tau;
574        use mdarray::DTensor;
575
576        let n_points = tau.len();
577        let n_poles = self.poles.len();
578        DTensor::<f64, 2>::from_fn([n_points, n_poles], |idx| {
579            let tau_val = tau[idx[0]];
580            let pole = self.poles[idx[1]];
581            let pole_weight = self.pole_weights[idx[1]];
582            match S::STATISTICS {
583                Statistics::Fermionic => {
584                    gtau_single_pole::<S>(tau_val, pole, self.beta) * pole_weight
585                }
586                Statistics::Bosonic => {
587                    if pole == 0.0 {
588                        self.zero_pole_tau_limit()
589                    } else if pole > 0.0 {
590                        let tau_norm = normalize_tau::<S>(tau_val, self.beta).0;
591                        let denominator = -(-self.beta * pole).exp_m1();
592                        -(-tau_norm * pole).exp() * pole_weight / denominator
593                    } else {
594                        let tau_norm = normalize_tau::<S>(tau_val, self.beta).0;
595                        let denominator = -(self.beta * pole).exp_m1();
596                        (pole * (self.beta - tau_norm)).exp() * pole_weight / denominator
597                    }
598                }
599            }
600        })
601    }
602
603    fn evaluate_matsubara(
604        &self,
605        freqs: &[crate::freq::MatsubaraFreq<S>],
606    ) -> mdarray::DTensor<num_complex::Complex<f64>, 2> {
607        use mdarray::DTensor;
608        use num_complex::Complex;
609
610        let n_points = freqs.len();
611        let n_poles = self.poles.len();
612
613        // Evaluate MatsubaraPoles basis functions
614        DTensor::<Complex<f64>, 2>::from_fn([n_points, n_poles], |idx| {
615            let freq = &freqs[idx[0]];
616            let pole = self.poles[idx[1]];
617            let pole_weight = self.pole_weights[idx[1]];
618
619            // iν = i * π * (2n + ζ) / β
620            let iv = freq.value_imaginary(self.beta);
621
622            // u_i(iν) = pole_weight / (iν - pole_i), where `pole_weight`
623            // matches the ω-domain normalization of the source IR basis.
624            if S::STATISTICS == Statistics::Bosonic && pole == 0.0 {
625                if crate::freq::is_zero(freq) {
626                    Complex::new(self.zero_pole_matsubara_limit(), 0.0)
627                } else {
628                    Complex::new(0.0, 0.0)
629                }
630            } else {
631                Complex::new(pole_weight, 0.0) / (iv - Complex::new(pole, 0.0))
632            }
633        })
634    }
635
636    fn evaluate_omega(&self, _omega: &[f64]) -> mdarray::DTensor<f64, 2> {
637        // TODO(#205): For the IR basis, evaluate_omega returns V_l(omega).
638        // For DLR, the "basis functions" in omega-space are single-pole
639        // functions (conceptually delta functions at the pole positions),
640        // which do not have a well-defined continuous representation on the
641        // real-frequency axis analogous to V_l(omega).  A proper
642        // implementation would require either:
643        //   (a) returning the IR basis's V_l(omega) (but DLR does not store
644        //       the IR basis), or
645        //   (b) defining an appropriate discretized representation for the
646        //       pole basis in omega-space.
647        // Until the semantics are clarified, this remains unimplemented.
648        unimplemented!(
649            "evaluate_omega is not well-defined for DLR; \
650             use the underlying IR basis for real-frequency evaluation"
651        )
652    }
653
654    fn default_omega_sampling_points(&self) -> Vec<f64> {
655        // DLR poles ARE the omega sampling points
656        self.poles.clone()
657    }
658}
659
660#[cfg(test)]
661mod tests {
662    use super::*;
663    use crate::traits::{Bosonic, Fermionic};
664
665    /// Generic test for periodicity/anti-periodicity
666    fn test_periodicity_generic<S: StatisticsType>(expected_sign: f64, stat_name: &str) {
667        let beta = 1.0;
668        let omega = 5.0;
669
670        // Test periodicity by comparing G(τ) with G(τ-β)
671        // Since normalize_tau is restricted to [-β, β], we test:
672        // For τ ∈ (0, β]: compare G(τ) with G(τ-β)
673        // For fermions: G(τ) should equal -G(τ-β)
674        // For bosons: G(τ) should equal G(τ-β)
675        for tau in [0.1, 0.3, 0.7] {
676            let g_tau = gtau_single_pole::<S>(tau, omega, beta);
677            let g_tau_minus_beta = gtau_single_pole::<S>(tau - beta, omega, beta);
678
679            // For fermions: G(τ) = -G(τ-β) → G(τ-β) = -G(τ)
680            // For bosons: G(τ) = G(τ-β)
681            let expected = expected_sign * g_tau;
682
683            assert!(
684                (expected - g_tau_minus_beta).abs() < 1e-14,
685                "{} periodicity violated at τ={}: G(τ)={}, G(τ-β)={}, expected={}",
686                stat_name,
687                tau,
688                g_tau,
689                g_tau_minus_beta,
690                expected
691            );
692        }
693    }
694
695    #[test]
696    fn test_fermionic_antiperiodicity() {
697        // Fermions: G(τ+β) = -G(τ)
698        test_periodicity_generic::<Fermionic>(-1.0, "Fermionic");
699    }
700
701    #[test]
702    fn test_bosonic_periodicity() {
703        // Bosons: G(τ+β) = G(τ)
704        test_periodicity_generic::<Bosonic>(1.0, "Bosonic");
705    }
706
707    #[test]
708    fn test_generic_function_matches_specific() {
709        let beta = 1.0;
710        let omega = 5.0;
711        let tau = 0.5;
712
713        // Test that generic function matches specific functions
714        let g_f_specific = fermionic_single_pole(tau, omega, beta);
715        let g_f_generic = gtau_single_pole::<Fermionic>(tau, omega, beta);
716
717        let g_b_specific = bosonic_single_pole(tau, omega, beta);
718        let g_b_generic = gtau_single_pole::<Bosonic>(tau, omega, beta);
719
720        assert!(
721            (g_f_specific - g_f_generic).abs() < 1e-14,
722            "Fermionic: specific={}, generic={}",
723            g_f_specific,
724            g_f_generic
725        );
726        assert!(
727            (g_b_specific - g_b_generic).abs() < 1e-14,
728            "Bosonic: specific={}, generic={}",
729            g_b_specific,
730            g_b_generic
731        );
732    }
733}
734
735#[cfg(test)]
736#[path = "dlr_tests.rs"]
737mod dlr_tests;