Skip to main content

sparse_ir_core/
freq.rs

1//! Matsubara frequency implementation for SparseIR
2//!
3//! This module provides Matsubara frequency types for both fermionic and
4//! bosonic statistics, matching the C++ implementation in freq.hpp.
5
6use num_complex::Complex64;
7use std::cmp::{Eq, Ord, Ordering, PartialEq, PartialOrd};
8use std::fmt;
9use std::ops::{Add, Neg, Sub};
10
11use crate::error::Error;
12use crate::traits::{Bosonic, Fermionic, Statistics, StatisticsType};
13
14/// Matsubara frequency for a specific statistics type
15///
16/// This represents the Matsubara frequency ν = nπ/β, where:
17/// - n is the stored integer (the *reduced* Matsubara frequency, returned by
18///   [`n`](Self::n)): odd for fermionic statistics (n = ±1, ±3, …) and even
19///   for bosonic statistics (n = 0, ±2, ±4, …)
20/// - β is the inverse temperature, passed to [`value`](Self::value) and
21///   [`value_imaginary`](Self::value_imaginary)
22///
23/// In terms of the conventional Matsubara index m of ν = (2m + ζ)π/β, with
24/// ζ = 1 for fermionic and ζ = 0 for bosonic statistics, the stored integer is
25/// n = 2m + ζ; it is *not* m itself. [`new`](Self::new) rejects an n whose
26/// parity does not match the statistics.
27///
28/// The statistics type S is checked at compile time to ensure type safety.
29///
30/// # Examples
31/// ```
32/// use sparse_ir::{BosonicFreq, FermionicFreq};
33/// use std::f64::consts::PI;
34///
35/// let beta = 10.0;
36///
37/// // n = 1 is the lowest positive fermionic frequency, ω = π/β (k = 0).
38/// let w = FermionicFreq::new(1).unwrap();
39/// assert_eq!(w.n(), 1);
40/// assert!((w.value(beta) - PI / beta).abs() < 1e-15);
41///
42/// // n = 2 is the lowest positive bosonic frequency, ω = 2π/β (k = 1).
43/// let nu = BosonicFreq::new(2).unwrap();
44/// assert!((nu.value(beta) - 2.0 * PI / beta).abs() < 1e-15);
45///
46/// // The parity of n must match the statistics.
47/// assert!(FermionicFreq::new(2).is_err());
48/// assert!(BosonicFreq::new(1).is_err());
49/// ```
50///
51/// Adding or subtracting frequencies gives the statistics of the result:
52/// `FermionicFreq ± FermionicFreq` is a [`BosonicFreq`], and a fermionic and
53/// a bosonic frequency (in either order) give a [`FermionicFreq`].
54#[derive(Debug, Clone, Copy)]
55pub struct MatsubaraFreq<S: StatisticsType> {
56    n: i64,
57    _phantom: std::marker::PhantomData<S>,
58}
59
60// Type aliases for convenience
61pub type FermionicFreq = MatsubaraFreq<Fermionic>;
62pub type BosonicFreq = MatsubaraFreq<Bosonic>;
63
64impl<S: StatisticsType> MatsubaraFreq<S> {
65    /// Get the reduced Matsubara frequency n, where ν = nπ/β
66    ///
67    /// n is odd for fermionic and even for bosonic statistics.
68    pub fn n(&self) -> i64 {
69        self.n
70    }
71
72    /// Create a new Matsubara frequency
73    ///
74    /// # Arguments
75    /// * `n` - The reduced Matsubara frequency n of ν = nπ/β: odd for
76    ///   fermionic statistics, even for bosonic statistics
77    ///
78    /// # Errors
79    /// [`Error::InvalidMatsubaraIndex`] if `n` is even for fermionic or odd
80    /// for bosonic statistics
81    ///
82    /// # Examples
83    /// ```
84    /// use sparse_ir::freq::{FermionicFreq, BosonicFreq};
85    ///
86    /// let fermionic = FermionicFreq::new(1).unwrap();  // OK: odd n for fermionic
87    /// let bosonic = BosonicFreq::new(0).unwrap();      // OK: even n for bosonic
88    /// ```
89    pub fn new(n: i64) -> Result<Self, Error> {
90        // Check if the frequency is allowed for this statistics type
91        let allowed = match S::STATISTICS {
92            Statistics::Fermionic => n % 2 != 0, // Fermionic: odd n only
93            Statistics::Bosonic => n % 2 == 0,   // Bosonic: even n only
94        };
95
96        if !allowed {
97            return Err(Error::InvalidMatsubaraIndex {
98                n,
99                statistics: S::STATISTICS,
100            });
101        }
102
103        Ok(Self {
104            n,
105            _phantom: std::marker::PhantomData,
106        })
107    }
108
109    /// Create a Matsubara frequency without validation
110    ///
111    /// # Safety
112    /// This function bypasses the validation in `new()`. Only use when you're
113    /// certain the frequency is valid for the statistics type.
114    pub unsafe fn new_unchecked(n: i64) -> Self {
115        Self {
116            n,
117            _phantom: std::marker::PhantomData,
118        }
119    }
120
121    /// Get the reduced Matsubara frequency n, where ν = nπ/β (same as [`n`](Self::n))
122    pub fn get_n(&self) -> i64 {
123        self.n
124    }
125
126    /// Compute the real frequency value: n * π / β
127    ///
128    /// # Arguments
129    /// * `beta` - Inverse temperature
130    ///
131    /// # Returns
132    /// The real frequency value
133    pub fn value(&self, beta: f64) -> f64 {
134        self.n as f64 * std::f64::consts::PI / beta
135    }
136
137    /// Compute the imaginary frequency value: i * n * π / β
138    ///
139    /// # Arguments
140    /// * `beta` - Inverse temperature
141    ///
142    /// # Returns
143    /// The imaginary frequency value as a complex number
144    pub fn value_imaginary(&self, beta: f64) -> Complex64 {
145        Complex64::new(0.0, self.value(beta))
146    }
147
148    /// Get the statistics type
149    pub fn statistics(&self) -> Statistics {
150        S::STATISTICS
151    }
152
153    /// Convert to i64 (for compatibility with C++ operator long long())
154    pub fn into_i64(self) -> i64 {
155        self.n
156    }
157}
158
159// Default implementations
160impl Default for FermionicFreq {
161    fn default() -> Self {
162        // Default fermionic frequency is n=1 (smallest positive odd frequency)
163        unsafe { Self::new_unchecked(1) }
164    }
165}
166
167impl Default for BosonicFreq {
168    fn default() -> Self {
169        // Default bosonic frequency is n=0 (zero frequency)
170        unsafe { Self::new_unchecked(0) }
171    }
172}
173
174// Conversion to i64
175impl<S: StatisticsType> From<MatsubaraFreq<S>> for i64 {
176    fn from(freq: MatsubaraFreq<S>) -> Self {
177        freq.n
178    }
179}
180
181// Operator overloading: addition and subtraction
182//
183// The parity of n + m and n - m follows from the parities of n and m, so the
184// output type carries the statistics of the result: fermionic ± fermionic is
185// bosonic, fermionic ± bosonic (either order) is fermionic, bosonic ±
186// bosonic is bosonic. The i64 arithmetic is the usual one (a panic on
187// overflow in debug builds, wrapping in release builds); wrapping keeps the
188// parity, so the output is always a valid frequency.
189macro_rules! impl_freq_arithmetic {
190    ($lhs:ty, $rhs:ty => $out:ty) => {
191        impl Add<$rhs> for $lhs {
192            type Output = $out;
193
194            fn add(self, other: $rhs) -> $out {
195                // SAFETY: not a memory-safety requirement; the parity of the
196                // sum is that of $out (see above).
197                unsafe { <$out>::new_unchecked(self.n + other.n) }
198            }
199        }
200
201        impl Sub<$rhs> for $lhs {
202            type Output = $out;
203
204            fn sub(self, other: $rhs) -> $out {
205                // SAFETY: as for `add`
206                unsafe { <$out>::new_unchecked(self.n - other.n) }
207            }
208        }
209    };
210}
211
212impl_freq_arithmetic!(FermionicFreq, FermionicFreq => BosonicFreq);
213impl_freq_arithmetic!(FermionicFreq, BosonicFreq => FermionicFreq);
214impl_freq_arithmetic!(BosonicFreq, FermionicFreq => FermionicFreq);
215impl_freq_arithmetic!(BosonicFreq, BosonicFreq => BosonicFreq);
216
217// Operator overloading: Negation
218impl<S: StatisticsType> Neg for MatsubaraFreq<S> {
219    type Output = Self;
220
221    fn neg(self) -> Self {
222        // Negation preserves parity, so this is always valid
223        unsafe { Self::new_unchecked(-self.n) }
224    }
225}
226
227// Comparison operators
228impl<S: StatisticsType> PartialEq for MatsubaraFreq<S> {
229    fn eq(&self, other: &Self) -> bool {
230        self.n == other.n
231    }
232}
233
234impl<S: StatisticsType> Eq for MatsubaraFreq<S> {}
235
236impl<S: StatisticsType> PartialOrd for MatsubaraFreq<S> {
237    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
238        Some(self.cmp(other))
239    }
240}
241
242impl<S: StatisticsType> Ord for MatsubaraFreq<S> {
243    fn cmp(&self, other: &Self) -> Ordering {
244        self.n.cmp(&other.n)
245    }
246}
247
248// Hash implementation (needed for some collections)
249impl<S: StatisticsType> std::hash::Hash for MatsubaraFreq<S> {
250    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
251        self.n.hash(state);
252    }
253}
254
255// Display implementation
256impl<S: StatisticsType> fmt::Display for MatsubaraFreq<S> {
257    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258        match self.n {
259            0 => write!(f, "0"),
260            1 => write!(f, "π/β"),
261            -1 => write!(f, "-π/β"),
262            n => write!(f, "{}π/β", n),
263        }
264    }
265}
266
267// Utility functions
268/// Get the sign of a Matsubara frequency
269#[allow(dead_code)]
270pub(crate) fn sign<S: StatisticsType>(freq: &MatsubaraFreq<S>) -> i32 {
271    if freq.n > 0 {
272        1
273    } else if freq.n < 0 {
274        -1
275    } else {
276        0
277    }
278}
279
280/// Get the fermionic sign based on statistics type
281#[allow(dead_code)]
282pub(crate) fn fermionic_sign<S: StatisticsType>() -> i32 {
283    match S::STATISTICS {
284        Statistics::Fermionic => -1,
285        Statistics::Bosonic => 1,
286    }
287}
288
289/// Create a zero frequency (bosonic only)
290#[allow(dead_code)]
291pub(crate) fn zero() -> BosonicFreq {
292    unsafe { BosonicFreq::new_unchecked(0) }
293}
294
295/// Check if a frequency is zero
296#[allow(dead_code)]
297#[doc(hidden)]
298pub fn is_zero<S: StatisticsType>(freq: &MatsubaraFreq<S>) -> bool {
299    match S::STATISTICS {
300        Statistics::Fermionic => false, // Fermionic frequencies are never zero
301        Statistics::Bosonic => freq.n == 0,
302    }
303}
304
305/// Compare two Matsubara frequencies of potentially different statistics types
306#[allow(dead_code)]
307pub(crate) fn is_less<S1: StatisticsType, S2: StatisticsType>(
308    a: &MatsubaraFreq<S1>,
309    b: &MatsubaraFreq<S2>,
310) -> bool {
311    a.get_n() < b.get_n()
312}
313
314/// Factory function to create Statistics from zeta value
315#[allow(dead_code)]
316pub(crate) fn create_statistics(zeta: i64) -> Result<Statistics, String> {
317    match zeta {
318        1 => Ok(Statistics::Fermionic),
319        0 => Ok(Statistics::Bosonic),
320        _ => Err(format!("Unknown statistics type: zeta={}", zeta)),
321    }
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327
328    #[test]
329    fn test_fermionic_frequency_creation() {
330        // Valid fermionic frequencies (odd n)
331        let freq1 = FermionicFreq::new(1).unwrap();
332        assert_eq!(freq1.get_n(), 1);
333
334        let freq3 = FermionicFreq::new(3).unwrap();
335        assert_eq!(freq3.get_n(), 3);
336
337        let freq_neg1 = FermionicFreq::new(-1).unwrap();
338        assert_eq!(freq_neg1.get_n(), -1);
339
340        // Invalid fermionic frequencies (even n)
341        assert!(FermionicFreq::new(0).is_err());
342        assert!(FermionicFreq::new(2).is_err());
343        assert!(FermionicFreq::new(-2).is_err());
344    }
345
346    #[test]
347    fn test_bosonic_frequency_creation() {
348        // Valid bosonic frequencies (even n)
349        let freq0 = BosonicFreq::new(0).unwrap();
350        assert_eq!(freq0.get_n(), 0);
351
352        let freq2 = BosonicFreq::new(2).unwrap();
353        assert_eq!(freq2.get_n(), 2);
354
355        let freq_neg2 = BosonicFreq::new(-2).unwrap();
356        assert_eq!(freq_neg2.get_n(), -2);
357
358        // Invalid bosonic frequencies (odd n)
359        assert!(BosonicFreq::new(1).is_err());
360        assert!(BosonicFreq::new(3).is_err());
361        assert!(BosonicFreq::new(-1).is_err());
362    }
363
364    #[test]
365    fn test_frequency_values() {
366        let beta = 2.0;
367        let pi = std::f64::consts::PI;
368
369        let fermionic = FermionicFreq::new(1).unwrap();
370        assert!((fermionic.value(beta) - pi / 2.0).abs() < 1e-14);
371
372        let bosonic = BosonicFreq::new(0).unwrap();
373        assert_eq!(bosonic.value(beta), 0.0);
374
375        let bosonic2 = BosonicFreq::new(2).unwrap();
376        assert!((bosonic2.value(beta) - pi).abs() < 1e-14);
377    }
378
379    #[test]
380    fn test_imaginary_values() {
381        let beta = 2.0;
382        let pi = std::f64::consts::PI;
383
384        let fermionic = FermionicFreq::new(1).unwrap();
385        let imag = fermionic.value_imaginary(beta);
386        assert!((imag.re - 0.0).abs() < 1e-14);
387        assert!((imag.im - pi / 2.0).abs() < 1e-14);
388    }
389
390    #[test]
391    fn test_operator_overloading() {
392        // Bosonic: even + even = even (valid)
393        let bfreq2 = BosonicFreq::new(2).unwrap();
394        let bfreq4 = BosonicFreq::new(4).unwrap();
395
396        let sum = bfreq2 + bfreq4;
397        assert_eq!(sum.get_n(), 6);
398
399        let diff = bfreq4 - bfreq2;
400        assert_eq!(diff.get_n(), 2);
401
402        // Negation preserves parity (valid for both statistics)
403        let freq1 = FermionicFreq::new(1).unwrap();
404        let neg = -freq1;
405        assert_eq!(neg.get_n(), -1);
406
407        let neg_b = -bfreq2;
408        assert_eq!(neg_b.get_n(), -2);
409    }
410
411    /// Sums and differences have the statistics of their parity: fermionic ±
412    /// fermionic is bosonic, fermionic ± bosonic is fermionic. Before the
413    /// change the output had the type of the inputs, so FermionicFreq(1) +
414    /// FermionicFreq(1) was a FermionicFreq with the even n = 2 in release
415    /// builds (a debug_assert only).
416    #[test]
417    fn test_sum_and_difference_follow_the_statistics() {
418        let f1 = FermionicFreq::new(1).unwrap();
419        let f3 = FermionicFreq::new(3).unwrap();
420        let b2 = BosonicFreq::new(2).unwrap();
421
422        let sum: BosonicFreq = f1 + f3;
423        assert_eq!(sum, BosonicFreq::new(4).unwrap());
424        let diff: BosonicFreq = f1 - f3;
425        assert_eq!(diff, BosonicFreq::new(-2).unwrap());
426
427        let fb_sum: FermionicFreq = f1 + b2;
428        assert_eq!(fb_sum, FermionicFreq::new(3).unwrap());
429        let fb_diff: FermionicFreq = f3 - b2;
430        assert_eq!(fb_diff, FermionicFreq::new(1).unwrap());
431        let bf_sum: FermionicFreq = b2 + f1;
432        assert_eq!(bf_sum, FermionicFreq::new(3).unwrap());
433        let bf_diff: FermionicFreq = b2 - f3;
434        assert_eq!(bf_diff, FermionicFreq::new(-1).unwrap());
435
436        let bb: BosonicFreq = b2 + b2;
437        assert_eq!(bb, BosonicFreq::new(4).unwrap());
438    }
439
440    #[test]
441    fn test_comparison_operators() {
442        let freq1 = FermionicFreq::new(1).unwrap();
443        let freq3 = FermionicFreq::new(3).unwrap();
444        let freq1_copy = FermionicFreq::new(1).unwrap();
445
446        assert_eq!(freq1, freq1_copy);
447        assert_ne!(freq1, freq3);
448        assert!(freq1 < freq3);
449        assert!(freq3 > freq1);
450        assert!(freq1 <= freq1_copy);
451        assert!(freq1 >= freq1_copy);
452    }
453
454    #[test]
455    fn test_utility_functions() {
456        let fermionic = FermionicFreq::new(1).unwrap();
457        let bosonic = BosonicFreq::new(0).unwrap();
458        let bosonic2 = BosonicFreq::new(2).unwrap();
459
460        // Sign function
461        assert_eq!(sign(&fermionic), 1);
462        assert_eq!(sign(&bosonic), 0);
463        assert_eq!(sign(&-fermionic), -1);
464
465        // Fermionic sign
466        assert_eq!(fermionic_sign::<Fermionic>(), -1);
467        assert_eq!(fermionic_sign::<Bosonic>(), 1);
468
469        // Zero frequency
470        let zero_freq = zero();
471        assert_eq!(zero_freq.get_n(), 0);
472
473        // Is zero
474        assert!(!is_zero(&fermionic));
475        assert!(is_zero(&bosonic));
476        assert!(!is_zero(&bosonic2));
477
478        // Is less
479        assert!(is_less(&fermionic, &bosonic2));
480        assert!(!is_less(&bosonic2, &fermionic));
481    }
482
483    #[test]
484    fn test_statistics_creation() {
485        assert_eq!(create_statistics(1).unwrap(), Statistics::Fermionic);
486        assert_eq!(create_statistics(0).unwrap(), Statistics::Bosonic);
487        assert!(create_statistics(2).is_err());
488    }
489
490    #[test]
491    fn test_display() {
492        let freq1 = FermionicFreq::new(1).unwrap();
493        let freq_neg1 = FermionicFreq::new(-1).unwrap();
494        let freq0 = BosonicFreq::new(0).unwrap();
495        let freq2 = BosonicFreq::new(2).unwrap();
496
497        assert_eq!(format!("{}", freq1), "π/β");
498        assert_eq!(format!("{}", freq_neg1), "-π/β");
499        assert_eq!(format!("{}", freq0), "0");
500        assert_eq!(format!("{}", freq2), "2π/β");
501    }
502
503    #[test]
504    fn test_default_implementations() {
505        let default_fermionic = FermionicFreq::default();
506        assert_eq!(default_fermionic.get_n(), 1);
507
508        let default_bosonic = BosonicFreq::default();
509        assert_eq!(default_bosonic.get_n(), 0);
510    }
511
512    #[test]
513    fn test_conversion_to_i64() {
514        let freq = FermionicFreq::new(3).unwrap();
515        let n: i64 = freq.into();
516        assert_eq!(n, 3);
517
518        let n_direct = freq.into_i64();
519        assert_eq!(n_direct, 3);
520    }
521
522    #[test]
523    fn test_sign() {
524        let fermionic_pos = FermionicFreq::new(3).unwrap();
525        assert_eq!(sign(&fermionic_pos), 1);
526
527        let fermionic_neg = FermionicFreq::new(-5).unwrap();
528        assert_eq!(sign(&fermionic_neg), -1);
529
530        let bosonic_zero = BosonicFreq::new(0).unwrap();
531        assert_eq!(sign(&bosonic_zero), 0);
532
533        let bosonic_pos = BosonicFreq::new(4).unwrap();
534        assert_eq!(sign(&bosonic_pos), 1);
535    }
536
537    #[test]
538    fn test_fermionic_sign() {
539        assert_eq!(fermionic_sign::<Fermionic>(), -1);
540        assert_eq!(fermionic_sign::<Bosonic>(), 1);
541    }
542
543    #[test]
544    fn test_zero() {
545        let zero_freq = zero();
546        assert_eq!(zero_freq.get_n(), 0);
547        assert_eq!(zero_freq.value(1.0), 0.0);
548    }
549
550    #[test]
551    fn test_is_zero() {
552        // Fermionic frequencies are never zero
553        let fermionic = FermionicFreq::new(1).unwrap();
554        assert!(!is_zero(&fermionic));
555
556        // Bosonic zero frequency
557        let bosonic_zero = BosonicFreq::new(0).unwrap();
558        assert!(is_zero(&bosonic_zero));
559
560        // Non-zero bosonic frequency
561        let bosonic_nonzero = BosonicFreq::new(2).unwrap();
562        assert!(!is_zero(&bosonic_nonzero));
563    }
564
565    #[test]
566    fn test_is_less() {
567        let freq1 = FermionicFreq::new(1).unwrap();
568        let freq3 = FermionicFreq::new(3).unwrap();
569        assert!(is_less(&freq1, &freq3));
570        assert!(!is_less(&freq3, &freq1));
571
572        // Compare different statistics types
573        let fermionic = FermionicFreq::new(1).unwrap();
574        let bosonic = BosonicFreq::new(2).unwrap();
575        assert!(is_less(&fermionic, &bosonic));
576
577        // Same frequency
578        assert!(!is_less(&freq1, &freq1));
579    }
580
581    #[test]
582    fn test_create_statistics() {
583        // Fermionic (zeta = 1)
584        let fermionic = create_statistics(1).unwrap();
585        assert_eq!(fermionic, Statistics::Fermionic);
586
587        // Bosonic (zeta = 0)
588        let bosonic = create_statistics(0).unwrap();
589        assert_eq!(bosonic, Statistics::Bosonic);
590
591        // Invalid zeta
592        assert!(create_statistics(2).is_err());
593        assert!(create_statistics(-1).is_err());
594    }
595
596    /// The parity check holds for negative n, whose remainder is negative.
597    #[test]
598    fn test_new_rejects_wrong_parity_with_typed_error() {
599        use crate::error::Error;
600
601        assert_eq!(
602            FermionicFreq::new(2).unwrap_err(),
603            Error::InvalidMatsubaraIndex {
604                n: 2,
605                statistics: Statistics::Fermionic,
606            }
607        );
608        assert_eq!(
609            BosonicFreq::new(-3).unwrap_err(),
610            Error::InvalidMatsubaraIndex {
611                n: -3,
612                statistics: Statistics::Bosonic,
613            }
614        );
615        assert_eq!(
616            BosonicFreq::new(-3).unwrap_err().to_string(),
617            "Matsubara frequency n = -3 is not allowed for bosonic statistics"
618        );
619        assert_eq!(FermionicFreq::new(-1).unwrap().n(), -1);
620        assert_eq!(BosonicFreq::new(-2).unwrap().n(), -2);
621    }
622}