sparse_ir_basis/basis.rs
1//! Finite temperature basis for SparseIR
2//!
3//! This module provides the `FiniteTempBasis` type which represents the
4//! intermediate representation (IR) basis for a given temperature.
5
6use std::sync::Arc;
7
8use crate::error::{
9 Error, require_accuracy, require_nonzero_size, require_positive_finite, require_threshold,
10};
11use crate::kernel::{CentrosymmKernel, KernelProperties, LogisticKernel};
12use crate::poly::{PiecewiseLegendrePoly, PiecewiseLegendrePolyVector, default_sampling_points};
13use crate::polyfourier::PiecewiseLegendreFTVector;
14use crate::sve::{SVEResult, TworkType, compute_sve};
15use crate::traits::{Bosonic, Fermionic, StatisticsType};
16
17// Re-export Statistics enum for C-API
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum Statistics {
20 Fermionic,
21 Bosonic,
22}
23
24/// Finite temperature basis for imaginary time/frequency Green's functions
25///
26/// For a continuation kernel `K` from real frequencies `ω ∈ [-ωmax, ωmax]` to
27/// imaginary time `τ ∈ [0, β]`, this type stores the truncated singular
28/// value expansion or IR basis:
29///
30/// ```text
31/// K(τ, ω) ≈ sum(u[l](τ) * s[l] * v[l](ω) for l in 0..L)
32/// ```
33///
34/// This basis is inferred from a reduced form by appropriate scaling of
35/// the variables.
36///
37/// # Type Parameters
38///
39/// * `K` - Kernel type implementing `KernelProperties + CentrosymmKernel`
40/// * `S` - Statistics type (`Fermionic` or `Bosonic`)
41#[derive(Clone)]
42pub struct FiniteTempBasis<K, S>
43where
44 K: KernelProperties + CentrosymmKernel + Clone + 'static,
45 S: StatisticsType,
46{
47 /// The kernel used to construct this basis
48 kernel: K,
49
50 /// The SVE result (in scaled variables)
51 sve_result: Arc<SVEResult>,
52
53 /// Accuracy of the basis (relative error)
54 accuracy: f64,
55
56 /// Inverse temperature β
57 beta: f64,
58
59 /// Left singular functions on imaginary time axis τ ∈ [0, β]
60 /// Arc for efficient sharing (large immutable data)
61 u: Arc<PiecewiseLegendrePolyVector>,
62
63 /// Right singular functions on real frequency axis ω ∈ [-ωmax, ωmax]
64 /// Arc for efficient sharing (large immutable data)
65 v: Arc<PiecewiseLegendrePolyVector>,
66
67 /// Singular values
68 s: Vec<f64>,
69
70 /// Left singular functions on Matsubara frequency axis (Fourier transform of u)
71 /// Arc for efficient sharing (large immutable data)
72 uhat: Arc<PiecewiseLegendreFTVector<S>>,
73
74 /// Full uhat (before truncation to basis size)
75 /// Arc for efficient sharing (large immutable data, used for Matsubara sampling)
76 uhat_full: Arc<PiecewiseLegendreFTVector<S>>,
77
78 _phantom: std::marker::PhantomData<S>,
79}
80
81/// Check that `sve_result` is an SVE on [-1, 1] × [-1, 1], the domain of
82/// the kernels in the scaled variables x = 2τ/β - 1 and y = ω/ωmax
83///
84/// The tolerance is the one of the Fourier transform of the basis functions
85/// (`PiecewiseLegendreFT::new`), which requires the interval [-1, 1].
86fn check_unit_domain(sve_result: &SVEResult) -> Result<(), Error> {
87 let domain =
88 |funcs: &PiecewiseLegendrePolyVector| funcs.get_polys().first().map(|p| (p.xmin, p.xmax));
89 let (Some(u), Some(v)) = (domain(&sve_result.u), domain(&sve_result.v)) else {
90 return Err(Error::EmptyInput { name: "sve_result" });
91 };
92 let is_unit = |(lo, hi): (f64, f64)| (lo + 1.0).abs() <= 1e-12 && (hi - 1.0).abs() <= 1e-12;
93 if is_unit(u) && is_unit(v) {
94 Ok(())
95 } else {
96 Err(Error::InvalidParameter {
97 name: "sve_result",
98 value: format!("an SVE on [{:?}, {:?}] × [{:?}, {:?}]", u.0, u.1, v.0, v.1),
99 reason: "must be an SVE on [-1, 1] × [-1, 1]".to_string(),
100 })
101 }
102}
103
104/// Knots and widths of `poly` with the first and last knot set exactly to
105/// -1 and 1
106///
107/// `check_unit_domain` accepts an SVE whose domain differs from [-1, 1] by up
108/// to 1e-12 (e.g. from `spir_sve_result_from_matrix`). Scaled to τ and ω,
109/// such knots end inside [0, β] and [-ωmax, ωmax], where evaluating at the
110/// ends panics. The width of an end segment is recomputed only if its knot
111/// moved, so an SVE with exact ends keeps its knots and widths bit for bit.
112fn unit_knots_and_widths(poly: &PiecewiseLegendrePoly) -> (Vec<f64>, Vec<f64>) {
113 let mut knots = poly.knots.clone();
114 let mut widths = poly.delta_x.clone();
115 let last = knots.len() - 1;
116 if knots[0] != -1.0 {
117 knots[0] = -1.0;
118 widths[0] = knots[1] - knots[0];
119 }
120 if knots[last] != 1.0 {
121 knots[last] = 1.0;
122 widths[last - 1] = knots[last] - knots[last - 1];
123 }
124 (knots, widths)
125}
126
127impl<K, S> FiniteTempBasis<K, S>
128where
129 K: KernelProperties + CentrosymmKernel + Clone + 'static,
130 S: StatisticsType,
131{
132 // ========== Getters ==========
133
134 /// Get a reference to the kernel
135 pub fn kernel(&self) -> &K {
136 &self.kernel
137 }
138
139 /// Get the SVE result the basis was built from
140 ///
141 /// It is not truncated to the basis size: a basis limited by `max_size`
142 /// or `epsilon` keeps all singular functions of the SVE.
143 pub fn sve_result(&self) -> &Arc<SVEResult> {
144 &self.sve_result
145 }
146
147 /// Get the accuracy of the basis
148 pub fn accuracy(&self) -> f64 {
149 self.accuracy
150 }
151
152 /// Get the inverse temperature β
153 pub fn beta(&self) -> f64 {
154 self.beta
155 }
156
157 /// Get the left singular functions (u) on imaginary time axis
158 pub fn u(&self) -> &Arc<PiecewiseLegendrePolyVector> {
159 &self.u
160 }
161
162 /// Get the right singular functions (v) on real frequency axis
163 pub fn v(&self) -> &Arc<PiecewiseLegendrePolyVector> {
164 &self.v
165 }
166
167 /// Get the singular values
168 pub fn s(&self) -> &[f64] {
169 &self.s
170 }
171
172 /// Get the left singular functions on Matsubara frequency axis (uhat)
173 pub fn uhat(&self) -> &Arc<PiecewiseLegendreFTVector<S>> {
174 &self.uhat
175 }
176
177 /// Get the full uhat (before truncation)
178 ///
179 /// Holds the Matsubara transforms of all singular functions of
180 /// [`sve_result`](Self::sve_result), not only of the `size()` basis
181 /// functions; the default Matsubara sampling points use them.
182 pub fn uhat_full(&self) -> &Arc<PiecewiseLegendreFTVector<S>> {
183 &self.uhat_full
184 }
185
186 // ========== Other methods ==========
187
188 /// Get the frequency cutoff ωmax
189 pub fn wmax(&self) -> f64 {
190 self.kernel.lambda() / self.beta
191 }
192
193 /// Get default Matsubara sampling points as i64 indices (for C-API)
194 ///
195 /// # Errors
196 ///
197 /// [`Error::NotSupported`] if the basis functions have no definite parity
198 /// (an SVE that is not centrosymmetric, e.g. from `compute_sve_general`;
199 /// #183)
200 pub fn default_matsubara_sampling_points_i64(
201 &self,
202 positive_only: bool,
203 ) -> Result<Vec<i64>, Error>
204 where
205 S: 'static,
206 {
207 Ok(self
208 .default_matsubara_sampling_points(positive_only)?
209 .into_iter()
210 .map(|f| f.n())
211 .collect())
212 }
213
214 /// Get default Matsubara sampling points as i64 indices with mitigate parameter (for C-API)
215 ///
216 /// # Errors
217 ///
218 /// [`Error::NotSupported`] if the basis functions have no definite parity
219 /// (an SVE that is not centrosymmetric, e.g. from `compute_sve_general`;
220 /// #183)
221 pub fn default_matsubara_sampling_points_i64_with_mitigate(
222 &self,
223 positive_only: bool,
224 mitigate: bool,
225 n_points: usize,
226 ) -> Result<Vec<i64>, Error>
227 where
228 S: 'static,
229 {
230 if !self.kernel().is_centrosymmetric() {
231 return Err(Error::NotSupported {
232 what: "default Matsubara sampling points of a basis whose kernel is not \
233 centrosymmetric: they rely on the parity of the basis functions"
234 .to_string(),
235 });
236 }
237 let fence = mitigate;
238 let freqs = Self::default_matsubara_sampling_points_impl(
239 &self.uhat_full,
240 n_points,
241 fence,
242 positive_only,
243 )?;
244 Ok(freqs.into_iter().map(|f| f.n()).collect())
245 }
246
247 /// Create a new FiniteTempBasis
248 ///
249 /// # Arguments
250 ///
251 /// * `kernel` - Kernel implementing `KernelProperties + CentrosymmKernel`
252 /// * `beta` - Inverse temperature (β > 0)
253 /// * `epsilon` - Accuracy of the basis, in (0, 1). `None` selects the best
254 /// accuracy of the working precision (about 1.6e-16).
255 /// * `max_size` - Maximum number of basis functions (optional). It limits
256 /// the basis, not the SVE: the SVE is computed and kept in full, as in
257 /// [`from_sve_result`](Self::from_sve_result) with an untruncated SVE.
258 /// The default sampling points and [`accuracy`](Self::accuracy) of the
259 /// basis use the singular functions beyond it.
260 ///
261 /// # Returns
262 ///
263 /// A new FiniteTempBasis
264 ///
265 /// # Errors
266 ///
267 /// * [`Error::InvalidParameter`] if `beta` is not positive and finite,
268 /// `epsilon` is not in (0, 1), or `max_size` is `Some(0)`. These are
269 /// checked before the SVE is computed.
270 /// * The errors of [`compute_sve`]: [`Error::NonFiniteInput`] if the
271 /// discretized kernel has a non-finite entry, [`Error::DecompositionFailed`]
272 /// if an SVD of the SVE fails
273 /// * The errors of [`from_sve_result`](Self::from_sve_result)
274 pub fn new(
275 kernel: K,
276 beta: f64,
277 epsilon: Option<f64>,
278 max_size: Option<usize>,
279 ) -> Result<Self, Error> {
280 // Validate before the (expensive) SVE
281 require_positive_finite("beta", beta)?;
282 require_accuracy("epsilon", epsilon)?;
283 require_nonzero_size("max_size", max_size)?;
284
285 // Compute the SVE without a size limit; `from_sve_result` truncates
286 // only the basis to `max_size`. The default sampling points of a basis
287 // of size L are the roots of u_L (tau) and v_L (omega) and the sign
288 // changes of uhat_L or uhat_{L+1} (Matsubara), and its accuracy is
289 // s_L / s_0. An SVE truncated to `max_size` functions lacks them, and
290 // the point selection would fall back to the extrema of the last
291 // function (issue #285).
292 //
293 // Convention-matched with SparseIR.jl 1.1.4 (src/basis.jl,
294 // `FiniteTempBasis(statistics, β, ωmax, ε; max_size, kernel,
295 // sve_result=SVEResult(kernel; ε))`): its default SVE takes no `lmax`,
296 // and `part(sve_result; ε, max_size)` truncates the basis only. No
297 // code was ported.
298 let sve_result = compute_sve(
299 kernel.clone(),
300 epsilon,
301 None, // cutoff
302 None, // no limit on the number of singular values
303 TworkType::Auto,
304 )?;
305
306 Self::from_sve_result(kernel, beta, sve_result, epsilon, max_size)
307 }
308
309 /// Create basis from existing SVE result
310 ///
311 /// This is useful when you want to reuse the same SVE computation
312 /// for both fermionic and bosonic bases.
313 ///
314 /// `max_size` (and `epsilon`) truncate the basis functions and singular
315 /// values only. `sve_result` is kept as given: the default sampling points
316 /// and [`accuracy`](Self::accuracy) use its singular functions beyond the
317 /// basis, so pass an untruncated SVE to get the points of SparseIR.jl.
318 ///
319 /// # Errors
320 ///
321 /// * [`Error::InvalidParameter`] if `beta` is not positive and finite,
322 /// `epsilon` is not in [0, 1) (0 keeps every singular value), `max_size`
323 /// is `Some(0)`, or `sve_result` is not an SVE on [-1, 1] × [-1, 1]
324 /// * [`Error::EmptyInput`] if `sve_result` has no singular functions
325 /// * The errors of [`SVEResult::part`] (for an `SVEResult` whose public
326 /// fields break its invariants)
327 pub fn from_sve_result(
328 kernel: K,
329 beta: f64,
330 sve_result: SVEResult,
331 epsilon: Option<f64>,
332 max_size: Option<usize>,
333 ) -> Result<Self, Error> {
334 require_positive_finite("beta", beta)?;
335 require_threshold("epsilon", epsilon)?;
336 require_nonzero_size("max_size", max_size)?;
337 check_unit_domain(&sve_result)?;
338
339 // Get truncated u, s, v from SVE result
340 let (u_sve, s_sve, v_sve) = sve_result.part(epsilon, max_size)?;
341
342 // Calculate accuracy
343 let accuracy = if sve_result.s.len() > s_sve.len() {
344 sve_result.s[s_sve.len()] / sve_result.s[0]
345 } else {
346 sve_result.s[sve_result.s.len() - 1] / sve_result.s[0]
347 };
348
349 // Get kernel parameters
350 let lambda = kernel.lambda();
351 let omega_max = lambda / beta;
352
353 // Scale polynomials to new variables
354 // tau = β/2 * (x + 1), w = ωmax * y
355
356 // Transform u: x ∈ [-1, 1] → τ ∈ [0, β]
357 let (x_knots, x_widths) = unit_knots_and_widths(&u_sve.get_polys()[0]);
358 let u_knots: Vec<f64> = x_knots.iter().map(|&x| beta / 2.0 * (x + 1.0)).collect();
359 let u_delta_x: Vec<f64> = x_widths.iter().map(|&dx| beta / 2.0 * dx).collect();
360 let u_symm: Vec<i32> = u_sve.get_polys().iter().map(|p| p.symm).collect();
361
362 let u = u_sve.rescale_domain(u_knots, Some(u_delta_x), Some(u_symm))?;
363
364 // Transform v: y ∈ [-1, 1] → ω ∈ [-ωmax, ωmax]
365 let (y_knots, y_widths) = unit_knots_and_widths(&v_sve.get_polys()[0]);
366 let v_knots: Vec<f64> = y_knots.iter().map(|&y| omega_max * y).collect();
367 let v_delta_x: Vec<f64> = y_widths.iter().map(|&dy| omega_max * dy).collect();
368 let v_symm: Vec<i32> = v_sve.get_polys().iter().map(|p| p.symm).collect();
369
370 let v = v_sve.rescale_domain(v_knots, Some(v_delta_x), Some(v_symm))?;
371
372 // Scale singular values to τ = β(x + 1)/2 and ω = ωmax y. A kernel with
373 // `ypower` carries that power of y = ω/ωmax, so its physical form is
374 // K(τ, ω) = ωmax^ypower K(x, y) and
375 // s_scaled = sqrt(β/2 * ωmax) * ωmax^ypower * s_sve,
376 // e.g. S_l = sqrt(β ωmax³/2) s_l for RegularizedBoseKernel (irbasis
377 // paper, Chikano et al., CPC 240, 181 (2019), arXiv:1807.05237, Eq. (25)).
378 let ypower = kernel.ypower();
379 let scale_factor = (beta / 2.0 * omega_max).sqrt() * omega_max.powi(ypower);
380 let s: Vec<f64> = s_sve.iter().map(|&x| scale_factor * x).collect();
381
382 // Construct uhat (Fourier transform of u)
383 // HACK: Fourier transforms only work on unit interval, so we scale the data
384 let uhat_base_full = sve_result.u.scale_data(beta.sqrt());
385 let conv_rad = kernel.conv_radius();
386
387 // Create statistics marker instance using Default trait
388 // S is a zero-sized type (ZST) like Fermionic or Bosonic
389 let stat_marker = S::default();
390
391 let uhat_full = PiecewiseLegendreFTVector::<S>::from_poly_vector(
392 &uhat_base_full,
393 stat_marker,
394 Some(conv_rad),
395 )?;
396
397 // Truncate uhat to basis size
398 let uhat_polyvec: Vec<_> = uhat_full.polyvec.iter().take(s.len()).cloned().collect();
399 let uhat = PiecewiseLegendreFTVector::from_vector(uhat_polyvec);
400
401 Ok(Self {
402 kernel,
403 sve_result: Arc::new(sve_result),
404 accuracy,
405 beta,
406 u: Arc::new(u),
407 v: Arc::new(v),
408 s,
409 uhat: Arc::new(uhat),
410 uhat_full: Arc::new(uhat_full),
411 _phantom: std::marker::PhantomData,
412 })
413 }
414
415 /// Get the size of the basis (number of basis functions)
416 pub fn size(&self) -> usize {
417 self.s.len()
418 }
419
420 /// Get the cutoff parameter Λ = β * ωmax
421 pub fn lambda(&self) -> f64 {
422 self.kernel.lambda()
423 }
424
425 /// Get the frequency cutoff ωmax
426 pub fn omega_max(&self) -> f64 {
427 self.lambda() / self.beta
428 }
429
430 /// Get significance of each singular value (s[i] / s[0])
431 pub fn significance(&self) -> Vec<f64> {
432 let s0 = self.s[0];
433 self.s.iter().map(|&s| s / s0).collect()
434 }
435
436 /// Get default tau sampling points
437 ///
438 /// Returns sampling points in imaginary time τ ∈ [-β/2, β/2].
439 ///
440 /// Roots are found with symmetry exploitation (matching Python 1.x / Julia v1),
441 /// then mapped to [-β/2, β/2] by folding τ_physical ∈ [0, β] around β/2.
442 ///
443 /// # Errors
444 ///
445 /// * [`Error::NotSupported`] if the default points are not defined for
446 /// this basis: its SVE has so few singular functions that the last one
447 /// has no extrema (e.g. `compute_sve` with `max_num_svals = Some(2)`)
448 pub fn default_tau_sampling_points(&self) -> Result<Vec<f64>, Error> {
449 let points = self.default_tau_sampling_points_size_requested(self.size())?;
450 let basis_size = self.size();
451 if points.len() < basis_size {
452 debug_warn!(
453 "Number of tau sampling points ({}) is less than basis size ({}). \
454 Basis parameters: beta={}, wmax={}, epsilon={:.2e}",
455 points.len(),
456 basis_size,
457 self.beta,
458 self.wmax(),
459 self.accuracy()
460 );
461 }
462 Ok(points)
463 }
464
465 /// Get default tau sampling points with a requested size
466 ///
467 /// Returns sampling points in τ ∈ [-β/2, β/2].
468 ///
469 /// # Errors
470 ///
471 /// * [`Error::NotSupported`] if the default points are not defined for
472 /// this basis: its SVE has so few singular functions that the last one
473 /// has no extrema (e.g. `compute_sve` with `max_num_svals = Some(2)`)
474 pub fn default_tau_sampling_points_size_requested(
475 &self,
476 size_requested: usize,
477 ) -> Result<Vec<f64>, Error> {
478 let x = default_sampling_points(&self.sve_result.u, "u", size_requested)?;
479 let half_beta = self.beta / 2.0;
480 // Map roots to physical tau ∈ [0, β], then fold to [-β/2, β/2]
481 let mut smpl_taus: Vec<f64> = x
482 .iter()
483 .map(|&xi| {
484 let tau = half_beta * (xi + 1.0);
485 if tau <= half_beta {
486 tau
487 } else {
488 tau - self.beta
489 }
490 })
491 .collect();
492 smpl_taus.sort_by(|a, b| a.partial_cmp(b).unwrap());
493 Ok(smpl_taus)
494 }
495
496 /// Get default Matsubara frequency sampling points
497 ///
498 /// Returns sampling points as MatsubaraFreq objects: the sign changes of the
499 /// first discarded Matsubara basis function (its extrema when that function
500 /// is not available); bosonic sets always include n = 0.
501 ///
502 /// # Arguments
503 /// * `positive_only` - If true, returns only non-negative frequencies
504 ///
505 /// # Returns
506 /// Vector of Matsubara frequency sampling points
507 ///
508 /// # Errors
509 ///
510 /// [`Error::NotSupported`] if the basis functions have no definite parity
511 /// (an SVE that is not centrosymmetric, e.g. from `compute_sve_general`;
512 /// #183)
513 pub fn default_matsubara_sampling_points(
514 &self,
515 positive_only: bool,
516 ) -> Result<Vec<crate::freq::MatsubaraFreq<S>>, Error>
517 where
518 S: 'static,
519 {
520 if !self.kernel().is_centrosymmetric() {
521 return Err(Error::NotSupported {
522 what: "default Matsubara sampling points of a basis whose kernel is not \
523 centrosymmetric: they rely on the parity of the basis functions"
524 .to_string(),
525 });
526 }
527 let fence = false;
528 let points = Self::default_matsubara_sampling_points_impl(
529 &self.uhat_full,
530 self.size(),
531 fence,
532 positive_only,
533 )?;
534 let basis_size = self.size();
535 // For positive_only=true, we need 2*n_sampling_points >= basis_size
536 // For positive_only=false, we need n_sampling_points >= basis_size
537 let effective_points = if positive_only {
538 2 * points.len()
539 } else {
540 points.len()
541 };
542 if effective_points < basis_size {
543 debug_warn!(
544 "Number of Matsubara sampling points ({}{}) is less than basis size ({}). \
545 Basis parameters: beta={}, wmax={}, epsilon={:.2e}",
546 points.len(),
547 if positive_only { " × 2" } else { "" },
548 basis_size,
549 self.beta,
550 self.wmax(),
551 self.accuracy()
552 );
553 }
554 Ok(points)
555 }
556
557 /// Fence Matsubara sampling points to improve conditioning
558 ///
559 /// This function adds additional sampling points near the outer frequencies
560 /// to improve the conditioning of the sampling matrix. This is particularly
561 /// important for Matsubara sampling where we cannot freely choose sampling points.
562 ///
563 /// Implementation matches C++ version in `basis.hpp` (lines 407-452).
564 fn fence_matsubara_sampling(
565 omega_n: &mut Vec<crate::freq::MatsubaraFreq<S>>,
566 positive_only: bool,
567 ) where
568 S: StatisticsType + 'static,
569 {
570 use crate::freq::{BosonicFreq, MatsubaraFreq};
571
572 if omega_n.is_empty() {
573 return;
574 }
575
576 // Collect outer frequencies
577 let mut outer_frequencies = Vec::new();
578 if positive_only {
579 outer_frequencies.push(omega_n[omega_n.len() - 1]);
580 } else {
581 outer_frequencies.push(omega_n[0]);
582 outer_frequencies.push(omega_n[omega_n.len() - 1]);
583 }
584
585 for wn_outer in outer_frequencies {
586 let outer_val = wn_outer.n();
587 // In SparseIR.jl-v1, ωn_diff is always created as BosonicFreq
588 // This ensures diff_val is always even (valid for Bosonic)
589 let mut diff_val = 2 * (0.025 * outer_val as f64).round() as i64;
590
591 // Handle edge case: if diff_val is 0, set it to 2 (minimum even value for Bosonic)
592 if diff_val == 0 {
593 diff_val = 2;
594 }
595
596 // Get the n value from BosonicFreq (same as diff_val since it's even)
597 let wn_diff = BosonicFreq::new(diff_val).unwrap().n();
598
599 // Sign function: returns +1 if n > 0, -1 if n < 0, 0 if n == 0
600 // Matches C++ implementation: (a.get_n() > 0) - (a.get_n() < 0)
601 let sign_val = if outer_val > 0 {
602 1
603 } else if outer_val < 0 {
604 -1
605 } else {
606 0
607 };
608
609 // Check original size before adding (C++ checks wn.size() before each push)
610 let original_size = omega_n.len();
611 if original_size >= 20 {
612 // For Fermionic: wn_outer.n is odd, wn_diff is even, so wn_outer.n ± wn_diff is odd (valid)
613 // For Bosonic: wn_outer.n is even, wn_diff is even, so wn_outer.n ± wn_diff is even (valid)
614 let new_n = outer_val - sign_val * wn_diff;
615 if let Ok(new_freq) = MatsubaraFreq::<S>::new(new_n) {
616 omega_n.push(new_freq);
617 }
618 }
619 if original_size >= 42 {
620 let new_n = outer_val + sign_val * wn_diff;
621 if let Ok(new_freq) = MatsubaraFreq::<S>::new(new_n) {
622 omega_n.push(new_freq);
623 }
624 }
625 }
626
627 // Sort and remove duplicates using BTreeSet
628 let omega_n_set: std::collections::BTreeSet<MatsubaraFreq<S>> = omega_n.drain(..).collect();
629 *omega_n = omega_n_set.into_iter().collect();
630 }
631
632 /// Default Matsubara sampling points for a basis of size `l` from the
633 /// Matsubara basis functions `uhat_full`: the sign changes of
634 /// `uhat_full[l]` (after the parity adjustment of `l`), or the extrema of
635 /// the last function when `uhat_full` has no function `l`; bosonic sets
636 /// always include n = 0. `fence` adds points near the outer frequencies.
637 ///
638 /// # Errors
639 ///
640 /// * [`Error::EmptyInput`] if `uhat_full` is empty
641 /// * [`Error::NotSupported`] if the functions have no definite parity
642 /// (symm = 0, as from an SVE that is not centrosymmetric, #183)
643 pub(crate) fn default_matsubara_sampling_points_impl(
644 uhat_full: &PiecewiseLegendreFTVector<S>,
645 l: usize,
646 fence: bool,
647 positive_only: bool,
648 ) -> Result<Vec<crate::freq::MatsubaraFreq<S>>, Error>
649 where
650 S: StatisticsType + 'static,
651 {
652 use crate::freq::MatsubaraFreq;
653 use crate::polyfourier::{find_extrema, sign_changes};
654 use std::collections::BTreeSet;
655
656 if uhat_full.is_empty() {
657 return Err(Error::EmptyInput { name: "uhat_full" });
658 }
659
660 let mut l_requested = l;
661
662 // Adjust l_requested based on statistics (same as C++)
663 if S::STATISTICS == crate::traits::Statistics::Fermionic && l_requested % 2 != 0 {
664 l_requested += 1;
665 } else if S::STATISTICS == crate::traits::Statistics::Bosonic && l_requested % 2 == 0 {
666 l_requested += 1;
667 }
668
669 // Choose sign_changes or find_extrema based on l_requested
670 let mut omega_n = if l_requested < uhat_full.len() {
671 sign_changes(&uhat_full[l_requested], positive_only)?
672 } else {
673 find_extrema(&uhat_full[uhat_full.len() - 1], positive_only)?
674 };
675
676 // For bosons, include zero frequency explicitly to prevent conditioning issues
677 if S::STATISTICS == crate::traits::Statistics::Bosonic {
678 omega_n.push(MatsubaraFreq::<S>::new(0).unwrap());
679 }
680
681 // Sort and remove duplicates using BTreeSet
682 let omega_n_set: BTreeSet<MatsubaraFreq<S>> = omega_n.into_iter().collect();
683 let mut omega_n: Vec<MatsubaraFreq<S>> = omega_n_set.into_iter().collect();
684
685 // Check expected size
686 let expected_size = if positive_only {
687 l_requested.div_ceil(2)
688 } else {
689 l_requested
690 };
691
692 if omega_n.len() != expected_size {
693 debug_warn!(
694 "Requested {} sampling frequencies for basis size L = {}, but got {}.",
695 expected_size,
696 l,
697 omega_n.len()
698 );
699 }
700
701 // Apply fencing if requested (same as C++ implementation)
702 if fence {
703 Self::fence_matsubara_sampling(&mut omega_n, positive_only);
704 }
705
706 Ok(omega_n)
707 }
708 /// Get default omega (real frequency) sampling points
709 ///
710 /// Returns sampling points on the real-frequency axis ω ∈ [-ωmax, ωmax].
711 /// These are used as pole locations for the Discrete Lehmann Representation (DLR).
712 ///
713 /// The sampling points are chosen as the roots of the L-th basis function
714 /// in the spectral domain (v), which provides near-optimal conditioning.
715 ///
716 /// # Returns
717 /// Vector of real-frequency sampling points in [-ωmax, ωmax]
718 ///
719 /// # Errors
720 ///
721 /// * [`Error::NotSupported`] if the default points are not defined for
722 /// this basis: its SVE has so few singular functions that the last one
723 /// has no extrema (e.g. `compute_sve` with `max_num_svals = Some(2)`)
724 pub fn default_omega_sampling_points(&self) -> Result<Vec<f64>, Error> {
725 let sz = self.size();
726
727 // Use UNTRUNCATED sve_result.v (same as C++)
728 // C++: default_sampling_points(*(sve_result->v), sz)
729 let y = default_sampling_points(&self.sve_result.v, "v", sz)?;
730
731 // Scale to [-ωmax, ωmax]
732 let wmax = self.kernel.lambda() / self.beta;
733 let omega_points: Vec<f64> = y.into_iter().map(|yi| wmax * yi).collect();
734
735 Ok(omega_points)
736 }
737}
738
739// ============================================================================
740// Trait implementations
741// ============================================================================
742
743impl<K, S> crate::basis_trait::Basis<S> for FiniteTempBasis<K, S>
744where
745 K: KernelProperties + CentrosymmKernel + Clone + 'static,
746 S: StatisticsType + 'static,
747{
748 fn beta(&self) -> f64 {
749 self.beta
750 }
751
752 fn wmax(&self) -> f64 {
753 self.kernel.lambda() / self.beta
754 }
755
756 fn lambda(&self) -> f64 {
757 self.kernel.lambda()
758 }
759
760 fn size(&self) -> usize {
761 self.size()
762 }
763
764 fn accuracy(&self) -> f64 {
765 self.accuracy
766 }
767
768 fn significance(&self) -> Vec<f64> {
769 if let Some(&first_s) = self.s.first() {
770 self.s.iter().map(|&s| s / first_s).collect()
771 } else {
772 vec![]
773 }
774 }
775
776 fn svals(&self) -> Vec<f64> {
777 self.s.clone()
778 }
779
780 fn default_tau_sampling_points(&self) -> Result<Vec<f64>, Error> {
781 self.default_tau_sampling_points()
782 }
783
784 fn default_matsubara_sampling_points(
785 &self,
786 positive_only: bool,
787 ) -> Result<Vec<crate::freq::MatsubaraFreq<S>>, Error> {
788 self.default_matsubara_sampling_points(positive_only)
789 }
790
791 fn evaluate_tau(&self, tau: &[f64]) -> Result<crate::Matrix<f64>, Error> {
792 use crate::matrix::Mat;
793 use crate::taufuncs::normalize_tau;
794
795 let basis_size = self.size();
796
797 // Normalize every τ ∈ [-β, β] to [0, β] with the statistics sign
798 // first; this rejects a τ outside [-β, β] and NaN. The u polynomials
799 // are on [0, β] exactly (from_sve_result sets their ends), so
800 // evaluating them cannot fail.
801 let normalized = tau
802 .iter()
803 .map(|&t| normalize_tau::<S>(t, self.beta))
804 .collect::<Result<Vec<(f64, f64)>, Error>>()?;
805 if normalized.is_empty() {
806 // An empty set of points gives an empty matrix.
807 return Ok(Mat::<f64>::from_elem([0, basis_size], 0.0).into_typed());
808 }
809
810 // Result: matrix[i, l] = u_l(tau[i])
811 Ok(Mat::<f64>::from_fn([normalized.len(), basis_size], |idx| {
812 let (tau_norm, sign) = normalized[idx[0]];
813 sign * self.u[idx[1]].evaluate(tau_norm)
814 })
815 .into_typed())
816 }
817
818 fn evaluate_matsubara(
819 &self,
820 freqs: &[crate::freq::MatsubaraFreq<S>],
821 ) -> Result<crate::Matrix<num_complex::Complex<f64>>, Error> {
822 use crate::matrix::Mat;
823 use num_complex::Complex;
824
825 let n_points = freqs.len();
826 let basis_size = self.size();
827 if n_points == 0 {
828 // See evaluate_tau.
829 return Ok(
830 Mat::<Complex<f64>>::from_elem([0, basis_size], Complex::new(0.0, 0.0))
831 .into_typed(),
832 );
833 }
834
835 // Evaluate each basis function at all Matsubara frequencies
836 // Result: matrix[i, l] = uhat_l(iν[i])
837 Ok(Mat::<Complex<f64>>::from_fn([n_points, basis_size], |idx| {
838 let i = idx[0]; // frequency index
839 let l = idx[1]; // basis function index
840 self.uhat[l].evaluate(&freqs[i])
841 })
842 .into_typed())
843 }
844
845 fn evaluate_omega(&self, omega: &[f64]) -> Result<crate::Matrix<f64>, Error> {
846 use crate::matrix::Mat;
847
848 let basis_size = self.size();
849 // The v polynomials are on [-ωmax, ωmax] exactly (from_sve_result
850 // sets their ends).
851 let domain = (self.v.xmin(), self.v.xmax());
852 if let Some(&w) = omega.iter().find(|&&w| !(w >= domain.0 && w <= domain.1)) {
853 return Err(Error::OutOfDomain {
854 name: "omega",
855 value: w,
856 domain,
857 });
858 }
859 if omega.is_empty() {
860 // See evaluate_tau.
861 return Ok(Mat::<f64>::from_elem([0, basis_size], 0.0).into_typed());
862 }
863
864 // Result: matrix[i, l] = V_l(omega[i])
865 Ok(Mat::<f64>::from_fn([omega.len(), basis_size], |idx| {
866 self.v[idx[1]].evaluate(omega[idx[0]])
867 })
868 .into_typed())
869 }
870
871 fn default_omega_sampling_points(&self) -> Result<Vec<f64>, Error> {
872 self.default_omega_sampling_points()
873 }
874}
875
876// ============================================================================
877// Type aliases
878// ============================================================================
879
880/// Type alias for fermionic basis with LogisticKernel
881pub type FermionicBasis = FiniteTempBasis<LogisticKernel, Fermionic>;
882
883/// Type alias for bosonic basis with LogisticKernel
884pub type BosonicBasis = FiniteTempBasis<LogisticKernel, Bosonic>;
885
886/// Default Matsubara sampling points for a basis of size `l`, from the
887/// Matsubara basis functions `uhat_full` alone
888///
889/// The points are the sign changes of `uhat_full[l]` (after the parity
890/// adjustment of `l`), or the extrema of the last function when `uhat_full`
891/// has no function `l`; bosonic sets always include n = 0. `fence` adds
892/// points near the outer frequencies. This is what
893/// [`FiniteTempBasis::default_matsubara_sampling_points`] computes, for a
894/// caller that holds the functions but not the basis.
895///
896/// # Errors
897///
898/// * [`Error::EmptyInput`] if `uhat_full` is empty
899/// * [`Error::NotSupported`] if the functions have no definite parity
900/// (symm = 0, as from an SVE that is not centrosymmetric, #183)
901pub fn default_matsubara_sampling_points_from_uhat<S>(
902 uhat_full: &PiecewiseLegendreFTVector<S>,
903 l: usize,
904 fence: bool,
905 positive_only: bool,
906) -> Result<Vec<crate::freq::MatsubaraFreq<S>>, Error>
907where
908 S: StatisticsType + 'static,
909{
910 FiniteTempBasis::<LogisticKernel, S>::default_matsubara_sampling_points_impl(
911 uhat_full,
912 l,
913 fence,
914 positive_only,
915 )
916}
917
918#[cfg(test)]
919#[path = "basis_tests.rs"]
920mod basis_tests;