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}