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