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}