Skip to main content

sparse_ir_core/
matsubara_sampling.rs

1//! Sparse sampling in Matsubara frequencies
2//!
3//! This module provides Matsubara frequency sampling for transforming between
4//! IR basis coefficients and values at sparse Matsubara frequencies.
5
6use crate::Matrix;
7use crate::error::{Error, Result};
8use crate::fitters::{ComplexMatrixFitter, ComplexToRealFitter, InplaceFitter};
9use crate::freq::MatsubaraFreq;
10use crate::gemm::GemmBackendHandle;
11use crate::sampling::{check_finite_matrix, check_sampling_matrix_shape, mat_from_matrix};
12use crate::traits::StatisticsType;
13use num_complex::Complex;
14use std::marker::PhantomData;
15use tenferro_tensor::{TypedTensor, TypedTensorView, TypedTensorViewMut};
16
17type C64 = Complex<f64>;
18
19mod sealed {
20    pub trait Sealed {}
21    impl Sealed for f64 {}
22    impl Sealed for num_complex::Complex<f64> {}
23}
24
25/// Coefficient types that Matsubara sampling can evaluate (`f64` or
26/// `Complex<f64>`).
27///
28/// This provides compile-time dispatch between the real-coefficient and
29/// complex-coefficient kernels.
30pub trait MatsubaraCoeffs: tenferro_tensor::TensorScalar + Copy + sealed::Sealed {
31    /// Evaluate coefficients using the given sampler
32    ///
33    /// # Errors
34    ///
35    /// * [`Error::AxisOutOfRange`] if `dim` is not an axis of `coeffs`
36    /// * [`Error::ShapeMismatch`] of the input if `coeffs` does not have
37    ///   `basis_size` along `dim`
38    fn evaluate_nd_with<S: StatisticsType>(
39        sampler: &MatsubaraSampling<S>,
40        backend: Option<&GemmBackendHandle>,
41        coeffs: &TypedTensor<Self>,
42        dim: usize,
43    ) -> Result<TypedTensor<C64>>;
44
45    /// Evaluate along axis `dim` into an output view.
46    fn evaluate_nd_to_with<S: StatisticsType>(
47        sampler: &MatsubaraSampling<S>,
48        backend: Option<&GemmBackendHandle>,
49        coeffs: &TypedTensorView<'_, Self>,
50        dim: usize,
51        out: &mut TypedTensorViewMut<'_, C64>,
52    ) -> Result<()>;
53}
54
55impl MatsubaraCoeffs for f64 {
56    fn evaluate_nd_with<S: StatisticsType>(
57        sampler: &MatsubaraSampling<S>,
58        backend: Option<&GemmBackendHandle>,
59        coeffs: &TypedTensor<Self>,
60        dim: usize,
61    ) -> Result<TypedTensor<C64>> {
62        sampler.fitter.evaluate_nd_dz(backend, coeffs, dim)
63    }
64
65    fn evaluate_nd_to_with<S: StatisticsType>(
66        sampler: &MatsubaraSampling<S>,
67        backend: Option<&GemmBackendHandle>,
68        coeffs: &TypedTensorView<'_, Self>,
69        dim: usize,
70        out: &mut TypedTensorViewMut<'_, C64>,
71    ) -> Result<()> {
72        sampler.fitter.evaluate_nd_dz_to(backend, coeffs, dim, out)
73    }
74}
75
76impl MatsubaraCoeffs for C64 {
77    fn evaluate_nd_with<S: StatisticsType>(
78        sampler: &MatsubaraSampling<S>,
79        backend: Option<&GemmBackendHandle>,
80        coeffs: &TypedTensor<Self>,
81        dim: usize,
82    ) -> Result<TypedTensor<C64>> {
83        sampler.fitter.evaluate_nd_zz(backend, coeffs, dim)
84    }
85
86    fn evaluate_nd_to_with<S: StatisticsType>(
87        sampler: &MatsubaraSampling<S>,
88        backend: Option<&GemmBackendHandle>,
89        coeffs: &TypedTensorView<'_, Self>,
90        dim: usize,
91        out: &mut TypedTensorViewMut<'_, C64>,
92    ) -> Result<()> {
93        sampler.fitter.evaluate_nd_zz_to(backend, coeffs, dim, out)
94    }
95}
96
97/// `Ok` if no point is negative, as positive-only samplings require (#247)
98///
99/// # Errors
100///
101/// [`Error::InvalidMatsubaraIndex`] for the first negative point
102fn check_non_negative<S: StatisticsType>(points: &[MatsubaraFreq<S>]) -> Result<(), Error> {
103    match points.iter().find(|f| f.n() < 0) {
104        Some(freq) => Err(Error::InvalidMatsubaraIndex {
105            n: freq.n(),
106            statistics: S::STATISTICS,
107        }),
108        None => Ok(()),
109    }
110}
111
112/// Some part of `z` that is not finite: the real part if it is not, else
113/// the imaginary part
114fn non_finite_part(z: Complex<f64>) -> Option<f64> {
115    if !z.re.is_finite() {
116        Some(z.re)
117    } else if !z.im.is_finite() {
118        Some(z.im)
119    } else {
120        None
121    }
122}
123
124/// Matsubara sampling for full frequency range (positive and negative)
125///
126/// General complex problem without symmetry assumptions.
127/// Supports both real and complex coefficients.
128pub struct MatsubaraSampling<S: StatisticsType> {
129    sampling_points: Vec<MatsubaraFreq<S>>,
130    fitter: ComplexMatrixFitter,
131    _phantom: PhantomData<S>,
132}
133
134impl<S: StatisticsType> MatsubaraSampling<S> {
135    /// Create Matsubara sampling with default sampling points
136    ///
137    /// Uses the default sampling points of the basis (symmetric: positive and
138    /// negative frequencies).
139    ///
140    /// # Errors
141    ///
142    /// The errors of [`Basis::default_matsubara_sampling_points`](crate::basis_trait::Basis::default_matsubara_sampling_points)
143    /// (NotSupported for a DLR or for basis functions without a definite
144    /// parity, #183)
145    pub fn new(basis: &impl crate::basis_trait::Basis<S>) -> Result<Self>
146    where
147        S: 'static,
148    {
149        let sampling_points = basis.default_matsubara_sampling_points(false)?;
150        Self::with_sampling_points(basis, sampling_points)
151    }
152
153    /// Create Matsubara sampling with custom sampling points
154    ///
155    /// The points may be in any order, and are kept in the given order:
156    /// [`Self::sampling_points`] returns them unchanged, and index i along the
157    /// sampling-point axis of `evaluate` and `fit` refers to
158    /// `sampling_points[i]`.
159    ///
160    /// Duplicate points are accepted; they only raise the condition number.
161    ///
162    /// # Errors
163    ///
164    /// * [`Error::EmptyInput`] if `sampling_points` is empty
165    /// * The errors of [`Basis::evaluate_matsubara`](crate::basis_trait::Basis::evaluate_matsubara)
166    pub fn with_sampling_points(
167        basis: &impl crate::basis_trait::Basis<S>,
168        sampling_points: Vec<MatsubaraFreq<S>>,
169    ) -> Result<Self>
170    where
171        S: 'static,
172    {
173        if sampling_points.is_empty() {
174            return Err(Error::EmptyInput {
175                name: "sampling_points",
176            });
177        }
178        let matrix = mat_from_matrix(&basis.evaluate_matsubara(&sampling_points)?)?;
179        Ok(Self {
180            sampling_points,
181            fitter: ComplexMatrixFitter::new(matrix),
182            _phantom: PhantomData,
183        })
184    }
185
186    /// Create Matsubara sampling with custom sampling points and pre-computed matrix
187    ///
188    /// This constructor is useful when the sampling matrix is already computed
189    /// (e.g., from external sources or for testing).
190    ///
191    /// # Arguments
192    /// * `sampling_points` - Matsubara frequency sampling points, in any order
193    /// * `matrix` - Pre-computed sampling matrix (n_points × basis_size); row i
194    ///   belongs to `sampling_points[i]`
195    ///
196    /// The points are kept in the given order: [`Self::sampling_points`]
197    /// returns them unchanged, and index i along the sampling-point axis of
198    /// `evaluate` and `fit` refers to `sampling_points[i]`.
199    ///
200    /// Duplicate points are accepted; they only raise the condition number.
201    ///
202    /// # Errors
203    ///
204    /// * [`Error::EmptyInput`] if `sampling_points` is empty, or `matrix`
205    ///   has no columns
206    /// * [`Error::ShapeMismatch`] of the input if `matrix` does not have one
207    ///   row per point
208    /// * [`Error::NonFiniteInput`] for the first entry of `matrix` with a NaN
209    ///   or infinite part
210    pub fn from_matrix(
211        sampling_points: Vec<MatsubaraFreq<S>>,
212        matrix: &Matrix<C64>,
213    ) -> Result<Self> {
214        let matrix = mat_from_matrix(matrix)?;
215        check_sampling_matrix_shape(sampling_points.len(), *matrix.shape())?;
216        check_finite_matrix(&matrix, non_finite_part)?;
217        Ok(Self {
218            sampling_points,
219            fitter: ComplexMatrixFitter::new(matrix),
220            _phantom: PhantomData,
221        })
222    }
223
224    /// Get sampling points
225    pub fn sampling_points(&self) -> &[MatsubaraFreq<S>] {
226        &self.sampling_points
227    }
228
229    /// Number of sampling points
230    pub fn n_sampling_points(&self) -> usize {
231        self.sampling_points.len()
232    }
233
234    /// Basis size
235    pub fn basis_size(&self) -> usize {
236        self.fitter.basis_size()
237    }
238
239    /// Get the sampling matrix
240    pub fn matrix(&self) -> &Matrix<C64> {
241        self.fitter.matrix()
242    }
243
244    /// Condition number of the sampling matrix, which fitting solves with
245    ///
246    /// Returns `σ_max / σ_min`, the ratio of the largest to the smallest of the
247    /// `min(n_sampling_points, basis_size)` singular values of the complex
248    /// `n_sampling_points × basis_size` matrix [`Self::matrix`]. It bounds how
249    /// much [`Self::fit`] can amplify relative errors in the values.
250    ///
251    /// Returns `f64::INFINITY` if the smallest singular value is below `1e-15`
252    /// (numerically singular matrix). The singular value decomposition is the
253    /// one fitting uses: it is computed by the first call to this method or to
254    /// a fit, then cached.
255    ///
256    /// # Errors
257    ///
258    /// [`Error::DecompositionFailed`] if the singular value decomposition
259    /// fails, which a matrix of finite entries does not cause in practice
260    /// (the constructors reject non-finite entries)
261    pub fn condition_number(&self) -> Result<f64> {
262        self.fitter.condition_number()
263    }
264
265    /// Evaluate complex basis coefficients at sampling points
266    ///
267    /// # Arguments
268    /// * `coeffs` - Complex basis coefficients (length = basis_size)
269    ///
270    /// # Returns
271    /// Complex values at Matsubara frequencies (length = n_sampling_points)
272    ///
273    /// # Errors
274    ///
275    /// * [`Error::ShapeMismatch`] of the input if `coeffs` does not have length
276    ///   `basis_size`
277    pub fn evaluate(&self, coeffs: &[C64]) -> Result<Vec<C64>> {
278        self.fitter.evaluate(None, coeffs)
279    }
280
281    /// Evaluate real basis coefficients at sampling points
282    pub fn evaluate_real(&self, coeffs: &[f64]) -> Result<Vec<C64>> {
283        self.fitter.evaluate_real(None, coeffs)
284    }
285
286    /// Fit complex basis coefficients from values at sampling points
287    ///
288    /// # Arguments
289    /// * `values` - Complex values at Matsubara frequencies (length = n_sampling_points)
290    ///
291    /// # Returns
292    /// Fitted complex basis coefficients (length = basis_size)
293    ///
294    /// # Errors
295    ///
296    /// * [`Error::ShapeMismatch`] of the input if `values` does not have length
297    ///   `n_sampling_points`
298    /// * [`Error::DecompositionFailed`] if the singular value decomposition
299    ///   fails
300    pub fn fit(&self, values: &[C64]) -> Result<Vec<C64>> {
301        self.fitter.fit(None, values)
302    }
303
304    /// Fit real basis coefficients (real part of the complex solution)
305    pub fn fit_real(&self, values: &[C64]) -> Result<Vec<f64>> {
306        self.fitter.fit_real(None, values)
307    }
308
309    /// Evaluate N-dimensional coefficients at Matsubara sampling points
310    ///
311    /// Supports both real (`f64`) and complex (`Complex<f64>`) coefficients and
312    /// always returns complex values at the Matsubara frequencies. The
313    /// implementation is selected at compile time through the `MatsubaraCoeffs`
314    /// trait.
315    ///
316    /// # Type Parameter
317    /// * `T` - Must implement `MatsubaraCoeffs` (currently `f64` or `Complex<f64>`)
318    ///
319    /// # Arguments
320    /// * `backend` - Optional GEMM backend handle (`None` uses the global dispatcher)
321    /// * `coeffs` - N-dimensional tensor of basis coefficients
322    /// * `dim` - Dimension along which to evaluate (must have size = basis_size)
323    ///
324    /// # Returns
325    /// N-dimensional tensor of complex values at Matsubara frequencies, with
326    /// dimension `dim` of size n_sampling_points
327    ///
328    /// # Errors
329    ///
330    /// * [`Error::AxisOutOfRange`] if `dim` is not an axis of `coeffs`
331    /// * [`Error::ShapeMismatch`] of the input if `coeffs` does not have
332    ///   `basis_size` along `dim`
333    ///
334    /// # Example
335    /// ```
336    /// use num_complex::Complex;
337    /// use sparse_ir::{FermionicBasis, LogisticKernel, MatsubaraSampling, TypedTensor};
338    ///
339    /// let beta = 10.0;
340    /// let wmax = 1.0;
341    /// let basis = FermionicBasis::new(LogisticKernel::new(beta * wmax).unwrap(), beta, Some(1e-6), None).unwrap();
342    /// let sampling = MatsubaraSampling::new(&basis).unwrap();
343    /// let (size, n_points) = (sampling.basis_size(), sampling.n_sampling_points());
344    ///
345    /// // Real coefficients: two sets stacked along axis 1 (column-major), evaluated along axis 0
346    /// let real_data: Vec<f64> = (0..2 * size)
347    ///     .map(|k| 1.0 / (1.0 + (k % size + k / size) as f64))
348    ///     .collect();
349    /// let coeffs_real = TypedTensor::from_vec_col_major(vec![size, 2], real_data.clone()).unwrap();
350    /// let values = sampling.evaluate_nd::<f64>(None, &coeffs_real, 0).unwrap();
351    /// assert_eq!(values.shape(), &[n_points, 2]);
352    ///
353    /// // Complex coefficients
354    /// let complex_data: Vec<Complex<f64>> =
355    ///     real_data.iter().map(|&x| Complex::new(x, -0.5 * x)).collect();
356    /// let coeffs_complex = TypedTensor::from_vec_col_major(vec![size, 2], complex_data.clone()).unwrap();
357    /// let values_z = sampling.evaluate_nd::<Complex<f64>>(None, &coeffs_complex, 0).unwrap();
358    ///
359    /// // Each column matches the 1-D `evaluate` of the corresponding coefficient set
360    /// let (values, values_z) = (values.host_data().unwrap(), values_z.host_data().unwrap());
361    /// for j in 0..2 {
362    ///     let real: Vec<Complex<f64>> = real_data[j * size..(j + 1) * size].iter().map(|&x| x.into()).collect();
363    ///     let complex = &complex_data[j * size..(j + 1) * size];
364    ///     let (expected, expected_z) = (sampling.evaluate(&real).unwrap(), sampling.evaluate(complex).unwrap());
365    ///     for i in 0..n_points {
366    ///         assert!((values[i + n_points * j] - expected[i]).norm() < 1e-12);
367    ///         assert!((values_z[i + n_points * j] - expected_z[i]).norm() < 1e-12);
368    ///     }
369    /// }
370    /// ```
371    pub fn evaluate_nd<T: MatsubaraCoeffs>(
372        &self,
373        backend: Option<&GemmBackendHandle>,
374        coeffs: &TypedTensor<T>,
375        dim: usize,
376    ) -> Result<TypedTensor<C64>> {
377        T::evaluate_nd_with(self, backend, coeffs, dim)
378    }
379
380    /// Evaluate real basis coefficients at Matsubara sampling points (N-dimensional)
381    ///
382    /// This method takes real coefficients and produces complex values, useful when
383    /// working with symmetry-exploiting representations or real-valued IR coefficients.
384    ///
385    /// # Arguments
386    /// * `backend` - Optional GEMM backend handle (None uses default)
387    /// * `coeffs` - N-dimensional tensor of real basis coefficients
388    /// * `dim` - Dimension along which to evaluate (must have size = basis_size)
389    ///
390    /// # Returns
391    /// N-dimensional tensor of complex values at Matsubara frequencies
392    ///
393    /// # Errors
394    ///
395    /// * [`Error::AxisOutOfRange`] if `dim` is not an axis of `coeffs`
396    /// * [`Error::ShapeMismatch`] of the input if `coeffs` does not have
397    ///   `basis_size` along `dim`
398    pub fn evaluate_nd_real(
399        &self,
400        backend: Option<&GemmBackendHandle>,
401        coeffs: &TypedTensor<f64>,
402        dim: usize,
403    ) -> Result<TypedTensor<C64>> {
404        self.fitter.evaluate_nd_dz(backend, coeffs, dim)
405    }
406
407    /// Fit N-dimensional array of complex values to complex basis coefficients
408    ///
409    /// # Arguments
410    /// * `backend` - Optional GEMM backend handle (None uses default)
411    /// * `values` - N-dimensional tensor of complex values at Matsubara frequencies
412    /// * `dim` - Dimension along which to fit (must have size = n_sampling_points)
413    ///
414    /// # Returns
415    /// N-dimensional tensor of complex basis coefficients
416    ///
417    /// # Errors
418    ///
419    /// * [`Error::AxisOutOfRange`] if `dim` is not an axis of `values`
420    /// * [`Error::ShapeMismatch`] of the input if `values` does not have
421    ///   `n_sampling_points` along `dim`
422    /// * [`Error::DecompositionFailed`] if the singular value decomposition
423    ///   fails
424    pub fn fit_nd(
425        &self,
426        backend: Option<&GemmBackendHandle>,
427        values: &TypedTensor<C64>,
428        dim: usize,
429    ) -> Result<TypedTensor<C64>> {
430        self.fitter.fit_nd_zz(backend, values, dim)
431    }
432
433    /// Fit N-dimensional array of complex values to real basis coefficients
434    ///
435    /// This method fits complex Matsubara values to real IR coefficients.
436    /// Takes the real part of the least-squares solution.
437    ///
438    /// # Arguments
439    /// * `backend` - Optional GEMM backend handle (None uses default)
440    /// * `values` - N-dimensional tensor of complex values at Matsubara frequencies
441    /// * `dim` - Dimension along which to fit (must have size = n_sampling_points)
442    ///
443    /// # Returns
444    /// N-dimensional tensor of real basis coefficients
445    ///
446    /// # Errors
447    ///
448    /// * [`Error::AxisOutOfRange`] if `dim` is not an axis of `values`
449    /// * [`Error::ShapeMismatch`] of the input if `values` does not have
450    ///   `n_sampling_points` along `dim`
451    /// * [`Error::DecompositionFailed`] if the singular value decomposition
452    ///   fails
453    pub fn fit_nd_real(
454        &self,
455        backend: Option<&GemmBackendHandle>,
456        values: &TypedTensor<C64>,
457        dim: usize,
458    ) -> Result<TypedTensor<f64>> {
459        self.fitter.fit_nd_zd(backend, values, dim)
460    }
461
462    /// Evaluate basis coefficients at Matsubara sampling points (N-dimensional) with in-place output
463    ///
464    /// # Type Parameters
465    /// * `T` - Coefficient type (f64 or Complex<f64>)
466    ///
467    /// # Arguments
468    /// * `coeffs` - N-dimensional tensor with `coeffs.shape().dim(dim) == basis_size`
469    /// * `dim` - Dimension along which to evaluate (0-indexed)
470    /// * `out` - Output tensor with `out.shape().dim(dim) == n_sampling_points` (Complex<f64>)
471    ///
472    /// # Errors
473    ///
474    /// * [`Error::AxisOutOfRange`] if `dim` is not an axis of `coeffs`
475    /// * [`Error::ShapeMismatch`] of the input if `coeffs` does not have
476    ///   `basis_size` along `dim`, and of the output if `out` does not have
477    ///   the shape of `coeffs` with `n_sampling_points` along `dim`
478    ///
479    /// Nothing is written to `out` then.
480    pub fn evaluate_nd_to<T: MatsubaraCoeffs>(
481        &self,
482        backend: Option<&GemmBackendHandle>,
483        coeffs: &TypedTensorView<'_, T>,
484        dim: usize,
485        out: &mut TypedTensorViewMut<'_, C64>,
486    ) -> Result<()> {
487        T::evaluate_nd_to_with(self, backend, coeffs, dim, out)
488    }
489
490    /// Fit N-dimensional complex values to complex coefficients with in-place output
491    ///
492    /// # Arguments
493    /// * `values` - N-dimensional tensor with `values.shape().dim(dim) == n_sampling_points`
494    /// * `dim` - Dimension along which to fit (0-indexed)
495    /// * `out` - Output tensor with `out.shape().dim(dim) == basis_size` (Complex<f64>)
496    ///
497    /// # Errors
498    ///
499    /// * [`Error::AxisOutOfRange`] if `dim` is not an axis of `values`
500    /// * [`Error::ShapeMismatch`] of the input if `values` does not have
501    ///   `n_sampling_points` along `dim`, and of the output if `out` does not have
502    ///   the shape of `values` with `basis_size` along `dim`
503    /// * [`Error::DecompositionFailed`] if the singular value decomposition
504    ///   fails
505    ///
506    /// Nothing is written to `out` then.
507    pub fn fit_nd_to(
508        &self,
509        backend: Option<&GemmBackendHandle>,
510        values: &TypedTensorView<'_, C64>,
511        dim: usize,
512        out: &mut TypedTensorViewMut<'_, C64>,
513    ) -> Result<()> {
514        self.fitter.fit_nd_zz_to(backend, values, dim, out)
515    }
516}
517
518impl<S: StatisticsType> InplaceFitter for MatsubaraSampling<S> {
519    fn n_points(&self) -> usize {
520        self.n_sampling_points()
521    }
522
523    fn basis_size(&self) -> usize {
524        self.basis_size()
525    }
526
527    fn evaluate_nd_dz_to(
528        &self,
529        backend: Option<&GemmBackendHandle>,
530        coeffs: &TypedTensorView<'_, f64>,
531        dim: usize,
532        out: &mut TypedTensorViewMut<'_, C64>,
533    ) -> Result<()> {
534        self.fitter.evaluate_nd_dz_to(backend, coeffs, dim, out)
535    }
536
537    fn evaluate_nd_zz_to(
538        &self,
539        backend: Option<&GemmBackendHandle>,
540        coeffs: &TypedTensorView<'_, C64>,
541        dim: usize,
542        out: &mut TypedTensorViewMut<'_, C64>,
543    ) -> Result<()> {
544        self.fitter.evaluate_nd_zz_to(backend, coeffs, dim, out)
545    }
546
547    fn fit_nd_zd_to(
548        &self,
549        backend: Option<&GemmBackendHandle>,
550        values: &TypedTensorView<'_, C64>,
551        dim: usize,
552        out: &mut TypedTensorViewMut<'_, f64>,
553    ) -> Result<()> {
554        self.fitter.fit_nd_zd_to(backend, values, dim, out)
555    }
556
557    fn fit_nd_zz_to(
558        &self,
559        backend: Option<&GemmBackendHandle>,
560        values: &TypedTensorView<'_, C64>,
561        dim: usize,
562        out: &mut TypedTensorViewMut<'_, C64>,
563    ) -> Result<()> {
564        self.fitter.fit_nd_zz_to(backend, values, dim, out)
565    }
566}
567
568/// Matsubara sampling for positive frequencies only
569///
570/// Exploits the symmetry `G(-iωn) = conj(G(iωn))` of physical Green's
571/// functions to fit real coefficients from values at non-negative
572/// frequencies. Supports: {0, 1, 2, 3, ...} (no negative frequencies)
573pub struct MatsubaraSamplingPositiveOnly<S: StatisticsType> {
574    sampling_points: Vec<MatsubaraFreq<S>>,
575    fitter: ComplexToRealFitter,
576    _phantom: PhantomData<S>,
577}
578
579impl<S: StatisticsType> MatsubaraSamplingPositiveOnly<S> {
580    /// Create Matsubara sampling with default positive-only sampling points
581    ///
582    /// Uses the default sampling points of the basis (non-negative frequencies only).
583    /// Exploits symmetry to reconstruct real coefficients.
584    ///
585    /// # Errors
586    ///
587    /// The errors of [`Basis::default_matsubara_sampling_points`](crate::basis_trait::Basis::default_matsubara_sampling_points)
588    /// (NotSupported for a DLR or for basis functions without a definite
589    /// parity, #183)
590    pub fn new(basis: &impl crate::basis_trait::Basis<S>) -> Result<Self>
591    where
592        S: 'static,
593    {
594        let sampling_points = basis.default_matsubara_sampling_points(true)?;
595        Self::with_sampling_points(basis, sampling_points)
596    }
597
598    /// Create Matsubara sampling with custom positive-only sampling points
599    ///
600    /// The points may be in any order, and are kept in the given order:
601    /// [`Self::sampling_points`] returns them unchanged, and index i along the
602    /// sampling-point axis of `evaluate` and `fit` refers to
603    /// `sampling_points[i]`.
604    ///
605    /// Duplicate points are accepted; they only raise the condition number.
606    ///
607    /// # Errors
608    ///
609    /// * [`Error::EmptyInput`] if `sampling_points` is empty
610    /// * [`Error::InvalidMatsubaraIndex`] if a point is negative
611    /// * The errors of [`Basis::evaluate_matsubara`](crate::basis_trait::Basis::evaluate_matsubara)
612    pub fn with_sampling_points(
613        basis: &impl crate::basis_trait::Basis<S>,
614        sampling_points: Vec<MatsubaraFreq<S>>,
615    ) -> Result<Self>
616    where
617        S: 'static,
618    {
619        if sampling_points.is_empty() {
620            return Err(Error::EmptyInput {
621                name: "sampling_points",
622            });
623        }
624        // Positive-only sampling uses non-negative frequencies only (#247).
625        check_non_negative(&sampling_points)?;
626        let matrix = mat_from_matrix(&basis.evaluate_matsubara(&sampling_points)?)?;
627        Ok(Self {
628            sampling_points,
629            fitter: ComplexToRealFitter::new(matrix),
630            _phantom: PhantomData,
631        })
632    }
633
634    /// Create Matsubara sampling (positive-only) with custom sampling points and pre-computed matrix
635    ///
636    /// This constructor is useful when the sampling matrix is already computed.
637    /// Uses symmetry to fit real coefficients from complex values at non-negative frequencies.
638    ///
639    /// # Arguments
640    /// * `sampling_points` - Matsubara frequency sampling points (must be
641    ///   non-negative), in any order
642    /// * `matrix` - Pre-computed sampling matrix (n_points × basis_size); row i
643    ///   belongs to `sampling_points[i]`
644    ///
645    /// The points are kept in the given order: [`Self::sampling_points`]
646    /// returns them unchanged, and index i along the sampling-point axis of
647    /// `evaluate` and `fit` refers to `sampling_points[i]`.
648    ///
649    /// Duplicate points are accepted; they only raise the condition number.
650    ///
651    /// # Errors
652    ///
653    /// * [`Error::EmptyInput`] if `sampling_points` is empty, or `matrix`
654    ///   has no columns
655    /// * [`Error::ShapeMismatch`] of the input if `matrix` does not have one
656    ///   row per point
657    /// * [`Error::InvalidMatsubaraIndex`] for the first negative point
658    /// * [`Error::NonFiniteInput`] for the first entry of `matrix` with a NaN
659    ///   or infinite part
660    pub fn from_matrix(
661        sampling_points: Vec<MatsubaraFreq<S>>,
662        matrix: &Matrix<C64>,
663    ) -> Result<Self> {
664        let matrix = mat_from_matrix(matrix)?;
665        check_sampling_matrix_shape(sampling_points.len(), *matrix.shape())?;
666        check_non_negative(&sampling_points)?;
667        check_finite_matrix(&matrix, non_finite_part)?;
668        Ok(Self {
669            sampling_points,
670            fitter: ComplexToRealFitter::new(matrix),
671            _phantom: PhantomData,
672        })
673    }
674
675    /// Get sampling points
676    pub fn sampling_points(&self) -> &[MatsubaraFreq<S>] {
677        &self.sampling_points
678    }
679
680    /// Number of sampling points
681    pub fn n_sampling_points(&self) -> usize {
682        self.sampling_points.len()
683    }
684
685    /// Basis size
686    pub fn basis_size(&self) -> usize {
687        self.fitter.basis_size()
688    }
689
690    /// Get the original complex sampling matrix
691    pub fn matrix(&self) -> &Matrix<C64> {
692        self.fitter.matrix()
693    }
694
695    /// Condition number of the real least-squares problem that fitting solves
696    ///
697    /// Fitting real coefficients `x` to complex values `g` at non-negative
698    /// frequencies solves `[Re A; Im A] x = [Re g; Im g]`, where `A` is the
699    /// complex `n_sampling_points × basis_size` matrix [`Self::matrix`]. This
700    /// returns `σ_max / σ_min`, the ratio of the largest to the smallest of the
701    /// `min(2 n_sampling_points, basis_size)` singular values of that real
702    /// `2 n_sampling_points × basis_size` matrix; it bounds how much
703    /// [`Self::fit`] can amplify relative errors in the values. It is not the
704    /// condition number of `A`: with `n_sampling_points ≈ basis_size / 2`, `A`
705    /// is wide, and its condition number can understate that amplification by
706    /// orders of magnitude.
707    ///
708    /// Returns `f64::INFINITY` if the smallest singular value is below `1e-15`
709    /// (numerically singular matrix). The singular value decomposition is the
710    /// one fitting uses: it is computed by the first call to this method or to
711    /// a fit, then cached.
712    ///
713    /// # Errors
714    ///
715    /// [`Error::DecompositionFailed`] if the singular value decomposition
716    /// fails, which a matrix of finite entries does not cause in practice
717    /// (the constructors reject non-finite entries)
718    pub fn condition_number(&self) -> Result<f64> {
719        self.fitter.condition_number()
720    }
721
722    /// Evaluate basis coefficients at sampling points
723    ///
724    /// # Errors
725    ///
726    /// * [`Error::ShapeMismatch`] of the input if `coeffs` does not have length
727    ///   `basis_size`
728    pub fn evaluate(&self, coeffs: &[f64]) -> Result<Vec<C64>> {
729        self.fitter.evaluate(None, coeffs)
730    }
731
732    /// Fit basis coefficients from values at sampling points
733    ///
734    /// # Errors
735    ///
736    /// * [`Error::ShapeMismatch`] of the input if `values` does not have length
737    ///   `n_sampling_points`
738    /// * [`Error::DecompositionFailed`] if the singular value decomposition
739    ///   fails
740    pub fn fit(&self, values: &[C64]) -> Result<Vec<f64>> {
741        self.fitter.fit(None, values)
742    }
743
744    /// Evaluate N-dimensional array of real basis coefficients at sampling points
745    ///
746    /// # Arguments
747    /// * `coeffs` - N-dimensional tensor of real basis coefficients
748    /// * `dim` - Dimension along which to evaluate (must have size = basis_size)
749    ///
750    /// # Returns
751    /// N-dimensional tensor of complex values at Matsubara frequencies
752    ///
753    /// # Errors
754    ///
755    /// * [`Error::AxisOutOfRange`] if `dim` is not an axis of `coeffs`
756    /// * [`Error::ShapeMismatch`] of the input if `coeffs` does not have
757    ///   `basis_size` along `dim`
758    pub fn evaluate_nd(
759        &self,
760        backend: Option<&GemmBackendHandle>,
761        coeffs: &TypedTensor<f64>,
762        dim: usize,
763    ) -> Result<TypedTensor<C64>> {
764        self.fitter.evaluate_nd_dz(backend, coeffs, dim)
765    }
766
767    /// Fit N-dimensional array of complex values to real basis coefficients
768    ///
769    /// # Arguments
770    /// * `backend` - Optional GEMM backend handle (None uses default)
771    /// * `values` - N-dimensional tensor of complex values at Matsubara frequencies
772    /// * `dim` - Dimension along which to fit (must have size = n_sampling_points)
773    ///
774    /// # Returns
775    /// N-dimensional tensor of real basis coefficients
776    ///
777    /// # Errors
778    ///
779    /// * [`Error::AxisOutOfRange`] if `dim` is not an axis of `values`
780    /// * [`Error::ShapeMismatch`] of the input if `values` does not have
781    ///   `n_sampling_points` along `dim`
782    /// * [`Error::DecompositionFailed`] if the singular value decomposition
783    ///   fails
784    pub fn fit_nd(
785        &self,
786        backend: Option<&GemmBackendHandle>,
787        values: &TypedTensor<C64>,
788        dim: usize,
789    ) -> Result<TypedTensor<f64>> {
790        self.fitter.fit_nd_zd(backend, values, dim)
791    }
792
793    /// Evaluate real basis coefficients at Matsubara sampling points (N-dimensional) with in-place output
794    ///
795    /// # Arguments
796    /// * `coeffs` - N-dimensional tensor of real coefficients with `coeffs.shape().dim(dim) == basis_size`
797    /// * `dim` - Dimension along which to evaluate (0-indexed)
798    /// * `out` - Output tensor with `out.shape().dim(dim) == n_sampling_points` (Complex<f64>)
799    ///
800    /// # Errors
801    ///
802    /// * [`Error::AxisOutOfRange`] if `dim` is not an axis of `coeffs`
803    /// * [`Error::ShapeMismatch`] of the input if `coeffs` does not have
804    ///   `basis_size` along `dim`, and of the output if `out` does not have
805    ///   the shape of `coeffs` with `n_sampling_points` along `dim`
806    ///
807    /// Nothing is written to `out` then.
808    pub fn evaluate_nd_to(
809        &self,
810        backend: Option<&GemmBackendHandle>,
811        coeffs: &TypedTensorView<'_, f64>,
812        dim: usize,
813        out: &mut TypedTensorViewMut<'_, C64>,
814    ) -> Result<()> {
815        self.fitter.evaluate_nd_dz_to(backend, coeffs, dim, out)
816    }
817
818    /// Fit N-dimensional complex values to real coefficients with in-place output
819    ///
820    /// # Arguments
821    /// * `values` - N-dimensional tensor with `values.shape().dim(dim) == n_sampling_points`
822    /// * `dim` - Dimension along which to fit (0-indexed)
823    /// * `out` - Output tensor with `out.shape().dim(dim) == basis_size` (f64)
824    ///
825    /// # Errors
826    ///
827    /// * [`Error::AxisOutOfRange`] if `dim` is not an axis of `values`
828    /// * [`Error::ShapeMismatch`] of the input if `values` does not have
829    ///   `n_sampling_points` along `dim`, and of the output if `out` does not have
830    ///   the shape of `values` with `basis_size` along `dim`
831    /// * [`Error::DecompositionFailed`] if the singular value decomposition
832    ///   fails
833    ///
834    /// Nothing is written to `out` then.
835    pub fn fit_nd_to(
836        &self,
837        backend: Option<&GemmBackendHandle>,
838        values: &TypedTensorView<'_, C64>,
839        dim: usize,
840        out: &mut TypedTensorViewMut<'_, f64>,
841    ) -> Result<()> {
842        self.fitter.fit_nd_zd_to(backend, values, dim, out)
843    }
844}
845
846impl<S: StatisticsType> InplaceFitter for MatsubaraSamplingPositiveOnly<S> {
847    fn n_points(&self) -> usize {
848        self.n_sampling_points()
849    }
850
851    fn basis_size(&self) -> usize {
852        self.basis_size()
853    }
854
855    fn evaluate_nd_dz_to(
856        &self,
857        backend: Option<&GemmBackendHandle>,
858        coeffs: &TypedTensorView<'_, f64>,
859        dim: usize,
860        out: &mut TypedTensorViewMut<'_, C64>,
861    ) -> Result<()> {
862        self.fitter.evaluate_nd_dz_to(backend, coeffs, dim, out)
863    }
864
865    fn evaluate_nd_zz_to(
866        &self,
867        backend: Option<&GemmBackendHandle>,
868        coeffs: &TypedTensorView<'_, C64>,
869        dim: usize,
870        out: &mut TypedTensorViewMut<'_, C64>,
871    ) -> Result<()> {
872        self.fitter.evaluate_nd_zz_to(backend, coeffs, dim, out)
873    }
874
875    fn fit_nd_zd_to(
876        &self,
877        backend: Option<&GemmBackendHandle>,
878        values: &TypedTensorView<'_, C64>,
879        dim: usize,
880        out: &mut TypedTensorViewMut<'_, f64>,
881    ) -> Result<()> {
882        self.fitter.fit_nd_zd_to(backend, values, dim, out)
883    }
884
885    fn fit_nd_zz_to(
886        &self,
887        backend: Option<&GemmBackendHandle>,
888        values: &TypedTensorView<'_, C64>,
889        dim: usize,
890        out: &mut TypedTensorViewMut<'_, C64>,
891    ) -> Result<()> {
892        self.fitter.fit_nd_zz_to(backend, values, dim, out)
893    }
894}