Skip to content

Quadrature cap warning: report the real change, name the integral, and test both - #349

Merged
susilehtola merged 2 commits into
masterfrom
quadrature-cap-warning
Sep 21, 2026
Merged

susilehtola merged 2 commits into
masterfrom
quadrature-cap-warning

Conversation

@susilehtola

Copy link
Copy Markdown
Owner

The adaptive quadrature's cap warning reported a relative change that was identically zero, by construction. This fixes that, makes what it reports testable, and makes the warning say which integral it is about — which turns out to matter, because the obvious reading of the warning pointed at the wrong integrand.

1. The reported change was always zero

In converge_block, the cap branch computed its relative change as (cur - prev) after prev = cur a few lines above — the difference of a matrix with itself. Every warning read:

  block magnitude 7.899e+00, relative change still 0.000e+00

which says "converged perfectly" directly under a sentence saying the opposite. It now reports prevdiff / scale, the last difference the convergence test itself saw. Where the cap is reached on the very first probe no comparison was ever made, and that now says so rather than also printing 0 — the old code conflated "no comparison" with "the change was small".

2. The warning was firing spuriously

With the number honest, the warning turned out to fire on diatomic-H2-hf-r, a CI case, reporting 1.2e-01. The source is kinetic()'s radial_integral(-1,0): the integrand 1/sinh(μ) diverges logarithmically at μ = 0, so the block magnitude grows with quadrature order. The divergence is confined to the row and column of the one basis function non-zero at μ = 0 — exactly the function remove_boundaries drops from every m ≠ 0 shell, the only consumer. The block is now judged without those entries (nskip), and returned whole.

H₂ is unchanged at −1.1336295702, the HF limit to all printed digits, and the warning no longer fires.

3. The label pointed at the wrong integrand

Every weight reported as a bare radial_integral, so a cap couldn't be traced to its integrand. That invites a specific misreading. quadrupole_zz() needs radial_integral(1,4) = sinh(μ)cosh⁴(μ), and a cap seen while building it naturally gets blamed on (1,4). But (1,4) converges to machine precision on the first comparison across the entire focal-separation range, even where the integrand spans eighteen orders of magnitude:

Rhalf μ_max (cosh μ_max)⁵ (1,4) block scale rel at n=60
0.7 4.05 1.9e+07 1.8e+06 7.7e-16
0.005 8.99 1.0e+18 9.3e+16 3.4e-16

It's entire, and Gauss-Lobatto resolves it. The warning that actually fires is kinetic()'s (-1,0), whose magnitude (7.9 on H₂, 8.1 in the diatomic CAS test) is the same class as a cap of magnitude ~7 reported against a quadrupole build — and any Hamiltonian calls kinetic(). The warning now names the (m, n).

4. Making it testable

converge_block sat in an anonymous namespace in basis.cpp, and the numbers its warning reports were computed inside the one-shot print guard — unobservable after the first cap in a process, and unreachable from outside basis.cpp entirely. A test that merely checks the warning fires passes on the original bug.

It moves to converge_block.h (a template over the probe, so a header is its natural home) and gains an optional CapReport, filled on every call whether or not anything prints. converge_block_test drives it with probes known in closed form:

case probe asserts
never converges every entry c·n rel == (512−320)/512 = 0.375 exactly, and > 0
capped on first probe seeded past 512 rel == −1: no comparison was made
converges constant block not capped, stops at n = 10
nskip row/col 0 grow as log n caps judged whole, converges with nskip = 1

It was verified to discriminate. With the old (cur − prev) computation reinstated it prints relative change still 0.000e+00 and three assertions fail.

Verification

converge_block_test   PASSED
diatomic-H2-hf-r      no warning, E = −1.1336295702 (unchanged)
ctest -L unit         10/10
ctest (full enabled)  60/60

🤖 Generated with Claude Code

susilehtola and others added 2 commits September 21, 2026 12:05
converge_block's cap warning computed its relative change as (cur - prev)
AFTER prev had been overwritten with cur three lines above, so it printed
"relative change still 0.000e+00" unconditionally -- however badly a block
was actually converging. Report prevdiff, the last difference the
convergence test itself saw, and say so explicitly in the one case where
the seed order already exceeded the cap and no difference was ever
computed.

