Repo-specific review guidance for torchquad. These rules have the highest
priority — they reflect what actually goes wrong in this repo and what torchquad
promises its users. CLAUDE.md is binding and may be cited directly in reviews
("backend-specific calls belong only in utils/, per CLAUDE.md rule 3"): a change
that makes a CLAUDE.md statement outdated is itself a finding.
torchquad is cited in published research that reports numerical results, runs on four backends (PyTorch, JAX, TensorFlow, NumPy) from one API, and is fully differentiable on the framework backends. That shapes the priority order below: correctness outranks everything, backend-agnosticism and vectorization are non-negotiable structural rules, and only then the general fail-hard / dead-code / simplicity concerns. Order is by how much damage the issue does here, not by how often it is typed.
This is the headline feature. A crash is recoverable; a wrong number in a cited
paper is not. Scrutinise anything touching a numerical kernel
(integration/, weights, grids, RNG, VEGAS map/stratification).
- Compare against analytic ground truth. New or changed integrators, samplers,
and error estimates must be checked against the closed-form integrals in
tests/integration_test_functions.py, not just "runs without error" — and against the whole collection viahelper_functions.py::compute_integration_test_errors(real + complex, 1-D/3-D/10-D), not one or two ad-hoc integrands. A reported error (e.g. VEGAS'ssdev/chi2) must itself be validated against the true error from these functions, not merely bound-checked. A bug fix needs a regression test that would fail on the old code. - A loosened tolerance is a finding until justified. If an assertion's tolerance was widened to make a test pass, the PR must explain why the extra error is real hardware/reduction-order non-associativity and not a genuine regression (issue #178/#179 is the cautionary tale — tolerances bumped to hide a GPU precision gap). Prefer testing method correctness in fp64; document any per-(backend, dtype, device) tolerance with its rationale.
- Determinism must survive. VEGAS, MonteCarlo, and any RNG-using path must
reproduce bit-for-bit given the same
RNG. Flag any change that reorders RNG draws, reseeds differently, or makes results seed-order-dependent without a deterministic-snapshot test. - Differentiability is load-bearing. torchquad's moat is autodiff through the
integral. Flag a
.detach(),.item(),float(...),int(...), in-place write, or NumPy round-trip on any tensor on the gradient path — it silently severs the graph. VEGAS map's fp64-cumsum branch is the known risk area. If the value flows to a gradient, a gradient test must cover it. - Cross-backend agreement is a property, not a hope. The same
(integrator, integrand, domain) should agree across backends to fp-ULP × a small
constant. Dtype-promotion,
anp.meshgrid-returns-a-list (#214), and integer-domain-produces-zero (#180) bugs hide here.
CLAUDE.md rule 3: code under integration/ and utils/ (except the three setup
modules) must go through from autoray import numpy as anp and
infer_backend(...). This is the single most torchquad-specific structural rule.
- Flag every raw
torch.*,jnp.*/jax.*,tf.*, or barenp.*inintegration/. Backend-specific paths belong only inutils/enable_cuda.py,utils/set_precision.py,utils/set_up_backend.py. - Backend must be inferred from the input tensors via
infer_backend(x), not assumed from a global or fromset_up_backend's last call. A torch tensor in must select torch math even if the default backend is something else. - Inline
if infer_backend(x) == "torch": ...special-cases scattered through a kernel are a smell — prefer a single named helper over duplicating the branch. A hardValueErrorrejecting JAX/TF (as VEGAS does) should read from a single supported-backends source of truth, not be duplicated. - New autoray
register/repeat/expand_dimsshims belong in a private compat module, not in__init__.py.
CLAUDE.md rule 4. Integrand evaluation is batched along the first dimension.
- Flag any Python-level
for/whileloop over sample points, grid points, or strata that could be a batched tensor op. "It's clearer as a loop" does not win here — on a GPU it is a correctness-of-performance red flag worth discussing before merge. - All integrand invocation goes through
BaseIntegrator.evaluate_integrand— it enforces backend consistency, weight broadcasting, and shape checks. Any code callingfn(points, *args)directly is a bug; flag it. - A genuinely un-vectorizable step is a design discussion, not a quiet loop.
CLAUDE.md rule 5. For every defaulted or None-tolerant value ask: should this
ever be absent? Should it raise instead?
- No
dict.get(key, default)orgetattr(obj, ..., default)for config / required inputs — directdict["key"]/ attribute access; a missing value is a bug that should surface asKeyError/AttributeError. - No silent precision downgrade and no silent backend/dtype coercion — raise on a dtype/backend mismatch rather than casting quietly (the integer-domain → 0 bug).
- No bare
except:/except Exception: pass. Broken invariants raise; recoverable issues uselogger.warning(). A swallowed exception on a numerical path is a top finding.
CLAUDE.md rule 6, issue #184. The library pollutes downstream apps if it touches global loguru state at import.
- Flag any
logger.add(...),logger.remove(), or loguru reconfiguration in library code (import-time or insideset_log_level). The correct lever islogger.disable("torchquad")/enable. Usefrom loguru import loggerdirectly; do not construct new sinks. - Logging level comes from
TORCHQUAD_LOG_LEVELviaset_log_level()only.
CLAUDE.md rule 13; vulture runs in CI. The recurring probe is "Can this ever happen? Is this branch reachable? Do we still need this?"
- Flag functions never called, values assigned but never read, stubs that fulfil
no base-class contract (the dead
RNG.uniformstub is the archetype), and guards for states that cannot occur. - Flag duplicated definitions — the same logic in two places is both a DRY and a
correctness risk (e.g.
grid_funcduplicated betweenintegration_grid.pyandgrid_integrator.py; the library-sidesys.path.appendinintegration/utils.py). Require one definition others import. - Delete rather than comment out or whitelist — git history is the archive. A symbol kept for API reasons needs a docstring saying why.
CLAUDE.md rules 8, 10, 13.
- A changed public signature must keep the old path working for one release with a
warnings.warn(..., DeprecationWarning), and the warning must actually be visible — greppyproject.toml'sfilterwarningsso it isn't suppressed. A suppressed self-deprecation warning that never resolves is exactly the anti-pattern being removed in 0.6. - A name in
__all__must not start with an underscore — the two contradict each other._deployment_testwas the archetype (underscore and in__all__); the fix was to drop it from__all__and keep it private (it is an internal self-test, not user-facing), not to make it public. No abbreviations in new code:integration_domainnotint_dom,function_valuesnotfvals.
CLAUDE.md rule 12.
- Guard clauses over deep nesting; keep bodies a couple of levels deep. Functions under ~100 lines, files under ~1000 — flag a PR that pushes past these instead of splitting.
- Prefer small functions and typed dataclasses over loose dicts threaded between
methods (the VEGAS result object is the canonical example — a dataclass with
value/sdev/chi²/dof, not a bare float or a dict). Put shared dataclasses in a
dependency-free module to avoid circular imports (
deployment_test's circular import back into the initializing package is the warning here).
CLAUDE.md rules 7, 9.
- Google-style docstrings (
Args,Returns,Raises) on public functions and classes. An integrator with an N constraint (Boole, Simpson's odd-N) or a backend restriction (VEGAS: no JAX/TF) must state it in aRaises:/Note:section — pydoclint is being added to gate this. - Keep
README.md,docs/source/, and CLAUDE.md in sync with code. Flag claims that overstate reality (stale benchmark numbers, "first GPU VEGAS", ROCm "investigating", deprecatedpytorchconda channel). Comments explain why, not what — a comment paraphrasing the next line is noise, delete it. - Every change needs a test: bug fixes get a regression test; new integrators get
both an analytic-correctness test and a cross-backend coverage test
(parametrized via
helper_functions.py). Tests run fromtests/.
- Drop unrelated diffs (notebook/log/
egg-infochurn, formatting-only noise mixed into a logic change). Defer broader cleanups to their own issue/PR rather than widening the current one. Runruff check+ruff format(line length 100) before review; CI gates on both, so a formatting-only round-trip in the diff is itself a finding.