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