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