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}