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