Skip to content

Add an arbitrary-precision generator for the angular grid tables - #110

Open
susilehtola wants to merge 17 commits into
wavefunction91:masterfrom
susilehtola:tools/angular-grid-generator
Open

Add an arbitrary-precision generator for the angular grid tables#110
susilehtola wants to merge 17 commits into
wavefunction91:masterfrom
susilehtola:tools/angular-grid-generator

Conversation

@susilehtola

Copy link
Copy Markdown
Collaborator

Adds tools/angular_grids, an offline generator that regenerates the
tabulated angular grids at arbitrary precision, plus the small runtime
helper the regenerated tables need.

Why

The shipped tables carry about 16 significant digits, so LebedevLaikov<T>
cannot be more accurate than double however wide T is. The format makes
this worse than it looks: the tables are unsuffixed floating literals inside
std::array<T, N>, and an unsuffixed literal has type double whatever T
is. Writing more digits into the existing format would change nothing.

Measured on the 110-point rule, integrating x^4 y^2 z^2:

precision current table regenerated
double 1.91e-15 1.74e-15
long double 1.26e-15 2.55e-19
cpp_bin_float_50 not achievable 1.94e-40

Approach

Finding a spherical quadrature rule and refining one are different problems,
and only the second is needed here. Construction is a global optimisation
with many local minima and wants a trust-region method, but only at double
precision. Refinement starts from a rule already correct to 16 digits, is
purely local, and Newton converges quadratically. For every family already
in the library, construction is done: the tables are its output.

Tractability comes from symmetry. Lebedev-Laikov and Delley are octahedrally
invariant; Ahrens-Beylkin is invariant under the icosahedral rotation group
(order 60, no inversion -- which is why those grids are not antipodally
symmetric, and why 15012 points divide as 15012/60 = 250.2 against 251
distinct weights). Womersley grids are equal-weight spherical designs with no
symmetry at all, handled matrix-free.

grid orbits free parameters
Lebedev-Laikov 5810 144 385
Delley 3470 90 234
Ahrens-Beylkin 15012 251 751

So the largest Lebedev rule is a 385-parameter solve rather than a
17430-parameter one.

Output format

Regenerated headers store the decimal digits as strings and convert them
through detail::grid_scalar<T>::parse at grid construction, which works for
the built-in types as well as Boost.Multiprecision and MPFR, and costs one
parse per grid. load() is templated on its containers, so it fills the
library's std::vector<cartesian_pt_t<T>> and std::vector<T> in exactly
the role detail::copy_grid plays today.

Verified

  • Delley fully regenerated, 18/18 at 40 digits.
  • switch_to_load applied to the Delley dispatch: 18 calls rewritten, the
    4.0*M_PI normalisation dropped, 9/9 library tests pass.
  • Tool regression suite 11/11 (8 octahedral, 3 designs), each checked against
    the full redundant condition set rather than the reduced one used to fit.
  • This branch builds clean and passes 9/9 without any table swapped in; the
    only library-side addition is util/grid_scalar.hpp.

Nothing shipped is changed by this PR -- the generator is offline and no
table is replaced here. Regenerating and switching a family is a deliberate
follow-up, one family at a time.

Numerics worth reviewing

The larger grids are badly conditioned, and getting them to converge took
several passes:

  • The Gauss-Newton step is a rank-revealing modified Gram-Schmidt QR of the
    Jacobian, never the normal equations. J^T J squares the condition number,
    and with a threshold on the normal equations the failures trade off against
    each other: delley_974 stalls at 3.1e-29 instead of 6.9e-51, while
    tightening the threshold puts delley_1454 back to "numerically singular".
  • Columns are equilibrated first -- a weight column and an angular column
    differ by orders of magnitude, and that disparity alone wrecks the solve.
  • Working precision is sized from the parameter count. Measured on the
    equilibrated Jacobian, the condition number is 2.0e+24 at 120 parameters
    (delley_1730) and 8.19e+26 at 140 (delley_2030). A fixed guard is simply the
    wrong model; the retry ladder is the backstop.
  • The step is damped and constrained to the orbits' domain. bk is
    (l, l, sqrt(1-2 l^2)) and only real for l <= 1/sqrt(2); an undamped step
    overshoots, mpmath returns a complex root, and it surfaces far away as a
    TypeError about ordering complex numbers.

