Skip to content

Enrich the tests, fix the wrapper defects they expose, and document the conventions - #86

Merged
shinaoka merged 24 commits into
mainlinefrom
test/enrich-coverage
Sep 25, 2026
Merged

shinaoka merged 24 commits into
mainlinefrom
test/enrich-coverage

Conversation

@shinaoka

@shinaoka shinaoka commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

Summary

This PR does three things:

  • Tests. It brings the test suite up to the mandatory coverage of spm-agent-rules/testing.md, including oracle tests whose reference values do not come from libsparseir.
  • Defects. It fixes the wrapper defects the new tests exposed. Backend defects are reported upstream and pinned with strict xfails.
  • Documentation. It documents the conventions the code implements, with the notation shared by the sparse-ir documentation, the tutorial and SparseIR.jl.

Behavior changes (user-visible)

  • Out-of-domain input: u(τ), v(ω), DLR poles and TauSampling points outside their domain raise ValueError before the C call. Before, this was a Rust panic surfacing as RuntimeError with status -7 (Out-of-domain τ/ω in spir_funcs_eval/spir_funcs_batch_eval and out-of-range points in spir_tau_sampling_new panic, collapsed to SPIR_INTERNAL_ERROR instead of SPIR_INVALID_ARGUMENT (core Rust + C ABI) sparse-ir-rs#266).

  • Parity: uhat(n) with the wrong parity raises ValueError (was status -5).

  • Empty input: empty evaluation arrays return an empty result of the right shape (was status -6).

  • TauSampling order: user-supplied points keep the given order. They used to be sorted silently, so evaluate() returned values in a different order from the points given.

  • Sampling points: these raise ValueError:

    • duplicated sampling points
    • negative points with positive_only=True
    • genuinely complex coefficients in evaluate() with positive_only=True
  • Parameters: these raise ValueError:

    • wmax <= 0 or eps <= 0
    • non-finite beta, wmax, eps or lambda_
    • max_size = 0
    • a kernel or SVE that does not match beta * wmax
    • RegularizedBoseKernel with fermionic statistics
  • Statistics: an invalid statistics string with kernel= given used to be accepted silently and built a bosonic basis. It now raises ValueError.

  • Augmented function sets: these now work:

    • AugmentedTauFunction.xmin/xmax and AugmentedMatsubaraFunction.zeta
    • the size, shape and slicing of augmented function sets
    • TauLinear.hat(scalar)

    aug.u[-1] now returns the last function; it used to return an augmentation.

  • Augmented positive_only sampling: MatsubaraSampling(AugmentedBasis(...), positive_only=True) with the default points works now. It always failed before, because the positive-only default points of augmented bases were a truncated set padded with zeros. They are now the non-negative half of the full set, as for a plain basis.

  • Basis truncation: basis[:n] now truncates a FiniteTempBasis to its n most significant singular values. It raised NotImplementedError, which also made AugmentedBasis slicing unusable. Closes Basis truncation (basis[:n]) is unimplemented, which makes AugmentedBasis slicing unusable #84.

  • Selections of basis functions:

  • MatsubaraSampling.cond with positive_only=True: it is now the condition number of the real least-squares problem that fit solves. The C library reports that of the complex matrix, for example 55 instead of 2e7 for an augmented basis (fix(capi): spir_sampling_get_cond_num reports the condition number of the complex half matrix for positive-only Matsubara samplings, not of the real least-squares problem that fit solves sparse-ir-rs#270).

  • Other checks:

    • A DLR on anything but a FiniteTempBasis raises TypeError; it used to be an AttributeError.
    • SVEResult raises TypeError for a non-numeric eps.
    • Matsubara indices beyond the int64 range raise ValueError without an overflow warning.
  • overlap: it checks its interval once instead of at every quadrature point (0.011 s → 0.007 s). It probes the integrand with a NumPy scalar and accepts Python-float results, so the README's GF(2) example runs again; it failed with AttributeError on w.clip.

  • TauConst and TauLinear are bosonic-only: statistics='F' raises ValueError.

    • For fermions, TauLinear.hat returned the bosonic transform, so its τ and Matsubara columns disagreed.
    • TauConst's docstring listed fermions while its constructor refused them, giving a false reason.
  • Augmentation instances: an instance passed to AugmentedBasis must have the basis's β, and TauConst/TauLinear a bosonic basis. They were passed through unchecked, so e.g. TauConst(5.0) augmented a β = 10 basis.

  • FiniteTempBasisSet.rescale keeps eps. It passed the basis accuracy, so eps shrank and the basis could grow (20 → 21 functions at eps = 1e-6).

  • RegularizedBoseKernel is deprecated and emits a DeprecationWarning; LogisticKernel is the default for both statistics. Its docstring had the sign of the denominator wrong and called it fermionic.

  • Documentation:

    • README and doc/tutorial.rst link to the v2 tutorial and sparse-ir-rs.
    • The stale copies in tutorials/ are removed.
    • doc/kernels.rst marks RegularizedBoseKernel deprecated and fixes the bosonic weight of LogisticKernel (1/tanh(βω/2), not 1/tanh(ω/ωmax)).

Documentation

The docstrings, README.rst and doc/ now state the conventions the code implements, with the notation of the new sparse-ir-doc page Notation and conventions (SpM-lab/sparse-ir-doc#5). Library code is unchanged: with docstrings removed, the syntax trees of the changed modules are identical.

  • Conventions now stated:
    • G(τ) = −⟨T c c†⟩ and G_l = −S_l ρ_l, with ρ = A for fermions and A/tanh(βω/2) for bosons;
    • the Fourier transform and its inverse;
    • orthonormality on [0, β] and [−ωmax, ωmax], and the sign rule U_l(β⁻) > 0;
    • S_l = √(βωmax/2) s_l;
    • the τ domain [−β, β] with its endpoints (+0.0 = 0⁺, β = β⁻, −0.0 = 0⁻, −β = (−β)⁺);
    • the reduced Matsubara frequency n (ν = nπ/β), positive_only (n ≥ 0), and the DLR functions u_p = −K(τ, ω̄_p) and û_p.
  • Wrong statements fixed:
    • the FiniteTempBasis example computed −G (gl = +s·v);
    • DLR poles are the roots of V_L, not "extrema";
    • dlr.py's formula omitted the bosonic tanh factor;
    • unfolded τ points are symmetric about 0, not about β/2;
    • SVEResult does not truncate at eps;
    • "fit only exists in the complex flavour";
    • "(scaled/unscaled)", the attribute V, "kernel inferred from statistics", ω points called "imaginary time", the reduced-frequency formula in _util.py and the −0.0 case of normalize_tau.
  • doc/kernels.rst: the section on custom kernels described an API that does not exist; it now lists the two supported kernels and how to pass one.
  • doc/conf.py: autoclass_content = "both", so constructor parameters render. The Parameters headers are in NumPy style.
  • Checks:
    • Every stated convention was checked numerically, e.g. the endpoint rules exactly, orthonormality to 3e−15, and the DLR formulas to 1e−15.
    • The examples in the text run and give what the text says; the FiniteTempBasis example reproduces 1/(iν − 2.5) to 4.5e−16.
    • Sphinx builds with 0 warnings.

Backend defects found

All were reported to SpM-lab/sparse-ir-rs. All but #273 are fixed on its main branch; the fixes reach this package with the next pylibsparseir release.

Tests

  • Counts: before, 181 passed (45 s). After, 345 passed, 5 xfailed (57 s). The suite passes on Python 3.10, 3.11, 3.12 and 3.13.
  • New files:
    • test_oracle.py
    • test_boundary.py
    • test_public_surface.py
    • test_integration.py (a port of SparseIR.jl's integration test)
    • _helpers.py
  • Existing tests: weak, commented-out and unseeded tests are rewritten.
  • Oracle mutation check: every broken reference must fail by at least 1e3 × atol.
O1 pole sign (tau)                       max error/atol = 3.33e+07  ok
O1 statistics swapped (iv)               max error/atol = 6.67e+07  ok
O1 n -> n+2 (iv)                         max error/atol = 2.44e+07  ok
O2 Fourier sign                          max error/atol = 1.09e+10  ok
O2 n -> n+2                              max error/atol = 8.12e+09  ok
O3 u(tau) instead of u(beta - tau)       max error/atol = 6.67e+07  ok
O3 Re/Im parity swapped                  max error/atol = 3.23e+07  ok
O3 periodicity sign swapped              max error/atol = 2.00e+10  ok
O4 DLR statistics swapped                max error/atol = 2.21e+10  ok
O4 DLR pole sign                         max error/atol = 1.00e+10  ok

Companion PRs: SpM-lab/SparseIR.jl#158 (Julia), SpM-lab/sparse-ir-doc#5 (notation page), SpM-lab/sparse-ir-rs#286 (#273 fix, merged).

🤖 Generated with Claude Code

shinaoka and others added 24 commits September 25, 2026 10:23
tests/_helpers.py holds the closed forms of a single pole, a composite
Gauss-Legendre rule, non-contiguous views and an error report that prints
the max abs error and the relative norm error. conftest.py gains a
session-scoped get_basis fixture that shares one SVE per (lambda, eps);
test_dlr.py uses it and drops its print calls.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
u(tau) outside [-beta, beta], v(omega) outside [-wmax, wmax] and DLR poles
outside the frequency window reached a Rust panic reported as an internal
error (SpM-lab/sparse-ir-rs#266); a reduced Matsubara index of the wrong
parity came back as status -5 and an empty point array as status -6. They
are now rejected, or answered, before the call: ValueError naming the
value, and an empty result of the right shape.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
TauSampling sorted user-supplied points silently, so evaluate() returned
values in another order than the points were given; it now keeps the
order and copies the array. Points outside [-beta, beta] (a Rust panic)
and duplicated points (a rank-deficient matrix) raise ValueError, as do
negative points with positive_only=True (SpM-lab/sparse-ir-rs#247).
With positive_only=True, evaluate() rejects genuinely complex
coefficients; fit() results stay accepted.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
wmax = 0, eps <= 0, a non-finite beta, wmax, eps or lambda_ and max_size = 0
now raise ValueError before the C library is called (it answered with
status -6 or a panic). A kernel or precomputed SVE whose cutoff or kernel
type does not match beta * wmax is rejected: the C library built a wrong
basis from it without complaint. RegularizedBoseKernel with fermionic
statistics raises ValueError, as in SparseIR.jl.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
AugmentedTauFunction.xmin/xmax, AugmentedMatsubaraFunction.zeta, the size,
shape and slicing of augmented function sets and TauLinear.hat() at a
scalar frequency raised AttributeError or TypeError, and aug.u[-1]
returned the last augmentation instead of the last function. The function
sets now carry size, xmin/xmax or zeta, and negative indices are resolved
against the whole set. The new smoke test calls every export and every
function set once.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
O1: a single pole against its closed forms in tau and Matsubara frequency
(both statistics, beta = 10 and 1000). O2: uhat against the Fourier
integral of u. O3: reflection, Re/Im parity and (anti)periodicity. O4:
sign changes, orthonormality, default-point structure and the closed
forms of the DLR functions. O5: round trips for Lambda = 0.1 .. 1e5 and
eps down to 1e-15. None of the references comes from libsparseir.

The asymptotic regime of uhat (|n| >= 40*Lambda) is wrong in the backend
for l = 1, 2 (mod 4) (SpM-lab/sparse-ir-rs#265); it is pinned by strict
xfails so that a backend fix makes them fail and get promoted.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Port of SparseIR.jl's integration test: random DLR coefficients with extra
dimensions (2, 3, 4) are taken to IR, tau and Matsubara both through the IR
basis and directly through the DLR, with the basis axis at every position,
for both statistics, positive_only on and off, real and complex data.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Removes the try/except-pass blocks in test_core.py and test_kernel.py,
restores the commented-out uhat and Matsubara-sampling checks, seeds the
remaining global random draws, and turns shape-only and finite-only
assertions into symmetry, round-trip and closed-form checks.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
AugmentedBasis.default_matsubara_sampling_points(positive_only=True) asked
the C library for the positive-only variant with the buffer size as the
point limit, which returned a truncated set padded with zeros
([0, 2, 4, 12, 0, 0, 0] for a bosonic basis with TauConst and TauLinear).
MatsubaraSampling(AugmentedBasis(...), positive_only=True) therefore always
failed: with a singular matrix or a parity error before this branch, with a
duplicate-point error about points the caller never passed after it. The
points are now the non-negative half of the full set, as for a plain basis.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- basis[:n] truncates a FiniteTempBasis to its n most significant singular
  values (it raised NotImplementedError, which also made AugmentedBasis
  slicing unusable; #84). Slices of augmented function sets keep their type.
- Slices and lists of basis functions always give a set, also of one
  function (u[0:1] returned a single function), and a set evaluates to
  shape (size,) + shape(x). Empty selections raise ValueError before the
  call; the C library panics on them (SpM-lab/sparse-ir-rs#269).
- Single functions have deriv, like the sets.
- Augmented function sets reject index lists with TypeError (they failed
  with "too many values to unpack") and truncation to the augmentations
  alone with ValueError; MatsubaraConst.hat checks the parity.
- MatsubaraSampling.cond with positive_only=True is the condition number of
  the real least-squares problem the fit solves; the C library reports that
  of the complex matrix, 55 instead of 2e7 for an augmented basis
  (SpM-lab/sparse-ir-rs#270).
- SVEResult raises TypeError for a non-numeric eps; overlap checks its
  interval once instead of every quadrature point (0.011 s -> 0.007 s); the
  u docstrings state the [-beta, beta] domain.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The strict xfails cite the issue URL (spec 12.3). The sign-change grid no
longer contains beta/2, where the odd functions vanish, and the O5 check of
the truncated basis asserts its precondition instead of skipping. Checks
that only asserted finite values now test the symmetric structure of the
default tau points and continuity at the interval ends. The unused
test_bases fixture, which built pylibsparseir objects, is removed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
DiscreteLehmannRepresentation(AugmentedBasis(...)) failed with
"AttributeError: 'AugmentedBasis' object has no attribute '_ptr'"; the DLR
is built on the C handle of an IR basis. It now raises TypeError naming
the requirement, as SparseIR.jl throws ArgumentError.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…wrappers

Two changes of the review fixes altered documented behavior that
SparseIR.jl pins in its tests; they are reverted here so the wrappers stay
in step:

- A one-element selection of u or v gives a single function again
  (SparseIR.jl: basis.u[2:2] isa PiecewiseLegendrePoly). The defect it was
  meant to fix, aug.u[:naug + 1] failing, is fixed where it was: augmented
  function sets accept a single function as their basis part, and single
  functions report size 1. Selections of uhat, including lists, still give
  sets (a list used to give a single-function object holding several).
- MatsubaraConst.hat accepts both parities again: its docstring says the
  statistics do not affect it, and SparseIR.jl tests the same.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A value such as 1e300 is integral as a float, so the integer check let it
reach the int64 cast, which printed an overflow warning before the
misleading "must be an integer" error. The range is now checked first.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The tutorial links pointed to the tutorial of sparse-ir v1, which is now
marked deprecated, and the C library links to the archived C++
libsparseir repository. They now point to
https://spm-lab.github.io/sparse-ir-tutorial-v2/ and to sparse-ir-rs,
which builds the libsparseir C API.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
tutorials/ held jupytext copies imported from pysparseir. Eleven of the
thirteen did `import pylibsparseir as sparse_ir` and could not run with
the current package, and four were stubs. The maintained tutorial is
https://spm-lab.github.io/sparse-ir-tutorial-v2/, which README.rst and
doc/tutorial.rst link.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
TauLinear('F') was accepted, but its Matsubara values were those of the
bosonic function (for example -3.487i at n = 1 instead of the transform
-2.220) and its derivative had the wrong sign at negative tau, so the tau
and Matsubara columns of a fermionic augmented basis disagreed. Both
tau augmentations now raise ValueError for fermions, as TauConst did. Its
error message claimed that the fermionic transform vanishes at every odd
frequency, which is false (it is 2i sqrt(beta)/(pi n)); it now says that
the augmentation is defined for bosons only. The docstrings state the
functions and their transforms, and the derivatives return floats for
integer tau arrays (np.full_like kept the integer dtype and returned 0).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
rescale passed the accuracy of the basis (the first discarded singular
value) as the new eps, so the rescaled set could keep one more function
than a set built with the original eps (21 instead of 20 at beta = 10,
wmax = 8, eps = 1e-6), contrary to its docstring and to
FiniteTempBasis.rescale. It now passes the original eps.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
overlap probed f with a Python float, while the quadrature calls it with
NumPy scalars, so functions written with array methods failed; the
README's GF(2) example (np.sqrt(1 - w.clip(-1, 1)**2)) raised
AttributeError. A function returning a Python float failed on f_res.dtype.
The probe now uses a NumPy scalar and np.asarray, and the README example
runs as written. The unused module-level _compute_overlap, which called
_compute_overlap_internal with the wrong arguments, is removed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…#273)

RegularizedBoseKernel now emits a DeprecationWarning; LogisticKernel is
the default kernel for both statistics. Its docstring had the sign of the
denominator wrong and called it fermionic; it now gives the kernel in
dimensionless and physical units (irbasis paper, Chikano et al., CPC 240,
181 (2019), Eqs. (1)-(3)).

For wmax != 1 libsparseir scales the singular values of a
RegularizedBoseKernel basis by wmax**-1 instead of wmax**+1
(SpM-lab/sparse-ir-rs#273). test_o6 compares sum_l U_l S_l V_l with the
physical kernel at wmax = 2 and is a strict xfail until the fix is
released. The deprecation warning is filtered in the test configuration;
test_kernel.py asserts it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
AugmentedBasis passed augmentation instances through unchecked, so a
TauConst(5.0) augmented a beta = 10 basis (normalized for the wrong beta,
and failing outside [-5, 5]) and a bosonic TauConst/TauLinear augmented a
fermionic basis. Instances now need the basis's beta, and TauConst and
TauLinear a bosonic basis; MatsubaraConst stays statistics-agnostic.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The kernel list now marks RegularizedBoseKernel as deprecated. The
bosonic weight of LogisticKernel is 1/tanh(βω/2), not 1/tanh(ω/ωmax), as
kernel.py already says: for a bosonic pole at ω0 = 0.1 (β = 10, ωmax = 8,
eps = 1e-12), G_l = -s_l v_l(ω0) w(ω0) reproduces 1/(iν - ω0) to 1.5e-13
with the former and misses it by 3.6e2 with the latter.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The docstrings now state the conventions of the notation page
(https://spm-lab.github.io/sparse-ir-doc/src/notation.html) as the code
implements them; each statement was checked numerically (beta = 10,
wmax = 8).

- FiniteTempBasis: the example computed -G (gl = +s v); with
  gl = -s v(2.5) it reproduces 1/(iν - 2.5) to 5e-16. The class now gives
  K(τ, ω) in physical units, G(τ) = -<T c(τ) c†(0)>, G_l = -S_l ρ_l with
  ρ = A (F) or A/tanh(βω/2) (B), orthonormality on [0, β] and
  [-ωmax, ωmax], the gauge U_l(β⁻) > 0, the U, S, V shared by both
  statistics, truncation S_l/S_0 >= eps and S_l = sqrt(βωmax/2) s_l.
  The attribute is v (not V), the kernel is not inferred from the
  statistics, the omega points are real frequencies (the roots of V_L),
  unfolded τ points lie in (-β/2, β/2] symmetric about 0 and folded
  ones in [0, β), and finite_temp_bases builds two kernels.
- u of all bases, TauSampling and the augmentations: τ in [-β, β],
  U(τ) = (-1)^ζ U(τ + β), +0.0 = 0⁺, beta = β⁻, -0.0 = 0⁻ and
  -beta = (-β)⁺.
- Matsubara: iν with the reduced frequency n, ν = nπ/β, not an index;
  the Fourier transform and its inverse; positive_only means n >= 0 and
  G(-iν) = G(iν)*, and fit then returns complex128 with zero imaginary
  part. fit's claim that only a complex C entry point exists is dropped.
- DLR: the poles are the roots of V_L, not extrema; u_p = -K(τ, ω_p);
  û_p = 1/(iν - ω_p) (F) and tanh(βω_p/2)/(iν - ω_p) (B);
  G_l = -S_l Σ_p V_l(ω_p) c_p.
- SVEResult does not truncate at eps (len 60 for eps = 1e-2 to 1e-15),
  cutoff and n_sv have no effect, and only s is exposed; compute takes
  kernel, not K.
- accuracy: the abstract default is significance[-1]; FiniteTempBasis
  returns S_L/S_0 and the DLR and augmented bases forward it.
- LogisticKernel: physical form, and the bosonic identity without the
  undefined ρ′; RegularizedBoseKernel acts on A(ω)/ω.
- poly: the [xmin, xmax] domain, period, default overlap ranges and the
  transform over [0, β]; _util: the reduced-frequency formula and the
  -0.0 rule of normalize_tau.
- NumPy-style section headers lose their colon so napoleon renders them.

Only docstrings changed: with docstrings stripped, the ASTs of all ten
modules equal those at the parent commit, and the test suite still gives
345 passed, 5 xfailed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- README.rst and doc/index.rst link the notation page, and the README
  summarizes the main conventions. The GF(2) example uses iν names
  (siv, Sigmaiv, G0iv, Giv) and still converges in 11 iterations with
  Im Σ(iν) < 0. The cutoff advice is given in terms of wmax instead of
  lambda_ (the kernel depends only on Λ in the dimensionless variables),
  and U_l, S_l, V_l are named with their orthonormality intervals.
- doc/kernels.rst: the "Custom kernels" section described an API that
  does not exist (callable kernels, hints(), FiniteTempBasis(K, 'F'), an
  undefined np). It now says that only the two kernels are supported and
  how to pass one. The introduction writes G(τ) = -∫ K w A with
  A = -Im G^R/π, gives both kernels in physical units and uses \tau and
  \omega inside math; the nonexistent __call__ members are no longer
  requested.
- doc/conf.py: autoclass_content = "both", so that the constructor
  parameters (statistics, beta, wmax, eps, ...) are rendered; the kernel
  constructor docstrings, now rendered, name lambda_.
- doc/otherbases.rst: fix the grammar and name the DLR poles.

The Sphinx HTML build gives no warnings, as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Basis truncation (basis[:n]) is unimplemented, which makes AugmentedBasis slicing unusable

1 participant