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