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 ///
561 /// The mathematical transform does not depend on the axis placement or the
562 /// memory order of the arrays, but the floating-point result can: a different
563 /// axis placement selects a different contraction geometry, and the GEMM
564 /// backend decides how that geometry is blocked. Bitwise agreement across
565 /// axis placements is not guaranteed, and conditioning or cancellation can
566 /// amplify the difference (SpM-lab/sparse-ir-rs#326).
567 pub fn from_ir_nd<T: FitScalar>(
568 &self,
569 backend: Option<&GemmBackendHandle>,
570 gl: &TypedTensor<T>,
571 dim: usize,
572 ) -> Result<TypedTensor<T>, Error> {
573 // An empty batch gives an empty result; the fit fails only if the
574 // SVD of the fitting matrix does.
575 self.require_ir()?.ir_to_dlr_nd(backend, gl, dim)
576 }
577
578 /// Convert DLR coefficients to IR (N-dimensional, generic over real/complex)
579 ///
580 /// # Type Parameters
581 /// * `T` - Element type (`f64` or `Complex<f64>`)
582 ///
583 /// # Arguments
584 /// * `g_dlr` - DLR coefficients as N-D tensor
585 /// * `dim` - Dimension along which to transform
586 ///
587 /// # Returns
588 /// IR coefficients as N-D tensor, with [`Self::ir_basis_size`] entries
589 /// along `dim`. An empty batch gives an empty result.
590 ///
591 /// # Errors
592 ///
593 /// * [`Error::AxisOutOfRange`] if `dim` is not an axis of `g_dlr`
594 /// * [`Error::ShapeMismatch`] of the input if `g_dlr` does not have
595 /// [`Basis::size`](crate::basis_trait::Basis::size) (the number of
596 /// poles) entries along `dim`
597 ///
598 /// The mathematical transform does not depend on the axis placement or the
599 /// memory order of the arrays, but the floating-point result can: a different
600 /// axis placement selects a different contraction geometry, and the GEMM
601 /// backend decides how that geometry is blocked. Bitwise agreement across
602 /// axis placements is not guaranteed, and conditioning or cancellation can
603 /// amplify the difference (SpM-lab/sparse-ir-rs#326).
604 pub fn to_ir_nd<T: FitScalar>(
605 &self,
606 backend: Option<&GemmBackendHandle>,
607 g_dlr: &TypedTensor<T>,
608 dim: usize,
609 ) -> Result<TypedTensor<T>, Error> {
610 self.require_ir()?.dlr_to_ir_nd(backend, g_dlr, dim)
611 }
612
613 fn require_ir(&self) -> Result<&IrDlrTransform, Error> {
614 self.ir.as_ref().ok_or_else(|| Error::NotSupported {
615 what: "IR <-> DLR conversion of a DLR that was not built from an IR basis; \
616 build an IrDlrTransform for the IR basis instead"
617 .to_string(),
618 })
619 }
620}
621
622impl<S> DiscreteLehmannRepresentation<S>
623where
624 S: StatisticsType + 'static,
625{
626 /// Imaginary-time interpolation nodes (sorted), one per pole.
627 pub fn tau_nodes(&self) -> &[f64] {
628 use crate::basis_trait::Basis;
629 self.tau_nodes.get_or_init(|| {
630 let taus: Vec<f64> = crate::dlr_id::tau_candidates(self.beta * self.wmax)
631 .iter()
632 .map(|t| self.beta * t.value())
633 .collect();
634 let mat = self
635 .evaluate_tau(&taus)
636 .expect("the candidate times lie in [0, beta]");
637 let rows = crate::dlr_id::select_rows(
638 mat.host_data().expect("an owned matrix is host-resident"),
639 taus.len(),
640 self.poles.len(),
641 self.poles.len(),
642 );
643 rows.into_iter().map(|i| taus[i]).collect()
644 })
645 }
646
647 /// Matsubara interpolation nodes as indices `n` (`ν = nπ/β`, sorted).
648 ///
649 /// With `positive_only`, nodes are chosen among `n >= 0` so that the
650 /// stacked real and imaginary parts determine the coefficients of a
651 /// Green's function with `G(-iν) = conj(G(iν))`.
652 pub fn matsubara_nodes(&self, positive_only: bool) -> &[i64] {
653 let cell = if positive_only {
654 &self.matsubara_nodes_positive
655 } else {
656 &self.matsubara_nodes
657 };
658 cell.get_or_init(|| self.select_matsubara_nodes(positive_only))
659 }
660
661 fn select_matsubara_nodes(&self, positive_only: bool) -> Vec<i64> {
662 use crate::basis_trait::Basis;
663 let zeta = match S::STATISTICS {
664 Statistics::Fermionic => 1,
665 Statistics::Bosonic => 0,
666 };
667 let ns = crate::dlr_id::matsubara_candidates(self.beta * self.wmax, zeta, positive_only);
668 let freqs: Vec<MatsubaraFreq<S>> = ns
669 .iter()
670 .map(|&n| MatsubaraFreq::new(n).expect("candidate parity matches statistics"))
671 .collect();
672 let mat = self
673 .evaluate_matsubara(&freqs)
674 .expect("evaluating the DLR functions at Matsubara frequencies cannot fail");
675 let data = mat.host_data().expect("an owned matrix is host-resident");
676 let (nf, r) = (freqs.len(), self.poles.len());
677 if !positive_only {
678 let rows = crate::dlr_id::select_rows(data, nf, r, r);
679 return rows.into_iter().map(|i| ns[i]).collect();
680 }
681 // Stack [Re; Im] so each frequency contributes two real rows.
682 let mut stacked = vec![0.0; 2 * nf * r];
683 for j in 0..r {
684 for i in 0..nf {
685 let z = data[i + nf * j];
686 stacked[i + 2 * nf * j] = z.re;
687 stacked[nf + i + 2 * nf * j] = z.im;
688 }
689 }
690 let mut out: Vec<i64> = crate::dlr_id::select_rows(&stacked, 2 * nf, r, r)
691 .into_iter()
692 .map(|i| ns[i % nf])
693 .collect();
694 out.sort_unstable();
695 out.dedup();
696 out
697 }
698}
699
700// ============================================================================
701// IR <-> DLR transform
702// ============================================================================
703
704/// Linear map between the coefficients of an IR basis and of a DLR on the
705/// same domain.
706///
707/// The DLR basis function of pole `ω_i` is expanded in the IR basis as
708/// `u_i = Σ_l T[l, i] U_l` with `T[l, i] = -s_l V_l(ω_i) (w_i / w^{IR}_i)`,
709/// where `w_i` and `w^{IR}_i` are the pole weights of the DLR and of the IR
710/// kernel. DLR -> IR applies `T`; IR -> DLR is its least-squares inverse.
711pub struct IrDlrTransform {
712 fitter: RealMatrixFitter,
713}
714
715impl IrDlrTransform {
716 /// The transform with the `ir_size x dlr_size` matrix `T` mapping DLR to
717 /// IR coefficients (see the type documentation). The IR crate builds it
718 /// from its basis.
719 #[doc(hidden)]
720 pub fn from_matrix(matrix: Mat<f64>) -> Self {
721 Self {
722 fitter: RealMatrixFitter::new(matrix),
723 }
724 }
725
726 /// IR basis size.
727 pub fn ir_size(&self) -> usize {
728 self.fitter.n_points()
729 }
730
731 /// Number of DLR poles.
732 pub fn dlr_size(&self) -> usize {
733 self.fitter.basis_size()
734 }
735
736 /// The `ir_size x dlr_size` matrix `T` mapping DLR to IR coefficients.
737 pub fn matrix(&self) -> &crate::Matrix<f64> {
738 self.fitter.matrix()
739 }
740
741 /// IR -> DLR along axis `dim` (least squares).
742 ///
743 /// The mathematical transform does not depend on the axis placement or the
744 /// memory order of the arrays, but the floating-point result can: a different
745 /// axis placement selects a different contraction geometry, and the GEMM
746 /// backend decides how that geometry is blocked. Bitwise agreement across
747 /// axis placements is not guaranteed, and conditioning or cancellation can
748 /// amplify the difference (SpM-lab/sparse-ir-rs#326).
749 ///
750 /// # Errors
751 /// The errors of [`DiscreteLehmannRepresentation::from_ir_nd`].
752 pub fn ir_to_dlr_nd<T: FitScalar>(
753 &self,
754 backend: Option<&GemmBackendHandle>,
755 gl: &TypedTensor<T>,
756 dim: usize,
757 ) -> Result<TypedTensor<T>, Error> {
758 self.fitter.fit_nd(backend, gl, dim)
759 }
760
761 /// DLR -> IR along axis `dim`.
762 ///
763 /// The mathematical transform does not depend on the axis placement or the
764 /// memory order of the arrays, but the floating-point result can: a different
765 /// axis placement selects a different contraction geometry, and the GEMM
766 /// backend decides how that geometry is blocked. Bitwise agreement across
767 /// axis placements is not guaranteed, and conditioning or cancellation can
768 /// amplify the difference (SpM-lab/sparse-ir-rs#326).
769 ///
770 /// # Errors
771 /// The errors of [`DiscreteLehmannRepresentation::to_ir_nd`].
772 pub fn dlr_to_ir_nd<T: FitScalar>(
773 &self,
774 backend: Option<&GemmBackendHandle>,
775 g_dlr: &TypedTensor<T>,
776 dim: usize,
777 ) -> Result<TypedTensor<T>, Error> {
778 self.fitter.evaluate_nd(backend, g_dlr, dim)
779 }
780}
781
782// ============================================================================
783// Basis trait implementation for DLR
784// ============================================================================
785
786impl<S> crate::basis_trait::Basis<S> for DiscreteLehmannRepresentation<S>
787where
788 S: StatisticsType + 'static,
789{
790 fn beta(&self) -> f64 {
791 self.beta
792 }
793
794 fn wmax(&self) -> f64 {
795 self.wmax
796 }
797
798 fn lambda(&self) -> f64 {
799 self.beta * self.wmax
800 }
801
802 fn size(&self) -> usize {
803 self.poles.len()
804 }
805
806 fn accuracy(&self) -> f64 {
807 self.accuracy
808 }
809
810 fn significance(&self) -> Vec<f64> {
811 // All poles are equally significant in DLR
812 vec![1.0; self.poles.len()]
813 }
814
815 fn svals(&self) -> Vec<f64> {
816 // All poles are equally significant in DLR (no singular value concept)
817 vec![1.0; self.poles.len()]
818 }
819
820 fn default_tau_sampling_points(&self) -> Result<Vec<f64>, Error> {
821 Ok(self.tau_nodes().to_vec())
822 }
823
824 fn default_matsubara_sampling_points(
825 &self,
826 positive_only: bool,
827 ) -> Result<Vec<crate::freq::MatsubaraFreq<S>>, Error> {
828 self.matsubara_nodes(positive_only)
829 .iter()
830 .map(|&n| crate::freq::MatsubaraFreq::new(n))
831 .collect()
832 }
833
834 fn evaluate_tau(&self, tau: &[f64]) -> Result<crate::Matrix<f64>, Error> {
835 let n_poles = self.poles.len();
836 // Normalize every τ first: this rejects a τ outside [-β, β] and NaN
837 // for every pole, including the bosonic pole at 0, whose limit does
838 // not depend on τ.
839 let normalized = tau
840 .iter()
841 .map(|&t| normalize_tau::<S>(t, self.beta))
842 .collect::<Result<Vec<(f64, f64)>, Error>>()?;
843 if normalized.is_empty() {
844 // An empty set of points gives an empty matrix.
845 return Ok(Mat::<f64>::from_elem([0, n_poles], 0.0).into_typed());
846 }
847 Ok(Mat::<f64>::from_fn([normalized.len(), n_poles], |idx| {
848 let (tau_norm, sign) = normalized[idx[0]];
849 let pole = self.poles[idx[1]];
850 let pole_weight = self.pole_weights[idx[1]];
851 match S::STATISTICS {
852 Statistics::Fermionic => {
853 sign * fermionic_single_pole_unchecked(tau_norm, pole, self.beta) * pole_weight
854 }
855 Statistics::Bosonic => {
856 // The bosonic sign of normalize_tau is always 1.
857 if pole == 0.0 {
858 self.zero_pole_tau_limit()
859 } else if pole > 0.0 {
860 let denominator = -(-self.beta * pole).exp_m1();
861 -(-tau_norm * pole).exp() * pole_weight / denominator
862 } else {
863 let denominator = -(self.beta * pole).exp_m1();
864 (pole * (self.beta - tau_norm)).exp() * pole_weight / denominator
865 }
866 }
867 }
868 })
869 .into_typed())
870 }
871
872 fn evaluate_matsubara(
873 &self,
874 freqs: &[crate::freq::MatsubaraFreq<S>],
875 ) -> Result<crate::Matrix<num_complex::Complex<f64>>, Error> {
876 use num_complex::Complex;
877
878 let n_points = freqs.len();
879 let n_poles = self.poles.len();
880 if n_points == 0 {
881 // See evaluate_tau.
882 return Ok(
883 Mat::<Complex<f64>>::from_elem([0, n_poles], Complex::new(0.0, 0.0)).into_typed(),
884 );
885 }
886
887 // Evaluate MatsubaraPoles basis functions
888 Ok(Mat::<Complex<f64>>::from_fn([n_points, n_poles], |idx| {
889 let freq = &freqs[idx[0]];
890 let pole = self.poles[idx[1]];
891 let pole_weight = self.pole_weights[idx[1]];
892
893 // iν = iπn/β, with n = freq.n() (odd for fermions, even for bosons)
894 let iv = freq.value_imaginary(self.beta);
895
896 // u_i(iν) = pole_weight / (iν - pole_i), where `pole_weight` is the
897 // regularizer w(β, ω_i) of the source kernel.
898 if S::STATISTICS == Statistics::Bosonic && pole == 0.0 {
899 if crate::freq::is_zero(freq) {
900 Complex::new(self.zero_pole_matsubara_limit(), 0.0)
901 } else {
902 Complex::new(0.0, 0.0)
903 }
904 } else {
905 Complex::new(pole_weight, 0.0) / (iv - Complex::new(pole, 0.0))
906 }
907 })
908 .into_typed())
909 }
910
911 fn evaluate_omega(&self, _omega: &[f64]) -> Result<crate::Matrix<f64>, Error> {
912 // TODO(#205): For the IR basis, evaluate_omega returns V_l(omega).
913 // For DLR, the "basis functions" in omega-space are single-pole
914 // functions (conceptually delta functions at the pole positions),
915 // which do not have a well-defined continuous representation on the
916 // real-frequency axis analogous to V_l(omega). A proper
917 // implementation would require either:
918 // (a) returning the IR basis's V_l(omega) (but DLR does not store
919 // the IR basis), or
920 // (b) defining an appropriate discretized representation for the
921 // pole basis in omega-space.
922 // Until the semantics are clarified, this is NotSupported.
923 Err(Error::NotSupported {
924 what: "evaluate_omega of a DLR: its pole functions have no real-frequency \
925 representation (#205); use the IR basis"
926 .to_string(),
927 })
928 }
929
930 fn default_omega_sampling_points(&self) -> Result<Vec<f64>, Error> {
931 // DLR poles ARE the omega sampling points
932 Ok(self.poles.clone())
933 }
934}
935
936#[cfg(test)]
937mod tests {
938 use super::*;
939 use crate::traits::{Bosonic, Fermionic};
940
941 /// Generic test for periodicity/anti-periodicity
942 fn test_periodicity_generic<S: StatisticsType>(expected_sign: f64, stat_name: &str) {
943 let beta = 1.0;
944 let omega = 5.0;
945
946 // Test periodicity by comparing G(τ) with G(τ-β)
947 // Since normalize_tau is restricted to [-β, β], we test:
948 // For τ ∈ (0, β]: compare G(τ) with G(τ-β)
949 // For fermions: G(τ) should equal -G(τ-β)
950 // For bosons: G(τ) should equal G(τ-β)
951 for tau in [0.1, 0.3, 0.7] {
952 let g_tau = gtau_single_pole::<S>(tau, omega, beta).unwrap();
953 let g_tau_minus_beta = gtau_single_pole::<S>(tau - beta, omega, beta).unwrap();
954
955 // For fermions: G(τ) = -G(τ-β) → G(τ-β) = -G(τ)
956 // For bosons: G(τ) = G(τ-β)
957 let expected = expected_sign * g_tau;
958
959 assert!(
960 (expected - g_tau_minus_beta).abs() < 1e-14,
961 "{} periodicity violated at τ={}: G(τ)={}, G(τ-β)={}, expected={}",
962 stat_name,
963 tau,
964 g_tau,
965 g_tau_minus_beta,
966 expected
967 );
968 }
969 }
970
971 #[test]
972 fn test_fermionic_antiperiodicity() {
973 // Fermions: G(τ+β) = -G(τ)
974 test_periodicity_generic::<Fermionic>(-1.0, "Fermionic");
975 }
976
977 #[test]
978 fn test_bosonic_periodicity() {
979 // Bosons: G(τ+β) = G(τ)
980 test_periodicity_generic::<Bosonic>(1.0, "Bosonic");
981 }
982
983 #[test]
984 fn test_generic_function_matches_specific() {
985 let beta = 1.0;
986 let omega = 5.0;
987 let tau = 0.5;
988
989 // Test that generic function matches specific functions
990 let g_f_specific = fermionic_single_pole(tau, omega, beta).unwrap();
991 let g_f_generic = gtau_single_pole::<Fermionic>(tau, omega, beta).unwrap();
992
993 let g_b_specific = bosonic_single_pole(tau, omega, beta).unwrap();
994 let g_b_generic = gtau_single_pole::<Bosonic>(tau, omega, beta).unwrap();
995
996 assert!(
997 (g_f_specific - g_f_generic).abs() < 1e-14,
998 "Fermionic: specific={}, generic={}",
999 g_f_specific,
1000 g_f_generic
1001 );
1002 assert!(
1003 (g_b_specific - g_b_generic).abs() < 1e-14,
1004 "Bosonic: specific={}, generic={}",
1005 g_b_specific,
1006 g_b_generic
1007 );
1008 }
1009}