Skip to content

Fix audit findings A-H: ctypes boundary, DLR basis functions, and test-suite self-sabotage - #85

Merged
shinaoka merged 1 commit into
mainlinefrom
fix-audit-findings
Sep 4, 2026
Merged

Fix audit findings A-H: ctypes boundary, DLR basis functions, and test-suite self-sabotage#85
shinaoka merged 1 commit into
mainlinefrom
fix-audit-findings

Conversation

@shinaoka

@shinaoka shinaoka commented Sep 4, 2026

Copy link
Copy Markdown
Member

Re-verified each of the eight audit findings against mainline (v2.1.3) before
fixing it, per AGENTS.md -> spm-agent-rules (rules/ffi-boundary.md,
rules/python.md, rules/testing.md).

Disposition

# Finding Disposition
A DLR .u/.uhat returned the IR basis functions, not the DLR ones Fixed properly. Re-verified: spir_basis_get_u/spir_basis_get_uhat accept the DLR spir_basis* handle, so no NotImplementedError and no C-API issue is needed. Both properties now build the DLR function sets lazily from the DLR handle, and g_dlr @ dlr.u(tau) reproduces gl @ basis.u(tau) (asserted in the new tests).
B No dtype-width normalization at the boundary Fixed. Swept every .ctypes.data_as site in the package (sampling.py, dlr.py, poly.py; basis.py, sve.py, kernel.py, augment.py have none). Each input goes through a new _util.as_boundary_real/as_boundary_complex/as_boundary_matsubara helper (np.ascontiguousarray(x, dtype=<explicit>)) with the pointer taken from the converted object.
C DLR __init__ pointer provenance Fixed. Poles are normalized first and the pointer is taken from the normalized array; shape/emptiness/finiteness are validated.
D FunctionSet.__call__ squeeze bugs Fixed. Documented contract: the result is (n_funcs,) + np.shape(x), the leading axis is dropped only for a single function, the point axes only for scalar x. FunctionSetFT.__call__ follows the same rule. This is what the previously-skipped test_zero_frequency expected (basis.v(np.array([0.0])).shape == (size, 1)).
E rescale raised NameError (undefined new_lambda) Fixed. Holds lambda_ == beta * wmax fixed, computes new_wmax = lambda_ / new_beta, reuses the existing kernel and SVE (both depend only on lambda_ and eps), and rejects non-positive new_beta with ValueError.
F Fermionic TauConst gives a rank-deficient basis Raises now. ValueError naming the reason: hat(n) == sqrt(beta) * (n == 0) and fermionic reduced frequencies are odd, so the augmentation column is identically zero. The two fermionic parametrizations in test_augment.py were converted into a pytest.raises test.
G Matsubara index truncation and i % n index wrap-around Fixed. check_reduced_matsubara rejects complex input (TypeError), non-finite and non-integral values (ValueError naming the offending value, e.g. 1.9), and wrong parity (naming "odd"/"even"). FunctionSetFT.__call__ validates before converting to int64. Both __getitem__ implementations now use _util.resolve_function_indices: negative indices resolve with Python semantics, out-of-range raises IndexError, non-integral raises ValueError.
H Test-suite self-sabotage Fixed. See below.

Additional tracking issues opened for things deliberately not forced in this PR:
#82 (should TauLinear also be bosonic-only?), #83 (restore the
SVEResult.part()-based coverage deleted from test_poly.py), #84 (basis
truncation basis[:n] is still unimplemented in the C API).

Finding H in detail

  • The 8 skips in tests/test_sve_advanced.py were not legitimate
    preconditions. They were except Exception: pytest.skip(...) blocks hiding a
    bug in the tests themselves: sve_result_new/basis_new were called with the
    Python LogisticKernel/RegularizedBoseKernel wrapper instead of
    kernel._ptr (TypeError: expected LP_LP__spir_kernel instance instead of LogisticKernel), and basis_new was called with 5 of its 7 arguments. Fixed
    and un-skipped; all 8 now run and pass.
  • Removed the try/except -> pytest.skip wrappers in
    tests/test_advanced_features.py (10 of them), tests/test_sampling_advanced.py,
    and the except Exception: print(...) in the test_bases fixture in
    tests/conftest.py, which handed tests a silently incomplete dict.
  • tests/test_poly.py: deleted the two dead triple-quoted blocks (8 stringified
    tests depending on the unavailable SVEResult.part(), tracked in Restore SVEResult.part()-based poly coverage once the C API exposes it #83), deleted
    the shadowed duplicate test_broadcast_uv that referenced undefined
    beta/atol, and added the missing assert on the no-op comparison at line 22.
  • The suite now has no skips at all.

Regression tests

New tests/test_ffi_boundary.py (68 tests), plus the converted augmentation tests:

  • dtype matrixfloat32/float64/complex64/complex128 and integer
    input for TauSampling.evaluate/fit, MatsubaraSampling.evaluate/fit,
    DLR.from_IR/to_IR, and FunctionSet.__call__; each compared against the
    float64 reference at the input dtype's own precision (relative to the norm,
    since the coefficient vectors span many decades).
  • non-contiguous input for every one of those entry points, and for DLR poles.
  • concrete exception types with match= — non-integral Matsubara index
    (1.9), wrong parity, complex indices, complex evaluation points, NaN input,
    out-of-range axis, out-of-range and non-integral function index, empty /
    multi-dimensional sampling points, non-positive rescale beta, fermionic
    TauConst.
  • nonzero-norm assertions throughout, so a silently zeroed result cannot pass.
  • shape tests for the finding-D semantics (whole set, single function, slice,
    scalar and tensorial x, for both u and uhat).
  • finding A correctness: g_dlr @ dlr.u(tau) and g_dlr @ dlr.uhat(n)
    checked against the IR result.
  • axis coverage (0, 1, -1, -2) with round-trip checks.
  • rescale round-trip (lambda_ preserved, spectrum preserved, basis usable).
  • __all__-derived smoke test and a from sparse_ir import * test.

