S2 — Numerical-contract catalog (precondition vs caveat sweep)#

The decision record that designed safe mode, preserved as approved (2026-06-19). The current-state statement of these contracts — present tense, migration arrows resolved — is the user page ../numerical_contracts.md.

The make-or-break input to dev/archive/safe_unsafe_mode_plan.md §5. Sweep of the verified modules (tucker_tensor_train, frame_variations_format, manifold, corewise, fitting + backend, probing) classifying each op’s numerical assumptions as an enforceable precondition or a non-enforced caveat.

APPROVED by Nick, 2026-06-19, with these decisions:

  1. retract precondition = ORTH only (not GAUGE — retract is gauge-invariant). ✓

  2. MANIFOLD.inner checks all of SF + ORTH + GAUGE (+ structural-minimal, below), the frame ones cached on the frozen objects. ✓

  3. Frontend-only enforcement for now; a backend mirror is deferred and needs careful discussion (current lean: no backend numerical checks). ✓

  4. No reclassifications. ✓

  5. Minimal ranks: do NOT retire — keep this catalog as the answer to the long-standing “which ops need minimal ranks” TBD (a reference that does not exist in the literature). In safe mode, check ranks are structurally minimal (has_minimal_ranks, cheap rank arithmetic); never run the numerical (SVD) check; document the requirement on each op. See the revised “Minimal ranks” section. The structurally-but-not-numerically-minimal gap is an adversarial edge case (failure mode: NaN / wrong result) we accept.

The classification rule#

  • Precondition — the op is undefined or numerically wrong without it. Safe mode checks it (and raises); unsafe mode / under-jit skips it.

  • Caveat — the op is valid and correct as computed; the property only governs what the result means (e.g. “this equals Hilbert–Schmidt”). Never enforced — it would reject legitimate use.

The classic trap: orthogonality/gaugedness for inner/norm is a caveat (they’re valid on any frame; those properties only make them equal HS), so it moves to the geometry’s inner/norm, not the raw op.

The numerical properties (and their checkers / cost)#

property

meaning

checker (exists)

cost

notes

same-frame

two tangents share one frame (= same tangent space)

new same_frame(b1,b2)

O(1) common case

b1 is b2 or frames_equal(b1.data,b2.data, rtol) — identity is the fast sufficient path; frames_equal (a value compare) is the complete fallback that also accepts the jit round-trip.

orthogonal

the frame is orthonormal (U/O/L/R conditions)

T3Frame.is_orthogonal()

contractions

a @cached_property candidate (frame is frozen → check once).

gauged

variations satisfy the gauge conditions (48)–(49)

T3Tangent.is_gauged()

contractions

per (frame,variations) pair; cacheable.

minimal rank

structurally-and-numerically minimal ranks

has_minimal_ranks (structural)

SVD (numerical)

numerical check intentionally skipped (§ “Minimal ranks”).

Checkers are per-stack-element. Every checker/residual above returns one verdict per stack element (shape stack_shape, scalar when unstacked) — see docs/batching_and_stacking.md §9. So a safe-mode precondition reduces with .all() at the call site (safety.require(frame.is_orthogonal(atol).all(), …)): it requires all stack elements to pass; safety.require itself stays a scalar gate. (The structural has_minimal_ranks is the one scalar-in-ragged exception — see that §9 note.)

Master table (by surface)#

SF = same-frame, ORTH = orthogonal frame, GAUGE = variations gauged. “—” = no numerical precondition (structural checks like shapes/ranks/check_fv_pair still apply, always, in both modes).

T3Tangent (after the §S3 rename)#

op

precondition (checked in safe mode)

caveat (never checked)

note

+,

SF

today an is-identity “structural” guard → becomes same_frame (the root-cause fix)

* scalar, __neg__

corewise_inner(other)

SF

“= HS iff ORTH+GAUGE+minimal”

renamed from inner; the caveat moves to MANIFOLD.inner

corewise_norm()

— (unary)

“= HS iff ORTH+GAUGE+minimal”

renamed from norm

normalized(), allclose(other)

allclose: SF

normalized is unary

to_dense, to_t3

realization is gauge-invariant, valid on any frame

probe/apply/entries (+transposes), *_derivatives (+transposes)

bare 𝒥/𝒥ᵀ — gauge-invariant, any frame; the Riemannian Π is the geometry’s, applied separately

to_vector/from_vector, zeros/unit/zeros_like, reverse, sum_tangents

structural / constructors

stack_tangents

SF (all leaves)

today identity

