Skip to main content

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;