Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
05fdd94
wip: guard the IQN least-squares against a non-finite matrix
NicholasEhsanRoy Oct 7, 2026
452212e
wip: the gradient bound leaves out a probe the pass does not resolve
NicholasEhsanRoy Oct 7, 2026
6216f77
wip: SVD input guard at every site, regression tests, claims and guide
NicholasEhsanRoy Oct 7, 2026
c0263d4
wip: additive probes stay in the bound; docs
NicholasEhsanRoy Oct 7, 2026
c24eb5b
wip: the plain magnitude rule; records
NicholasEhsanRoy Oct 7, 2026
0114b68
wip: seed for a resolved nonlinearity constant; simplify the spectral…
NicholasEhsanRoy Oct 7, 2026
db71f54
wip: a stuck call fails cleanly
NicholasEhsanRoy Oct 7, 2026
a4e908c
Merge remote-tracking branch 'origin/feat/p4-geom-diag-instruments' i…
NicholasEhsanRoy Oct 7, 2026
457f4b4
fix(coupling): an IQN step returns when its iterate leaves float rang…
NicholasEhsanRoy Oct 7, 2026
c3f1073
test(coupling): recapture the step programs on the IQN guard's commit
NicholasEhsanRoy Oct 7, 2026
93ddf8c
docs(soup): regenerate for MADD-ANO-231 and 232
NicholasEhsanRoy Oct 7, 2026
2cec694
test(coupling): the slow IQN cases state their premises (fori reports…
NicholasEhsanRoy Oct 7, 2026
bb106b4
Merge remote-tracking branch 'origin/release/0.4.0' (7acbe0d6: #255) …
NicholasEhsanRoy Oct 7, 2026
1866fb7
docs(registry): this branch's entries take MADD-ANO-233 and 234 (the …
NicholasEhsanRoy Oct 7, 2026
6c299a5
Merge remote-tracking branch 'origin/release/0.4.0' (88288746: #254, …
NicholasEhsanRoy Oct 7, 2026
7a6ae1c
test(coupling): the slow nonlinear hunt puts no floor on a rounding-d…
NicholasEhsanRoy Oct 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,8 @@ guidance; the itemized changes follow.
The `[verify]` extra now only pulls `hypothesis`.

### Fixed
- **A coupling group under `acceleration="iqn-ils"` or `"iqn-imvj"` returns from `step()` when its iterate leaves float range** (MADD-ANO-233; 0.3.0 and 0.3.1 under `solver="ift"` with `"iqn-imvj"`): the secant least-squares handed LAPACK's SVD a matrix holding an `inf`, and the step never returned. It now runs to `max_iterations` and reports `converged=False`, `residual=inf`, as the other accelerations do; steps of a finite run are unchanged, bit for bit. Action: none; on 0.3.x use `solver="fori"` or `"aitken"`.
- **`gradient_relative_error_bound` covers the constants one pass resolves** (MADD-ANO-234, never released): with a constant in it whose tangent at the returned iterate is rounding (the centre or curve of a nonlinearity evaluated on its centre) it read 1.09 for a relative error of 1.134, usable. Such a constant, and a gain whose whole value moves the pass by less than the residual's float floor, is no longer in the bound; the docstring says which constants are. Action: none.
- **`GraphManager.step`, `run` and `run_adaptive` no longer store a step that reshapes a state leaf** (`MADD-ANO-220`, in 0.1.0 to 0.3.1): a list given for a scalar constant (`BallNode(initial_velocity=[1.0, 2.0])`), or an external input of another shape at a later step, broadcast a scalar leaf, and the state's checkpoint then did not load after `reset_state()`. The step is refused by name (compared once per trace, host-side: no compiled program changes), as `run_scan` always did; the FMU sidecar and bridge refuse it too.
- **`coupling_diagnostics()` describes the step that ran when the state is written afterwards** (MADD-ANO-231, never released): the float floor under `spectral_error_bound`, `precision_limited` and `spectral_usable` was measured on the live state, so `set_node_state` after a step moved that step's report (a bound of 0.0, flag set, with every member written to zero). The graph now keeps what the step left. A checkpoint saved after a member was written says so (an optional archive member), and the graph that loads it reports that group's bound as NaN and its `*_usable` flags False with a `not_usable_reason` until it steps; `_reports` is now a reserved node name. Action: none.
- **`coupling_diagnostics()`: three numbers that read wrong with their flag set, and one found beside them** (MADD-ANO-222, 225, 226, 229, never released): `gradient_relative_error_bound` at the float floor took the change along one stand-in direction (5.1e-6 for 3.0e-5); `rho_spectral` read settled past eight interface scalars (0.273 for 0.219), from sampled rounding on a float32 group far from normal (0.250 for 0.206), and lost a loop below a field's rounding on a sub-cycled group (1e-12 for 1.25e-4).
Expand Down
32 changes: 32 additions & 0 deletions docs/developer_guide/coupling_algorithm_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,38 @@ Arnoldi factor, which is the resolvent restricted to the Krylov space and
read the bound 0.19x the true error on a ring whose secant falls outside
it; and Newton-Kantorovich takes the full-operator resolvent norm.

**Which constants are in it.** A relative error divides by the tangent's
size, so the worst is taken over the probes that have one: a probe whose
tangent is exactly zero is left out (the fixed point does not respond to
it), and so is a probe the pass does not *resolve* — moving the constant
by the probe's whole size (its own magnitude) moves one pass from `x_k`
by no more than the floor the distance carries, `‖s · F_c ċ‖ ≤ floor` in
the bound's own norm. Its right-hand side can then be what the iterate's last rounding,
or a cancellation inside the node's own derivative, left of it, and so is
the tangent solved from it: the saturating `c + tanh(s (u − c)) / s` has
`∂/∂s = d sech²(s d) / s − tanh(s d) / s²`, two terms of size `d / s`
that cancel to `−(2/3) s d³`, which at `d = 1e-12` is 24 orders below
either.
The case that showed it: the centre `c` and the curve `s` of a
nonlinearity `φ(u; c, s)` with `φ(c) = c`, `φ′(c) = 1`, evaluated where
`u = c`. The fixed point does not respond to either, the returned iterate
does by the rounding it is away, and the relative error of that gradient
is of order one at any iterate (the bound read 1.09 for 1.134 on a
converged float64 hub, usable, with right-hand sides of 2e-33 to 5e-18
for those probes beside ones of order one; without them it reads 5.3e-12
for an error of 5.3e-12 over the gains). Where such a constant *is*
resolved (an iterate stopped well short of the fixed point) it is in the
bound, which then reads above one: the relative error of that gradient
is of order one, and the bound says so. A weak constant is left out by
the same rule, although its tangent may be exact: a gain or a forcing
whose whole value moves the pass by less than the pass resolves. Nothing
the bound measures tells that case from the cancelled one (the
sensitivity of the tangent to a floor-sized change of the state read 1.6%
for the saturating curve above, whose relative error is 113%), so the
rule is the magnitude's. On 108 drawn float32 and float64 rings of three
members the bound moved on two (2.3e-5 to 6.8e-6, 1.8e-2 to 2.5e-4: a
weak gain was its worst probe) and no flag changed.

Measured `bound / true` (jaxlib 0.11.0, float32), the fixed point's
gradient from tight `ift` and `fori` arms that agree, every point a
fresh graph stepped through `gm.step()` and stopped early by
Expand Down
2 changes: 1 addition & 1 deletion docs/developer_guide/testing_standards.md
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@ The coupling search above scores against a closed form, which exists only for li
| gradient bound (CPL-093, CPL-095) | two scores: over the gains, biases and mapping weights (`gradient`), and over each nonlinearity's `c` and `s` (`gradient_vanishing`), which the fixed point does not respond to, so that the relative error is of order one at any other iterate |
| floor (CPL-097, CPL-100) | the exact residual is the reference's |

Per push: one cell (`tri`, three members with one nonlinearity each, float32, five Jacobi passes), twenty derandomised examples a score, and two seed examples on which a number is within an eighth of its limit. Slow: 115 random examples a score on each of six blocks of seven cells (every structure, each nonlinearity, both dtypes, every configuration), not shrunk (a shrink is thousands of steps). An example that plain passes carry out of float range is not stepped on a cell with a quasi-Newton acceleration (see the pinned finding in the module). To add a nonlinearity, add a branch to `phi` with `phi(c) = c` and `phi'(c) = 1`; to add a structure, add it to `STRUCTURES` (single-rate: a sub-cycled member reads its input interpolated, and `c` would not be its input at the fixed point).
Per push: one cell (`tri`, three members with one nonlinearity each, float32, five Jacobi passes), twenty derandomised examples a score, and two seed examples on which a number is within an eighth of its limit. Slow: 115 random examples a score on each of six blocks of seven cells (every structure, each nonlinearity, both dtypes, every configuration), not shrunk (a shrink is thousands of steps). The gradient bound is scored twice, over the gains, biases and weights and over each nonlinearity's own constants (`gradient_vanishing`), and in both only over the constants the pass resolves (`PassReference.pass_responses` above `RESOLVED_MARGIN` floors: CPL-093's condition). To add a nonlinearity, add a branch to `phi` with `phi(c) = c` and `phi'(c) = 1`; to add a structure, add it to `STRUCTURES` (single-rate: a sub-cycled member reads its input interpolated, and `c` would not be its input at the fixed point).

**The twins with the same interface reading** (`tests/property/geometry_graphs.py`, `tests/property/test_interface_reading_twins.py`). `convergence_norm="interface"` reads what each internal edge delivers. The node-inlined twin of a geometry edge moves the mapping into the *target*, so its edges deliver the raw source field and its interface norm is another norm. Two twins keep the reading:

Expand Down
7 changes: 7 additions & 0 deletions docs/release_notes/v0.4.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -7382,6 +7382,13 @@ its nodes on different clocks; now a `ValueError` at `compile()` and a 400 at
as a node's timestep and clamped `/sim/profile`'s counts silently; now a 422.
`231` (never released): a coupling report was measured on the live state and
moved when the state was written after the step.
`233` (resolved; carried by 0.3.0 and 0.3.1): a coupling group under
`acceleration="iqn-ils"` or `"iqn-imvj"` whose iterate left float range before
its cap never returned from `step()`; it now runs to the cap and reports
`converged=False`, `residual=inf`.
`234` (resolved, never released): `gradient_relative_error_bound` read 1.09 for
a relative error of 1.134 with a constant in it whose tangent is rounding; a
constant one pass does not resolve is no longer in the bound.

## Verification evidence

Expand Down
17 changes: 16 additions & 1 deletion docs/validation/coupling_claims.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1997,6 +1997,7 @@ claims:
- tests/core/test_coupling_claims_graphs.py::test_unconverged_means_at_the_cap_or_not_finite
- tests/core/test_coupling_non_finite_state.py::test_a_non_finite_field_no_edge_reads_is_still_a_non_finite_state
- tests/core/test_coupling_sixteen_bit_norms_in_float32.py::test_a_sixteen_bit_residual_at_the_default_rtol_is_finite_and_held_in_float32
- tests/core/test_coupling_iqn_returns_on_a_non_finite_iterate.py::test_a_step_whose_iterate_leaves_float_range_under_iqn_returns
status: verified
domains:
f32: tests/core/test_coupling_claims_graphs.py::test_unconverged_means_at_the_cap_or_not_finite
Expand Down Expand Up @@ -3140,6 +3141,8 @@ claims:
- tests/core/test_coupling_non_finite_state.py::test_strict_convergence_names_the_non_finite_state
- tests/core/test_coupling_sixteen_bit_norms_in_float32.py::test_a_sixteen_bit_residual_at_the_default_rtol_is_finite_and_held_in_float32
- tests/core/test_coupling_sixteen_bit_norms_in_float32.py::test_strict_convergence_names_a_state_beyond_its_dtypes_range
- tests/core/test_coupling_iqn_returns_on_a_non_finite_iterate.py::test_a_step_whose_iterate_leaves_float_range_under_iqn_returns
- tests/core/test_coupling_iqn_returns_on_a_non_finite_iterate.py::test_the_secant_update_returns_on_every_non_finite_input
status: verified
domains:
f32: tests/core/test_coupling_non_finite_state.py::test_a_non_finite_state_never_reads_as_converged
Expand Down Expand Up @@ -3690,7 +3693,16 @@ claims:
read 5.1e-6 for a true 3.0e-5 at the float floor: MADD-ANO-226,
resolved); where the pass has no reverse mode and more than 64 state
entries that norm is unavailable and the bound is inf unless the
right-hand side does not depend on the state.
right-hand side does not depend on the state. A constant the pass
resolves: moving it by its own magnitude moves one pass from x_k by
more than the residual's float floor, in the bound's norm, and its
tangent is not exactly zero. A constant below that (the centre or the
curve of a nonlinearity evaluated on its centre: the fixed point does
not respond to it, and its gradient at x_k is rounding; and a gain or
forcing so weak that its whole value moves the pass by less than the
floor) is not in the bound, which says nothing of it; it read 1.09 for
a relative error of 1.134 with those probes in it (MADD-ANO-234,
resolved).
oracle: the fixed point's gradient from the float64 solve or from tight ift and fori arms that agree.
tests:
- tests/core/test_coupling_gradient_error_bound.py::test_the_gradient_bound_holds_across_an_early_exit_sweep
Expand All @@ -3708,6 +3720,9 @@ claims:
- tests/core/test_coupling_gradient_bound_far_from_the_fixed_point.py::test_a_usable_gradient_bound_holds_on_the_audit_pair_at_every_cap
- tests/core/test_coupling_gradient_bound_far_from_the_fixed_point.py::test_far_from_its_fixed_point_the_gradient_bound_holds_or_is_withdrawn
- tests/property/test_coupling_targeted_search.py::test_a_usable_gradient_bound_is_never_below_the_error_per_push
- tests/property/test_coupling_nonlinear_search.py::test_a_usable_gradient_bound_is_never_below_the_error_on_the_nonlinear_cell_per_push
- tests/property/test_coupling_nonlinear_search.py::test_a_usable_gradient_bound_covers_a_nonlinearity_s_own_constants_per_push
- tests/property/test_coupling_nonlinear_search.py::test_a_constant_the_pass_does_not_resolve_is_not_in_the_gradient_bound
status: verified
notes: >-
16bit domain failing on d2429a6: jax.grad and jax.jvp through a bfloat16
Expand Down
121 changes: 121 additions & 0 deletions docs/validation/known_anomalies.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15636,3 +15636,124 @@ anomalies:
needs a GPU run with the bound's parts printed.
verification: []
github_issue: null

- anomaly_id: "MADD-ANO-233"
title: "A coupling group under IQN whose iterate left float range before its cap never returned from step()"
description: >
`acceleration="iqn-ils"` and `"iqn-imvj"` solve a least-squares
problem on the secant columns each pass, through an SVD
(`jnp.linalg.pinv`; `jnp.linalg.lstsq` in v0.3.0 and v0.3.1). On the
pass where part of the iterate overflowed, the new column held `inf`
in those entries and finite numbers in the rest, and LAPACK's SVD does
not return on such a matrix: `step()` never returned, with no error
and one core busy. Measured (CPU, jaxlib 0.11.0, each probe under a
wall-clock limit): a fan-out hub of four members with products of two
fields in float64, Jacobi, cap 120, started a whole field from its
fixed point -- no return in 240 s, where a converging step of the
same compiled graph takes 0.05 s; a pair whose first entry is `exp`
of itself beside three that contract -- no return in 25 s, float32
and float64, both IQN kinds, `solver="ift"` and `"fori"`. The same
groups under `"none"` and `"aitken"` ran to the cap and reported
`converged=False`, `residual=inf`. `jnp.linalg.svd` and `pinv`
alone: no return on 4 x 4 to 16 x 16 matrices with one `inf` entry or
one `inf` column, nor on one with a row and a column at `finfo.max`;
a NaN beside the `inf`, or every entry `inf`, returns. At v0.3.0 and
v0.3.1 (`git archive` of each tag): `solver="ift"` with
`acceleration="iqn-imvj"` does not return on the `exp` pair in either
dtype; the default `solver="fori"` solved the normal equations by LU
and returns a NaN state. v0.2.1 and earlier have no `solver="ift"`.
Resolved in 0.4.0: the secant matrix is replaced by zeros before the
factorisation wherever the sum of its squares is not finite, the
quasi-Newton step is then not taken (the Aitken fallback's is), and
the group runs to its cap and reports the state as not finite, as
the other accelerations do. A finite matrix passes through
unchanged: 200 random finite calls of the update and 108 drawn
three-member rings stepped twice return the same bits before and
after. The same guard stands in front of every other SVD of the
coupling analysis (the spectral amplification and the gradient
bound's operator norms).
severity: "major"
safety_relevance: "context_dependent"
safety_relevance_rationale: >
Not silent: the call does not return and nothing wrong is reported.
`major` because a simulation that diverges is the case a caller's
own checks exist for, and none of them runs: no exception, no
report, a loop that stops. `context_dependent` because it needs a
quasi-Newton acceleration (not the default) and an iterate that
overflows part of its entries before the cap.
affected_components:
- "maddening.core.coupling.acceleration.iqn_ils_update"
- "maddening.core.coupling.group.CouplingGroup.acceleration"
affected_versions: ">=0.3.0, <0.4.0"
workaround: >
On v0.3.x use `solver="fori"` (the default there) with a
quasi-Newton acceleration, or `acceleration="aitken"`; or run the
step under a wall-clock limit.
resolution_status: "resolved"
resolution_version: "0.4.0"
residual_risk: >
The guard is on the matrix handed to the SVD, measured on CPU with
jaxlib 0.11.0; other LAPACK-backed calls of the coupling analysis
(`qr`, `solve`, `inv`, `eigvals`) returned on every non-finite and
extreme finite matrix tried (288 and 576 cases a dtype) and are not
guarded, except `eigvals`, which already was. A group that diverges
under IQN still spends its whole cap of passes.
verification:
- "tests/core/test_coupling_iqn_returns_on_a_non_finite_iterate.py::test_a_step_whose_iterate_leaves_float_range_under_iqn_returns"
- "tests/core/test_coupling_iqn_returns_on_a_non_finite_iterate.py::test_the_secant_update_returns_on_every_non_finite_input"
- "tests/core/test_coupling_iqn_returns_on_a_non_finite_iterate.py::test_no_svd_of_the_coupling_analysis_is_handed_a_matrix_that_is_not_finite"
- "tests/property/test_coupling_nonlinear_search.py::test_a_group_that_leaves_float_range_under_iqn_still_returns"
github_issue: null

- anomaly_id: "MADD-ANO-234"
title: "gradient_relative_error_bound read below the error, gradient_bound_usable=True, with a constant in it whose tangent is rounding: 1.09 for a relative error of 1.134"
description: >
The bound is the worst, over one probe per scalar constant, of a
bound on that probe's tangent error over the tangent's size. For a
constant the fixed point does not respond to -- the centre or the
curve of a nonlinearity evaluated on its centre -- the tangent at the
returned iterate is what the iterate's last rounding, or a
cancellation inside the node's derivative, left of it, and a relative
error of it is of order one or undefined. Measured (CPU, jaxlib
0.11.0): on a float64 fan-out hub of four members with saturating
gains, converged in four passes, the right-hand sides of those probes
were 2e-33 to 5e-18 beside ones of order one, the bound read 1.09 and
the relative error of the gradient with respect to one curve was
1.134 (absolute: 1.7e-34, beside gradients up to 3.5e3),
`gradient_bound_usable=True`. On a float32 ring stalled on its start
the float32 tangents of those constants are exactly zero and were
never in the bound (1.4e-6); in float64 at the same iterate one is
6e-26 with a relative error of 2.7. Found by the nonlinear coupling
search. No release carries the gradient bound. Resolved in 0.4.0:
a probe is in the bound where moving its constant by its own
magnitude moves one pass by more than the residual's float floor;
the docstring and
the claim say so. The hub then reads 5.3e-12 for an error of
5.3e-12 over its gains.
severity: "major"
safety_relevance: "context_dependent"
safety_relevance_rationale: >
A bound below its quantity with its flag set is a silent wrong
result, never `minor`. Not `critical`: the gradient concerned is
1e-34 beside others of 1e3, and a bound that reads above one already
says the relative error is not bounded. `context_dependent` because
whether the bound gates anything is the program's choice.
affected_components:
- "maddening.core.graph_manager.GraphManager.coupling_diagnostics"
affected_versions: "none"
workaround: >
None needed in 0.4.0.
resolution_status: "resolved"
resolution_version: "0.4.0"
residual_risk: >
The rule is a magnitude: a gain so weak that its whole value moves
the pass by less than the floor is left out of the bound too,
although its tangent may be exact (on 108 drawn rings the bound moved
on two, 1.8e-2 to 2.5e-4 on one, and no flag changed). Nothing the
bound measures tells that case from the cancelled one. A constant
left out is not named in the report; one Jacobian-vector product of
the pass tells.
verification:
- "tests/property/test_coupling_nonlinear_search.py::test_a_constant_the_pass_does_not_resolve_is_not_in_the_gradient_bound"
- "tests/property/test_coupling_nonlinear_search.py::test_a_usable_gradient_bound_covers_a_nonlinearity_s_own_constants_per_push"
github_issue: null
Loading
Loading