Test counts: 101 passed / 8 skipped -> 181 passed / 0 skipped.

Other defects fixed in passing

Negative axes are resolved before reaching C (which takes a non-negative target
dimension) and out-of-range axes raise IndexError; non-finite input is rejected
before it reaches a factorization; returned handles are null-checked and statuses
compared against COMPUTATION_SUCCESS; the bare except: pass in both
release() methods and a stray print(type(...)) in PiecewiseLegendrePoly.overlap
are gone; basis._slice_to_size (an AttributeError at both call sites in
augment.py) is replaced by a local range-checking _slice_to_size;
AugmentedTauFunction.xmax returned xmin; AugmentedTauFunction.deriv passed
*daug where the constructor takes a list; and __all__ no longer names the
nonexistent TauPoles/MatsubaraPoles, which made from sparse_ir import *
raise AttributeError.

Behavior changes worth reviewer attention

  • TauSampling on a MatsubaraConst-augmented (vertex) basis now raises
    ValueError explaining that the augmentation is undefined in imaginary time,
    instead of handing NaNs to the C factorization. No existing test covered this
    path; a new one does.
  • TauConst(beta, 'F') now raises instead of building a rank-deficient basis.
  • Non-integral or wrong-parity Matsubara indices now raise instead of being
    truncated or accepted.

No REPOSITORY_RULES.md override applies to this change beyond what it already
documents (status codes from pylibsparseir.constants, no local CDLL, and the
version-consistency check).

Version

pyproject.toml 2.1.3 -> 2.1.4; python check_libsparseir_version_consistency.py
passes (pylibsparseir>=0.8.3,<0.10.0 in both pyproject.toml and
.conda/meta.yaml).

🤖 Generated with Claude Code

…t suite

Source fixes:

* DLR `.u`/`.uhat` returned the *IR* basis functions, so
  `g_dlr @ dlr.u(tau)` silently produced wrong values.  The C API accepts
  the DLR handle in `spir_basis_get_u`/`spir_basis_get_uhat`, so these
  properties now build the DLR function sets from the DLR handle (A).
* Every remaining boundary crossing normalizes its input with
  `np.ascontiguousarray(x, dtype=<explicit>)` and takes the pointer from
  the converted object: sampling evaluate/fit (both classes, real and
  complex paths), DLR `from_IR`/`to_IR` real paths, DLR poles, and the
  evaluation points in `poly.py`.  A float32 or non-contiguous array was
  previously reinterpreted through a `c_double` pointer, reading 8 bytes
  per 4-byte element (B, C).
* `FunctionSet.__call__`/`FunctionSetFT.__call__` had squeeze bugs; the
  returned shape is now `(n_funcs,) + np.shape(x)` with the function axis
  dropped only for a single function and the point axes only for scalar
  `x` (D).
* `FiniteTempBasis.rescale` raised `NameError` on an undefined
  `new_lambda`; it now holds `lambda_ == beta * wmax` fixed, reuses the
  SVE, and validates `new_beta > 0` (E).
* `TauConst` rejects fermionic statistics: its Fourier transform is
  `sqrt(beta) * (n == 0)` and fermionic reduced frequencies are odd, so
  the augmentation column vanished identically and the basis was
  rank-deficient (F).
* Reduced Matsubara indices are validated for integrality and parity
  instead of being truncated by `int()`/`astype(np.int64)`, and the
  `i % n` index wrap-around in both `__getitem__` implementations is
  replaced by explicit negative-index resolution plus `IndexError` (G).

Also fixed in passing: axis normalization (negative axes resolved before
they reach C, out-of-range axes raise `IndexError`), non-finite input
rejected before it reaches a factorization, null handle checks, status
compared against `COMPUTATION_SUCCESS`, the bare `except: pass` in both
`release()` methods, a stray `print` in `overlap`, the `AttributeError`s
from `basis._slice_to_size`, `AugmentedTauFunction.xmax` returning
`xmin`, the `*daug` argument mismatch in `AugmentedTauFunction.deriv`,
and `__all__` naming nonexistent `TauPoles`/`MatsubaraPoles` (which made
`from sparse_ir import *` raise).

Test suite (H):

* The 8 skips in `test_sve_advanced.py` were except-skip blocks hiding a
  test bug: `sve_result_new`/`basis_new` were called with the Python
  kernel wrapper instead of `kernel._ptr` (and with the wrong arity).
  Fixed and un-skipped; the suite now has no skips.
* Removed the try/except-skip wrappers in `test_advanced_features.py`,
  `test_sampling_advanced.py` and `tests/conftest.py`.
* Deleted the dead triple-quoted test blocks and the shadowed duplicate
  `test_broadcast_uv` in `test_poly.py` (tracked in #83), and added the
  missing `assert` on line 22.
* New `tests/test_ffi_boundary.py`: dtype matrix
  (float32/float64/complex64/complex128 plus integer) and non-contiguous
  input for every boundary function touched, axis coverage, concrete
  exception types with `match=`, nonzero-norm assertions, DLR function
  reconstruction against the IR result, shape tests for the squeeze
  semantics, `rescale` round-trip, and an `__all__` smoke test.

Test counts: 101 passed / 8 skipped -> 181 passed / 0 skipped.
Version 2.1.3 -> 2.1.4.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@shinaoka
shinaoka merged commit bd34c74 into mainline Sep 4, 2026
9 checks passed
@shinaoka
shinaoka deleted the fix-audit-findings branch September 4, 2026 12:46
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.

1 participant