Skip to main content

sparse_ir_core/
taufuncs.rs

1//! Imaginary time τ normalization utilities
2//!
3//! This module provides utility functions for normalizing imaginary time τ
4//! with appropriate boundary conditions (periodic/anti-periodic).
5//!
6//! These utilities are used internally (e.g., in DLR implementation) and
7//! can also be used by external C-API or FFI layers.
8
9use crate::error::{Error, require_positive_finite};
10use crate::traits::{Statistics, StatisticsType};
11
12/// Check if τ is outside the normal range [0, β]
13///
14/// Returns whether τ is in an "odd" period (outside [0, β]).
15/// This determines the sign for anti-periodic (fermionic) functions.
16///
17/// # Arguments
18/// * `tau` - Imaginary time
19/// * `beta` - Inverse temperature
20///
21/// # Returns
22/// * `true` if τ < 0 or τ > β (odd period), `false` otherwise
23#[inline]
24#[allow(dead_code)]
25fn is_odd_period(tau: f64, beta: f64) -> bool {
26    tau < 0.0 || tau > beta
27}
28
29/// Normalize τ to the range [0, β] with statistics-dependent boundary conditions
30///
31/// Handles boundary conditions based on statistics:
32/// - Fermions: Anti-periodic G(τ + β) = -G(τ)
33/// - Bosons: Periodic G(τ + β) = G(τ)
34///
35/// # Type Parameters
36/// * `S` - Statistics type (Fermionic or Bosonic)
37///
38/// # Arguments
39/// * `tau` - Imaginary time in range [-β, β]
40/// * `beta` - Inverse temperature
41///
42/// # Returns
43/// * `(tau_normalized, sign)` - Normalized τ ∈ [0, β] and sign factor
44///
45/// # Errors
46///
47/// * [`Error::InvalidParameter`] if `beta` is not positive and finite
48/// * [`Error::OutOfDomain`] if `tau` is outside [-β, β] or NaN
49///
50/// # Boundary Interpretation
51/// * `β` is interpreted as `β-` (left limit at β): `tau == beta` stays in normal range
52/// * `-β` is read as (-β)⁺: it wraps to 0, with a sign flip for fermions
53/// * `-0.0` (negative zero) is treated as being in the odd period for fermions
54///
55/// # Special Cases
56/// For Fermionic statistics:
57/// * `tau = -0.0` (negative zero) → `(tau_normalized = β, sign = -1.0)`
58/// * `tau = 0.0` (positive zero) → `(tau_normalized = 0.0, sign = 1.0)`
59/// * `tau ∈ [-β, 0)` → wraps to [0, β] with `sign = -1.0`
60///
61/// For Bosonic statistics:
62/// * `tau = -0.0` (negative zero) → `(tau_normalized = β, sign = 1.0)` (periodic)
63/// * `tau = 0.0` (positive zero) → `(tau_normalized = 0.0, sign = 1.0)`
64/// * `tau ∈ [-β, 0)` → wraps to [0, β] with `sign = 1.0`
65///
66/// # Examples
67/// ```
68/// use sparse_ir::taufuncs::normalize_tau;
69/// use sparse_ir::traits::{Bosonic, Fermionic};
70///
71/// // Normal negative value
72/// let (tau_norm, sign) = normalize_tau::<Fermionic>(-0.3, 1.0).unwrap();
73/// assert!((tau_norm - 0.7).abs() < 1e-14);
74/// assert_eq!(sign, -1.0);
75///
76/// // Negative zero
77/// let (tau_norm, sign) = normalize_tau::<Fermionic>(-0.0, 1.0).unwrap();
78/// assert!((tau_norm - 1.0).abs() < 1e-14);
79/// assert_eq!(sign, -1.0);
80///
81/// // -β wraps to 0 (with a sign flip for fermions only)
82/// assert_eq!(normalize_tau::<Fermionic>(-1.0, 1.0).unwrap(), (0.0, -1.0));
83/// assert_eq!(normalize_tau::<Bosonic>(-1.0, 1.0).unwrap(), (0.0, 1.0));
84///
85/// // τ outside [-β, β] is an error
86/// assert!(normalize_tau::<Fermionic>(1.5, 1.0).is_err());
87/// ```
88pub fn normalize_tau<S: StatisticsType>(tau: f64, beta: f64) -> Result<(f64, f64), Error> {
89    // Normalize τ ∈ [-β, β] to [0, β]
90    require_positive_finite("beta", beta)?;
91    if !(tau >= -beta && tau <= beta) {
92        return Err(Error::OutOfDomain {
93            name: "tau",
94            value: tau,
95            domain: (-beta, beta),
96        });
97    }
98
99    // Special handling for negative zero
100    if tau.is_sign_negative() && tau == 0.0 {
101        // tau = -0.0
102        return Ok(match S::STATISTICS {
103            Statistics::Fermionic => (beta, -1.0), // Anti-periodic: wraps to beta with sign flip
104            Statistics::Bosonic => (beta, 1.0),    // Periodic: wraps to beta with sign unchanged
105        });
106    }
107
108    // If already in [0, β], return as-is with sign = 1
109    if tau >= 0.0 && tau <= beta {
110        return Ok((tau, 1.0));
111    }
112
113    // tau ∈ [-β, 0): wrap to [0, β]
114    // For tau ∈ (-β, 0): tau_normalized = tau + β
115    // For tau = -β: wraps to 0
116    let tau_normalized = tau + beta;
117
118    // Sign depends on statistics
119    let sign = match S::STATISTICS {
120        Statistics::Fermionic => -1.0, // Anti-periodic: sign flips
121        Statistics::Bosonic => 1.0,    // Periodic: sign stays
122    };
123
124    Ok((tau_normalized, sign))
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130    use crate::traits::{Bosonic, Fermionic};
131
132    #[test]
133    fn test_is_odd_period() {
134        let beta = 1.0;
135
136        // Normal range
137        assert!(!is_odd_period(0.0, beta));
138        assert!(!is_odd_period(0.5, beta));
139        assert!(!is_odd_period(beta, beta)); // β is β-
140
141        // Odd periods
142        assert!(is_odd_period(-0.1, beta));
143        assert!(is_odd_period(1.1, beta));
144    }
145
146    #[test]
147    fn test_normalize_tau_generic_fermionic() {
148        let beta = 1.0;
149
150        // Normal range
151        let (tau_norm, sign) = normalize_tau::<Fermionic>(0.5, beta).unwrap();
152        assert!((tau_norm - 0.5).abs() < 1e-14);
153        assert!((sign - 1.0).abs() < 1e-14);
154
155        // At β (interpreted as β-)
156        let (tau_norm, sign) = normalize_tau::<Fermionic>(beta, beta).unwrap();
157        assert!((tau_norm - beta).abs() < 1e-14);
158        assert!((sign - 1.0).abs() < 1e-14);
159
160        // At 0
161        let (tau_norm, sign) = normalize_tau::<Fermionic>(0.0, beta).unwrap();
162        assert!(tau_norm.abs() < 1e-14);
163        assert!((sign - 1.0).abs() < 1e-14);
164
165        // Negative range
166        let (tau_norm, sign) = normalize_tau::<Fermionic>(-0.3, beta).unwrap();
167        assert!((tau_norm - 0.7).abs() < 1e-14);
168        assert!((sign - (-1.0)).abs() < 1e-14);
169
170        // Test -β: wraps to 0 with sign flip
171        let (tau_norm, sign) = normalize_tau::<Fermionic>(-beta, beta).unwrap();
172        assert!(tau_norm.abs() < 1e-14); // wraps to 0
173        assert!((sign - (-1.0)).abs() < 1e-14);
174    }
175
176    #[test]
177    fn test_normalize_tau_negative_zero_fermionic() {
178        // Test negative zero: -0.0 should be treated as being in odd period
179        let beta = 1.0;
180
181        // Negative zero should normalize to beta with sign = -1
182        let (tau_norm, sign) = normalize_tau::<Fermionic>(-0.0, beta).unwrap();
183        assert!(
184            (tau_norm - beta).abs() < 1e-14,
185            "Expected tau_normalized = {}, got {}",
186            beta,
187            tau_norm
188        );
189        assert!(
190            (sign - (-1.0)).abs() < 1e-14,
191            "Expected sign = -1.0, got {}",
192            sign
193        );
194
195        // Positive zero should stay at 0 with sign = 1
196        let (tau_norm, sign) = normalize_tau::<Fermionic>(0.0, beta).unwrap();
197        assert!(
198            tau_norm.abs() < 1e-14,
199            "Expected tau_normalized = 0.0, got {}",
200            tau_norm
201        );
202        assert!(
203            (sign - 1.0).abs() < 1e-14,
204            "Expected sign = 1.0, got {}",
205            sign
206        );
207
208        // Verify that Rust distinguishes -0.0 from 0.0
209        assert_ne!(
210            (-0.0_f64).to_bits(),
211            0.0_f64.to_bits(),
212            "Rust should distinguish -0.0 from 0.0"
213        );
214    }
215
216    #[test]
217    fn test_normalize_tau_generic_bosonic() {
218        let beta = 1.0;
219
220        // Normal range
221        let (tau_norm, sign) = normalize_tau::<Bosonic>(0.5, beta).unwrap();
222        assert!((tau_norm - 0.5).abs() < 1e-14);
223        assert!((sign - 1.0).abs() < 1e-14);
224
225        // At β (interpreted as β-)
226        let (tau_norm, sign) = normalize_tau::<Bosonic>(beta, beta).unwrap();
227        assert!((tau_norm - beta).abs() < 1e-14);
228        assert!((sign - 1.0).abs() < 1e-14);
229
230        // At 0
231        let (tau_norm, sign) = normalize_tau::<Bosonic>(0.0, beta).unwrap();
232        assert!(tau_norm.abs() < 1e-14);
233        assert!((sign - 1.0).abs() < 1e-14);
234
235        // Negative range
236        let (tau_norm, sign) = normalize_tau::<Bosonic>(-0.3, beta).unwrap();
237        assert!((tau_norm - 0.7).abs() < 1e-14);
238        assert!((sign - 1.0).abs() < 1e-14);
239
240        // Test -β: wraps to 0, sign stays 1 (periodic)
241        let (tau_norm, sign) = normalize_tau::<Bosonic>(-beta, beta).unwrap();
242        assert!(tau_norm.abs() < 1e-14); // wraps to 0
243        assert!((sign - 1.0).abs() < 1e-14);
244    }
245
246    /// τ outside [-β, β] (NaN and infinities included) and a β that is not
247    /// positive and finite are typed errors. Before the change the first
248    /// panicked, NaN passed through as a NaN τ, and β was not checked.
249    #[test]
250    fn test_normalize_tau_rejects_invalid_arguments() {
251        use crate::error::Error;
252
253        for tau in [1.5, -1.5, f64::INFINITY, f64::NEG_INFINITY] {
254            assert_eq!(
255                normalize_tau::<Fermionic>(tau, 1.0),
256                Err(Error::OutOfDomain {
257                    name: "tau",
258                    value: tau,
259                    domain: (-1.0, 1.0),
260                })
261            );
262        }
263        assert!(matches!(
264            normalize_tau::<Bosonic>(f64::NAN, 1.0),
265            Err(Error::OutOfDomain { name: "tau", value, .. }) if value.is_nan()
266        ));
267        for beta in [0.0, -1.0, f64::NAN, f64::INFINITY] {
268            assert!(
269                matches!(
270                    normalize_tau::<Fermionic>(0.5, beta),
271                    Err(Error::InvalidParameter { name: "beta", .. })
272                ),
273                "beta = {beta}"
274            );
275        }
276    }
277}