Skip to main content

sparse_ir_dlr/
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::error::{Error, require_finite, require_positive_finite};
8use crate::fitters::FitScalar;
9use crate::fitters::RealMatrixFitter;
10use crate::freq::MatsubaraFreq;
11use crate::gemm::GemmBackendHandle;
12use crate::matrix::Mat;
13use crate::taufuncs::normalize_tau;
14use crate::traits::{Statistics, StatisticsType};
15use num_complex::Complex;
16use std::marker::PhantomData;
17use std::sync::OnceLock;
18use tenferro_tensor::TypedTensor;
19
20/// Generic single-pole Green's function at imaginary time τ
21///
22/// Computes G(τ) for either fermionic or bosonic statistics based on the type parameter S.
23/// Both use G(τ) = -exp(-ω×τ) / (1 ± exp(-β×ω)) (+ for fermions, - for bosons),
24/// the imaginary-time counterpart of [`giwn_single_pole`]'s G(iν) = 1/(iν - ω).
25///
26/// # Type Parameters
27/// * `S` - Statistics type (Fermionic or Bosonic)
28///
29/// # Arguments
30/// * `tau` - Imaginary time τ ∈ [-β, β]; τ < 0 is mapped to [0, β] by
31///   (anti-)periodicity (see [`fermionic_single_pole`] and [`bosonic_single_pole`])
32/// * `omega` - Pole position (real frequency)
33/// * `beta` - Inverse temperature
34///
35/// # Returns
36/// Real-valued Green's function G(τ)
37///
38/// # Errors
39///
40/// * [`Error::InvalidParameter`] if `beta` is not positive and finite, or
41///   `omega` is not finite
42/// * [`Error::OutOfDomain`] if `tau` is outside [-β, β] or NaN
43///
44/// # Example
45/// ```
46/// use sparse_ir::traits::{Bosonic, Fermionic};
47/// use sparse_ir::{bosonic_single_pole, fermionic_single_pole, gtau_single_pole};
48///
49/// let g_f = gtau_single_pole::<Fermionic>(0.5, 5.0, 1.0).unwrap();
50/// assert_eq!(g_f, fermionic_single_pole(0.5, 5.0, 1.0).unwrap());
51///
52/// let g_b = gtau_single_pole::<Bosonic>(0.5, 5.0, 1.0).unwrap();
53/// assert_eq!(g_b, bosonic_single_pole(0.5, 5.0, 1.0).unwrap());
54/// ```
55pub fn gtau_single_pole<S: StatisticsType>(tau: f64, omega: f64, beta: f64) -> Result<f64, Error> {
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 negative τ with anti-periodic boundary conditions:
67/// - G(τ + β) = -G(τ) (fermionic anti-periodicity)
68/// - Valid for τ ∈ [-β, β]; τ is normalized with
69///   [`normalize_tau`]
70///
71/// # Arguments
72/// * `tau` - Imaginary time τ ∈ [-β, β]
73/// * `omega` - Pole position (real frequency)
74/// * `beta` - Inverse temperature
75///
76/// # Returns
77/// Real-valued Green's function G(τ)
78///
79/// # Errors
80///
81/// * [`Error::InvalidParameter`] if `beta` is not positive and finite, or
82///   `omega` is not finite
83/// * [`Error::OutOfDomain`] if `tau` is outside [-β, β] or NaN
84///
85/// # Example
86/// ```
87/// use sparse_ir::fermionic_single_pole;
88///
89/// let beta = 1.0;
90/// let omega = 5.0;
91/// let tau = 0.5 * beta;
92/// let g = fermionic_single_pole(tau, omega, beta).unwrap();
93///
94/// let expected = -(-omega * tau).exp() / (1.0 + (-beta * omega).exp());
95/// assert!((g - expected).abs() < 1e-15);
96///
97/// // Anti-periodicity: G(τ - β) = -G(τ)
98/// assert!((fermionic_single_pole(tau - beta, omega, beta).unwrap() + g).abs() < 1e-15);
99/// ```
100pub fn fermionic_single_pole(tau: f64, omega: f64, beta: f64) -> Result<f64, Error> {
101    use crate::traits::Fermionic;
102
103    // Normalize τ to [0, β] and track sign from anti-periodicity
104    // G(τ + β) = -G(τ) for fermions
105    let (tau_normalized, sign) = normalize_tau::<Fermionic>(tau, beta)?;
106    require_finite("omega", omega)?;
107    Ok(sign * fermionic_single_pole_unchecked(tau_normalized, omega, beta))
108}
109
110/// Fermionic single-pole G(τ) for τ already in [0, β], without the sign of
111/// the antiperiodic continuation
112///
113/// Checked callers only: β positive and finite, ω finite. The DLR checks its
114/// poles in `from_ir_with_poles` and they cannot be changed afterwards.
115pub(crate) fn fermionic_single_pole_unchecked(tau_normalized: f64, omega: f64, beta: f64) -> f64 {
116    // Avoid overflow for large negative ω by factoring out exp(βω).
117    // Both branches keep the exponent non-positive.
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/// Compute bosonic single-pole Green's function at imaginary time τ
126///
127/// Evaluates G(τ) = -exp(-ω×τ) / (1 - exp(-β×ω)) for a single pole at frequency ω.
128///
129/// This is the imaginary-time counterpart of [`giwn_single_pole`]:
130/// G(iνn) = ∫₀^β dτ exp(iνn×τ) G(τ) = 1/(iνn - ω). G(τ) is negative for ω > 0
131/// and positive for ω < 0, the same sign convention as [`fermionic_single_pole`]
132/// and the bosonic τ functions of [`DiscreteLehmannRepresentation`].
133///
134/// Supports negative τ with periodic boundary conditions:
135/// - G(τ + β) = G(τ) (bosonic periodicity)
136/// - Valid for τ ∈ [-β, β]; τ is normalized with
137///   [`normalize_tau`]
138///
139/// ω = 0 is a genuine pole of the Bose factor, so the result is infinite there:
140/// `-inf` for `omega = +0.0` (the ω → 0⁺ limit) and `+inf` for `omega = -0.0`.
141/// [`DiscreteLehmannRepresentation`] evaluates a zero pole through its finite,
142/// regularized limit instead.
143///
144/// # Arguments
145/// * `tau` - Imaginary time τ ∈ [-β, β]
146/// * `omega` - Pole position (real frequency)
147/// * `beta` - Inverse temperature
148///
149/// # Returns
150/// Real-valued Green's function G(τ)
151///
152/// # Errors
153///
154/// * [`Error::InvalidParameter`] if `beta` is not positive and finite, or
155///   `omega` is not finite
156/// * [`Error::OutOfDomain`] if `tau` is outside [-β, β] or NaN
157///
158/// # Example
159/// ```
160/// use sparse_ir::bosonic_single_pole;
161///
162/// let beta = 1.0;
163/// let omega = 5.0;
164/// let tau = 0.5 * beta;
165/// let g = bosonic_single_pole(tau, omega, beta).unwrap();
166///
167/// let expected = -(-omega * tau).exp() / (1.0 - (-beta * omega).exp());
168/// assert!((g - expected).abs() <= 1e-14 * expected.abs());
169/// assert!(g < 0.0);
170///
171/// // Periodicity: G(τ - β) = G(τ)
172/// assert!((bosonic_single_pole(tau - beta, omega, beta).unwrap() - g).abs() < 1e-15);
173/// ```
174pub fn bosonic_single_pole(tau: f64, omega: f64, beta: f64) -> Result<f64, Error> {
175    use crate::traits::Bosonic;
176
177    // Normalize τ to [0, β] using periodicity
178    // G(τ + β) = G(τ) for bosons
179    let tau_normalized = normalize_tau::<Bosonic>(tau, beta)?.0;
180    require_finite("omega", omega)?;
181
182    // Avoid overflow for large negative ω by factoring out exp(βω): both
183    // branches keep the exponents non-positive. expm1 keeps the Bose
184    // denominator 1 - exp(-β|ω|) accurate for small β|ω|. This is the same
185    // form as the bosonic arm of `DiscreteLehmannRepresentation::evaluate_tau`.
186    // At ω = ±0 the denominator is a signed zero and the result is ∓inf.
187    if omega >= 0.0 {
188        // 1 - exp(-βω) = -expm1(-βω)
189        let denominator = -(-beta * omega).exp_m1();
190        Ok(-(-omega * tau_normalized).exp() / denominator)
191    } else {
192        // -exp(-ωτ) / (1 - exp(-βω)) = exp(ω(β - τ)) / (1 - exp(βω))
193        let denominator = -(beta * omega).exp_m1();
194        Ok((omega * (beta - tau_normalized)).exp() / denominator)
195    }
196}
197
198/// Generic single-pole Green's function at Matsubara frequency
199///
200/// Computes G(iν) = 1/(iν - ω) for a single pole at frequency ω.
201///
202/// # Type Parameters
203/// * `S` - Statistics type (Fermionic or Bosonic)
204///
205/// # Arguments
206/// * `matsubara_freq` - Matsubara frequency
207/// * `omega` - Pole position (real frequency)
208/// * `beta` - Inverse temperature
209///
210/// # Returns
211/// Complex-valued Green's function G(iν)
212///
213/// # Errors
214///
215/// [`Error::InvalidParameter`] if `beta` is not positive and finite, or
216/// `omega` is not finite
217pub fn giwn_single_pole<S: StatisticsType>(
218    matsubara_freq: &MatsubaraFreq<S>,
219    omega: f64,
220    beta: f64,
221) -> Result<Complex<f64>, Error> {
222    require_positive_finite("beta", beta)?;
223    require_finite("omega", omega)?;
224    // G(iν) = 1/(iν - ω)
225    let wn = matsubara_freq.value(beta);
226    let denominator = Complex::new(0.0, 1.0) * wn - Complex::new(omega, 0.0);
227    Ok(Complex::new(1.0, 0.0) / denominator)
228}
229
230// ============================================================================
231// Discrete Lehmann Representation
232// ============================================================================
233
234/// Discrete Lehmann Representation (DLR)
235///
236/// The DLR is a variant of the IR basis based on a "sketching" of the analytic
237/// continuation kernel K. Instead of using singular value expansion, it represents
238/// Green's functions as a linear combination of poles on the real-frequency axis:
239///
240/// ```text
241/// G(iν) = Σ_i a[i] * reg[i] / (iν - ω[i])
242/// ```
243///
244/// where:
245/// - `ω[i]` are pole positions on the real axis
246/// - `a[i]` are expansion coefficients
247/// - `reg[i]` are the kernel regularizers `w(β, ω_i)`
248///
249/// [`regularizers()`](Self::regularizers) returns `w(β, ω_i)`: 1 for fermions
250/// and `tanh(βω_i/2)` for bosons with `LogisticKernel`, and `ω_i` for
251/// `RegularizedBoseKernel`. The DLR functions are `-K(τ, ω_i)` and its Fourier
252/// transform for the physical kernel `K(τ, ω) = Σ_l u_l(τ) s_l v_l(ω)` of the
253/// source basis.
254///
255/// Two constructions are available:
256/// - **independent (default)**: [`DiscreteLehmannRepresentation::new`] /
257///   [`DlrBuilder`] select the poles by an interpolative decomposition of the
258///   discretized logistic kernel (Kaye, Chen, Parcollet, PRB 105, 235115),
259///   without building an IR basis;
260/// - **IR-derived**: `DiscreteLehmannRepresentation::from_ir` (trait
261///   `DlrFromIr` of the IR crate) uses the default real-frequency sampling
262///   points of an IR basis as poles, and `from_ir_with_poles` takes the poles.
263///
264/// Conversions to and from an IR basis are provided by [`IrDlrTransform`];
265/// an IR-derived DLR carries one for its source basis.
266///
267/// Imaginary-time and Matsubara nodes for square interpolation are selected
268/// by a row interpolative decomposition and are exposed through
269/// [`Basis::default_tau_sampling_points`](crate::basis_trait::Basis::default_tau_sampling_points)
270/// and
271/// [`Basis::default_matsubara_sampling_points`](crate::basis_trait::Basis::default_matsubara_sampling_points),
272/// so [`TauSampling::new`](crate::TauSampling::new) and
273/// [`MatsubaraSampling::new`](crate::MatsubaraSampling::new) work on a DLR.
274///
275/// # Type Parameters
276/// * `S` - Statistics type (Fermionic or Bosonic)
277pub struct DiscreteLehmannRepresentation<S>
278where
279    S: StatisticsType,
280{
281    /// Pole positions on the real-frequency axis ω ∈ [-ωmax, ωmax]
282    poles: Vec<f64>,
283
284    /// Inverse temperature β
285    beta: f64,
286
287    /// Maximum frequency ωmax
288    wmax: f64,
289
290    /// Power with which the source kernel scales the spectral variable.
291    kernel_ypower: i32,
292
293    /// Accuracy of the representation
294    accuracy: f64,
295
296    /// Regularizers for each pole: regularizer[i] = w(β, ω_i) of the logistic
297    /// kernel, or of the kernel of the source IR basis for an IR-derived DLR
298    regularizers: Vec<f64>,
299
300    /// Pole weights used in tau and Matsubara evaluations.
301    ///
302    /// They equal `regularizers`: the τ functions are
303    /// `-w_i e^{-τω_i} / (1 ± e^{-βω_i})` and the Matsubara functions
304    /// `w_i / (iν - ω_i)`, which is `-K(τ, ω_i)` and its Fourier transform for
305    /// the physical kernel of the source basis.
306    pole_weights: Vec<f64>,
307
308    /// IR <-> DLR transform for the source basis of an IR-derived DLR.
309    ir: Option<IrDlrTransform>,
310
311    /// Lazily selected interpolation nodes.
312    tau_nodes: OnceLock<Vec<f64>>,
313    matsubara_nodes: OnceLock<Vec<i64>>,
314    matsubara_nodes_positive: OnceLock<Vec<i64>>,
315
316    /// Marker for statistics type
317    _phantom: PhantomData<S>,
318}
319
320/// Regularizer `w(β, ω)` of the logistic kernel: 1 for fermions and
321/// `tanh(βω/2)` for bosons
322fn logistic_regularizer<S: StatisticsType>(beta: f64, omega: f64) -> f64 {
323    match S::STATISTICS {
324        Statistics::Fermionic => 1.0,
325        Statistics::Bosonic => (0.5 * beta * omega).tanh(),
326    }
327}
328
329/// Builder for the independent (interpolative-decomposition) DLR.
330///
331/// ```
332/// use sparse_ir::{DlrBuilder, Fermionic};
333/// let dlr = DlrBuilder::<Fermionic>::new(10.0, 10.0)
334///     .accuracy(1e-10)
335///     .build()
336///     .unwrap();
337/// assert!(!dlr.poles().is_empty());
338/// ```
339#[derive(Debug, Clone)]
340pub struct DlrBuilder<S: StatisticsType> {
341    beta: f64,
342    wmax: f64,
343    accuracy: f64,
344    max_size: Option<usize>,
345    _phantom: PhantomData<S>,
346}
347
348impl<S: StatisticsType + 'static> DlrBuilder<S> {
349    /// Default target accuracy, `1e-14`.
350    ///
351    /// The poles are selected by a pivoted Gram–Schmidt factorization of the
352    /// kernel in double precision, whose residuals cannot fall much below
353    /// `1e-15`. An accuracy below about `1e-14` is therefore beyond what the
354    /// selection can resolve: it keeps adding near-redundant poles chosen by
355    /// rounding, which makes the representation larger and its fits *less*
356    /// accurate. `1e-14` is the tolerance the reference implementations use
357    /// at full precision (cppdlr warns below it).
358    pub const DEFAULT_ACCURACY: f64 = 1e-14;
359
360    /// Start a DLR for inverse temperature `beta` and frequency cutoff `wmax`.
361    pub fn new(beta: f64, wmax: f64) -> Self {
362        Self {
363            beta,
364            wmax,
365            accuracy: Self::DEFAULT_ACCURACY,
366            max_size: None,
367            _phantom: PhantomData,
368        }
369    }
370
371    /// Target relative accuracy of the kernel interpolation.
372    ///
373    /// Values below about `1e-14` are beyond double-precision resolution; see
374    /// [`Self::DEFAULT_ACCURACY`].
375    pub fn accuracy(mut self, accuracy: f64) -> Self {
376        self.accuracy = accuracy;
377        self
378    }
379
380    /// Upper bound on the number of poles.
381    pub fn max_size(mut self, max_size: usize) -> Self {
382        self.max_size = Some(max_size);
383        self
384    }
385
386    /// Select the poles and build the representation.
387    ///
388    /// # Errors
389    /// [`Error::InvalidParameter`] if `beta` or `wmax` is not positive and
390    /// finite, the accuracy is not in (0, 1), or `max_size` is 0.
391    pub fn build(self) -> Result<DiscreteLehmannRepresentation<S>, Error> {
392        require_positive_finite("beta", self.beta)?;
393        require_positive_finite("wmax", self.wmax)?;
394        crate::error::require_accuracy("accuracy", Some(self.accuracy))?;
395        crate::error::require_nonzero_size("max_size", self.max_size)?;
396        let lambda = self.beta * self.wmax;
397        let poles: Vec<f64> = crate::dlr_id::select_poles(lambda, self.accuracy, self.max_size)
398            .into_iter()
399            .map(|w| w / self.beta)
400            .collect();
401        let regularizers = poles
402            .iter()
403            .map(|&pole| logistic_regularizer::<S>(self.beta, pole))
404            .collect();
405        Ok(DiscreteLehmannRepresentation::from_parts(
406            self.beta,
407            self.wmax,
408            self.accuracy,
409            poles,
410            regularizers,
411            0,
412            None,
413        ))
414    }
415}
416
417impl<S> DiscreteLehmannRepresentation<S>
418where
419    S: StatisticsType,
420{
421    pub fn kernel_ypower(&self) -> i32 {
422        self.kernel_ypower
423    }
424
425    pub fn pole_weights(&self) -> &[f64] {
426        &self.pole_weights
427    }
428
429    /// Pole positions on the real-frequency axis, in the order they were
430    /// given to `from_ir_with_poles` (sorted ascending when chosen by
431    /// [`Self::new`] or `from_ir`)
432    pub fn poles(&self) -> &[f64] {
433        &self.poles
434    }
435
436    /// Regularizers of the poles: `regularizers[i] = w(β, poles[i])` of the
437    /// kernel of the IR basis this DLR was built from (of the logistic
438    /// kernel for an independent DLR)
439    pub fn regularizers(&self) -> &[f64] {
440        &self.regularizers
441    }
442
443    /// Number of functions of the IR basis this DLR was built from: the
444    /// extent of the IR axis of [`Self::from_ir_nd`] and [`Self::to_ir_nd`].
445    /// `None` for a DLR built independently of an IR basis.
446    pub fn ir_basis_size(&self) -> Option<usize> {
447        self.ir.as_ref().map(IrDlrTransform::ir_size)
448    }
449
450    /// IR <-> DLR transform of the source basis (IR-derived DLR only).
451    pub fn ir_transform(&self) -> Option<&IrDlrTransform> {
452        self.ir.as_ref()
453    }
454
455    /// Build a DLR independently of any IR basis (default construction).
456    ///
457    /// Equivalent to `DlrBuilder::new(beta, wmax).accuracy(accuracy).build()`.
458    /// An `accuracy` below about `1e-14` is beyond double-precision
459    /// resolution and makes the DLR larger without making it more accurate;
460    /// see [`DlrBuilder::DEFAULT_ACCURACY`].
461    ///
462    /// # Errors
463    /// See [`DlrBuilder::build`].
464    pub fn new(beta: f64, wmax: f64, accuracy: f64) -> Result<Self, Error>
465    where
466        S: 'static,
467    {
468        DlrBuilder::new(beta, wmax).accuracy(accuracy).build()
469    }
470
471    /// Assemble a DLR from checked parts: the constructors of the IR crate
472    /// build on this after validating the poles against their basis.
473    ///
474    /// `regularizers[i]` is the kernel regularizer `w(β, poles[i])`, which is
475    /// also the weight of the pole functions; `kernel_ypower` is 0 or 1 when a
476    /// bosonic pole lies at 0. `ir` is the transform to the source IR basis,
477    /// if any.
478    #[doc(hidden)]
479    pub fn from_parts(
480        beta: f64,
481        wmax: f64,
482        accuracy: f64,
483        poles: Vec<f64>,
484        regularizers: Vec<f64>,
485        kernel_ypower: i32,
486        ir: Option<IrDlrTransform>,
487    ) -> Self {
488        let pole_weights = regularizers.clone();
489        Self {
490            poles,
491            beta,
492            wmax,
493            kernel_ypower,
494            accuracy,
495            regularizers,
496            pole_weights,
497            ir,
498            tau_nodes: OnceLock::new(),
499            matsubara_nodes: OnceLock::new(),
500            matsubara_nodes_positive: OnceLock::new(),
501            _phantom: PhantomData,
502        }
503    }
504
505    fn zero_pole_tau_limit(&self) -> f64 {
506        match self.kernel_ypower {
507            // -lim_{ω→0} w(β, ω) e^{-τω} / (1 - e^{-βω}) with w = tanh(βω/2) or ω
508            0 => -0.5,
509            1 => -1.0 / self.beta,
510            // Unreachable: from_ir_with_poles rejects a bosonic pole at 0 for any
511            // other ypower, and neither the poles nor the kernel can be
512            // changed afterwards.
513            _ => panic!(
514                "DLR tau evaluation does not support kernel ypower = {}",
515                self.kernel_ypower
516            ),
517        }
518    }
519
520    fn zero_pole_matsubara_limit(&self) -> f64 {
521        match self.kernel_ypower {
522            // lim_{ω→0} w(β, ω) / (0 - ω) at n = 0 with w = tanh(βω/2) or ω
523            0 => -0.5 * self.beta,
524            1 => -1.0,
525            // Unreachable: from_ir_with_poles rejects a bosonic pole at 0 for any
526            // other ypower, and neither the poles nor the kernel can be
527            // changed afterwards.
528            _ => panic!(
529                "DLR Matsubara evaluation does not support kernel ypower = {}",
530                self.kernel_ypower
531            ),
532        }
533    }
534
535    // ========================================================================
536    // Public API (generic, user-friendly)
537    // ========================================================================
538
539    /// Convert IR coefficients to DLR (N-dimensional, generic over real/complex)
540    ///
541    /// # Type Parameters
542    /// * `T` - Element type (`f64` or `Complex<f64>`)
543    ///
544    /// # Arguments
545    /// * `gl` - IR coefficients as N-D tensor
546    /// * `dim` - Dimension along which to transform
547    ///
548    /// # Returns
549    /// DLR coefficients as N-D tensor, with [`Basis::size`](crate::basis_trait::Basis::size)
550    /// (the number of poles) entries along `dim`. An empty batch (a zero
551    /// extent on another axis) gives an empty result.
552    ///
553    /// # Errors
554    ///
555    /// * [`Error::AxisOutOfRange`] if `dim` is not an axis of `gl`
556    /// * [`Error::ShapeMismatch`] of the input if `gl` does not have
557    ///   [`Self::ir_basis_size`] entries along `dim`
558    /// * [`Error::DecompositionFailed`] if the SVD of the fitting matrix fails
559    ///   (its entries are finite, since the poles are)
560    pub fn from_ir_nd<T: FitScalar>(
561        &self,
562        backend: Option<&GemmBackendHandle>,
563        gl: &TypedTensor<T>,
564        dim: usize,
565    ) -> Result<TypedTensor<T>, Error> {
566        // An empty batch gives an empty result; the fit fails only if the
567        // SVD of the fitting matrix does.
568        self.require_ir()?.ir_to_dlr_nd(backend, gl, dim)
569    }
570
571    /// Convert DLR coefficients to IR (N-dimensional, generic over real/complex)
572    ///
573    /// # Type Parameters
574    /// * `T` - Element type (`f64` or `Complex<f64>`)
575    ///
576    /// # Arguments
577    /// * `g_dlr` - DLR coefficients as N-D tensor
578    /// * `dim` - Dimension along which to transform
579    ///
580    /// # Returns
581    /// IR coefficients as N-D tensor, with [`Self::ir_basis_size`] entries
582    /// along `dim`. An empty batch gives an empty result.
583    ///
584    /// # Errors
585    ///
586    /// * [`Error::AxisOutOfRange`] if `dim` is not an axis of `g_dlr`
587    /// * [`Error::ShapeMismatch`] of the input if `g_dlr` does not have
588    ///   [`Basis::size`](crate::basis_trait::Basis::size) (the number of
589    ///   poles) entries along `dim`
590    pub fn to_ir_nd<T: FitScalar>(
591        &self,
592        backend: Option<&GemmBackendHandle>,
593        g_dlr: &TypedTensor<T>,
594        dim: usize,
595    ) -> Result<TypedTensor<T>, Error> {
596        self.require_ir()?.dlr_to_ir_nd(backend, g_dlr, dim)
597    }
598
599    fn require_ir(&self) -> Result<&IrDlrTransform, Error> {
600        self.ir.as_ref().ok_or_else(|| Error::NotSupported {
601            what: "IR <-> DLR conversion of a DLR that was not built from an IR basis; \
602                   build an IrDlrTransform for the IR basis instead"
603                .to_string(),
604        })
605    }
606}
607
608impl<S> DiscreteLehmannRepresentation<S>
609where
610    S: StatisticsType + 'static,
611{
612    /// Imaginary-time interpolation nodes (sorted), one per pole.
613    pub fn tau_nodes(&self) -> &[f64] {
614        use crate::basis_trait::Basis;
615        self.tau_nodes.get_or_init(|| {
616            let taus: Vec<f64> = crate::dlr_id::tau_candidates(self.beta * self.wmax)
617                .iter()
618                .map(|t| self.beta * t.value())
619                .collect();
620            let mat = self
621                .evaluate_tau(&taus)
622                .expect("the candidate times lie in [0, beta]");
623            let rows = crate::dlr_id::select_rows(
624                mat.host_data().expect("an owned matrix is host-resident"),
625                taus.len(),
626                self.poles.len(),
627                self.poles.len(),
628            );
629            rows.into_iter().map(|i| taus[i]).collect()
630        })
631    }
632
633    /// Matsubara interpolation nodes as indices `n` (`ν = nπ/β`, sorted).
634    ///
635    /// With `positive_only`, nodes are chosen among `n >= 0` so that the
636    /// stacked real and imaginary parts determine the coefficients of a
637    /// Green's function with `G(-iν) = conj(G(iν))`.
638    pub fn matsubara_nodes(&self, positive_only: bool) -> &[i64] {
639        let cell = if positive_only {
640            &self.matsubara_nodes_positive
641        } else {
642            &self.matsubara_nodes
643        };
644        cell.get_or_init(|| self.select_matsubara_nodes(positive_only))
645    }
646
647    fn select_matsubara_nodes(&self, positive_only: bool) -> Vec<i64> {
648        use crate::basis_trait::Basis;
649        let zeta = match S::STATISTICS {
650            Statistics::Fermionic => 1,
651            Statistics::Bosonic => 0,
652        };
653        let ns = crate::dlr_id::matsubara_candidates(self.beta * self.wmax, zeta, positive_only);
654        let freqs: Vec<MatsubaraFreq<S>> = ns
655            .iter()
656            .map(|&n| MatsubaraFreq::new(n).expect("candidate parity matches statistics"))
657            .collect();
658        let mat = self
659            .evaluate_matsubara(&freqs)
660            .expect("evaluating the DLR functions at Matsubara frequencies cannot fail");
661        let data = mat.host_data().expect("an owned matrix is host-resident");
662        let (nf, r) = (freqs.len(), self.poles.len());
663        if !positive_only {
664            let rows = crate::dlr_id::select_rows(data, nf, r, r);
665            return rows.into_iter().map(|i| ns[i]).collect();
666        }
667        // Stack [Re; Im] so each frequency contributes two real rows.
668        let mut stacked = vec![0.0; 2 * nf * r];
669        for j in 0..r {
670            for i in 0..nf {
671                let z = data[i + nf * j];
672                stacked[i + 2 * nf * j] = z.re;
673                stacked[nf + i + 2 * nf * j] = z.im;
674            }
675        }
676        let mut out: Vec<i64> = crate::dlr_id::select_rows(&stacked, 2 * nf, r, r)
677            .into_iter()
678            .map(|i| ns[i % nf])
679            .collect();
680        out.sort_unstable();
681        out.dedup();
682        out
683    }
684}
685
686// ============================================================================
687// IR <-> DLR transform
688// ============================================================================
689
690/// Linear map between the coefficients of an IR basis and of a DLR on the
691/// same domain.
692///
693/// The DLR basis function of pole `ω_i` is expanded in the IR basis as
694/// `u_i = Σ_l T[l, i] U_l` with `T[l, i] = -s_l V_l(ω_i) (w_i / w^{IR}_i)`,
695/// where `w_i` and `w^{IR}_i` are the pole weights of the DLR and of the IR
696/// kernel. DLR -> IR applies `T`; IR -> DLR is its least-squares inverse.
697pub struct IrDlrTransform {
698    fitter: RealMatrixFitter,
699}
700
701impl IrDlrTransform {
702    /// The transform with the `ir_size x dlr_size` matrix `T` mapping DLR to
703    /// IR coefficients (see the type documentation). The IR crate builds it
704    /// from its basis.
705    #[doc(hidden)]
706    pub fn from_matrix(matrix: Mat<f64>) -> Self {
707        Self {
708            fitter: RealMatrixFitter::new(matrix),
709        }
710    }
711
712    /// IR basis size.
713    pub fn ir_size(&self) -> usize {
714        self.fitter.n_points()
715    }
716
717    /// Number of DLR poles.
718    pub fn dlr_size(&self) -> usize {
719        self.fitter.basis_size()
720    }
721
722    /// The `ir_size x dlr_size` matrix `T` mapping DLR to IR coefficients.
723    pub fn matrix(&self) -> &crate::Matrix<f64> {
724        self.fitter.matrix()
725    }
726
727    /// IR -> DLR along axis `dim` (least squares).
728    ///
729    /// # Errors
730    /// The errors of [`DiscreteLehmannRepresentation::from_ir_nd`].
731    pub fn ir_to_dlr_nd<T: FitScalar>(
732        &self,
733        backend: Option<&GemmBackendHandle>,
734        gl: &TypedTensor<T>,
735        dim: usize,
736    ) -> Result<TypedTensor<T>, Error> {
737        self.fitter.fit_nd(backend, gl, dim)
738    }
739
740    /// DLR -> IR along axis `dim`.
741    ///
742    /// # Errors
743    /// The errors of [`DiscreteLehmannRepresentation::to_ir_nd`].
744    pub fn dlr_to_ir_nd<T: FitScalar>(
745        &self,
746        backend: Option<&GemmBackendHandle>,
747        g_dlr: &TypedTensor<T>,
748        dim: usize,
749    ) -> Result<TypedTensor<T>, Error> {
750        self.fitter.evaluate_nd(backend, g_dlr, dim)
751    }
752}
753
754// ============================================================================
755// Basis trait implementation for DLR
756// ============================================================================
757
758impl<S> crate::basis_trait::Basis<S> for DiscreteLehmannRepresentation<S>
759where
760    S: StatisticsType + 'static,
761{
762    fn beta(&self) -> f64 {
763        self.beta
764    }
765
766    fn wmax(&self) -> f64 {
767        self.wmax
768    }
769
770    fn lambda(&self) -> f64 {
771        self.beta * self.wmax
772    }
773
774    fn size(&self) -> usize {
775        self.poles.len()
776    }
777
778    fn accuracy(&self) -> f64 {
779        self.accuracy
780    }
781
782    fn significance(&self) -> Vec<f64> {
783        // All poles are equally significant in DLR
784        vec![1.0; self.poles.len()]
785    }
786
787    fn svals(&self) -> Vec<f64> {
788        // All poles are equally significant in DLR (no singular value concept)
789        vec![1.0; self.poles.len()]
790    }
791
792    fn default_tau_sampling_points(&self) -> Result<Vec<f64>, Error> {
793        Ok(self.tau_nodes().to_vec())
794    }
795
796    fn default_matsubara_sampling_points(
797        &self,
798        positive_only: bool,
799    ) -> Result<Vec<crate::freq::MatsubaraFreq<S>>, Error> {
800        self.matsubara_nodes(positive_only)
801            .iter()
802            .map(|&n| crate::freq::MatsubaraFreq::new(n))
803            .collect()
804    }
805
806    fn evaluate_tau(&self, tau: &[f64]) -> Result<crate::Matrix<f64>, Error> {
807        let n_poles = self.poles.len();
808        // Normalize every τ first: this rejects a τ outside [-β, β] and NaN
809        // for every pole, including the bosonic pole at 0, whose limit does
810        // not depend on τ.
811        let normalized = tau
812            .iter()
813            .map(|&t| normalize_tau::<S>(t, self.beta))
814            .collect::<Result<Vec<(f64, f64)>, Error>>()?;
815        if normalized.is_empty() {
816            // An empty set of points gives an empty matrix.
817            return Ok(Mat::<f64>::from_elem([0, n_poles], 0.0).into_typed());
818        }
819        Ok(Mat::<f64>::from_fn([normalized.len(), n_poles], |idx| {
820            let (tau_norm, sign) = normalized[idx[0]];
821            let pole = self.poles[idx[1]];
822            let pole_weight = self.pole_weights[idx[1]];
823            match S::STATISTICS {
824                Statistics::Fermionic => {
825                    sign * fermionic_single_pole_unchecked(tau_norm, pole, self.beta) * pole_weight
826                }
827                Statistics::Bosonic => {
828                    // The bosonic sign of normalize_tau is always 1.
829                    if pole == 0.0 {
830                        self.zero_pole_tau_limit()
831                    } else if pole > 0.0 {
832                        let denominator = -(-self.beta * pole).exp_m1();
833                        -(-tau_norm * pole).exp() * pole_weight / denominator
834                    } else {
835                        let denominator = -(self.beta * pole).exp_m1();
836                        (pole * (self.beta - tau_norm)).exp() * pole_weight / denominator
837                    }
838                }
839            }
840        })
841        .into_typed())
842    }
843
844    fn evaluate_matsubara(
845        &self,
846        freqs: &[crate::freq::MatsubaraFreq<S>],
847    ) -> Result<crate::Matrix<num_complex::Complex<f64>>, Error> {
848        use num_complex::Complex;
849
850        let n_points = freqs.len();
851        let n_poles = self.poles.len();
852        if n_points == 0 {
853            // See evaluate_tau.
854            return Ok(
855                Mat::<Complex<f64>>::from_elem([0, n_poles], Complex::new(0.0, 0.0)).into_typed(),
856            );
857        }
858
859        // Evaluate MatsubaraPoles basis functions
860        Ok(Mat::<Complex<f64>>::from_fn([n_points, n_poles], |idx| {
861            let freq = &freqs[idx[0]];
862            let pole = self.poles[idx[1]];
863            let pole_weight = self.pole_weights[idx[1]];
864
865            // iν = iπn/β, with n = freq.n() (odd for fermions, even for bosons)
866            let iv = freq.value_imaginary(self.beta);
867
868            // u_i(iν) = pole_weight / (iν - pole_i), where `pole_weight` is the
869            // regularizer w(β, ω_i) of the source kernel.
870            if S::STATISTICS == Statistics::Bosonic && pole == 0.0 {
871                if crate::freq::is_zero(freq) {
872                    Complex::new(self.zero_pole_matsubara_limit(), 0.0)
873                } else {
874                    Complex::new(0.0, 0.0)
875                }
876            } else {
877                Complex::new(pole_weight, 0.0) / (iv - Complex::new(pole, 0.0))
878            }
879        })
880        .into_typed())
881    }
882
883    fn evaluate_omega(&self, _omega: &[f64]) -> Result<crate::Matrix<f64>, Error> {
884        // TODO(#205): For the IR basis, evaluate_omega returns V_l(omega).
885        // For DLR, the "basis functions" in omega-space are single-pole
886        // functions (conceptually delta functions at the pole positions),
887        // which do not have a well-defined continuous representation on the
888        // real-frequency axis analogous to V_l(omega).  A proper
889        // implementation would require either:
890        //   (a) returning the IR basis's V_l(omega) (but DLR does not store
891        //       the IR basis), or
892        //   (b) defining an appropriate discretized representation for the
893        //       pole basis in omega-space.
894        // Until the semantics are clarified, this is NotSupported.
895        Err(Error::NotSupported {
896            what: "evaluate_omega of a DLR: its pole functions have no real-frequency \
897                   representation (#205); use the IR basis"
898                .to_string(),
899        })
900    }
901
902    fn default_omega_sampling_points(&self) -> Result<Vec<f64>, Error> {
903        // DLR poles ARE the omega sampling points
904        Ok(self.poles.clone())
905    }
906}
907
908#[cfg(test)]
909mod tests {
910    use super::*;
911    use crate::traits::{Bosonic, Fermionic};
912
913    /// Generic test for periodicity/anti-periodicity
914    fn test_periodicity_generic<S: StatisticsType>(expected_sign: f64, stat_name: &str) {
915        let beta = 1.0;
916        let omega = 5.0;
917
918        // Test periodicity by comparing G(τ) with G(τ-β)
919        // Since normalize_tau is restricted to [-β, β], we test:
920        // For τ ∈ (0, β]: compare G(τ) with G(τ-β)
921        // For fermions: G(τ) should equal -G(τ-β)
922        // For bosons: G(τ) should equal G(τ-β)
923        for tau in [0.1, 0.3, 0.7] {
924            let g_tau = gtau_single_pole::<S>(tau, omega, beta).unwrap();
925            let g_tau_minus_beta = gtau_single_pole::<S>(tau - beta, omega, beta).unwrap();
926
927            // For fermions: G(τ) = -G(τ-β) → G(τ-β) = -G(τ)
928            // For bosons: G(τ) = G(τ-β)
929            let expected = expected_sign * g_tau;
930
931            assert!(
932                (expected - g_tau_minus_beta).abs() < 1e-14,
933                "{} periodicity violated at τ={}: G(τ)={}, G(τ-β)={}, expected={}",
934                stat_name,
935                tau,
936                g_tau,
937                g_tau_minus_beta,
938                expected
939            );
940        }
941    }
942
943    #[test]
944    fn test_fermionic_antiperiodicity() {
945        // Fermions: G(τ+β) = -G(τ)
946        test_periodicity_generic::<Fermionic>(-1.0, "Fermionic");
947    }
948
949    #[test]
950    fn test_bosonic_periodicity() {
951        // Bosons: G(τ+β) = G(τ)
952        test_periodicity_generic::<Bosonic>(1.0, "Bosonic");
953    }
954
955    #[test]
956    fn test_generic_function_matches_specific() {
957        let beta = 1.0;
958        let omega = 5.0;
959        let tau = 0.5;
960
961        // Test that generic function matches specific functions
962        let g_f_specific = fermionic_single_pole(tau, omega, beta).unwrap();
963        let g_f_generic = gtau_single_pole::<Fermionic>(tau, omega, beta).unwrap();
964
965        let g_b_specific = bosonic_single_pole(tau, omega, beta).unwrap();
966        let g_b_generic = gtau_single_pole::<Bosonic>(tau, omega, beta).unwrap();
967
968        assert!(
969            (g_f_specific - g_f_generic).abs() < 1e-14,
970            "Fermionic: specific={}, generic={}",
971            g_f_specific,
972            g_f_generic
973        );
974        assert!(
975            (g_b_specific - g_b_generic).abs() < 1e-14,
976            "Bosonic: specific={}, generic={}",
977            g_b_specific,
978            g_b_generic
979        );
980    }
981}