stack_frame, unstack_*

stack_frame deliberately stacks different bases

is_orthogonal/is_gauged/has_minimal_ranks, minimal_ranks, tangent_space_dimension

checkers — they are the checks; keep non-enforcing

Geometries (the semantic inner/norm live here — Nick’s refinement)#

op

precondition

caveat

note

MANIFOLD.inner(t1,t2)

SF + ORTH + GAUGE(both)

minimal (for exact HS)

the HS inner product; checks ORTH+GAUGE, can’t cheaply check minimal

MANIFOLD.norm(t)

ORTH + GAUGE

minimal

unary (no SF)

COREWISE.inner(t1,t2)

SF

Euclidean; no ORTH/GAUGE (the corewise frame is non-orthonormal by design)

COREWISE.norm(t)

Euclidean, unary

MANIFOLD.project (Π), project_oblique

ORTH

minimal (for oblique’s HS-matching purpose)

the orthogonal-gauge projection needs an orthonormal frame to be the HS-orthogonal one

MANIFOLD.retract(p)

ORTH

minimal (rank preservation)

precondition implied, not currently documented — confirm

MANIFOLD.project_ambient(frame,grad)

ORTH

docstring already states “Requires orthogonal; minimal NOT required” ✓

MANIFOLD.transport(v,new_frame)

ORTH (new_frame)

= project_ambient onto the new frame

MANIFOLD.randn/random_orthogonal/randn_like

ORTH (via project)

minimal (for a true Gaussian on T_xM)

docstring already careful: “for a non-orthogonal frame it is merely gauged”

MANIFOLD.frame, COREWISE.frame, COREWISE.{randn,project,retract,randn_like}

frame produces the frame; corewise ops are gauge-free by design

GaussNewtonModel (fitting)#

op

precondition

note

gradient, gn_hessian(p), jacobian(p), gn_quadratic(p), evaluate(p)

SF (p vs model.frame)

today _require_at_frame identity → same_frame. The frame’s ORTH (manifold) is guaranteed by geometry.frame(x) at construction — a construction invariant, not a per-op check. evaluate/two-form use corewise_inner (coordinate metric), so SF, not HS.

TuckerTensorTrain, corewise, probingprecondition-free#

surface

precondition

why

TuckerTensorTrain.inner/norm

exact HS for any cores (use_orthogonalization is stability, not correctness)

TuckerTensorTrain.apply/entries/probe (+ ambient/corewise transposes), t3svd/t3svd_dense, t3m/__mul__/+/, to_dense/from_canonical/constructors

exact for any cores; only structural shape/rank checks

corewise.* (corewise_dot/norm/add/sub/scale/stack_*)

the raw coordinate (“basic types”) layer; structural shape-match only