With that fixed the warning immediately reported 1.191e-01 on
diatomic-H2-hf-r, a CI case. Real, but harmless: radial_integral(-1,0) is
the integral of B_i B_j / sinh(mu), which diverges logarithmically at
mu = 0. The convergence history shows the block MAGNITUDE growing with the
quadrature order (5.57 -> 6.96 -> 7.90) while every other block reaches
1e-15 at n=160, and the growth is exactly logarithmic: the 320 -> 512 step
is a factor 1.6, and 1.39 * log(512/320)/log(2) = 0.94, the measured
difference to two digits.

The divergence is confined to the row and column of the one basis function
that is non-zero at mu = 0 -- and that is exactly the function Nbf() drops
from every m != 0 shell, which is the only place the integral is used. It
is discarded before the matrix is ever seen, which is WHY it is dropped.
So converge_block gains an nskip parameter excluding those entries from
the convergence test (the block is still returned whole), and
radial_integral passes it for m < 0.

H2 is unchanged at -1.1336295702, the HF limit to all printed digits, and
the warning no longer fires.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous commit makes the quadrature cap warning report the relative change
the convergence test actually saw, instead of a value that was identically
zero. This one makes that checkable, and fixes the other half of why the
warning was hard to act on.

WHAT THE WARNING COULD NOT SAY

Every weight passed to radial_integral reported as a bare "radial_integral",
so a cap could not be traced to its integrand. That produced a misattribution
worth recording, because the obvious reading is wrong.

A cap seen while building quadrupole_zz() -- whose I14 is radial_integral(1,4),
sinh(mu) cosh^4(mu) -- invites blaming (1,4). But (1,4) converges to machine
precision on the FIRST comparison across the whole focal-separation range, even
where the integrand spans eighteen orders of magnitude:

    Rhalf  mu_max  (cosh mu_max)^5   (1,4) block scale   rel at n=60
    0.7    4.05    1.9e+07           1.8e+06             7.7e-16
    0.005  8.99    1.0e+18           9.3e+16             3.4e-16

It is entire, and Gauss-Lobatto resolves it. The warning that actually fires is
kinetic()'s radial_integral(-1,0): 1/sinh(mu), logarithmically divergent at
mu = 0 in exactly the row and column remove_boundaries discards -- which the
previous commit already stops judging the block on. Its reported magnitude
(7.9 on diatomic-H2-hf-r, 8.1 in the diatomic CAS test) is the same class as a
cap of magnitude ~7 reported against a quadrupole build. Any Hamiltonian calls
kinetic(), so the label, not the integrand, was the problem. The warning now
names the (m, n).

MAKING IT TESTABLE

converge_block sat in an anonymous namespace in basis.cpp, and the numbers its
warning reports were computed INSIDE the one-shot print guard -- so after the
first cap in a process they were unobservable, and from outside basis.cpp
there was no way to reach them at all. A test that merely checks the warning
FIRES passes on the original bug.

So it moves to converge_block.h (it is a template over the probe; a header is
its natural home) and gains an optional CapReport, filled on every call
whether or not anything prints:

  - rel is the change the test last saw, over the judged sub-block;
  - rel == -1 means the cap was reached on the very first probe, so no
    comparison was made -- previously also reported as 0, indistinguishable
    from "the change was small";
  - twoe_nmax / twoe_cap_warned become inline variables, one per program as
    before.

converge_block_test drives it with probes known in closed form. The
non-converging one (every entry c*n) must report rel = (512-320)/512 = 0.375
exactly. It was verified to DISCRIMINATE: with the old (cur - prev)
computation reinstated it prints "relative change still 0.000e+00" and three
assertions fail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@susilehtola
susilehtola merged commit a0bbbff into master Sep 21, 2026
1 check passed
@susilehtola
susilehtola deleted the quadrature-cap-warning branch September 21, 2026 09:58
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