sparse_ir_minipole/minipole/mod.rs
1//! Port of the minimal pole method of Green-Phys/MiniPole.
2//!
3//! Ported from <https://github.com/Green-Phys/MiniPole> at commit `15e4a54`
4//! (MIT License, Copyright (c) 2024 lzphy; see `LICENSE-THIRD-PARTY` of this
5//! crate): `con_map.py` (`ConMapGeneric`, `ConMapGapless`),
6//! `mini_pole_dlr.py` and `mini_pole.py`. L. Zhang and E. Gull, Phys. Rev. B
7//! 110, 035154 (2024); L. Zhang, Y. Yu and E. Gull, Phys. Rev. B 110, 235131
8//! (2024).
9//!
10//! Differences from the reference:
11//! - the error tolerance (or the number of poles) is required: the automatic
12//! choice by knee detection (`kneed`) is not ported;
13//! - the contour integrals of [`mini_pole`] use composite Gauss–Legendre
14//! quadrature instead of QUADPACK's QAWO, to the same tolerance;
15//! - arrays are column-major tensors, and the channels of a matrix-valued
16//! function are its trailing axes.
17//!
18//! # Entry points
19//!
20//! - [`mini_pole_dlr_from`] takes a DLR and the coefficients `g_l` that
21//! `MatsubaraSampling::fit_nd` returns for it, and forms the residues
22//! `A_l = g_l w_l` with `dlr.pole_weights()`. Bosonic DLR coefficients are
23//! therefore not residues; do not pass them to [`mini_pole_dlr`].
24//! - [`mini_pole_dlr`] takes real poles `x_l`, their actual residues `A_l`
25//! (pole axis first, channels on the trailing axes, column-major) and `β`.
26//! - [`mini_pole`] takes Matsubara data on a uniform grid of non-negative
27//! *physical* frequencies `ω_n` (real numbers, not indices and not `iω_n`).
28//!
29//! All three return a [`MiniPoleResult`]; [`MiniPoleResult::evaluate`] sums the
30//! poles and the constant term at arbitrary complex `z`.
31//!
32//! # The contour of the DLR entry points
33//!
34//! Without symmetry ([`MiniPoleDlrParams::symmetry`] false), the conformal map
35//! uses the imaginary-axis segment `[iω_{n0}, iω_{nmax}]` with
36//! `ω_n = (2n + 1)π/β`, **also for a bosonic DLR**. This is a contour index,
37//! not the physical bosonic grid and not the reduced Matsubara index `n`
38//! (frequency `nπ/β`) of the rest of the library and of the C API.
39//!
40//! - `n0` is chosen by the caller; there is no automatic choice for DLR input.
41//! Increasing it raises the lower end of the contour.
42//! - `nmax: None` uses the numerical value of `β` in the units of the input.
43//! An explicit `Some(nmax)` must exceed `n0`; it is a floating-point cutoff,
44//! not a number of samples.
45//! - With symmetry the gapless map uses the lower end only, and `nmax` does
46//! not enter.
47//!
48//! Changing the segment changes the mapped poles and moments that ESPRIT
49//! sees, so the same coefficients and tolerance need not give the same number
50//! of poles.
51//!
52//! # Tolerance and number of poles
53//!
54//! `err` is the ESPRIT tolerance: absolute with [`ErrType::Abs`] (the
55//! default), relative to the largest singular value with [`ErrType::Rel`].
56//! It is **not** a bound on the reconstruction error of `G`. With the DLR
57//! entry points one may instead set `m = Some(count)` and `err = None` when the
58//! model order is known; one of the two is required, because the automatic
59//! choice by knee detection is not ported.
60//!
61//! # Matsubara input ([`mini_pole`])
62//!
63//! - At least three finite, non-negative, strictly increasing, uniformly
64//! spaced frequencies, with data of shape `[n_w]` or `[n_w, n_orb, n_orb]`.
65//! Sparse DLR/IR sampling nodes, irregular grids and negative frequencies
66//! are not accepted; fit a DLR and use [`mini_pole_dlr_from`] for those.
67//! - `n0` is a **position in the supplied array**: the contour starts at
68//! `w[n0]` and, without symmetry, ends at the last element. There is no
69//! `nmax`; extend the data for a larger upper end. The default is
70//! [`N0::Auto`] with `shift: 0`; inspect [`MiniPoleResult::n0`] and compare
71//! with [`N0::Fixed`] if needed. With symmetry `w[n0]` must be positive, so a
72//! bosonic zero frequency cannot be the lower end of the gapless map.
73//! - `err` is required and should be at least the noise level of the data.
74//! [`MiniPoleResult::err_max`] reports the precision of the first ESPRIT
75//! interpolation, not a certified reconstruction error; it is `None` for
76//! DLR input.
77//! - [`MiniPoleParams::g_symmetric`] symmetrizes matrix data as
78//! `G_ij = G_ji`, independently of the up-down `symmetry` flag.
79//! [`MiniPoleParams::compute_const`] fits a constant term and cannot be
80//! combined with `symmetry`.
81//! - Residues default to a least-squares fit in [`Plane::Z`] without symmetry
82//! and to the mapped [`Plane::W`] with symmetry;
83//! [`MiniPoleParams::include_n0`] also includes the first `n0` points in the
84//! z-plane fit.
85//!
86//! # C API
87//!
88//! `spir_minipole_from_matsubara` takes reduced Matsubara indices `n`
89//! (`1, 3, 5, ...` for fermions, `0, 2, 4, ...` for bosons) and converts them
90//! as `ω = nπ/β`; a negative `n0` selects the automatic choice and `n0_shift`
91//! adds to it. `spir_minipole_from_dlr` follows [`mini_pole_dlr_from`], with
92//! `nmax <= 0` selecting `β`. The getters expose the poles, residues, constant,
93//! the `n0` used and the Matsubara-only `err_max`.
94//!
95//! A worked example with figures is the MiniPole page of the sparse-ir Rust
96//! user guide: <https://spm-lab.github.io/sparse-ir-rs/tutorials/minipole.html>.
97
98mod con_map;
99mod mini_pole;
100mod mini_pole_dlr;
101mod quad;
102
103pub use crate::esprit::{ErrType, Esprit, EspritParams};
104pub use con_map::{ConMap, ConMapGapless, ConMapGeneric};
105pub use mini_pole::{MiniPoleParams, N0, Plane, mini_pole};
106pub use mini_pole_dlr::{MiniPoleDlrParams, mini_pole_dlr, mini_pole_dlr_from};
107
108use crate::error::Result;
109use num_complex::Complex;
110use tenferro_tensor::TypedTensor;
111
112type C64 = Complex<f64>;
113
114/// A minimal pole representation `G(z) = Σ_j A_j / (z - ξ_j) + C`.
115#[derive(Debug)]
116pub struct MiniPoleResult {
117 /// Pole locations `ξ_j`, sorted by real part.
118 pub pole_location: Vec<C64>,
119 /// Pole weights `A_j`, shape `[M, ...]`.
120 pub pole_weight: TypedTensor<C64>,
121 /// Constant term `C`, one entry per channel (column-major).
122 pub constant: Vec<C64>,
123 /// Moments (contour integrals) `h_k`, shape `[K, ...]`.
124 pub h_k: TypedTensor<C64>,
125 /// The ESPRIT approximation of `h_k`.
126 pub esprit: Esprit,
127 /// `n0` used for the contour.
128 pub n0: usize,
129 /// Precision of the first approximation of the Matsubara data
130 /// ([`mini_pole`] only).
131 pub err_max: Option<f64>,
132}
133
134impl MiniPoleResult {
135 /// Evaluate `G(z)`; returns shape `[len(z), ...]`.
136 ///
137 /// # Errors
138 /// Propagates tensor construction errors.
139 pub fn evaluate(&self, z: &[C64]) -> Result<TypedTensor<C64>> {
140 let shape = self.pole_weight.shape().to_vec();
141 let r = self.pole_location.len();
142 let d: usize = shape[1..].iter().product();
143 let a = self.pole_weight.host_data()?;
144 let mut out = vec![C64::new(0.0, 0.0); z.len() * d];
145 for (i, &zi) in z.iter().enumerate() {
146 for c in 0..d {
147 out[i + z.len() * c] = self.constant[c];
148 }
149 for (j, &p) in self.pole_location.iter().enumerate() {
150 let f = (zi - p).inv();
151 for c in 0..d {
152 out[i + z.len() * c] += a[j + r * c] * f;
153 }
154 }
155 }
156 let mut out_shape = shape;
157 out_shape[0] = z.len();
158 Ok(TypedTensor::from_vec_col_major(out_shape, out)?)
159 }
160}
161
162/// Sort the poles by real part and build the result.
163#[allow(clippy::too_many_arguments)]
164fn assemble(
165 location: Vec<C64>,
166 weight: Vec<C64>,
167 constant: Vec<C64>,
168 h: Vec<C64>,
169 hshape: Vec<usize>,
170 esprit: Esprit,
171 n0: usize,
172 err_max: Option<f64>,
173) -> Result<MiniPoleResult> {
174 let r = location.len();
175 let d = constant.len();
176 let mut order: Vec<usize> = (0..r).collect();
177 order.sort_by(|&a, &b| location[a].re.total_cmp(&location[b].re));
178 let pole_location: Vec<C64> = order.iter().map(|&j| location[j]).collect();
179 let mut w = vec![C64::new(0.0, 0.0); r * d];
180 for (jn, &j) in order.iter().enumerate() {
181 for c in 0..d {
182 w[jn + r * c] = weight[j + r * c];
183 }
184 }
185 let mut wshape = hshape.clone();
186 wshape[0] = r;
187 Ok(MiniPoleResult {
188 pole_location,
189 pole_weight: TypedTensor::from_vec_col_major(wshape, w)?,
190 constant,
191 h_k: TypedTensor::from_vec_col_major(hshape, h)?,
192 esprit,
193 n0,
194 err_max,
195 })
196}
197
198#[cfg(test)]
199mod tests;