probing.* (*_from_sweep, precompute_*, *_tangent/*_transpose)

bare 𝒥/𝒥ᵀ contractions; gauge/frame-agnostic

frame_variations_format: t3_orthogonal_representations, constructors, check_fv_pair, checkers

(structural)

check_fv_pair is structural (shapes) → always enforced; orthogonal-rep produces an orthonormal (and squashed→minimal) frame

The blurs this fixes (current code conflations)#

  1. The same-frame guard is labelled “structural” but is numerical (fitting._require_at_frame docstring “Structural guard”; T3Tangent._check_same_tangent_space via is). The root cause.same_frame (identity fast-path + frames_equal), safe-mode, eager-only. This is what unblocks frame-as-leaf.

  2. T3Tangent.inner/norm fold the HS caveat into the op (.. warning:: equals HS only when orthogonal and gauged). → split: MANIFOLD.inner/norm (HS, checks ORTH+GAUGE+SF) vs corewise_inner/corewise_norm (raw, checks SF). The op no longer pretends to be HS.

  3. retract / MANIFOLD.project ORTH precondition is silent (not in the docstrings). → state + check ORTH. (Confirm retract needs only ORTH, not GAUGE — gauge is a redundancy retract is invariant to.)

  4. “some tangent ops only correct when minimal ranks — which exactly is TBD” (manifold.py/bvf). → resolved as a caveat everywhere (see below); the TBD note can be replaced with the table above.

Minimal ranks — the resolution of the long-standing TBD (EMPIRICALLY verified — this is the reference)#

Minimal rank splits into structural (has_minimal_ranks — ranks equal the structural minimum; cheap integer arithmetic) and numerical (has_numerically_minimal_ranks — no stored rank numerically redundant; needs an SVD for a tensor, free for an orthonormal frame). An experiment settled which ops actually need it (tests/-style script run 2026-06-19; each op compared on a minimal vs a non-minimal orthogonal frame against the dense oracle):

op

needs minimal?

evidence

MANIFOLD.inner / norm (= corewise_inner when gauged)

NO

corewise_inner(gauged) == dense HS exactly on the non-minimal orthonormal frame. Orthogonal + gauged suffices (the CLAUDE.md “+ minimal” was wrong).

manifold_dim / tangent_space_dimension

NO

the formula matches the true SVD rank (103) on the non-minimal frame. Correct for any ranks.

MANIFOLD.retract

soft caveat only

still a valid first-order retraction on a non-minimal frame (rel.err 6e-6); it just drops the numerically-redundant rank (tucker 4→3) — desirable cleanup, not an error. Minimal buys only strict rank preservation.

MANIFOLD.randn / project_oblique

NO (correctness)

gauged direction is valid on any orthonormal frame; “true Gaussian / HS-matching” is on whatever the stored-rank tangent space is, which is correct.

MANIFOLD.project/project_ambient/transport, gauge Π

NO

orthogonal suffices (already known).

Verdict: minimal rank is NOT a correctness precondition for any verified op. So we do not wire it as a checked precondition — a structural-minimal check would raise on legitimate rank-redundant bases for no correctness reason. (Also corrects an earlier claim: t3_orthogonal_representations does not guarantee structural minimality — e.g. randn((10,11,12),(4,5,4),(1,2,3,1))has_minimal_ranks=False.)

Minimal rank lives on as a caveat (retract strict rank-preservation) and as diagnostic checkers (built, not wired into any op): structural has_minimal_ranks (existing); numerical TuckerTensorTrain.has_numerically_minimal_ranks(rtol) (structural-first → t3svd compare) and T3Frame.has_numerically_minimal_ranks(atol) (orthogonal + structurally-minimal ⟹ numerically-minimal, no SVD; non-orthogonal frames return False — the SVD path for them is deferred, “maybe we don’t need it”). Super-safe mode was considered and dropped (no op needs it; it would be a ~no-op for orthogonal frames). Naming convention: bare minimal_ranks = structural; numerically_minimal = numerical.

Implementation notes (for S3–S5, not decisions here)#

  • same_frame(b1, b2) = b1 is b2 or safety.frames_equal_or_skip(b1.data, b2.data) — the is fast-path keeps the common eager case O(1); the value compare only runs (and only matters) when objects differ but values match (jit round-trip) or genuinely differ. The tolerance is jax-aware (rtol_jax if any input is a jax array, else rtol_numpy); under a trace it skips (returns pass). Mechanism in t3toolbox/safety.py (S1, done).

  • Where checks live (razor): to serve raw-.data users, the ORTH/GAUGE checks belong at the backend functions that consume raw data (tv_operations.*, which already have is_orthogonal-style residual helpers), with the frontend methods inheriting them. same_frame is a frontend concept (it needs the two T3Frame objects); the backend’s analogue is “the caller passed one shared frame”, so the same-frame check is naturally frontend-only.

  • Cost mitigation (done in S5): cache the atol-independent residual, not the atol-bool — T3Frame.orthogonality_residual and T3Tangent.gauge_residual are @cached_propertys, and is_orthogonal(atol)/is_gauged(atol) compare against them. A fixed frame/tangent in an inner loop is contracted once; the atol stays a free parameter.

  • Trace detection (done in S5): the eager-only skip must fire even when the checked operand is a closed-over concrete array (not a tracer) while globally inside a trace — a jnp op then still yields a constant tracer. safety.is_tracing detects the global trace state (jax.core.trace_state_clean, with a committed-array-probe fallback), not just whether the passed arrays are tracers.

Sign-off questions for Nick#

  1. retract precondition = ORTH only (not GAUGE)? My read: yes — retract is gauge-invariant; gauge only relabels the variations, not the represented step. Confirm.

  2. MANIFOLD.inner should check both operands GAUGE + the frame ORTH (3 checks) — accept the cost in safe mode (mitigated by caching), or check only SF + GAUGE and treat ORTH as a frame-construction invariant (since MANIFOLD.frame guarantees it)? Leaning: check all three for honesty, cache the frame ones.

  3. Backend-level enforcement for raw-.data users (ORTH/GAUGE checks in tv_operations), or frontend-only for now? Leaning: frontend-first (S3–S5), backend mirror later.

  4. Any op above you’d reclassify (precondition ⇄ caveat)?