Every one of those was found by running the full batch, not by the regression
suite, which uses small well-conditioned grids. If this lands, the suite would
benefit from at least one large size per family even at the cost of runtime.

Not covered

The 552-point Ahrens-Beylkin table (#108) cannot be regenerated by refinement.
Its points admit no valid weighting at all: the conditions are linear in the
weights, and a least-squares solve over the full order-39 set gives 2.4e-02
with five negative weights, against 8.3e-14 for AB-612 as a control. So the
positions are wrong rather than imprecise. Gauss-Newton diverges from them and
Levenberg-Marquardt plateaus five orders short. That size needs the original
Ahrens-Beylkin data, or to stay withdrawn as #101 leaves it.

🤖 Generated with Claude Code

susilehtola and others added 17 commits August 29, 2026 19:29
The tabulated Lebedev-Laikov and Delley grids carry about 16 significant
digits, so LebedevLaikov<T> cannot be more accurate than double however
wide T is. The format makes this worse than it looks: the tables are
unsuffixed floating literals inside std::array<T,N>, and an unsuffixed
literal has type double whatever T is, so writing more digits into the
existing format would change nothing at all.

Add a generator that refines the existing rules to any requested
precision.

Finding a spherical quadrature rule and refining one are different
problems, and only the second is needed here. Construction is a global
optimisation with many local minima and wants second-order globalisation
(a trust-region solver), but only at double precision, since the goal is
just to land in the right basin. Refinement starts from a rule already
correct to 16 digits, is purely local, and Newton converges quadratically
-- two or three iterations reach any precision, with no globalisation.
For the existing families construction is already done: the tables are
the result.

What makes it cheap is symmetry. Both families are octahedrally
invariant, so points fall into orbits carrying one weight and zero, one
or two angular parameters. The 5810-point Lebedev rule is a
385-parameter dense Newton solve rather than a 17430-parameter one.

The exactness conditions are imposed on even monomials in x and y only:
on the unit sphere z^2 = 1 - x^2 - y^2, so every even monomial reduces to
a fixed linear combination of those, and imposing them implies the rest
at about a seventh of the cost. Results are always verified against the
full redundant set, never the reduced one.

Emitted headers carry decimal strings rather than literals, converted
through detail::grid_scalar<T>::parse at construction. That works for the
built-in types as well as Boost.Multiprecision and MPFR, and costs one
parse per grid.

Measured on the 110-point rule integrating x^4 y^2 z^2:

                       current table   regenerated
    double               1.91e-15        1.74e-15
    long double          1.26e-15        2.55e-19
    cpp_bin_float_50     unreachable     1.94e-40

This commit adds the tool and the scalar-parsing helper only; no shipped
table is regenerated yet. Ahrens-Beylkin needs icosahedral orbit algebra
and Womersley needs a matrix-free least-squares step; both are described
in the README.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FDTFYJMQ76iujDFNHzZyXF
Adds the Womersley path to the generator. These grids have no symmetry to
exploit -- every point is its own orbit -- so the unknowns are the 2N
tangential degrees of freedom of N points and the Jacobian is never
formed: CGLS needs only J*v and J^T*u.

Points are carried as unit 3-vectors with steps taken in each point's
tangent plane, not as (theta, phi). The spherical chart is singular at
the poles and real designs put points there: the 32-point design has one
at exactly z = 1, where d(p)/d(phi) vanishes. In that chart the Jacobian
acquires a structurally zero column that is a coordinate artifact rather
than a symmetry, and rotations about x and y are not expressible as
finite tangent vectors at all. Measured on the 32-point design:

                    zero cols   near-null   rotation generators
    (theta, phi)            1           3   3.4e-02, 1.3e-16, 2.0e-16
    tangent frame           0           3   2.0e-16, 1.4e-16, 2.9e-16

The remaining three-dimensional null space is the genuine one: any
rotation of a design is a design. It needs no special handling, because
CGLS started from a zero step keeps every iterate in range(J^T), which is
orthogonal to the null space, and so returns the minimum-norm -- already
rotation-projected -- step. Design.rotation_generators() exposes them
anyway, since a trust-region method used to *construct* a design does
need to project them out.

Two defects found by measurement rather than by reasoning, both recorded
in the source so they are not reintroduced:

* The tabulated points are unit vectors only to double precision, and
  carrying 3-vectors inherits that inconsistency with |p| = 1. It
  corrupts the Jacobian at the same 1e-16 level the refinement is trying
  to work below -- 8.0e-16 relative error against finite differences,
  1.7e-21 after renormalising -- and stalled the first Newton step. The
  (theta, phi) chart had normalised implicitly.

* CG terminates in rank-many steps only in exact arithmetic; its rate
  goes as the square root of the condition number. A cap of 2*2N starves
  it, and the outer iteration then converges convincingly to a spurious
  floor. On the 50-point design, capped at 200 iterations the residual
  stalls at 2.45e-18, while at 600 the same first step reaches 2.16e-28.
  The cap is now 12*2N with the residual tolerance doing the real
  stopping and a stagnation guard for early exit.

With both fixed, against the full monomial set:

    womersley_32 (order 7):  7.39e-16 -> 3.82e-30 -> 9.18e-41
    womersley_50 (order 9):  5.75e-16 -> 2.16e-28 -> 4.96e-41

Conditions use monomials with c in {0,1}: on the sphere z^2 reduces,
leaving exactly (order+1)^2 of them -- the dimension of the polynomials
of degree <= order on S^2 -- rather than C(order+3,3). For order 7 that
is 64 rather than 120.

read_grid() also learns the second weight format: the Womersley headers
synthesise their equal weights with create_array<N,T>(4*M_PI/N) instead
of listing them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FDTFYJMQ76iujDFNHzZyXF
The AB grids are invariant under the icosahedral rotation group I (order
60) rather than the octahedral group, and without inversion -- which is
why they are not antipodally symmetric and why 15012 points divide as
15012/60 = 250.2 against 251 distinct weights.

The group is constructed numerically instead of from memorised
generators: it is exactly the set of rotations carrying the
icosahedron's vertices to themselves, and each is fixed by naming where
one vertex and one of its neighbours go. Verified: 60 elements, all
proper rotations, max |R^T R - I| = 3.9e-31.

Every tabulated AB grid decomposes cleanly:

    AB-552     10 orbits   12 + 9x60      28 free parameters
    AB-612     11 orbits   12 + 10x60     31 free parameters
    AB-15012  251 orbits   12 + 250x60   751 free parameters

so even the largest is a 751-parameter problem rather than a
45036-parameter one.

Two tolerance defects fixed along the way, both the same shape -- a
tolerance tighter than the accuracy of the data it is applied to:

* read_grid()'s numeric pattern required an exponent. ahrens_beylkin_552
  mixes plain decimals with exponent form inside one array, so the
  parser silently read 48 points where there are 552. Any file in that
  format was being misread.

* orbit() de-duplicated at 1e-18 while its input is tabulated to double
  precision, so images that should coincide differ by ~1e-16 and a
  12-point orbit came back as 60 near-duplicates. De-duplication is by
  distance rather than by formatting coordinates, since a coordinate
  that should be zero comes out as +-1e-31 and two such values have
  different decimal representations.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FDTFYJMQ76iujDFNHzZyXF
Adds the icosahedral refinement path, mirroring the octahedral one. Each
orbit carries one weight; a generic 60-point orbit carries two more
parameters as the position of its representative, stepped in that point's
tangent plane rather than through spherical angles. Because the orbit is
generated by the group action, the derivative of an orbit point with
respect to a tangent step of its representative is just the group element
applied to that step.

    AB-72    order 14   2 orbits   4 params   1.75e-15 -> 1.58e-30
    AB-132   order 19   3 orbits   7 params   4.34e-16 -> 7.89e-31
    AB-192   order 23   4 orbits  10 params   7.66e-16 -> 1.58e-30

two Newton steps each, from the tabulated double-precision data.

Two defects found and fixed while getting there:

* decompose() used the rounded weight as both the grouping key and the
  weight value, truncating it to the key's 12 digits. It showed up as
  1.7e-11 in the sum of weights while the points matched to 4.7e-33.

* A 12-, 20- or 30-point orbit has no free parameter -- its position is
  fixed by the symmetry -- but taking the representative from tabulated
  double-precision data makes it a *generic* point whose 60 images
  cluster in fives rather than coinciding. De-duplication then keeps an
  arbitrary member of each cluster and injects ~1e-16 of asymmetry into a
  grid that should be exactly symmetric. Special orbits are now snapped
  onto the exact vertex, face-centre or edge-midpoint position, rotated
  onto the tabulated one.

The second was masked by the residual: summing a monomial over all 60
group images annihilates odd moments identically, whatever the
representative, so the odd-moment conditions carry no information about
the fit and could not see the asymmetry. Verification against the
materialised grid does see it, and now agrees with the residual.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FDTFYJMQ76iujDFNHzZyXF
Three changes, all needed before the larger Ahrens-Beylkin grids are
reachable.

Condition selection. The full set is (order+1)^2 monomials -- 1600 at
order 39 -- against a few dozen unknowns, since the icosahedral symmetry
makes most of them dependent. Candidates are now visited stratified
across degree and kept when their Jacobian row increases the column
rank, stopping early once the rank is full. AB-192 drops from 576
conditions to 30. Results are always verified against the full set, so a
bad selection surfaces as a failed verification rather than a silently
wrong grid.

Image cache hoisted out of residual_and_jacobian. Selection asks for many
single-row Jacobians, and rebuilding the 60 group images per row
dominated: AB-192 selection went from 27s to 9s, and its refinement from
45s to 1s.

Rank-aware step. The Jacobian is not always full rank -- AB-312 comes out
13 of 16 -- and both a normal-equations LU and mpmath's qr_solve refuse a
singular matrix outright. The step is now solved only in an independent
set of columns, found by modified Gram-Schmidt with a relative threshold,
leaving the unresolved directions at zero. Same lesson as the Womersley
path, where CGLS gave the minimum-norm step for free.

Also records a negative result: refining AB-552 from its tabulated data
diverges, 0.4619 -> 0.03831 -> 145.9. That is expected rather than
surprising -- its points are wrong, not merely imprecise, and no
weighting of them integrates to order 39 -- but it settles that the
corrupt table cannot be recovered by Newton refinement. It needs
globalisation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FDTFYJMQ76iujDFNHzZyXF
generate.py now dispatches on the family: octahedral refinement for
Lebedev-Laikov and Delley, icosahedral for Ahrens-Beylkin, and the
equal-weight design path for Womersley. Each verifies against the full
condition set before emitting, and refuses to write a table that did not
reach the requested precision.

    ahrens_beylkin_72: order 14, 2 icosahedral orbits, 4 free parameters
      selected 12 of 225 conditions, rank 4/4
      1.754e-15 -> 2.091e-33
      verified 2.431e-32 against the full condition set

The emitted load() is now templated on its containers, so it fills the
library's std::vector<cartesian_pt_t<T>> and std::vector<T> directly --
the same role detail::copy_grid plays for the literal tables, which is
what the eventual switch needs. Verified by filling those vectors from a
generated header.

Two smaller fixes: the emit namespace and title tables only knew the two
octahedral families, so an Ahrens-Beylkin header came out in namespace
`ahrens_beylkin` rather than `AhrensBeylkinGrids`; and condition
selection now caps its walk, since a genuinely rank-deficient Jacobian
means the rank never fills and the search would otherwise visit all 1600
candidates. An incomplete basis is harmless -- the step solver handles
deficiency, and verification against the full set is the real guard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FDTFYJMQ76iujDFNHzZyXF
batch.py regenerates every tabulated size of a family, reading the sizes
from the family's own algebraic_order_by_npts table so the two cannot
drift apart. Sizes are independent, so they run in separate processes. A
size that fails to reach the requested precision is reported and skipped
rather than written, so a partial run never emits an unverified table.

    delley: 6 sizes, 30 digits, 4 jobs
      ok    delley_14     err 2.552e-51
      ok    delley_26     err 4.786e-51
      ok    delley_50     err 5.264e-51
      ok    delley_110    err 1.246e-49
      ok    delley_194    err 6.018e-49
      ok    delley_302    err 2.163e-48

switch_to_load.py points a family's dispatch header at the regenerated
tables:

    detail::copy_grid<delley_50<RealType>>(points, weights)
        ->  delley_50<RealType>::load(points, weights)

and drops the `weights[i] *= 4.0*M_PI` loop for the two families whose
shipped tables are normalised to one -- a regenerated table already
carries the 4*pi convention. The loop index is `auto` in delley.hpp and
`size_t` in lebedev_laikov.hpp, so the pattern does not pin the type.

Dry runs cover all four families: 31 calls in lebedev_laikov, 18 in
delley, 56 in ahrens_beylkin, 125 in womersley.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FDTFYJMQ76iujDFNHzZyXF
Records what is known about the one table that cannot be regenerated by
refinement, with AB-612 as a control throughout.

The decisive result is that the AB-552 points admit no valid weighting.
The exactness conditions are linear in the weights, so the best possible
weights for a fixed point set follow from one least-squares solve:

    AB-612 (known good)   8.3e-14, all positive
    AB-552                2.4e-02, five negative weights

The positions are wrong, not just the weights, and not merely imprecise.

Refinement behaves accordingly: Gauss-Newton diverges (0.4619 -> 0.03831
-> 145.9), and Levenberg-Marquardt globalises the step and makes real
progress (0.4619 -> 9.99e-4 -> 7.75e-6 -> 1.19e-7) but plateaus five
orders short, on a condition subset that does not determine all 28
parameters.

Also records what is *not* evidence, since I nearly reported it as such:
the Jacobian's rank over low-degree conditions is 6 against 28
parameters, which looks alarming until AB-612 is measured the same way
and gives rank 6 against 31. Few icosahedral invariants exist at low
degree, so that number says nothing about either grid.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FDTFYJMQ76iujDFNHzZyXF
Pointing --out at the family's own data directory destroys the input.
Generation reads the shipped literal table for each size, so a partial
run replaces the tables the remaining sizes still need, and every later
size then fails with "substring not found" -- which is what happened, on
a scratch worktree, when I did exactly this.

Generate into a separate directory and install the results deliberately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FDTFYJMQ76iujDFNHzZyXF
The octahedral refinement still solved its normal equations with a plain
lu_solve, and mpmath refuses a singular matrix outright. That surfaced as
delley_1454 failing with "matrix is numerically singular" at 20 digits
while succeeding at 40 -- so it is a robustness property of the solver
rather than of any particular grid, and it would have bitten whichever
size happened to be marginal at the chosen precision.

Move independent_columns() and the step solver into linalg.py and use
them from both paths. delley_1454 now regenerates at 20 digits, verified
to 1.287e-24 against the full condition set for order 65. Regression
suite 11/11.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FDTFYJMQ76iujDFNHzZyXF
The rank-aware step I added earlier used a fixed 1e-16 threshold on the
normal equations, and that discarded columns which were merely
ill-conditioned rather than null. delley_974 lost its convergence as a
result -- 3.11e-29 instead of 6.93e-51, failing the generator's own
precision check. The batch caught it; the unit tests did not.

Chasing the threshold does not work, because the two failure modes trade
off against each other:

    threshold              delley_974 @ 40    delley_1454 @ 20
    fixed 1e-16            stalls 3.1e-29     solves
    precision-tied         6.93e-51           "numerically singular"
    QR, 1e-14 cut          11 iters, 2e-33    diverges, 1.7e+146
    QR, precision-tied     6.93e-51, 2 iters  needs more precision

The tension is not in the threshold. It is that J^T J squares the
condition number. The step is now a rank-revealing modified Gram-Schmidt
QR of J itself, back-substituted, never forming the normal equations.
Checked against a synthetic rank-deficient system where it finds rank 2
and leaves a 1e-31 residual.

The threshold that remains cuts only what is unresolvable at the working
precision, so it discards genuinely null directions and keeps everything
else. delley_1454 at 20 digits is then simply a case that needs more
precision than it was given -- 102 parameters at that conditioning are
not resolvable in 20 digits by any of these methods, and it refines
cleanly at 40. The generator's final verification catches it and refuses
to write the table, which is the right outcome.

Regression suite 11/11.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FDTFYJMQ76iujDFNHzZyXF
Records why the step is a rank-revealing QR of the Jacobian rather than a
solve of the normal equations, and that a size needing more working
precision than it is given fails verification instead of being written.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FDTFYJMQ76iujDFNHzZyXF
The octahedral orbit types carry implicit constraints. bk is
(l, l, sqrt(1-2 l^2)) and is only real for l <= 1/sqrt(2); ck and dk have
their own. A Newton step can overshoot one, and mpmath then returns a
complex square root:

    l = 0.30000000   sqrt(1-2 l^2) = 0.90553851
    l = 0.70710678   sqrt(1-2 l^2) = 5.7931539e-5
    l = 0.72000000   sqrt(1-2 l^2) = (0.0 + 0.19183326j)

The complex value then propagates silently until something tries to order
two of them, which is where it finally surfaced -- as
"TypeError: '<=' not supported between instances of 'mpc' and 'mpc'",
far from the cause. It took out delley_2354, 2702, 3074 and 3470, the
four largest sizes in the family.

Halve the step until every orbit base is real again. Backtracking is
cheaper than reparameterising the orbits, and it leaves the well-behaved
sizes untouched.

Found by the batch, not by the test suite: the eleven regression cases
use small grids whose parameters sit well inside their domains, and only
the 2354-and-larger sizes push one to the boundary.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FDTFYJMQ76iujDFNHzZyXF
Backtracking only on the orbits' domain was not enough. It stops the
complex-square-root failure, but on the larger Delley grids the undamped
step is simply bad: delley_2354 (order 83, 64 orbits, 161 parameters)
walks away from a starting residual of 1.1e-16 to 6.4, 6.2, 5.6 over
successive iterations.

Halve the step until it both keeps every orbit real and reduces the
residual, and give up rather than accept an increase. This is ordinary
damped Newton; the quadratic convergence on the well-behaved sizes is
unaffected, since the full step is accepted on the first try there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FDTFYJMQ76iujDFNHzZyXF
A weight column and an angular column differ by orders of magnitude, and
that disparity alone wrecks the conditioning of the least-squares solve.
On delley_2354 (order 83, 161 parameters) the undamped step is unusable,
and once damped the iteration crawls -- a factor of two per step instead
of squaring.

Scale every column to unit norm before the QR and unscale the step
afterwards. It costs one pass over the Jacobian and is the difference
between converging and not.

Regression suite 11/11.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FDTFYJMQ76iujDFNHzZyXF
The guard digits were a fixed margin, and that is wrong because the
conditioning varies enormously with grid size. Measured after column
equilibration, delley_1730's Jacobian has a condition number of 2.0e+24
-- so 24 of the working digits are gone before the step is even computed,
and the larger sizes are worse. That is what has been defeating the four
biggest Delley grids: a bad step from an under-resourced solve, which
then overshot an orbit's domain and surfaced as a complex square root.

Rather than predict the conditioning, try a modest guard and retry with
more when the final verification falls short. The well-conditioned sizes
pay nothing, since they pass on the first attempt.

This is the root cause behind the last three commits. Each of those was a
correct fix for a real symptom -- complex roots, divergence, crawling
convergence -- but all three were downstream of a step computed with
insufficient precision for the conditioning.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FDTFYJMQ76iujDFNHzZyXF
Retrying on failure works but wastes a long attempt first, and on the
biggest Delley grids that attempt takes many minutes. The conditioning is
predictable enough to start from a sensible guard: measured on the
equilibrated Jacobian it is 2.0e+24 at 120 parameters (delley_1730) and
8.19e+26 at 140 (delley_2030), so log10(cond) is roughly 0.15 * params.

The extra guard this gives:

    delley_110      0        delley_2030    24
    delley_974     10        delley_2354    28
    delley_1730    20        delley_3470    42

Small grids are unaffected and pay nothing. The retry ladder stays as the
backstop for anything the estimate underestimates.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FDTFYJMQ76iujDFNHzZyXF
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