Skip to main content

gam_sae/
encode.rs

1//! Kantorovich-certified encode atlas (issue #1010).
2//!
3//! Encoding a row `x ∈ ℝᵖ` against a frozen multi-atom dictionary is the joint
4//! coordinate problem
5//!
6//! ```text
7//! min_{t_1,…,t_K}  ½‖x − Σ_k z_k B_kᵀΦ_k(t_k)‖² + Σ_k prior_k(t_k).
8//! ```
9//!
10//! [`joint_encode_refine_row`] solves that objective with the shared residual.
11//! The per-atom atlas below is an initializer and a standalone single-atom
12//! projection facility; composing its independent optima is not a multi-atom
13//! encode. With the amplitude `z_k` and decoder block `B_k` held fixed, Newton on
14//! a single-atom field `F(t) = ∇f_k(t)` converges from a start `t₀` into the unique
15//! root in a certified ball whenever the **Newton–Kantorovich** quantity
16//!
17//! ```text
18//! h = β · η · L ≤ ½,    β = ‖F'(t₀)⁻¹‖,   η = ‖F'(t₀)⁻¹ F(t₀)‖,
19//! ```
20//!
21//! where `L` is a Lipschitz constant of `F'` (the Hessian of `f_k`) on a region
22//! containing the Newton iterates. `h` is CHECKABLE per row in `O(q³)`
23//! (`q = latent_dim`, tiny), so each fast-path encode carries its own
24//! exactness certificate.
25//!
26//! ## The closed-form Hessian-Lipschitz constant `L`
27//!
28//! Write `m(t) = z·BᵀΦ(t) ∈ ℝᵖ` (the reconstruction) and `r(t) = m(t) − x`.
29//! Then `f = ½‖r‖² + prior` and, differentiating three times,
30//!
31//! ```text
32//! ∇³f = 3·sym(J_mᵀ : ∇²m) + ⟨r, ∇³m⟩ + ∇³prior,
33//! ```
34//!
35//! so an operator-norm bound on the chart is
36//!
37//! ```text
38//! L ≤ 3·‖J_m‖·‖∇²m‖ + ‖r‖·‖∇³m‖ + L_prior,
39//! ```
40//!
41//! with `‖∂^g m‖ ≤ |z|·(Σ_m ‖B_{m,:}‖)·B_g`, where `B_g = sup_chart max_m
42//! ‖∂^g Φ_m‖` is the per-column jet sup of the basis family — closed form per
43//! family ([`BasisHessianLipschitz`]). `‖r‖` is bounded by `‖x‖ +
44//! |z|·(Σ_m‖B_{m,:}‖)·B_0`. The ARD/von-Mises prior `L_prior` is a closed-form
45//! constant from the prior strength. Every bound is conservative (an
46//! over-estimate of `L` only SHRINKS the certified radius — it can never
47//! certify a row that does not converge).
48//!
49//! ## Pipeline
50//!
51//! 1. **Offline, per atom** ([`EncodeAtlas::build`]): chart centers `t_c` on the
52//!    atom's coordinate grid (the SHAPE_BAND grid idiom), each with a certified
53//!    Newton radius `R_c` solved from the Kantorovich inequality at the
54//!    worst-case in-chart start.
55//! 2. **Online, per row** ([`EncodeAtlas::certified_encode_row`]): route to the
56//!    nearest chart, start from its distilled IFT predictor, take one or two
57//!    Newton steps, then the `h ≤ ½` check AT the start point is the per-row
58//!    certificate.
59//! 3. **Uncertified tail**: rows whose start fails `h ≤ ½` are FLAGGED (counted
60//!    in [`EncodeResult::encode_uncertified_count`]) and must be routed by the
61//!    caller to the existing exact multi-start solve. No approximation enters
62//!    silently.
63
64use ndarray::{Array1, Array2, ArrayView1, ArrayView2, Axis};
65use opt::constants::{ARMIJO_C1, BACKTRACK_CONTRACTION};
66use opt::{AcceptedStep, BacktrackConfig, backtracking_line_search};
67
68use crate::candidate_index::{AtomFrameSketch, SaeCandidateIndex, auto_candidate_budget};
69use crate::manifold::{
70    AffineCoordinateEvaluator, CylinderHarmonicEvaluator, DuchonCoordinateEvaluator,
71    EuclideanPatchEvaluator, PeriodicHarmonicEvaluator, SaeBasisEvaluator, SaeManifoldAtom,
72    SphereChartEvaluator, TorusHarmonicEvaluator,
73};
74use gam_linalg::faer_ndarray::FaerEigh;
75
76use faer::Side;
77
78/// The Kantorovich convergence threshold `h ≤ ½`. Below this the Newton
79/// iteration is guaranteed to converge quadratically into the unique root in
80/// the certified ball; at or above it the start is uncertified.
81pub const KANTOROVICH_THRESHOLD: f64 = 0.5;
82
83/// Row count at or above which the corpus-rate certified-encode batch
84/// (`certified_encode_batch` / `certified_encode_with_index`) fans its
85/// per-row encodes out over rayon. Below this the per-row Newton + chart
86/// routing is cheap enough that the fan-out overhead does not pay; matched to
87/// the same order as the arrow-Schur `SCHUR_MATVEC_PARALLEL_ROW_MIN` gate so
88/// short batches inside an outer atom-level fan-out stay sequential.
89pub(crate) const ENCODE_BATCH_PARALLEL_ROW_MIN: usize = 256;
90
91/// Minimum frame alignment `‖Uₖᵀd‖/‖d‖ ∈ [0,1]` the routed atom must have for an
92/// index-routed encode to be attempted at all — a FIT-QUALITY floor, NOT a
93/// routing-correctness gate (#1026, corrected by the #1777 exact-routing path).
94///
95/// Routing itself is now EXACT: the index-routed encode picks the atom via
96/// [`SaeCandidateIndex::route_exact`], which returns the GLOBAL argmax of the
97/// routing score (the universal-bound LSH fast path, else a full-scan fallback) —
98/// so there is no "missed-better-ungathered-atom" hole left for this constant to
99/// patch. What remains is a different, honest question: even the globally-best
100/// atom may align only weakly with a row (no atom in the dictionary fits it). A
101/// finite alignment below this floor means the best available atom is a poor fit,
102/// so the row is flagged and routed to the exact multi-start fallback rather than
103/// encoded against an atom it barely belongs to. (Previously this same comparison
104/// double-served as a recall proxy; that role is gone — `route_exact` guarantees
105/// recall — leaving only the fit-quality role described here.)
106pub(crate) const CANDIDATE_ROUTING_MIN_ALIGNMENT: f64 = 0.5;
107
108/// Number of nearest charts the CERTIFIED encode refines in before returning the
109/// lowest-reconstruction-error certified result. A single nearest chart is not
110/// globally sound where the decoded manifold folds near itself (both competing
111/// basins' charts reconstruct near the fold, so both rank among the nearest by
112/// ambient distance); refining the top few captures the global basin. For a
113/// unimodal atom all candidates converge to the same root, so K>1 is a no-op.
114pub(crate) const CERTIFIED_ROUTING_TOPK: usize = 4;
115
116/// Newton refinement convergence floor. Once a refinement step's length `‖δ‖`
117/// falls below this (relative to the coordinate scale `1 + ‖t‖`), the iterate has
118/// reached the certified root to f64 resolution: applying the step cannot move `t`
119/// meaningfully, and the remaining fixed-budget steps only re-accumulate round-off.
120/// Stopping there is STRICTLY more accurate than draining a fixed step budget on a
121/// well-conditioned quadratic Newton tail, and it removes that tail's per-step
122/// `evaluate` + `second_jet` cost (the dominant per-row encode work). The batched
123/// and per-row encodes share this rule, so they stay bit-identical.
124pub(crate) const NEWTON_REFINE_CONVERGED_EPS: f64 = 1.0e-12;
125
126/// Global-minimum short-circuit floor for top-K certified routing. The
127/// reconstruction error `‖x − z·m(t)‖` is bounded below by 0, so a certified
128/// candidate whose residual already sits at the ambient noise floor
129/// (`≤ this · (1 + ‖x‖)`) is provably the global optimum over the charts — no
130/// competing chart can reach a strictly lower residual. The remaining candidates'
131/// refinement is then skipped. Conservative (a genuine second basin of the same
132/// target reconstructs the SAME point, so returning the first is a valid encode).
133pub(crate) const CERTIFIED_GLOBAL_MIN_RECON_FLOOR: f64 = 1.0e-11;
134
135/// A chart region on an atom's latent coordinate: a center `t_c` plus a
136/// certified in-chart radius. Over the ball `‖t − t_c‖ ≤ radius` the jet sup
137/// bounds returned by [`BasisHessianLipschitz`] hold, so the Kantorovich
138/// constant `L` computed from them is valid for any start in the ball.
139///
140/// For radial (Duchon) families the chart also carries the minimum kernel-center
141/// distance `exclusion_r_min` (a lower bound on `‖t − c_k‖` over the chart) that
142/// bounds the otherwise-singular `1/r` radial tails (issue #1010).
143#[derive(Debug, Clone)]
144pub struct ChartRegion {
145    /// Chart center coordinate `t_c` (length = latent_dim).
146    pub center: Array1<f64>,
147    /// In-chart radius in the coordinate metric.
148    pub radius: f64,
149    /// For radial (Duchon) families: a lower bound on `‖t − c_k‖` over the
150    /// chart, across every kernel center `c_k`. `None` for non-radial families.
151    pub exclusion_r_min: Option<f64>,
152    /// For radial (Duchon) families: an upper bound on `‖t − c_k‖` over the
153    /// chart, across every kernel center `c_k`. `None` for non-radial families.
154    pub radial_r_max: Option<f64>,
155}
156
157impl ChartRegion {
158    pub fn new(center: Array1<f64>, radius: f64) -> Self {
159        Self {
160            center,
161            radius,
162            exclusion_r_min: None,
163            radial_r_max: None,
164        }
165    }
166
167    pub fn with_radial_bounds(mut self, r_min: f64, r_max: f64) -> Self {
168        self.exclusion_r_min = Some(r_min);
169        self.radial_r_max = Some(r_max);
170        self
171    }
172
173    /// A jet-sup certificate is only meaningful over a genuine region. Even
174    /// families whose bounds are manifold-global constants (the sup over any
175    /// chart equals the global sup) must refuse a malformed chart rather than
176    /// certify garbage geometry.
177    pub(crate) fn assert_valid(&self) {
178        assert!(
179            self.radius.is_finite()
180                && self.radius >= 0.0
181                && self.center.iter().all(|c| c.is_finite()),
182            "ChartRegion must have a finite center and a finite non-negative radius"
183        );
184    }
185}
186
187/// Per-column sup-norm bounds on the first three coordinate jets of a basis
188/// family `Φ(t)`, valid over a stated [`ChartRegion`] (issue #1010). These are
189/// the analytic ingredients of the Hessian-Lipschitz constant `L` — see the
190/// module docs for the assembly. `value_sup` bounds `max_m |Φ_m|`,
191/// `jacobian_sup`/`hessian_sup`/`third_sup` bound `max_m ‖∂^g Φ_m‖`.
192pub trait BasisHessianLipschitz {
193    fn value_sup(&self, chart: &ChartRegion) -> f64;
194    fn jacobian_sup(&self, chart: &ChartRegion) -> f64;
195    fn hessian_sup(&self, chart: &ChartRegion) -> f64;
196    fn third_sup(&self, chart: &ChartRegion) -> f64;
197}
198
199/// Sup over the circle of the `g`-th derivative of any single harmonic column
200/// of a `num_basis`-wide Fourier basis `[1, sin(2π h t), cos(2π h t), …]`:
201/// `(2π·H)^g` for the top harmonic `H = (num_basis − 1)/2`. The constant column
202/// contributes `0` for `g ≥ 1`, so the top harmonic dominates; the bound is
203/// global (the trig magnitudes are `≤ 1` everywhere, independent of the chart).
204pub(crate) fn harmonic_jet_sup(num_basis: usize, order: u32) -> f64 {
205    let top_harmonic = num_basis.saturating_sub(1) / 2;
206    let omega = std::f64::consts::TAU * top_harmonic as f64;
207    omega.powi(order as i32)
208}
209
210impl BasisHessianLipschitz for PeriodicHarmonicEvaluator {
211    fn value_sup(&self, chart: &ChartRegion) -> f64 {
212        chart.assert_valid();
213        1.0
214    }
215    fn jacobian_sup(&self, chart: &ChartRegion) -> f64 {
216        chart.assert_valid();
217        harmonic_jet_sup(self.num_basis, 1)
218    }
219    fn hessian_sup(&self, chart: &ChartRegion) -> f64 {
220        chart.assert_valid();
221        harmonic_jet_sup(self.num_basis, 2)
222    }
223    fn third_sup(&self, chart: &ChartRegion) -> f64 {
224        chart.assert_valid();
225        harmonic_jet_sup(self.num_basis, 3)
226    }
227}
228
229impl BasisHessianLipschitz for TorusHarmonicEvaluator {
230    /// Tensor product of per-axis circle harmonics. A torus basis column is a
231    /// product of single-axis harmonics, each bounded as in the circle case.
232    /// The `g`-th coordinate jet routes `g` derivative operators across the
233    /// `latent_dim` factors (Leibniz); each routing contributes a product of
234    /// per-axis derivative magnitudes. A per-column sup is therefore bounded by
235    /// the top single-axis frequency to the `g`-th power times the number of
236    /// such routings (`latent_dim^g`, the count of operator-to-axis maps).
237    fn value_sup(&self, chart: &ChartRegion) -> f64 {
238        chart.assert_valid();
239        1.0
240    }
241    fn jacobian_sup(&self, chart: &ChartRegion) -> f64 {
242        chart.assert_valid();
243        torus_jet_sup(self.num_harmonics(), self.latent_dim(), 1)
244    }
245    fn hessian_sup(&self, chart: &ChartRegion) -> f64 {
246        chart.assert_valid();
247        torus_jet_sup(self.num_harmonics(), self.latent_dim(), 2)
248    }
249    fn third_sup(&self, chart: &ChartRegion) -> f64 {
250        chart.assert_valid();
251        torus_jet_sup(self.num_harmonics(), self.latent_dim(), 3)
252    }
253}
254
255/// Per-column `g`-th jet sup for the torus harmonic basis: `(2π·H)^g ·
256/// latent_dim^g`, where `H = num_harmonics` is the top per-axis frequency and
257/// `latent_dim^g` over-counts the Leibniz routings of `g` operators across the
258/// product factors (a conservative bound — each routing's per-axis magnitude is
259/// `≤ (2π H)^{#ops on that axis}`, and the products telescope to `(2π H)^g`).
260pub(crate) fn torus_jet_sup(num_harmonics: usize, latent_dim: usize, order: u32) -> f64 {
261    let omega = std::f64::consts::TAU * num_harmonics as f64;
262    omega.powi(order as i32) * (latent_dim as f64).powi(order as i32)
263}
264
265impl BasisHessianLipschitz for SphereChartEvaluator {
266    /// The 7-column lat/lon chart `[1, x, y, z, xy, yz, xz]` with
267    /// `x = cos(lat)cos(lon)`, `y = cos(lat)sin(lon)`, `z = sin(lat)`. Each of
268    /// `x, y, z` is a product of two unit-frequency trig factors, so its `g`-th
269    /// coordinate jet is a sum of `2^g` products of `{sin,cos}` (each `≤ 1`):
270    /// magnitude `≤ 2^g` for `g ≥ 1`, `≤ 1` for `g = 0`. The bilinear columns
271    /// `xy, yz, xz` are products of two such coordinates; by Leibniz over the
272    /// product, their `g`-th jet is bounded by `Σ_{i=0}^{g} C(g,i)·(2^i)·(2^{g−i})
273    /// = (2+2)^g = 4^g` (using `‖∂^i u‖ ≤ 2^i`, `|u| ≤ 1`). The bilinear columns
274    /// dominate, so the per-column sup is `4^g` (`g ≥ 1`). Bounds are global
275    /// constants — the chart box `lat ∈ [-π/2, π/2]` does not enlarge them.
276    fn value_sup(&self, chart: &ChartRegion) -> f64 {
277        chart.assert_valid();
278        1.0
279    }
280    fn jacobian_sup(&self, chart: &ChartRegion) -> f64 {
281        chart.assert_valid();
282        4.0
283    }
284    fn hessian_sup(&self, chart: &ChartRegion) -> f64 {
285        chart.assert_valid();
286        16.0
287    }
288    fn third_sup(&self, chart: &ChartRegion) -> f64 {
289        chart.assert_valid();
290        64.0
291    }
292}
293
294impl BasisHessianLipschitz for AffineCoordinateEvaluator {
295    /// The affine basis `[1, t₁, …, t_d]` is degree ≤ 1: its first jet has unit
296    /// columns, and all second and third jets vanish. The value sup is
297    /// `max(1, ‖t‖)` over the chart, bounded by `1 + ‖t_c‖ + radius`.
298    fn value_sup(&self, chart: &ChartRegion) -> f64 {
299        let center_norm = chart.center.dot(&chart.center).sqrt();
300        1.0 + center_norm + chart.radius
301    }
302    fn jacobian_sup(&self, chart: &ChartRegion) -> f64 {
303        chart.assert_valid();
304        1.0
305    }
306    fn hessian_sup(&self, chart: &ChartRegion) -> f64 {
307        chart.assert_valid();
308        0.0
309    }
310    fn third_sup(&self, chart: &ChartRegion) -> f64 {
311        chart.assert_valid();
312        0.0
313    }
314}
315
316impl BasisHessianLipschitz for EuclideanPatchEvaluator {
317    /// Monomials of total degree ≤ `max_degree` in `t ∈ ℝ^d`. Over the ball of
318    /// radius `R` about `t_c`, each coordinate is bounded by `ρ = ‖t_c‖∞ + R`.
319    /// A monomial `t^α` with `|α| = q` has `g`-th partials bounded (crudely) by
320    /// the descending-factorial coefficient `q·(q−1)···(q−g+1) ≤ q^g` times
321    /// `ρ^{max(q−g,0)}`, and there are at most `d^g` partial routings, so the
322    /// per-column `g`-th jet sup is `≤ d^g · D^g · ρ^{max(D−g,0)}` with
323    /// `D = max_degree`. Conservative; D is small for patch evaluators.
324    fn value_sup(&self, chart: &ChartRegion) -> f64 {
325        let rho = patch_rho(chart);
326        let d = self.max_degree as i32;
327        rho.powi(d).max(1.0)
328    }
329    fn jacobian_sup(&self, chart: &ChartRegion) -> f64 {
330        patch_jet_sup(self.latent_dim, self.max_degree, chart, 1)
331    }
332    fn hessian_sup(&self, chart: &ChartRegion) -> f64 {
333        patch_jet_sup(self.latent_dim, self.max_degree, chart, 2)
334    }
335    fn third_sup(&self, chart: &ChartRegion) -> f64 {
336        patch_jet_sup(self.latent_dim, self.max_degree, chart, 3)
337    }
338}
339
340impl BasisHessianLipschitz for CylinderHarmonicEvaluator {
341    /// Cylinder `S¹ × ℝ` product basis `Φ_{c,l} = c(t₀)·l(t₁)`, the circle
342    /// (periodic harmonic) factor on axis 0 crossed with the monomial line
343    /// factor on axis 1. Because the two factors depend on disjoint coordinates,
344    /// the order-`g` coordinate jet in any cell is exactly
345    /// `c^{(k₀)}(t₀)·l^{(k₁)}(t₁)` with `k₀ + k₁ = g`, so the per-column sup is
346    /// the max over the split `k₀ + k₁ = g` of the product of the two per-axis
347    /// per-order sups: the circle factor contributes `1` at order 0 and
348    /// `(2π·H)^{k₀}` at order `k₀ ≥ 1` (trig magnitudes `≤ 1`); the line factor
349    /// contributes the monomial-patch sup `D^{k₁}·ρ^{max(D−k₁,0)}` (`D = line
350    /// degree`, `ρ = ‖t_c‖∞ + radius`). Bounds are global in the periodic axis
351    /// and chart-local in the line axis.
352    fn value_sup(&self, chart: &ChartRegion) -> f64 {
353        cylinder_jet_sup(self.circle_harmonics, self.line_degree, chart, 0)
354    }
355    fn jacobian_sup(&self, chart: &ChartRegion) -> f64 {
356        cylinder_jet_sup(self.circle_harmonics, self.line_degree, chart, 1)
357    }
358    fn hessian_sup(&self, chart: &ChartRegion) -> f64 {
359        cylinder_jet_sup(self.circle_harmonics, self.line_degree, chart, 2)
360    }
361    fn third_sup(&self, chart: &ChartRegion) -> f64 {
362        cylinder_jet_sup(self.circle_harmonics, self.line_degree, chart, 3)
363    }
364}
365
366/// Per-column order-`g` jet sup of the cylinder product basis: the max over
367/// `k₀ + k₁ = g` of `circle_axis_sup(k₀) · line_axis_sup(k₁)`, where the circle
368/// axis sup is `(2π·H)^{k₀}` (`1` at `k₀ = 0`) and the line axis sup is the
369/// monomial-patch bound `D^{k₁}·ρ^{max(D−k₁,0)}` (`1` at `k₁ = 0`). See the
370/// [`CylinderHarmonicEvaluator`] doc comment for the derivation.
371pub(crate) fn cylinder_jet_sup(
372    circle_harmonics: usize,
373    line_degree: usize,
374    chart: &ChartRegion,
375    order: u32,
376) -> f64 {
377    let omega = std::f64::consts::TAU * circle_harmonics as f64;
378    let big_d = line_degree as f64;
379    let rho = patch_rho(chart);
380    let mut best = 0.0_f64;
381    for k0 in 0..=order {
382        let k1 = order - k0;
383        let circle = if k0 == 0 { 1.0 } else { omega.powi(k0 as i32) };
384        let line = if k1 == 0 {
385            rho.powi(line_degree as i32).max(1.0)
386        } else {
387            let residual = line_degree.saturating_sub(k1 as usize) as i32;
388            // `.max(1.0)` as in `patch_jet_sup`: for ρ < 1 a lower-degree line
389            // monomial dominates the k1-th derivative, so the bare `ρ^residual`
390            // underestimates the line-factor sup. The value case (k1==0) already
391            // clamps; this completes it for the derivative orders.
392            big_d.powi(k1 as i32) * rho.powi(residual).max(1.0)
393        };
394        best = best.max(circle * line);
395    }
396    best
397}
398
399/// Sup-norm radius `ρ = ‖t_c‖∞ + radius` of the chart (the coordinate magnitude
400/// bound used by the monomial-patch jet bounds).
401pub(crate) fn patch_rho(chart: &ChartRegion) -> f64 {
402    let center_inf = chart
403        .center
404        .iter()
405        .fold(0.0_f64, |acc, &v| acc.max(v.abs()));
406    center_inf + chart.radius
407}
408
409/// Per-column `g`-th jet sup for a monomial patch of max degree `D` in `d`
410/// coordinates over the chart: `d^g · D^g · ρ^{max(D−g,0)}` (see the
411/// [`EuclideanPatchEvaluator`] doc comment for the derivation).
412pub(crate) fn patch_jet_sup(
413    latent_dim: usize,
414    max_degree: usize,
415    chart: &ChartRegion,
416    order: u32,
417) -> f64 {
418    let d = latent_dim as f64;
419    let big_d = max_degree as f64;
420    let rho = patch_rho(chart);
421    let residual_degree = max_degree.saturating_sub(order as usize) as i32;
422    // `.max(1.0)`: for ρ < 1 (small charts near the origin) the g-th jet sup is NOT
423    // dominated by the max-degree monomial `t^D` (whose g-th derivative ~ ρ^{D-g}
424    // shrinks with ρ) but by a LOWER-degree monomial whose g-th derivative is a
425    // larger constant — e.g. {1,t,t²}'s jacobian sup is the linear term's constant
426    // `1`, which exceeds `2ρ` when ρ < ½. Without the clamp the bound underestimates
427    // the true sup (numerically: D=3, ρ=0.1, g=1 → formula 0.03 vs true 1.0), which
428    // would make the certificate's Lipschitz `L` too small → a FALSE certificate.
429    // `D^g · max(ρ^{D-g}, 1)` upper-bounds `max_{q∈[g,D]} (q!/(q-g)!)·ρ^{q-g}` for
430    // all ρ (the `q=g` term gives `g! ≤ D^g`, the `q=D` term gives `≤ D^g·ρ^{D-g}`).
431    d.powi(order as i32) * big_d.powi(order as i32) * rho.powi(residual_degree).max(1.0)
432}
433
434impl BasisHessianLipschitz for DuchonCoordinateEvaluator {
435    /// Radial-kernel basis `Φ_m(t) = φ(r_m)`, `r_m = ‖t − c_m‖`, plus a
436    /// polynomial nullspace block. For the cubic Duchon kernel `φ(r) = r³` the
437    /// radial derivatives are `φ' = 3r²`, `φ'' = 6r`, `φ''' = 6`. The chain rule
438    /// to coordinate jets introduces `1/r` factors through the unit radial
439    /// direction `u = (t − c)/r` and the projector `(I − uuᵀ)/r`, so over a
440    /// chart the jets are bounded by combining the radial-derivative magnitudes
441    /// at the worst-case radius with the inverse-radius tail at the chart's
442    /// EXCLUSION radius `r_min` (the closest a chart point gets to any center):
443    ///
444    /// ```text
445    /// ‖∇φ‖    ≤ |φ'|                              ≤ 3 r_max²
446    /// ‖∇²φ‖   ≤ |φ''| + |φ'|/r                    ≤ 6 r_max + 3 r_max²/r_min
447    /// ‖∇³φ‖   ≤ |φ'''| + 3|φ''|/r + 3|φ'|/r²      ≤ 6 + 18 r_max/r_min + 9 r_max²/r_min²
448    /// ```
449    ///
450    /// (the `1/r`, `1/r²` tails are bounded by `1/r_min`, `1/r_min²`). The
451    /// polynomial nullspace block is degree ≤ `order`; its jets are bounded like
452    /// the monomial patch with `D = order`. The per-column sup is the max of the
453    /// kernel and polynomial bounds. The `r³` kernel is itself `C²` (no
454    /// singularity) so these tails are conservative but finite for any
455    /// `r_min > 0`; the atlas refines charts to keep `r_min` bounded away from 0.
456    fn value_sup(&self, chart: &ChartRegion) -> f64 {
457        let r_max = chart.radial_r_max.unwrap_or(chart.radius);
458        let poly = duchon_poly_jet_sup(self.centers.ncols(), self.order_degree(), chart, 0);
459        (r_max.powi(3)).max(poly)
460    }
461    fn jacobian_sup(&self, chart: &ChartRegion) -> f64 {
462        let r_max = chart.radial_r_max.unwrap_or(chart.radius);
463        let kernel = 3.0 * r_max * r_max;
464        let poly = duchon_poly_jet_sup(self.centers.ncols(), self.order_degree(), chart, 1);
465        kernel.max(poly)
466    }
467    fn hessian_sup(&self, chart: &ChartRegion) -> f64 {
468        let r_max = chart.radial_r_max.unwrap_or(chart.radius);
469        let r_min = chart
470            .exclusion_r_min
471            .unwrap_or(chart.radius)
472            .max(f64::MIN_POSITIVE);
473        let kernel = 6.0 * r_max + 3.0 * r_max * r_max / r_min;
474        let poly = duchon_poly_jet_sup(self.centers.ncols(), self.order_degree(), chart, 2);
475        kernel.max(poly)
476    }
477    fn third_sup(&self, chart: &ChartRegion) -> f64 {
478        let r_max = chart.radial_r_max.unwrap_or(chart.radius);
479        let r_min = chart
480            .exclusion_r_min
481            .unwrap_or(chart.radius)
482            .max(f64::MIN_POSITIVE);
483        let kernel = 6.0 + 18.0 * r_max / r_min + 9.0 * r_max * r_max / (r_min * r_min);
484        let poly = duchon_poly_jet_sup(self.centers.ncols(), self.order_degree(), chart, 3);
485        kernel.max(poly)
486    }
487}
488
489/// Polynomial-block degree of a Duchon nullspace order, used to bound the
490/// nullspace columns like a monomial patch.
491trait DuchonOrderDegree {
492    fn order_degree(&self) -> usize;
493}
494
495impl DuchonOrderDegree for DuchonCoordinateEvaluator {
496    fn order_degree(&self) -> usize {
497        match self.order {
498            gam_terms::basis::DuchonNullspaceOrder::Zero => 0,
499            gam_terms::basis::DuchonNullspaceOrder::Linear => 1,
500            gam_terms::basis::DuchonNullspaceOrder::Degree(d) => d,
501        }
502    }
503}
504
505/// Per-column `g`-th jet sup of the Duchon polynomial nullspace block, treated
506/// as a monomial patch of degree `order_degree`.
507pub(crate) fn duchon_poly_jet_sup(
508    latent_dim: usize,
509    order_degree: usize,
510    chart: &ChartRegion,
511    order: u32,
512) -> f64 {
513    if order_degree == 0 {
514        return if order == 0 { 1.0 } else { 0.0 };
515    }
516    patch_jet_sup(latent_dim, order_degree, chart, order)
517}
518
519/// Decoder magnitude `Σ_m ‖B_{m,:}‖₂` of an atom's frozen decoder block: the
520/// factor that converts a per-column `Φ`-jet sup `B_g` into a reconstruction
521/// jet sup `‖∂^g m‖ ≤ |z|·decoder_row_norm_sum·B_g`.
522pub(crate) fn decoder_row_norm_sum(decoder: ArrayView2<'_, f64>) -> f64 {
523    let mut acc = 0.0;
524    for row in decoder.rows() {
525        acc += row.dot(&row).sqrt();
526    }
527    acc
528}
529
530#[derive(Debug, Clone, Copy)]
531pub(crate) struct ReconstructionJetSups {
532    pub(crate) value: f64,
533    pub(crate) jacobian: f64,
534    pub(crate) hessian: f64,
535    pub(crate) third: f64,
536}
537
538pub(crate) fn pair_trig_decoder_sup(
539    sin_row: ArrayView1<'_, f64>,
540    cos_row: ArrayView1<'_, f64>,
541) -> f64 {
542    let aa = sin_row.dot(&sin_row);
543    let bb = cos_row.dot(&cos_row);
544    let ab = sin_row.dot(&cos_row);
545    let trace = aa + bb;
546    let disc = ((aa - bb) * (aa - bb) + 4.0 * ab * ab).sqrt();
547    (0.5 * (trace + disc)).sqrt()
548}
549
550/// Per-harmonic reconstruction jet sups of a periodic atom. `decoder` MUST be
551/// the FULL-width decoder on the standard `[1, sin 2πt, cos 2πt, …]` inner
552/// basis: the row pairing below identifies rows `(2h−1, 2h)` as the harmonic-`h`
553/// `(sin, cos)` pair and prices them at `ω = 2πh`. A #1117 rank-reduced decoder
554/// `B̃ = Qᵀ B` has NO such row meaning (each reduced row mixes all harmonics),
555/// so callers re-expand through `Q` first — see [`reconstruction_jet_sups`].
556pub(crate) fn periodic_reconstruction_jet_sups(
557    decoder: ArrayView2<'_, f64>,
558) -> ReconstructionJetSups {
559    let mut value = 0.0;
560    let mut jacobian = 0.0;
561    let mut hessian = 0.0;
562    let mut third = 0.0;
563    if decoder.nrows() > 0 {
564        value += decoder.row(0).dot(&decoder.row(0)).sqrt();
565    }
566    let harmonics = decoder.nrows().saturating_sub(1) / 2;
567    for h in 1..=harmonics {
568        let sin_idx = 2 * h - 1;
569        let cos_idx = 2 * h;
570        let amp = pair_trig_decoder_sup(decoder.row(sin_idx), decoder.row(cos_idx));
571        let omega = std::f64::consts::TAU * h as f64;
572        value += amp;
573        jacobian += omega * amp;
574        hessian += omega.powi(2) * amp;
575        third += omega.powi(3) * amp;
576    }
577    for row in (1 + 2 * harmonics)..decoder.nrows() {
578        let amp = decoder.row(row).dot(&decoder.row(row)).sqrt();
579        value += amp;
580        let omega = std::f64::consts::TAU * harmonics.max(1) as f64;
581        jacobian += omega * amp;
582        hessian += omega.powi(2) * amp;
583        third += omega.powi(3) * amp;
584    }
585    ReconstructionJetSups {
586        value,
587        jacobian,
588        hessian,
589        third,
590    }
591}
592
593pub(crate) fn reconstruction_jet_sups(
594    atom: &SaeManifoldAtom,
595    sups: JetSups,
596) -> ReconstructionJetSups {
597    // `sups` bounds the FULL-width family (see `family_jet_sups`), so the
598    // decoder it pairs with must be the full-width pre-image `B = Q B̃` when the
599    // atom was #1117 rank-reduced. The reconstruction is identical through that
600    // frame (`Φ̃ B̃ = Φ (Q B̃)`), so the bound is exact-in-structure; pairing the
601    // full sups with the reduced `B̃` instead would price periodic rows at the
602    // wrong harmonic and mismatch the family the sups were taken over.
603    let full_decoder = atom
604        .reduced_column_map
605        .is_some()
606        .then(|| atom.full_width_decoder());
607    let decoder = full_decoder
608        .as_ref()
609        .map_or_else(|| atom.decoder_coefficients.view(), |b| b.view());
610    if matches!(
611        atom.basis_kind(),
612        crate::manifold::SaeAtomBasisKind::Periodic
613    ) {
614        periodic_reconstruction_jet_sups(decoder)
615    } else {
616        let decoder_norm_sum = decoder_row_norm_sum(decoder);
617        ReconstructionJetSups {
618            value: decoder_norm_sum * sups.value,
619            jacobian: decoder_norm_sum * sups.jacobian,
620            hessian: decoder_norm_sum * sups.hessian,
621            third: decoder_norm_sum * sups.third,
622        }
623    }
624}
625
626/// The Hessian-Lipschitz constant `L` of the per-row encode objective `f_k` on
627/// a chart, assembled in closed form from the basis jet sups and the decoder /
628/// amplitude / target magnitudes. See the module docs for the derivation:
629///
630/// ```text
631/// L ≤ 3·‖J_m‖·‖∇²m‖ + ‖r‖·‖∇³m‖ + L_prior,
632/// ‖∂^g m‖ ≤ |z|·S_B·B_g,   S_B = Σ_m ‖B_{m,:}‖,
633/// ‖r‖ ≤ ‖x‖ + |z|·S_B·B_0,
634/// ```
635///
636/// `prior_lipschitz` is the caller-supplied closed-form `L_prior` of the
637/// ARD/von-Mises coordinate prior (`0.0` if no prior is active on the encode).
638pub(crate) fn hessian_lipschitz_constant(
639    recon_sups: ReconstructionJetSups,
640    amplitude: f64,
641    target_norm: f64,
642    prior_lipschitz: f64,
643) -> f64 {
644    let z = amplitude.abs();
645    let m_jac = z * recon_sups.jacobian;
646    let m_hess = z * recon_sups.hessian;
647    let m_third = z * recon_sups.third;
648    let recon_value = z * recon_sups.value;
649    let r_norm = target_norm + recon_value;
650    3.0 * m_jac * m_hess + r_norm * m_third + prior_lipschitz
651}
652
653/// One offline-certified chart: a center, its Kantorovich constants, and the
654/// certified Newton-convergence radius `R_c` solved from `h = β·η·L ≤ ½` at the
655/// worst-case in-chart start.
656#[derive(Debug, Clone)]
657pub struct CertifiedChart {
658    pub region: ChartRegion,
659    /// Closed-form Hessian-Lipschitz constant `L` over the chart.
660    pub lipschitz: f64,
661    /// `β = ‖F'(t_c)⁻¹‖` at the chart center (worst-case in-chart start uses
662    /// the center's curvature; the radius is solved so the certificate holds for
663    /// any start in the ball).
664    pub beta_center: f64,
665    /// Certified Newton radius: starts within `radius` of `t_c` satisfy `h ≤ ½`.
666    pub certified_radius: f64,
667    /// Distilled amortized-encoder Jacobian for this chart (#1026 ladder item 3).
668    ///
669    /// The exact encode map `x ↦ t` solves `F(t; x) = J_m(t)ᵀ(m(t) − x) = 0`. By
670    /// the implicit function theorem its derivative at the converged root is
671    /// `dt/dx = −(∂_t F)⁻¹ (∂_x F) = H⁻¹ J_m` (since `∂_x F = −J_m`), so the
672    /// first-order Taylor expansion of the encode map about this chart's center
673    /// `t_c` is the closed-form AFFINE predictor
674    ///
675    /// ```text
676    /// t(x) ≈ t_c + (1/z) · A₁ · (x − z · m₁(t_c)),   A₁ = (J₁ᵀJ₁ + ridge·I)⁻¹ J₁,
677    /// ```
678    ///
679    /// with `J₁ = Bᵀ J_Φ(t_c)` and `m₁(t_c) = BᵀΦ(t_c)` the AMPLITUDE-1
680    /// reconstruction jets (the amplitude `z` factors out analytically, so the
681    /// stored Jacobian is amplitude-free). This is the DISTILLED amortized
682    /// encoder of the #1026 thread: the per-row Hessian factorization + Newton
683    /// iteration is moved OFFLINE into this `d × p` matrix, leaving a single
684    /// `O(d·p)` mat-vec online — no per-row eigendecomposition, no second-jet
685    /// evaluation. The Kantorovich certificate is still evaluated AT the
686    /// predicted start, so the amortized prediction is trusted iff `h ≤ ½` and an
687    /// uncertified row still routes to the exact multi-start solve (the encoder
688    /// approximates inference, the certificate keeps it honest — the thread's
689    /// "encoder + certificate-gated exact fallback" deployment). `None` when the
690    /// center's Gauss–Newton block is singular (no certifiable amortization).
691    pub amortized_jacobian: Option<Array2<f64>>,
692    /// Amplitude-1 chart-center reconstruction `m₁(t_c) = BᵀΦ(t_c)` (length `p`),
693    /// the anchor the amortized predictor expands the encode map around.
694    pub recon_center: Array1<f64>,
695    /// Precomputed affine-predictor CONSTANT term `base = t_c − A₁·m₁(t_c)` (length
696    /// `d`), so the online amortized encode of a row `x` at amplitude `z` is the
697    /// single mat-vec `t̂ = base + (1/z)·A₁·x` with NO per-row `A₁·m₁` recompute.
698    /// Hoisting this atom-static term offline is what lets the massive-K index-routed
699    /// fast paths run a single allocation-free pass over rows (rather than a per-atom
700    /// GEMM sub-batch that degenerates to one row per group when `K ≫ N`). `None`
701    /// exactly when `amortized_jacobian` is `None` (singular Gauss–Newton block).
702    pub amortized_base: Option<Array1<f64>>,
703}
704
705/// The per-atom encode atlas: a set of certified charts covering the atom's
706/// coordinate domain, plus the decoder/amplitude scaling needed to recompute a
707/// per-row certificate online.
708#[derive(Debug, Clone)]
709pub struct AtomEncodeAtlas {
710    pub atom_index: usize,
711    pub latent_dim: usize,
712    pub decoder_norm_sum: f64,
713    pub charts: Vec<CertifiedChart>,
714}
715
716/// Result of a certified encode over a batch of rows, carrying the honesty
717/// flag: how many rows could NOT be certified and were flagged for the exact
718/// multi-start fallback (issue #1010 — no approximation enters silently).
719#[derive(Debug, Clone)]
720pub struct EncodeResult {
721    /// Per-row encoded latent coordinates (`n_rows × latent_dim`).
722    pub coords: Array2<f64>,
723    /// Per-row certificate: `true` ⇒ the row's start satisfied `h ≤ ½` and the
724    /// 1–2 Newton steps are exact-into-the-certified-ball; `false` ⇒ flagged.
725    pub certified: Vec<bool>,
726    /// Count of rows that could not be certified. These ride the payload so the
727    /// caller routes them to the exact multi-start encode — honesty, never
728    /// silent. Equals `certified.iter().filter(|c| !**c).count()`.
729    pub encode_uncertified_count: usize,
730}
731
732/// Result of solving the frozen dictionary's joint coordinate objective over a
733/// batch. `converged[row]` is a numerical first-order stationarity verdict for
734/// the shared-residual objective, not a Newton--Kantorovich certificate.
735#[derive(Debug, Clone)]
736pub struct JointEncodeResult {
737    /// Per-atom coordinate blocks, each shaped `n_rows × latent_dim_k`.
738    pub coords: Vec<Array2<f64>>,
739    /// Joint row solve reached the first-order tolerance.
740    pub converged: Vec<bool>,
741    /// Exact tally of `false` entries in `converged`.
742    pub unconverged_count: usize,
743}
744
745impl JointEncodeResult {
746    pub(crate) fn new(coords: Vec<Array2<f64>>, converged: Vec<bool>) -> Self {
747        let unconverged_count = converged.iter().filter(|ok| !**ok).count();
748        Self {
749            coords,
750            converged,
751            unconverged_count,
752        }
753    }
754}
755
756impl EncodeResult {
757    pub(crate) fn from_rows(coords: Array2<f64>, certified: Vec<bool>) -> Self {
758        let encode_uncertified_count = certified.iter().filter(|c| !**c).count();
759        Self {
760            coords,
761            certified,
762            encode_uncertified_count,
763        }
764    }
765}
766
767/// The honest cost breakdown of the encode tax (reviewer condition #3). Every
768/// (row, atom) encode lands in exactly one of three tiers, in ascending cost:
769///
770///   1. **amortized-certified** — the one-mat-vec distilled predictor's start
771///      already satisfies the Kantorovich `h ≤ ½` certificate. Cheapest.
772///   2. **Newton-rescued** — the amortized start is uncertified, but the
773///      certified IFT-warm-start Newton encode lands a certified root. Middling.
774///   3. **multi-start fallback** — neither certifies, so the row rides the exact
775///      multi-start solve. This is the true cost MULTIPLIER at scale, and its
776///      fraction GROWS with atom similarity / co-activation interference (the
777///      per-row joint `(t, a)` landscape multiplies basins that no per-atom
778///      certificate covers). Reporting it is what keeps the encode-tax story
779///      honest — an SAE's one-matmul encode has no analogue of this tail.
780///
781/// The tiers partition the (row, atom) grid: `amortized_certified +
782/// newton_rescued + multistart_fallback == n_rows · n_atoms`.
783#[derive(Debug, Clone, Default)]
784pub struct FallbackTelemetry {
785    pub n_rows: usize,
786    pub n_atoms: usize,
787    /// (row, atom) encodes certified by the cheap amortized predictor.
788    pub amortized_certified: usize,
789    /// (row, atom) encodes the amortized predictor missed but the certified
790    /// Newton warm-start rescued.
791    pub newton_rescued: usize,
792    /// (row, atom) encodes neither tier certified — routed to the exact
793    /// multi-start solve.
794    pub multistart_fallback: usize,
795}
796
797impl FallbackTelemetry {
798    /// Total (row, atom) encodes accounted for.
799    #[must_use]
800    pub fn total(&self) -> usize {
801        self.n_rows * self.n_atoms
802    }
803
804    /// Fraction of encodes the cheap amortized predictor certified outright.
805    #[must_use]
806    pub fn amortized_fraction(&self) -> f64 {
807        let t = self.total();
808        if t == 0 {
809            0.0
810        } else {
811            self.amortized_certified as f64 / t as f64
812        }
813    }
814
815    /// Fraction of encodes rescued by the certified Newton warm-start.
816    #[must_use]
817    pub fn newton_fraction(&self) -> f64 {
818        let t = self.total();
819        if t == 0 {
820            0.0
821        } else {
822            self.newton_rescued as f64 / t as f64
823        }
824    }
825
826    /// Fraction of encodes that fell through to the exact multi-start solve —
827    /// the encode-tax cost multiplier.
828    #[must_use]
829    pub fn multistart_fraction(&self) -> f64 {
830        let t = self.total();
831        if t == 0 {
832            0.0
833        } else {
834            self.multistart_fallback as f64 / t as f64
835        }
836    }
837
838    /// Fold another atom's tallies into this one (n_rows is shared across atoms;
839    /// n_atoms and the tier counts accumulate). Lets a caller aggregate the
840    /// per-atom telemetry of [`EncodeAtlas::encode_atom_with_fallback_telemetry`]
841    /// into one dictionary-wide breakdown.
842    pub fn accumulate(&mut self, other: &FallbackTelemetry) {
843        self.n_rows = other.n_rows;
844        self.n_atoms += other.n_atoms;
845        self.amortized_certified += other.amortized_certified;
846        self.newton_rescued += other.newton_rescued;
847        self.multistart_fallback += other.multistart_fallback;
848    }
849}
850
851/// Per-row Kantorovich certificate at a start `t₀` for one atom encode.
852#[derive(Debug, Clone, Copy)]
853pub struct RowCertificate {
854    pub beta: f64,
855    pub eta: f64,
856    pub lipschitz: f64,
857    /// `h = β·η·L`. The row is certified iff `h ≤ ½`.
858    pub h: f64,
859}
860
861impl RowCertificate {
862    pub fn certified(&self) -> bool {
863        self.h.is_finite() && self.h <= KANTOROVICH_THRESHOLD
864    }
865}
866
867#[derive(Debug, Clone)]
868struct CertifiedEncodeProbe {
869    coord: Array1<f64>,
870    final_cert: RowCertificate,
871}
872
873/// Canonical flat-axis polynomial degree of a cylinder `S¹ × ℝ` atom — the
874/// degree the topology-race builder ([`gam_solve::structure_harvest`]) uses
875/// for the line axis (`CylinderHarmonicEvaluator::new(_, 2)`). The encode atlas
876/// recovers the circle harmonic count from the basis width using this degree, so
877/// the two must agree.
878pub(crate) const SAE_CYLINDER_LINE_DEGREE: usize = 2;
879
880/// Build a basis-family handle for one atom from its [`SaeManifoldAtom`]. The
881/// atlas needs to evaluate the jet sups, which live on the concrete evaluator
882/// types; the atom carries the evaluator as `Arc<dyn SaeBasisEvaluator>`, so we
883/// reconstruct the family bound from the atom's basis kind + width + centers.
884///
885/// The width used is the FULL inner-basis width [`SaeManifoldAtom::full_basis_size`],
886/// never the stored (possibly #1117 rank-reduced) [`SaeManifoldAtom::basis_size`].
887/// After [`SaeManifoldAtom::reduce_basis_to_subspace`] the live columns are
888/// Q-mixtures `Φ̃ = Φ Q` of the fixed-width family, so a family rebuilt at the
889/// REDUCED width bounds the wrong function space — a 5-wide periodic atom reduced
890/// to `r = 3` would be bounded as a single-harmonic family (under-estimating the
891/// `g`-th jet by `2^g`), and a degree-2 patch reduced to `r = 2` would be bounded
892/// as affine (`L = 0` ⇒ every start "certifies": a FALSE Kantorovich
893/// certificate, violating the module invariant that every bound over-estimates).
894/// The sups returned here bound the FULL-width family; [`reconstruction_jet_sups`]
895/// pairs them with the full-width decoder pre-image `B = Q B̃`, against which the
896/// reconstruction is IDENTICAL (`Φ̃ B̃ = Φ (Q B̃)`), so the certificate frame never
897/// sees the reduction and stays sound.
898pub(crate) fn family_jet_sups(
899    atom: &SaeManifoldAtom,
900    chart: &ChartRegion,
901) -> Result<JetSups, String> {
902    use crate::manifold::SaeAtomBasisKind::*;
903    let m = atom.full_basis_size();
904    let d = atom.latent_dim();
905    let sups = match atom.basis_kind() {
906        Periodic => {
907            let ev = PeriodicHarmonicEvaluator::new(m)?;
908            JetSups::from_family(&ev, chart)
909        }
910        Torus => {
911            // Torus basis width is `(2H+1)^d`; recover the per-axis harmonic
912            // count `H` from `axis_m = m^(1/d)` rather than a sum formula.
913            let axis_m = integer_root(m, d.max(1));
914            let num_harmonics = axis_m.saturating_sub(1) / 2;
915            let ev = TorusHarmonicEvaluator::new(d, num_harmonics.max(1))?;
916            JetSups::from_family(&ev, chart)
917        }
918        Sphere => {
919            let ev = SphereChartEvaluator;
920            JetSups::from_family(&ev, chart)
921        }
922        ProjectivePlane | KleinBottle => {
923            return Err(
924                "EncodeAtlas: quotient spectral jet sup requires a plan-native bound; route this atom through exact analytic encode"
925                    .to_string(),
926            );
927        }
928        Cylinder => {
929            // Cylinder width is `(2H+1)·(D+1)` with the canonical flat-axis
930            // degree `D = SAE_CYLINDER_LINE_DEGREE` (the harvest convention).
931            // Recover the per-axis circle harmonic count `H` from
932            // `2H+1 = m/(D+1)`.
933            let ml = SAE_CYLINDER_LINE_DEGREE + 1;
934            if d != 2 || ml == 0 || m % ml != 0 {
935                return Err(format!(
936                    "EncodeAtlas: Cylinder atom requires latent_dim == 2 and width divisible by {ml}; got dim={d}, m={m}"
937                ));
938            }
939            let axis_mc = m / ml;
940            let h = axis_mc.saturating_sub(1) / 2;
941            let ev = CylinderHarmonicEvaluator::new(h.max(1), SAE_CYLINDER_LINE_DEGREE)?;
942            JetSups::from_family(&ev, chart)
943        }
944        Mobius => {
945            return Err(
946                "EncodeAtlas: Mobius jet bounds require its persisted harmonic and width \
947                 degrees; use the atom's exact analytic jets"
948                    .to_string(),
949            );
950        }
951        Linear | EuclideanPatch | Poincare => {
952            // The patch width fixes max_degree implicitly; bound by a degree that
953            // covers the column count (conservative). Degree d-patch column count
954            // grows fast; we recover the smallest degree whose patch is ≥ m.
955            // Poincare atoms use the same tangent-coordinate polynomial decoder;
956            // their intrinsic smoothness differs in the penalty, not in Phi(t).
957            let degree = euclidean_patch_degree(d, m);
958            let ev = EuclideanPatchEvaluator::new(d, degree)?;
959            JetSups::from_family(&ev, chart)
960        }
961        Duchon => {
962            // UNSOUND — DO NOT TRUST for a certificate (F2/F3). This bound
963            // hard-codes cubic `φ(r) = r³` radial jets and a single origin center
964            // (`duchon_centers_from_atom`), but the real Duchon kernel is the
965            // polyharmonic `c·r^(2m−d)` (with log variants) over the atom's
966            // data-placed centers — so it can UNDER-estimate L and issue a FALSE
967            // certificate (the module's own warning). The atom does not expose its
968            // real order / center matrix / scaling to this crate, so no sound bound
969            // is available. `build_atom_atlas_from_centers` therefore REFUSES to
970            // certify Duchon atoms (emits uncertified charts → exact-encode
971            // fallback) and never reaches this arm; it is retained only so the
972            // family dispatch is total. If a future change threads the real
973            // order/centers here, replace this with the true `φ_{m,d}` jet bounds.
974            let centers = duchon_centers_from_atom(atom);
975            let conservative_m = m.max(1);
976            let ev = DuchonCoordinateEvaluator::new(centers, conservative_m)?;
977            JetSups::from_family(&ev, chart)
978        }
979        Precomputed(name) => {
980            return Err(format!(
981                "EncodeAtlas: precomputed basis '{name}' has no closed-form jet sup; route to exact encode"
982            ));
983        }
984        // A finite-set (indicator) atom is piecewise constant — it has no
985        // continuous jet to bound, so there is no Kantorovich chart; route to the
986        // exact encode like any other non-differentiable basis.
987        FiniteSet => {
988            return Err(
989                "EncodeAtlas: finite-set (indicator) basis has no closed-form jet sup; \
990                 route to exact encode"
991                    .to_string(),
992            );
993        }
994    };
995    Ok(sups)
996}
997
998/// Smallest monomial-patch degree whose column count covers `m` basis columns.
999pub(crate) fn euclidean_patch_degree(latent_dim: usize, m: usize) -> usize {
1000    // Column count of a degree-D patch in d vars is C(d+D, D). Grow D until it
1001    // covers m; cap at m so a degenerate width still terminates.
1002    let mut degree = 0usize;
1003    while patch_column_count(latent_dim, degree) < m && degree < m {
1004        degree += 1;
1005    }
1006    degree
1007}
1008
1009/// Largest integer `a` with `a^k ≤ n` (the floor of the `k`-th root). Used to
1010/// recover the per-axis harmonic width `axis_m` from a torus basis width
1011/// `m = axis_m^d`.
1012pub(crate) fn integer_root(n: usize, k: usize) -> usize {
1013    if k == 0 {
1014        return 1;
1015    }
1016    if k == 1 {
1017        return n;
1018    }
1019    let mut a = 1usize;
1020    loop {
1021        let next = a + 1;
1022        let mut pow: u128 = 1;
1023        let mut overflow = false;
1024        for _ in 0..k {
1025            pow = pow.saturating_mul(next as u128);
1026            if pow > n as u128 {
1027                overflow = true;
1028                break;
1029            }
1030        }
1031        if overflow {
1032            return a;
1033        }
1034        a = next;
1035    }
1036}
1037
1038pub(crate) fn patch_column_count(latent_dim: usize, degree: usize) -> usize {
1039    // C(d + D, D)
1040    let mut num = 1u128;
1041    let mut den = 1u128;
1042    for i in 1..=degree {
1043        num *= (latent_dim + i) as u128;
1044        den *= i as u128;
1045    }
1046    (num / den) as usize
1047}
1048
1049/// Recover Duchon centers from an atom: when the evaluator is unavailable the
1050/// atlas falls back to the atom's own latent-coordinate hull as the center set,
1051/// which only affects the radial-tail bound conservatively.
1052pub(crate) fn duchon_centers_from_atom(atom: &SaeManifoldAtom) -> Array2<f64> {
1053    // One center at the origin in latent_dim space is a sound conservative
1054    // default: the chart's own r_min / r_max bracket the true radial range.
1055    Array2::<f64>::zeros((1, atom.latent_dim().max(1)))
1056}
1057
1058/// The four per-column jet sups of a basis family over a chart.
1059#[derive(Debug, Clone, Copy)]
1060pub(crate) struct JetSups {
1061    pub(crate) value: f64,
1062    pub(crate) jacobian: f64,
1063    pub(crate) hessian: f64,
1064    pub(crate) third: f64,
1065}
1066
1067impl JetSups {
1068    pub(crate) fn from_family<B: BasisHessianLipschitz>(family: &B, chart: &ChartRegion) -> Self {
1069        Self {
1070            value: family.value_sup(chart),
1071            jacobian: family.jacobian_sup(chart),
1072            hessian: family.hessian_sup(chart),
1073            third: family.third_sup(chart),
1074        }
1075    }
1076}
1077
1078/// Evaluate one atom's encode objective gradient `F(t) = ∇f_k(t)` and the FULL
1079/// Hessian `F'(t) = ∇²f_k(t)` at a single coordinate `t`, for a single target
1080/// row `x` and fixed amplitude `z`. With `m(t) = z·BᵀΦ(t)`, `r = m − x`,
1081/// `J_m = z·Bᵀ J_Φ`:
1082///
1083/// ```text
1084/// g_t[a]   = J_m[a] · r                                  (= ∇f)
1085/// H_tt[a,b] = J_m[a] · J_m[b] + r · ∂²m/∂t_a∂t_b         (= ∇²f, FULL Hessian)
1086/// ```
1087///
1088/// The certificate uses the FULL Hessian rather than the Gauss-Newton block
1089/// `J_mᵀ J_m`. This is the principled choice for Newton–Kantorovich: the
1090/// theorem certifies convergence of Newton on `F = ∇f` to the unique nearby
1091/// ROOT of `∇f`, but a root of `∇f` can be a maximum. The full Hessian is
1092/// positive-definite exactly on the genuine-minimum basin, so requiring
1093/// `λ_min(H) > 0` (finite `β`) is what flags a start that would otherwise let
1094/// Gauss-Newton march into the wrong root (e.g. the circle antipode, a local
1095/// max where `∇f = 0` but the full curvature is negative). The residual term
1096/// needs the basis second jet `∂²Φ/∂t²`; an evaluator without one returns
1097/// `None`, and the row is flagged (no silent Gauss-Newton fallback).
1098///
1099/// The Hessian returned is the TRUE `∇²f_k` — no Levenberg ridge is added
1100/// (F2). The Kantorovich certificate (`row_certificate`) and its `λ_min(H) > 0`
1101/// saddle gate must see the genuine field: a ridged `H + λI` certifies neither
1102/// the original objective nor a consistently regularized one (a
1103/// locally-constant reconstruction has `H = 0`, whose ridged `λI` would falsely
1104/// certify a non-isolated, non-unique root). Ridge stays only in the
1105/// UNCERTIFIED amortized predictor (`center_amortized_jacobian`/`center_beta`).
1106pub fn encode_grad_hess(
1107    atom: &SaeManifoldAtom,
1108    evaluator: &dyn SaeBasisEvaluator,
1109    t: ArrayView1<'_, f64>,
1110    x: ArrayView1<'_, f64>,
1111    amplitude: f64,
1112) -> Result<Option<(Array1<f64>, Array2<f64>)>, String> {
1113    // The bare Euclidean, prior-free objective — the historical field, bit-identical
1114    // to the metric-free encode (see [`encode_grad_hess_core`], `EncodeObjective`).
1115    encode_grad_hess_core(
1116        atom,
1117        evaluator,
1118        t,
1119        x,
1120        amplitude,
1121        &EncodeObjective::euclidean(),
1122    )
1123}
1124
1125/// The TRUE per-row encode objective's non-Euclidean ingredients (F3), so the
1126/// Kantorovich certificate certifies the SAME functional the fit optimized `t`
1127/// against — not a bare Euclidean stand-in.
1128///
1129/// The fit's per-row data loss is generalized least squares `½ rᵀ M_n r`
1130/// (`r = m(t) − x`) under a per-row output metric `M_n = U_n U_nᵀ`
1131/// ([`gam_problem::RowMetric`]), plus a per-axis latent coordinate prior
1132/// `Σ_a ArdAxisPrior(α_a, t_a)` (the ARD Gaussian / von-Mises energy the fit
1133/// placed on the coordinate). The metric-free encode drops BOTH, so its certified
1134/// root solves a different problem whenever a non-identity metric or an active
1135/// prior is present. `EncodeObjective` threads them back in:
1136///
1137/// * `metric_factor` — the per-row factor `U_n ∈ ℝ^{p×rank}`; `None` ⇒ `M = I`.
1138///   Whitening reduces to `M r = U(Uᵀr)` and `Jᵀ M J = (UᵀJ)ᵀ(UᵀJ)`.
1139/// * `prior_alpha` — per-axis ARD precision `α_a` (the caller folds in any row
1140///   weight); `None`/`0` ⇒ no prior on that axis. The period is read from the
1141///   atom's basis kind so a periodic axis uses the von-Mises energy.
1142/// * `metric_norm_bound` — a GLOBAL upper bound `max_n ‖M_n‖` used to scale the
1143///   offline chart Lipschitz (the per-row `M_n` enters `β, η` online; the
1144///   certificate needs a valid `L` upper bound, and `L_data` scales by `‖M‖`).
1145///
1146/// [`EncodeObjective::euclidean`] (all `None`, bound `1`) reproduces the metric-
1147/// free field bit-for-bit, so every existing caller is unchanged.
1148#[derive(Clone, Copy)]
1149pub struct EncodeObjective<'a> {
1150    /// Per-row output-metric factor `U ∈ ℝ^{p×rank}` (`M = U Uᵀ`). `None` ⇒ `I`.
1151    pub metric_factor: Option<ArrayView2<'a, f64>>,
1152    /// Per-axis ARD precision `α_a` (row weight folded in). `None` ⇒ no prior.
1153    pub prior_alpha: Option<&'a [f64]>,
1154    /// Global bound `max_n ‖M_n‖` scaling the offline chart Lipschitz. `1.0` for
1155    /// the Euclidean objective.
1156    pub metric_norm_bound: f64,
1157}
1158
1159impl<'a> EncodeObjective<'a> {
1160    /// The bare Euclidean, prior-free objective — every code path is bit-identical
1161    /// to the metric-free encode under this value.
1162    pub fn euclidean() -> Self {
1163        Self {
1164            metric_factor: None,
1165            prior_alpha: None,
1166            metric_norm_bound: 1.0,
1167        }
1168    }
1169
1170    /// Closed-form Lipschitz contribution of the latent prior's third derivative.
1171    /// The Gaussian (non-periodic) prior is quadratic, so `prior''' ≡ 0`; the
1172    /// von-Mises (periodic) prior has `hess = α cos(κt)` ⇒ `third = −α κ sin(κt)`,
1173    /// bounded by `α·κ` with `κ = 2π/period`. Summed over axes (a conservative
1174    /// bound on the diagonal third-order tensor's operator norm — over-estimating
1175    /// `L` only shrinks the certified radius, never certifies a divergent start).
1176    fn prior_lipschitz(&self, atom: &SaeManifoldAtom) -> f64 {
1177        let Some(alpha) = self.prior_alpha else {
1178            return 0.0;
1179        };
1180        let mut l = 0.0;
1181        for axis in 0..atom.latent_dim().min(alpha.len()) {
1182            if let Some(period) = latent_axis_period(atom, axis) {
1183                let kappa = std::f64::consts::TAU / period;
1184                l += alpha[axis].abs() * kappa;
1185            }
1186        }
1187        l
1188    }
1189
1190    /// The chart's Kantorovich Lipschitz for the TRUE objective: the stored
1191    /// Euclidean data-term bound `data_lipschitz` scaled by the global metric
1192    /// operator-norm bound (`½ rᵀM r`'s Hessian-Lipschitz is `‖M‖·L_data`), plus
1193    /// the prior's third-derivative bound. Reduces to `data_lipschitz` exactly for
1194    /// [`Self::euclidean`] (`1·L + 0`).
1195    fn effective_lipschitz(&self, atom: &SaeManifoldAtom, data_lipschitz: f64) -> f64 {
1196        self.metric_norm_bound * data_lipschitz + self.prior_lipschitz(atom)
1197    }
1198}
1199
1200/// Apply the per-row output metric `M = U Uᵀ` to a residual/tangent vector:
1201/// `M v = U (Uᵀ v)`, `U ∈ ℝ^{p×rank}`. `O(p·rank)`, never the dense `p×p`.
1202fn apply_row_metric(u: ArrayView2<'_, f64>, v: ArrayView1<'_, f64>) -> Array1<f64> {
1203    let utv = u.t().dot(&v); // Uᵀ v ∈ ℝ^rank
1204    u.dot(&utv) // U (Uᵀ v) ∈ ℝ^p
1205}
1206
1207/// Maximum Gauss--Newton iterations for the frozen-dictionary joint row solve.
1208const JOINT_ENCODE_MAX_ITER: usize = 64;
1209const JOINT_ENCODE_GRAD_TOL: f64 = 1.0e-10;
1210const JOINT_ENCODE_STEP_TOL: f64 = 1.0e-12;
1211
1212/// Floor on the Levenberg--Marquardt damping `λ` for the joint row solve. The
1213/// damping is carried and decayed *across* outer Gauss--Newton iterations
1214/// (warm-started at this value on entry, then raised on rejection and lowered on
1215/// acceptance), so it is a stateful trust-region parameter rather than an
1216/// `escalate_ridge` schedule — it is deliberately kept hand-rolled. This is the
1217/// initial `λ` seed at the smallest scale that still perturbs the Hessian.
1218const JOINT_ENCODE_DAMPING_FLOOR: f64 = 1.0e-10;
1219
1220/// Multiplicative growth applied to the LM damping `λ` whenever a damped step is
1221/// rejected (unfactorable, non-descent, or Armijo-exhausted). Numerically equal
1222/// to [`opt::constants::RIDGE_GROWTH`], but this loop is stateful across outer
1223/// iterations and is intentionally not routed through `escalate_ridge`.
1224const JOINT_ENCODE_DAMPING_GROWTH: f64 = 10.0;
1225
1226/// Multiplicative decay applied to the LM damping `λ` after an accepted step, so
1227/// the next outer iteration starts from a looser trust region. Kept above the
1228/// `f64::EPSILON · diag_scale` floor at the use site.
1229const JOINT_ENCODE_DAMPING_DECAY: f64 = 3.0;
1230
1231/// Maximum number of damping-escalation attempts within a single outer
1232/// Gauss--Newton iteration before the row is declared non-improvable.
1233const JOINT_ENCODE_DAMPING_MAX_ATTEMPTS: usize = 12;
1234
1235/// Maximum number of Armijo backtracking halvings for the inner line search on
1236/// each damped step. Passed as `max_steps` to the shared
1237/// [`opt::backtracking_line_search`] primitive.
1238const JOINT_ENCODE_ARMIJO_MAX_STEPS: usize = 24;
1239
1240fn joint_data_value_grad_hess(
1241    jac: ArrayView2<'_, f64>,
1242    residual: ArrayView1<'_, f64>,
1243    metric_factor: Option<ArrayView2<'_, f64>>,
1244) -> (f64, Array1<f64>, Array2<f64>) {
1245    let q = jac.nrows();
1246    let p = jac.ncols();
1247    let weighted_residual = match metric_factor.as_ref() {
1248        Some(u) => apply_row_metric(u.view(), residual),
1249        None => residual.to_owned(),
1250    };
1251    let value = 0.5 * residual.dot(&weighted_residual);
1252    let grad = jac.dot(&weighted_residual);
1253    let weighted_jac = match metric_factor.as_ref() {
1254        Some(u) => {
1255            let mut out = Array2::<f64>::zeros((q, p));
1256            for axis in 0..q {
1257                out.row_mut(axis)
1258                    .assign(&apply_row_metric(u.view(), jac.row(axis)));
1259            }
1260            out
1261        }
1262        None => jac.to_owned(),
1263    };
1264    let hess = jac.dot(&weighted_jac.t());
1265    (value, grad, hess)
1266}
1267
1268/// Value, exact gradient, and positive-semidefinite Gauss--Newton curvature for
1269/// the shared-residual multi-atom objective. The gradient is exact; the PSD
1270/// curvature is used only to choose a descent step, so damping changes neither
1271/// the objective nor its stationary points.
1272fn joint_encode_value_grad_hess(
1273    atoms: &[SaeManifoldAtom],
1274    coords: &[Array1<f64>],
1275    x: ArrayView1<'_, f64>,
1276    amplitudes: ArrayView1<'_, f64>,
1277    metric_factor: Option<ArrayView2<'_, f64>>,
1278) -> Result<(f64, Array1<f64>, Array2<f64>), String> {
1279    let k_atoms = atoms.len();
1280    if coords.len() != k_atoms || amplitudes.len() != k_atoms {
1281        return Err(format!(
1282            "joint encode: {} atoms require {} coordinate blocks and amplitudes; got {} and {}",
1283            k_atoms,
1284            k_atoms,
1285            coords.len(),
1286            amplitudes.len()
1287        ));
1288    }
1289    let p = x.len();
1290    if let Some(u) = metric_factor.as_ref() {
1291        if u.nrows() != p {
1292            return Err(format!(
1293                "joint encode: metric factor has {} rows but target has {p} outputs",
1294                u.nrows()
1295            ));
1296        }
1297    }
1298
1299    let mut offsets = Vec::with_capacity(k_atoms + 1);
1300    offsets.push(0usize);
1301    for (atom_idx, atom) in atoms.iter().enumerate() {
1302        if atom.output_dim() != p {
1303            return Err(format!(
1304                "joint encode: atom {atom_idx} output_dim {} != target width {p}",
1305                atom.output_dim()
1306            ));
1307        }
1308        if coords[atom_idx].len() != atom.latent_dim() {
1309            return Err(format!(
1310                "joint encode: atom {atom_idx} coordinate length {} != latent_dim {}",
1311                coords[atom_idx].len(),
1312                atom.latent_dim()
1313            ));
1314        }
1315        offsets.push(offsets[atom_idx] + atom.latent_dim());
1316    }
1317    let q = *offsets.last().unwrap_or(&0);
1318    let mut recon = Array1::<f64>::zeros(p);
1319    let mut jac = Array2::<f64>::zeros((q, p));
1320
1321    for (atom_idx, atom) in atoms.iter().enumerate() {
1322        let z = amplitudes[atom_idx];
1323        if !z.is_finite() {
1324            return Err(format!("joint encode: amplitude[{atom_idx}] is not finite"));
1325        }
1326        let Some(evaluator) = atom.basis_evaluator.as_ref() else {
1327            return Err(format!(
1328                "joint encode: atom {atom_idx} has no basis evaluator for its live coordinate block"
1329            ));
1330        };
1331        let d = atom.latent_dim();
1332        let m = atom.basis_size();
1333        let coord = coords[atom_idx]
1334            .view()
1335            .to_shape((1, d))
1336            .map_err(|e| format!("joint encode: atom {atom_idx} coordinate reshape: {e}"))?
1337            .to_owned();
1338        let (phi, dphi) = evaluator.evaluate(coord.view())?;
1339        if phi.dim() != (1, m) || dphi.dim() != (1, m, d) {
1340            return Err(format!(
1341                "joint encode: atom {atom_idx} evaluator returned phi {:?}, jet {:?}; expected (1,{m}) and (1,{m},{d})",
1342                phi.dim(),
1343                dphi.dim()
1344            ));
1345        }
1346        let start = offsets[atom_idx];
1347        for basis_col in 0..m {
1348            let phi_v = phi[[0, basis_col]];
1349            for out in 0..p {
1350                let b = atom.decoder_coefficients[[basis_col, out]];
1351                recon[out] += z * phi_v * b;
1352                for axis in 0..d {
1353                    jac[[start + axis, out]] += z * dphi[[0, basis_col, axis]] * b;
1354                }
1355            }
1356        }
1357    }
1358
1359    let residual = &recon - &x;
1360    let (mut value, mut grad, mut hess) =
1361        joint_data_value_grad_hess(jac.view(), residual.view(), metric_factor.clone());
1362
1363    for (atom_idx, atom) in atoms.iter().enumerate() {
1364        let Some(alpha) = atom.ard_precisions.as_deref() else {
1365            continue;
1366        };
1367        let start = offsets[atom_idx];
1368        for axis in 0..atom.latent_dim().min(alpha.len()) {
1369            if alpha[axis] == 0.0 {
1370                continue;
1371            }
1372            let prior = crate::manifold::ArdAxisPrior::eval(
1373                alpha[axis],
1374                coords[atom_idx][axis],
1375                latent_axis_period(atom, axis),
1376            );
1377            value += prior.value;
1378            grad[start + axis] += prior.grad;
1379            hess[[start + axis, start + axis]] += prior.psd_majorizer_hess();
1380        }
1381    }
1382    Ok((value, grad, hess))
1383}
1384
1385fn joint_encode_damped_step(
1386    hess: ArrayView2<'_, f64>,
1387    grad: ArrayView1<'_, f64>,
1388    damping: f64,
1389) -> Result<Option<Array1<f64>>, String> {
1390    let q = grad.len();
1391    if q == 0 {
1392        return Ok(Some(Array1::zeros(0)));
1393    }
1394    let mut system = Array2::<f64>::zeros((q, q));
1395    for i in 0..q {
1396        for j in 0..q {
1397            system[[i, j]] = 0.5 * (hess[[i, j]] + hess[[j, i]]);
1398        }
1399        system[[i, i]] += damping;
1400    }
1401    let (evals, evecs) = system
1402        .eigh(Side::Lower)
1403        .map_err(|e| format!("joint encode: damped eigensolve failed: {e:?}"))?;
1404    if evals.iter().any(|&v| !(v.is_finite() && v > 0.0)) {
1405        return Ok(None);
1406    }
1407    let mut step = Array1::<f64>::zeros(q);
1408    for (col, &lambda) in evals.iter().enumerate() {
1409        let v = evecs.column(col);
1410        let coefficient = -v.dot(&grad) / lambda;
1411        for row in 0..q {
1412            step[row] += coefficient * v[row];
1413        }
1414    }
1415    if step.iter().any(|v| !v.is_finite()) {
1416        Ok(None)
1417    } else {
1418        Ok(Some(step))
1419    }
1420}
1421
1422fn joint_encode_add_step(
1423    atoms: &[SaeManifoldAtom],
1424    coords: &[Array1<f64>],
1425    step: ArrayView1<'_, f64>,
1426    scale: f64,
1427) -> Vec<Array1<f64>> {
1428    let mut out = Vec::with_capacity(atoms.len());
1429    let mut offset = 0usize;
1430    for (atom_idx, atom) in atoms.iter().enumerate() {
1431        let mut next = coords[atom_idx].clone();
1432        for axis in 0..atom.latent_dim() {
1433            next[axis] += scale * step[offset + axis];
1434            if let Some(period) = latent_axis_period(atom, axis) {
1435                next[axis] = next[axis].rem_euclid(period);
1436            }
1437        }
1438        offset += atom.latent_dim();
1439        out.push(next);
1440    }
1441    out
1442}
1443
1444/// Refine one row against all co-active atoms using the shared reconstruction
1445/// residual. Independent atlas projections may be supplied as starts, but every
1446/// accepted step and the convergence test use
1447/// `Σ_k z_k B_kᵀΦ_k(t_k) - x`, including all cross-atom Jacobian blocks.
1448pub(crate) fn joint_encode_refine_row(
1449    atoms: &[SaeManifoldAtom],
1450    initial_coords: &[Array1<f64>],
1451    x: ArrayView1<'_, f64>,
1452    amplitudes: ArrayView1<'_, f64>,
1453    metric_factor: Option<ArrayView2<'_, f64>>,
1454) -> Result<(Vec<Array1<f64>>, bool), String> {
1455    let mut coords = initial_coords.to_vec();
1456    let q: usize = atoms.iter().map(SaeManifoldAtom::latent_dim).sum();
1457    if q == 0 {
1458        return Ok((coords, true));
1459    }
1460    let target_scale = 1.0 + x.dot(&x).sqrt();
1461    let mut damping = JOINT_ENCODE_DAMPING_FLOOR;
1462
1463    for _ in 0..JOINT_ENCODE_MAX_ITER {
1464        let (value, grad, hess) =
1465            joint_encode_value_grad_hess(atoms, &coords, x, amplitudes, metric_factor.clone())?;
1466        let grad_norm = grad.dot(&grad).sqrt();
1467        if grad_norm <= JOINT_ENCODE_GRAD_TOL * target_scale {
1468            return Ok((coords, true));
1469        }
1470        let diag_scale = (0..q)
1471            .map(|i| hess[[i, i]].abs())
1472            .fold(0.0_f64, f64::max)
1473            .max(1.0);
1474        damping = damping.max(f64::EPSILON * diag_scale);
1475
1476        let mut accepted = None;
1477        for _ in 0..JOINT_ENCODE_DAMPING_MAX_ATTEMPTS {
1478            let Some(step) = joint_encode_damped_step(hess.view(), grad.view(), damping)? else {
1479                damping *= JOINT_ENCODE_DAMPING_GROWTH;
1480                continue;
1481            };
1482            let directional = grad.dot(&step);
1483            if !(directional.is_finite() && directional < 0.0) {
1484                damping *= JOINT_ENCODE_DAMPING_GROWTH;
1485                continue;
1486            }
1487            // Armijo backtracking on the shared reconstruction objective, migrated
1488            // onto the shared `opt` primitive with bit-for-bit-identical semantics:
1489            // initial step 1.0, `BACKTRACK_CONTRACTION` (×0.5) contraction,
1490            // `JOINT_ENCODE_ARMIJO_MAX_STEPS` (24) trials, and the exact
1491            // sufficient-decrease test `F(t + s·d) ≤ F(t) + c₁·s·∇F·d`
1492            // (c₁ = `ARMIJO_C1` = 1e-4, no roundoff cushion — the pre-migration
1493            // loop had none). `trial(s)` evaluates the candidate coords (always
1494            // well defined, so never `Ok(None)`) and threads them through the
1495            // payload so the accepted trial is returned without recomputation.
1496            let base_value = value;
1497            let step_unit_norm = step.dot(&step).sqrt();
1498            let line_search = backtracking_line_search::<Vec<Array1<f64>>, String>(
1499                BacktrackConfig {
1500                    initial_step: 1.0,
1501                    contraction: BACKTRACK_CONTRACTION,
1502                    max_steps: JOINT_ENCODE_ARMIJO_MAX_STEPS,
1503                },
1504                |line_scale| {
1505                    let candidate = joint_encode_add_step(atoms, &coords, step.view(), line_scale);
1506                    let (candidate_value, _, _) = joint_encode_value_grad_hess(
1507                        atoms,
1508                        &candidate,
1509                        x,
1510                        amplitudes,
1511                        metric_factor.clone(),
1512                    )?;
1513                    Ok(Some((candidate_value, candidate)))
1514                },
1515                |line_scale, candidate_value| {
1516                    candidate_value <= base_value + ARMIJO_C1 * line_scale * directional
1517                },
1518            )?;
1519            if let Some(AcceptedStep {
1520                step: line_scale,
1521                payload: candidate,
1522                ..
1523            }) = line_search
1524            {
1525                accepted = Some((candidate, line_scale * step_unit_norm));
1526            }
1527            if accepted.is_some() {
1528                damping = (damping / JOINT_ENCODE_DAMPING_DECAY).max(f64::EPSILON * diag_scale);
1529                break;
1530            }
1531            damping *= JOINT_ENCODE_DAMPING_GROWTH;
1532        }
1533        let Some((next, step_norm)) = accepted else {
1534            return Ok((coords, false));
1535        };
1536        coords = next;
1537        if step_norm <= JOINT_ENCODE_STEP_TOL * target_scale {
1538            let (_, final_grad, _) =
1539                joint_encode_value_grad_hess(atoms, &coords, x, amplitudes, metric_factor.clone())?;
1540            let converged =
1541                final_grad.dot(&final_grad).sqrt() <= JOINT_ENCODE_GRAD_TOL * target_scale;
1542            return Ok((coords, converged));
1543        }
1544    }
1545    let (_, final_grad, _) =
1546        joint_encode_value_grad_hess(atoms, &coords, x, amplitudes, metric_factor)?;
1547    let converged = final_grad.dot(&final_grad).sqrt() <= JOINT_ENCODE_GRAD_TOL * target_scale;
1548    Ok((coords, converged))
1549}
1550
1551/// Objective-aware gradient/Hessian of the certified encode field (F3). With
1552/// [`EncodeObjective::euclidean`] this is bit-for-bit the historical metric-free
1553/// field; with a metric it whitens the residual through `M = U Uᵀ`, and with a
1554/// prior it adds the ARD/von-Mises gradient and (diagonal) Hessian.
1555pub(crate) fn encode_grad_hess_core(
1556    atom: &SaeManifoldAtom,
1557    evaluator: &dyn SaeBasisEvaluator,
1558    t: ArrayView1<'_, f64>,
1559    x: ArrayView1<'_, f64>,
1560    amplitude: f64,
1561    objective: &EncodeObjective<'_>,
1562) -> Result<Option<(Array1<f64>, Array2<f64>)>, String> {
1563    let d = atom.latent_dim();
1564    let p = atom.output_dim();
1565    let m = atom.basis_size();
1566    let coords = t.to_shape((1, d)).map_err(|e| e.to_string())?.to_owned();
1567    let (phi, jet) = evaluator.evaluate(coords.view())?;
1568    if phi.dim() != (1, m) {
1569        return Err(format!(
1570            "encode_grad_hess: evaluator returned phi {:?}, expected (1, {m})",
1571            phi.dim()
1572        ));
1573    }
1574    let decoder = &atom.decoder_coefficients;
1575    // Reconstruction m(t) = z · Bᵀ Φ(t)  ∈ ℝᵖ.
1576    let mut recon = Array1::<f64>::zeros(p);
1577    for basis_col in 0..m {
1578        let phi_v = phi[[0, basis_col]];
1579        if phi_v == 0.0 {
1580            continue;
1581        }
1582        for out in 0..p {
1583            recon[out] += amplitude * phi_v * decoder[[basis_col, out]];
1584        }
1585    }
1586    let residual = &recon - &x;
1587    // J_m[axis] = z · Bᵀ (∂Φ/∂t_axis)  ∈ ℝᵖ.
1588    let mut jm = Array2::<f64>::zeros((d, p));
1589    for axis in 0..d {
1590        for basis_col in 0..m {
1591            let dphi = jet[[0, basis_col, axis]];
1592            if dphi == 0.0 {
1593                continue;
1594            }
1595            for out in 0..p {
1596                jm[[axis, out]] += amplitude * dphi * decoder[[basis_col, out]];
1597            }
1598        }
1599    }
1600    // The full-Hessian residual term needs ∂²Φ/∂t². No second jet ⇒ no
1601    // certificate (flag), never a silent Gauss-Newton substitute.
1602    let second = match evaluator.second_jet_dyn(coords.view()) {
1603        Some(result) => result?,
1604        None => return Ok(None),
1605    };
1606    // F3 — metric whitening. The certified objective is `½ rᵀ M r`, so the field
1607    // reads the M-weighted residual `M r` and M-weighted image tangents `M J_m[a]`.
1608    // With no metric (`None`) `wr` aliases `residual` and `jb` aliases `jm.row(b)`,
1609    // so the assembly below is the historical Euclidean field bit-for-bit.
1610    let mr_owned;
1611    let wr: &Array1<f64> = match objective.metric_factor {
1612        Some(u) => {
1613            mr_owned = apply_row_metric(u, residual.view());
1614            &mr_owned
1615        }
1616        None => &residual,
1617    };
1618    let mjm: Option<Vec<Array1<f64>>> = objective
1619        .metric_factor
1620        .map(|u| (0..d).map(|a| apply_row_metric(u, jm.row(a))).collect());
1621    // (M-weighted) residual · decoder-row is INDEPENDENT of the (a,b) axes; hoist it
1622    // to one O(m·p) pass so the per-axis curvature term is a cheap O(m) dot.
1623    let mut rd = vec![0.0_f64; m];
1624    for (basis_col, rd_col) in rd.iter_mut().enumerate() {
1625        let mut dot = 0.0;
1626        for out in 0..p {
1627            dot += wr[out] * decoder[[basis_col, out]];
1628        }
1629        *rd_col = dot;
1630    }
1631    // g_t[a] = J_m[a] · (M r) ;  H_tt[a,b] = J_m[a]·(M J_m[b]) + (M r)·∂²m/∂t_a∂t_b.
1632    // The full Hessian is symmetric (Gauss-Newton block + symmetric second jet), so
1633    // compute the upper triangle and mirror — half the curvature work.
1634    let mut g = Array1::<f64>::zeros(d);
1635    let mut h = Array2::<f64>::zeros((d, d));
1636    for a in 0..d {
1637        let ja = jm.row(a);
1638        g[a] = ja.dot(wr);
1639        for b in a..d {
1640            // Gauss-Newton block `J_aᵀ M J_b` (Euclidean: `J_aᵀ J_b`).
1641            let mut hab = match &mjm {
1642                Some(v) => ja.dot(&v[b]),
1643                None => ja.dot(&jm.row(b)),
1644            };
1645            // (M-weighted) residual · second-jet curvature: (M r) · ∂²m_{ab},
1646            // ∂²m_{ab}[out] = z · Σ_basis (∂²Φ/∂t_a∂t_b) · B[basis, out].
1647            let mut curv = 0.0;
1648            for basis_col in 0..m {
1649                let d2phi = second[[0, basis_col, a, b]];
1650                if d2phi == 0.0 {
1651                    continue;
1652                }
1653                curv += amplitude * d2phi * rd[basis_col];
1654            }
1655            hab += curv;
1656            h[[a, b]] = hab;
1657            h[[b, a]] = hab;
1658        }
1659    }
1660    // F3 — latent coordinate prior. The fit placed an ARD (Gaussian) / von-Mises
1661    // (periodic) prior on `t`; its energy enters the certified objective, so its
1662    // gradient and (per-axis diagonal) Hessian enter the Newton field. Absent for
1663    // `EncodeObjective::euclidean` (no allocation, no arithmetic).
1664    if let Some(alpha) = objective.prior_alpha {
1665        for axis in 0..d.min(alpha.len()) {
1666            let alpha_axis = alpha[axis];
1667            if alpha_axis == 0.0 {
1668                continue;
1669            }
1670            let pr = crate::manifold::ArdAxisPrior::eval(
1671                alpha_axis,
1672                t[axis],
1673                latent_axis_period(atom, axis),
1674            );
1675            g[axis] += pr.grad;
1676            h[[axis, axis]] += pr.hess;
1677        }
1678    }
1679    // NO ridge: the certificate must use the TRUE Hessian (F2). See the doc above.
1680    Ok(Some((g, h)))
1681}
1682
1683/// Operator-norm of `H⁻¹` (i.e. `β = 1/λ_min(H)`) and the Newton step
1684/// `δ = −H⁻¹ g` with `η = ‖δ‖`, from a symmetric PSD `H` and gradient `g`.
1685/// Returns `None` when `H` is numerically singular (λ_min ≤ 0) — an
1686/// uncertifiable start.
1687pub(crate) fn beta_eta_newton(
1688    h: ArrayView2<'_, f64>,
1689    g: ArrayView1<'_, f64>,
1690) -> Result<Option<(f64, f64, Array1<f64>)>, String> {
1691    // Closed-form fast paths for the tiny latent dims that dominate SAE atoms
1692    // (`d = 1, 2`), avoiding a faer eigendecomposition + its heap allocations on
1693    // the hottest per-row Newton inner loop. `β = 1/λ_min(H)`, `δ = −H⁻¹g`, and the
1694    // `λ_min ≤ 0` gate are computed directly; the symmetric-`H` reads mirror
1695    // `eigh(Side::Lower)` (which uses the lower triangle) exactly.
1696    let d = h.nrows();
1697    if d == 1 {
1698        // A coordinate direction is certifiable only when the actual derivative
1699        // F'(t) has strictly positive, resolved curvature.
1700        let h00 = h[[0, 0]];
1701        if !(h00.is_finite() && h00 > 0.0) {
1702            return Ok(None);
1703        }
1704        let delta0 = -g[0] / h00;
1705        let mut delta = Array1::<f64>::zeros(1);
1706        delta[0] = delta0;
1707        return Ok(Some((1.0 / h00, delta0.abs(), delta)));
1708    }
1709    if d == 2 {
1710        // Symmetric H = [[a, b], [b, c]] read from the lower triangle.
1711        let a = h[[0, 0]];
1712        let b = h[[1, 0]];
1713        let c = h[[1, 1]];
1714        let tr = a + c;
1715        let det = a * c - b * b;
1716        // λ_min = ½(tr − √((a−c)² + 4b²)), λ_max = ½(tr + √…); ≥ 0 ⇒ PSD.
1717        let disc = ((a - c) * (a - c) + 4.0 * b * b).max(0.0).sqrt();
1718        let lambda_min = 0.5 * (tr - disc);
1719        let lambda_max = 0.5 * (tr + disc);
1720        let max_abs = lambda_min.abs().max(lambda_max.abs());
1721        if !(lambda_min.is_finite() && lambda_max.is_finite() && max_abs > 0.0) {
1722            return Ok(None);
1723        }
1724        let floor = gam_solve::arrow_schur::SPECTRAL_DEFLATION_REL_FLOOR * max_abs;
1725        // Healthy PD block — smallest eigenvalue clears the numerical null band —
1726        // keeps the closed-form fast path. A negative or unresolved eigenvalue is
1727        // not invertible as F'(t), so ordinary Kantorovich cannot certify it.
1728        if lambda_min > floor {
1729            // δ = −H⁻¹g with H⁻¹ = [[c, −b], [−b, a]] / det (det = λ_min·λ_max > 0).
1730            let inv_det = 1.0 / det;
1731            let g0 = g[0];
1732            let g1 = g[1];
1733            let d0 = -(c * g0 - b * g1) * inv_det;
1734            let d1 = -(a * g1 - b * g0) * inv_det;
1735            if !(d0.is_finite() && d1.is_finite()) {
1736                return Ok(None);
1737            }
1738            let mut delta = Array1::<f64>::zeros(2);
1739            delta[0] = d0;
1740            delta[1] = d1;
1741            let eta = (d0 * d0 + d1 * d1).sqrt();
1742            return Ok(Some((1.0 / lambda_min, eta, delta)));
1743        }
1744        return Ok(None);
1745    }
1746    beta_eta_newton_positive_definite(h, g)
1747}
1748
1749/// Spectral path for `d ≥ 3`. Ordinary Newton--Kantorovich requires the inverse
1750/// of the actual derivative `F'(t) = H`; quotienting or replacing a null
1751/// eigenvalue changes that derivative and therefore cannot certify the returned
1752/// iteration. We consequently accept only a numerically resolved positive-
1753/// definite Hessian and report every singular/indefinite start as uncertified.
1754fn beta_eta_newton_positive_definite(
1755    h: ArrayView2<'_, f64>,
1756    g: ArrayView1<'_, f64>,
1757) -> Result<Option<(f64, f64, Array1<f64>)>, String> {
1758    let d = h.nrows();
1759    // Symmetrise defensively before the eigendecomposition (the assembled Hessian
1760    // is symmetric only up to reduction order), mirroring the evidence routine.
1761    let mut sym = Array2::<f64>::zeros((d, d));
1762    for i in 0..d {
1763        for j in 0..d {
1764            let v = 0.5 * (h[[i, j]] + h[[j, i]]);
1765            if !v.is_finite() {
1766                return Ok(None);
1767            }
1768            sym[[i, j]] = v;
1769        }
1770    }
1771    let (vals, vecs) = sym
1772        .eigh(Side::Lower)
1773        .map_err(|e| format!("beta_eta_newton: eigh failed: {e:?}"))?;
1774    let max_abs = vals.iter().fold(
1775        0.0_f64,
1776        |acc, &v| if v.is_finite() { acc.max(v.abs()) } else { acc },
1777    );
1778    if !(max_abs.is_finite() && max_abs > 0.0) {
1779        return Ok(None);
1780    }
1781    let floor = gam_solve::arrow_schur::SPECTRAL_DEFLATION_REL_FLOOR * max_abs;
1782    if vals
1783        .iter()
1784        .any(|&lambda| !lambda.is_finite() || lambda <= floor)
1785    {
1786        return Ok(None);
1787    }
1788    let lambda_min = vals.iter().cloned().fold(f64::INFINITY, f64::min);
1789    if !(lambda_min.is_finite() && lambda_min > 0.0) {
1790        return Ok(None);
1791    }
1792    let beta = 1.0 / lambda_min;
1793    // Newton step δ = −H⁻¹g via the eigendecomposition of the actual Hessian.
1794    let mut delta = Array1::<f64>::zeros(d);
1795    for (col, &lam) in vals.iter().enumerate() {
1796        let vi = vecs.column(col);
1797        let coeff = vi.dot(&g) / lam;
1798        for row in 0..d {
1799            delta[row] -= coeff * vi[row];
1800        }
1801    }
1802    if delta.iter().any(|v| !v.is_finite()) {
1803        return Ok(None);
1804    }
1805    let eta = delta.dot(&delta).sqrt();
1806    Ok(Some((beta, eta, delta)))
1807}
1808
1809/// Compute the per-row Kantorovich certificate for encoding target row `x`
1810/// against atom `atom` at start coordinate `t₀`, with fixed amplitude `z` and
1811/// the chart's closed-form Lipschitz constant `lipschitz`. Returns the
1812/// certificate AND the Newton step `δ = −H⁻¹ g` so the caller can advance.
1813pub fn row_certificate(
1814    atom: &SaeManifoldAtom,
1815    evaluator: &dyn SaeBasisEvaluator,
1816    t0: ArrayView1<'_, f64>,
1817    x: ArrayView1<'_, f64>,
1818    amplitude: f64,
1819    lipschitz: f64,
1820) -> Result<(RowCertificate, Array1<f64>), String> {
1821    // Euclidean, prior-free objective — bit-identical to the metric-free encode.
1822    row_certificate_core(
1823        atom,
1824        evaluator,
1825        t0,
1826        x,
1827        amplitude,
1828        lipschitz,
1829        &EncodeObjective::euclidean(),
1830    )
1831}
1832
1833/// Objective-aware [`row_certificate`] (F3): the certificate `h = β·η·L` is
1834/// computed from the TRUE objective's gradient/Hessian ([`encode_grad_hess_core`])
1835/// so it certifies the metric- and prior-weighted field. `lipschitz` must already
1836/// be the objective's effective bound ([`EncodeObjective::effective_lipschitz`]).
1837pub(crate) fn row_certificate_core(
1838    atom: &SaeManifoldAtom,
1839    evaluator: &dyn SaeBasisEvaluator,
1840    t0: ArrayView1<'_, f64>,
1841    x: ArrayView1<'_, f64>,
1842    amplitude: f64,
1843    lipschitz: f64,
1844    objective: &EncodeObjective<'_>,
1845) -> Result<(RowCertificate, Array1<f64>), String> {
1846    let uncertified = || {
1847        (
1848            RowCertificate {
1849                beta: f64::INFINITY,
1850                eta: f64::INFINITY,
1851                lipschitz,
1852                h: f64::INFINITY,
1853            },
1854            Array1::<f64>::zeros(atom.latent_dim()),
1855        )
1856    };
1857    // No second jet ⇒ no full Hessian ⇒ uncertifiable (flag).
1858    let Some((g, h)) = encode_grad_hess_core(atom, evaluator, t0, x, amplitude, objective)? else {
1859        return Ok(uncertified());
1860    };
1861    match beta_eta_newton(h.view(), g.view())? {
1862        Some((beta, eta, delta)) => {
1863            let cert = RowCertificate {
1864                beta,
1865                eta,
1866                lipschitz,
1867                h: beta * eta * lipschitz,
1868            };
1869            Ok((cert, delta))
1870        }
1871        // Indefinite / negative-curvature full Hessian: the start is at or past
1872        // a basin boundary (a max/saddle of f), not the minimum basin — flag.
1873        None => Ok(uncertified()),
1874    }
1875}
1876
1877fn uncertified_certificate(lipschitz: f64) -> RowCertificate {
1878    RowCertificate {
1879        beta: f64::INFINITY,
1880        eta: f64::INFINITY,
1881        lipschitz,
1882        h: f64::INFINITY,
1883    }
1884}
1885
1886fn refine_certified_start(
1887    atom: &SaeManifoldAtom,
1888    evaluator: &dyn SaeBasisEvaluator,
1889    mut t: Array1<f64>,
1890    x: ArrayView1<'_, f64>,
1891    amplitude: f64,
1892    lipschitz: f64,
1893    newton_steps: usize,
1894    initial_cert: RowCertificate,
1895    mut delta: Array1<f64>,
1896    chart_center: ArrayView1<'_, f64>,
1897    chart_radius: f64,
1898    objective: &EncodeObjective<'_>,
1899) -> Result<Option<CertifiedEncodeProbe>, String> {
1900    assert!(initial_cert.certified());
1901    let mut final_cert = initial_cert;
1902    for _ in 0..newton_steps {
1903        // Convergence early-exit: the pending Newton step is below the coordinate
1904        // ULP scale, so `t + δ == t` to f64 resolution — the certified root is
1905        // reached and the remaining fixed-budget steps would only re-accumulate
1906        // round-off. This is where the well-conditioned quadratic Newton tail's
1907        // redundant `evaluate` + `second_jet` work is eliminated.
1908        if delta.dot(&delta).sqrt() <= NEWTON_REFINE_CONVERGED_EPS * (1.0 + t.dot(&t).sqrt()) {
1909            break;
1910        }
1911        let next = &t + &delta;
1912        // SOUNDNESS GUARD — same containment rule as `certify_with_basin_warmup`:
1913        // `lipschitz` is only a valid Hessian-Lipschitz bound inside this chart's
1914        // ball for the chart-local families. A refine iterate that leaves the ball
1915        // would have its certificate recomputed below with an `L` that no longer
1916        // bounds the true geometry there, so `h ≤ ½` would NOT imply Kantorovich
1917        // convergence — and the in-hand certificate at the previous iterate is
1918        // itself suspect (its guarantee needs `L` valid on the Newton sequence's
1919        // ball, part of which now lies outside the chart). Refuse and flag for the
1920        // exact fallback, exactly as the warm-up does. Wrap-aware distance, so
1921        // periodic-seam iterates are measured in the true manifold geometry.
1922        if latent_coordinate_distance(atom, next.view(), chart_center) > chart_radius {
1923            return Ok(None);
1924        }
1925        t = next;
1926        let (cert, next_delta) = row_certificate_core(
1927            atom,
1928            evaluator,
1929            t.view(),
1930            x,
1931            amplitude,
1932            lipschitz,
1933            objective,
1934        )?;
1935        if !cert.certified() {
1936            return Ok(None);
1937        }
1938        final_cert = cert;
1939        delta = next_delta;
1940    }
1941    Ok(Some(CertifiedEncodeProbe {
1942        coord: t,
1943        final_cert,
1944    }))
1945}
1946
1947/// Certify an encode probe from `t_start`, navigating into the Kantorovich basin
1948/// first if needed (#1154/#1026). The Kantorovich quantity `h = β·η·L` scales with
1949/// amplitude through `L`, so at unit amplitude a positive-definite chart-center /
1950/// distilled start can sit OUTSIDE the certified ball (`h > ½`). Rather than
1951/// flagging it uncertified immediately — which made the encoder certify ZERO
1952/// held-out rows at amplitude 1.0 and fall back to the exact solve for everything —
1953/// take plain Newton steps toward the root, re-certifying at each iterate, while
1954/// the Kantorovich quantity `h = β·η·L` keeps CONTRACTING toward the ½ bound. The
1955/// certificate at the landing point is a full Kantorovich guarantee from there
1956/// (`h ≤ ½` ⇒ Newton converges to the in-ball root), so this only ever WIDENS the
1957/// certified set; it never certifies a non-convergent start.
1958///
1959/// Termination is the natural Newton stopping rule — there is no arbitrary step
1960/// budget. The warm-up stops and flags for the exact fallback when either the start
1961/// is not steppable (indefinite / non-finite Hessian — at or past a basin boundary)
1962/// or a step fails to reduce `h` (the iterate is not approaching a certifiable
1963/// in-chart root: its root lies outside this chart's valid Lipschitz region, or the
1964/// start was past the basin — empirically the rows that miss *plateau*, so more
1965/// steps cannot help; the lever there is denser charts, not more iterations). On
1966/// success the start is refined `newton_steps` further by [`refine_certified_start`].
1967/// Minimum per-step *multiplicative* decrease of the Kantorovich `h` the basin
1968/// warm-up requires to keep stepping (FIX #4). A tiny geometric floor: a
1969/// continued step must contract `h` by at least this fraction. Chosen small so
1970/// it never bites a genuinely converging row (Newton in the Kantorovich regime
1971/// is at least geometric and quadratic once `h < 1`, contracting `h` far faster
1972/// than `1/64` per step) while still forcing termination on a plateau.
1973///
1974/// PRICED (#2071): the value only sets the termination bound
1975/// `N < ln(2·h₀) / −ln(1 − c)` (below) — it is a floor on "real progress", not a
1976/// tuning parameter, so any small `c ∈ (0, 1)` is correct; `1/64 = 2^-6` is priced
1977/// for its bound. What breaks at 10×: at `c = 1/6.4` the floor starts rejecting
1978/// slow-but-genuine geometric contractions near the `½` certifiable boundary
1979/// (false plateaus → premature exact fallback); at `c = 1/640` the plateau bound
1980/// loosens ~10× (`N` grows from a few hundred to a few thousand
1981/// `row_certificate` solves on a pathological row). `1/64` keeps the bound at a
1982/// few hundred while leaving a wide margin below Newton's actual contraction.
1983const WARMUP_MIN_MULTIPLICATIVE_DECREASE: f64 = 1.0 / 64.0;
1984
1985/// Kantorovich-quadratic acceptance coefficient for the basin warm-up (FIX #4).
1986/// Once `h < 1` a converging Newton step contracts quadratically (`h_new ≲ κ·h²`);
1987/// accepting that path in addition to the geometric floor makes the
1988/// "genuinely-converging rows are untouched" guarantee explicit. Kept `< 1` so
1989/// the quadratic path is itself a strict contraction (for `h < 1`,
1990/// `κ·h² < κ·h < h`), which preserves the termination bound below.
1991///
1992/// PRICED (#2071): the only constraint the termination proof imposes is `κ < 1`
1993/// (so the quadratic branch stays a strict contraction and cannot defeat the
1994/// geometric bound); `0.5` is the natural centre of `(0, 1)`, a factor-2 margin
1995/// below the `κ = 1` boundary. What breaks at 10×: `κ = 5` violates `κ < 1` and
1996/// the quadratic branch could ACCEPT an expanding step (`κ·h² > h` for
1997/// `h > 1/κ`), breaking termination; `κ = 0.05` merely tightens the quadratic
1998/// acceptance (fewer rows take the quadratic branch, more fall to the geometric
1999/// floor) with no correctness effect. Anything in `(0, 1)` is sound; `0.5` maximises
2000/// the margin.
2001const WARMUP_QUADRATIC_KAPPA: f64 = 0.5;
2002
2003/// Sufficient-decrease test for the basin-warmup loop (FIX #4).
2004///
2005/// Returns `true` while the warm-up should keep stepping. The previous exit rule
2006/// (`h_new >= h_prev` — strict decrease or quit) never fires for an `h`-sequence
2007/// that decreases *monotonically toward a limit above ½*: the increments fall
2008/// below one ulp long before `h` crosses the certifiable ½ bound, so a single
2009/// pathological row could spin ~1e15 full `row_certificate` solves (Hessian
2010/// build + solve) on the encode hot path. We instead require genuine
2011/// *multiplicative* progress each step, which matches Newton's actual behavior:
2012/// a healthy contraction clears the geometric floor by a wide margin (and the
2013/// quadratic path once `h < 1`), while a plateau (`h_new/h_prev → 1`) satisfies
2014/// neither branch and flags to the exact fallback.
2015///
2016/// Termination bound: the warm-up loop runs only while the row is uncertified
2017/// (`h_prev > ½`), and every continued step contracts `h` by at least the factor
2018/// `(1 − WARMUP_MIN_MULTIPLICATIVE_DECREASE)` (the quadratic branch, for `h < 1`,
2019/// contracts by `κ·h_prev < κ < 1`, i.e. even harder). Hence after `N` continued
2020/// steps `h ≤ (1 − c)^N · h₀`, and since the loop stops once `h ≤ ½` we get
2021/// `N < ln(2·h₀) / −ln(1 − c)` — a few hundred iterations at most, versus
2022/// unbounded before. No arbitrary fixed step cap is imposed; the bound is a
2023/// consequence of the contraction requirement, in keeping with the function's
2024/// no-magic-budget design.
2025fn warmup_progress_sufficient(h_new: f64, h_prev: f64) -> bool {
2026    if !(h_new.is_finite() && h_prev.is_finite()) {
2027        return false;
2028    }
2029    // Geometric floor: a real Newton contraction easily clears this.
2030    if h_new <= (1.0 - WARMUP_MIN_MULTIPLICATIVE_DECREASE) * h_prev {
2031        return true;
2032    }
2033    // Kantorovich-quadratic path (only once `h < 1`, where it is a strict
2034    // contraction): makes the no-regression guarantee for converging rows
2035    // explicit without ever admitting a non-contracting (plateau) step.
2036    h_prev < 1.0 && h_new <= WARMUP_QUADRATIC_KAPPA * h_prev * h_prev
2037}
2038
2039/// Whether the basin warm-up must REJECT the just-taken step (flag to the exact
2040/// fallback). FIX (F6): a step that just crossed into the certified region
2041/// (`h ≤ ½`) is ALWAYS accepted, even if its multiplicative decrease was below
2042/// the progress floor (e.g. `h: 0.501 → 0.499`, a legitimate but tiny cross of
2043/// the ½ bound). Only a step that is STILL uncertified AND failed to make
2044/// sufficient multiplicative progress is a plateau that must flag. The old code
2045/// ran the progress test unconditionally, so it could reject an in-hand
2046/// certificate and push the row to the exact fallback for nothing (a false
2047/// negative).
2048fn warmup_should_reject(next_certified: bool, h_new: f64, h_prev: f64) -> bool {
2049    !next_certified && !warmup_progress_sufficient(h_new, h_prev)
2050}
2051
2052fn certify_with_basin_warmup(
2053    atom: &SaeManifoldAtom,
2054    evaluator: &dyn SaeBasisEvaluator,
2055    t_start: Array1<f64>,
2056    x: ArrayView1<'_, f64>,
2057    amplitude: f64,
2058    lipschitz: f64,
2059    newton_steps: usize,
2060    chart_center: ArrayView1<'_, f64>,
2061    chart_radius: f64,
2062    objective: &EncodeObjective<'_>,
2063) -> Result<Option<CertifiedEncodeProbe>, String> {
2064    // SOUNDNESS GUARD: `lipschitz` is the chart's Hessian-Lipschitz sup, which is
2065    // only a valid bound over this chart's ball `‖t − center‖ ≤ radius` for the
2066    // chart-local families (`EuclideanPatch`/`Linear`/`Poincare` monomial patches,
2067    // `Cylinder` line axis, `Duchon` radial kernels). If a warm-up iterate leaves
2068    // that ball, `row_certificate` would compute `h = β·η·L` with an `L` that no
2069    // longer bounds the true geometry there, so `h ≤ ½` would NOT imply Kantorovich
2070    // convergence — a false certificate. (The `h`-contraction check does NOT catch
2071    // this: `h` can decrease monotonically toward an out-of-chart root the whole
2072    // way.) So we keep every certified iterate inside the chart; a row whose root is
2073    // outside this chart flags for the exact fallback — its lever is a denser grid,
2074    // not a step using an invalid `L`. Global-`L` families (periodic/torus/sphere)
2075    // route their points to charts whose centers are near the root, so the guard
2076    // rarely trips for them, and where it does the row was out-of-chart anyway.
2077    let in_chart = |t: &Array1<f64>| -> bool {
2078        // Wrap-aware chart containment. A raw Euclidean latent distance
2079        // `Σ(tᵢ − centerᵢ)²` mis-measures separation across the wrap seam of a
2080        // periodic axis: an iterate at `t = 0.99` against a chart centered at
2081        // `0.01` reads distance `0.98` instead of the true circle distance
2082        // `0.02`, so it is wrongly rejected at the start check and at every step
2083        // check — silently disabling the amortized encoder for a phase-localized
2084        // band of rows on every periodic / torus / cylinder-angle /
2085        // sphere-longitude chart and pushing that band to the multi-start
2086        // fallback. `latent_coordinate_distance` folds each axis onto its
2087        // `latent_axis_period`, so the containment ball is measured in the true
2088        // manifold geometry — exactly the geometry the chart's Lipschitz bound
2089        // `L` is valid over. This only ever moves points that are genuinely
2090        // near the center (small wrapped distance) back INTO the chart where
2091        // `L` holds, so it widens acceptance without weakening the soundness
2092        // guard (an iterate truly far from the center on the circle still fails).
2093        latent_coordinate_distance(atom, t.view(), chart_center) <= chart_radius
2094    };
2095    let mut t = t_start;
2096    // The distilled / chart-center start must itself be in-chart for its certificate
2097    // to be valid; a bad IFT prediction landing outside the chart is uncertifiable.
2098    if !in_chart(&t) {
2099        return Ok(None);
2100    }
2101    let (mut cert, mut delta) = row_certificate_core(
2102        atom,
2103        evaluator,
2104        t.view(),
2105        x,
2106        amplitude,
2107        lipschitz,
2108        objective,
2109    )?;
2110    while !cert.certified() {
2111        // Not steppable (indefinite / non-finite Hessian): flag.
2112        if !(cert.h.is_finite() && cert.beta.is_finite() && cert.eta.is_finite()) {
2113            return Ok(None);
2114        }
2115        let prev_h = cert.h;
2116        let next = &t + &delta;
2117        // Refuse to step where the chart's `L` is no longer valid (see guard above).
2118        if !in_chart(&next) {
2119            return Ok(None);
2120        }
2121        t = next;
2122        let (next_cert, next_delta) = row_certificate_core(
2123            atom,
2124            evaluator,
2125            t.view(),
2126            x,
2127            amplitude,
2128            lipschitz,
2129            objective,
2130        )?;
2131        cert = next_cert;
2132        delta = next_delta;
2133        // The warm-up only helps while h keeps *multiplicatively* contracting
2134        // toward ½. A plain strict-decrease test (`h >= prev_h`) never fires for
2135        // a sequence that decreases monotonically toward a limit above ½, so it
2136        // could spin ~1e15 `row_certificate` solves for one row; require genuine
2137        // multiplicative progress instead (bounded to a few hundred steps, see
2138        // `warmup_progress_sufficient`). Once a step fails that bar the iterate is
2139        // not converging to a certifiable in-chart root — flag for the exact
2140        // fallback (no arbitrary step budget; the bound falls out of the
2141        // contraction requirement).
2142        //
2143        // F6: only enforce the progress bar while STILL uncertified. If this step
2144        // just crossed `h ≤ ½` (e.g. 0.501 → 0.499, a decrease too small to clear
2145        // the multiplicative floor) the point is already certified — the loop
2146        // guard `while !cert.certified()` will exit and refine it. Running the
2147        // progress test unconditionally would falsely reject an in-hand certificate
2148        // and push the row to the exact fallback for nothing. See
2149        // [`warmup_should_reject`].
2150        if warmup_should_reject(cert.certified(), cert.h, prev_h) {
2151            return Ok(None);
2152        }
2153    }
2154    refine_certified_start(
2155        atom,
2156        evaluator,
2157        t,
2158        x,
2159        amplitude,
2160        lipschitz,
2161        newton_steps,
2162        cert,
2163        delta,
2164        chart_center,
2165        chart_radius,
2166        objective,
2167    )
2168}
2169
2170fn kantorovich_root_radius(cert: RowCertificate) -> f64 {
2171    if !cert.certified() || !(cert.eta.is_finite() && cert.eta >= 0.0) {
2172        return f64::INFINITY;
2173    }
2174    if cert.eta == 0.0 {
2175        return 0.0;
2176    }
2177    if !(cert.h.is_finite() && cert.h >= 0.0) {
2178        return f64::INFINITY;
2179    }
2180    let h = cert.h.min(KANTOROVICH_THRESHOLD);
2181    let discriminant = (1.0 - 2.0 * h).max(0.0).sqrt();
2182    let radius = 2.0 * cert.eta / (1.0 + discriminant);
2183    if radius.is_finite() {
2184        radius
2185    } else {
2186        f64::INFINITY
2187    }
2188}
2189
2190fn distilled_probe_tolerance(
2191    amortized: &CertifiedEncodeProbe,
2192    cold: &CertifiedEncodeProbe,
2193    amplitude: f64,
2194    x: ArrayView1<'_, f64>,
2195) -> f64 {
2196    let certified_radius =
2197        kantorovich_root_radius(amortized.final_cert) + kantorovich_root_radius(cold.final_cert);
2198    let coord_scale = amortized.coord.dot(&amortized.coord).sqrt()
2199        + cold.coord.dot(&cold.coord).sqrt()
2200        + x.dot(&x).sqrt()
2201        + amplitude.abs()
2202        + 1.0;
2203    certified_radius + 1024.0 * f64::EPSILON * coord_scale
2204}
2205
2206fn latent_coordinate_distance(
2207    atom: &SaeManifoldAtom,
2208    lhs: ArrayView1<'_, f64>,
2209    rhs: ArrayView1<'_, f64>,
2210) -> f64 {
2211    let mut acc = 0.0;
2212    for axis in 0..lhs.len().min(rhs.len()) {
2213        let mut diff = (lhs[axis] - rhs[axis]).abs();
2214        if let Some(period) = latent_axis_period(atom, axis) {
2215            let wrapped = diff.rem_euclid(period);
2216            diff = wrapped.min(period - wrapped);
2217        }
2218        acc += diff * diff;
2219    }
2220    acc.sqrt()
2221}
2222
2223fn latent_axis_period(atom: &SaeManifoldAtom, axis: usize) -> Option<f64> {
2224    use crate::manifold::SaeAtomBasisKind::*;
2225    match atom.basis_kind() {
2226        Periodic | Torus => Some(1.0),
2227        Cylinder if axis == 0 => Some(1.0),
2228        Sphere if axis == 1 => Some(std::f64::consts::TAU),
2229        _ => None,
2230    }
2231}
2232
2233/// Configuration for [`EncodeAtlas`] construction and online encode. All fields
2234/// are explicit; the atlas never reads global state and adds no CLI flags.
2235#[derive(Debug, Clone, Copy)]
2236pub struct AtlasConfig {
2237    /// Grid resolution per latent axis for offline chart centers (the
2238    /// SHAPE_BAND grid idiom).
2239    pub grid_resolution: usize,
2240    /// Levenberg ridge floor added to the per-row Gauss-Newton Hessian.
2241    pub ridge: f64,
2242    /// Number of online Newton refinement steps after a certified start (1 or 2
2243    /// per issue #1010).
2244    pub newton_steps: usize,
2245}
2246
2247impl Default for AtlasConfig {
2248    fn default() -> Self {
2249        Self {
2250            grid_resolution: 16,
2251            ridge: 1.0e-9,
2252            newton_steps: 2,
2253        }
2254    }
2255}
2256
2257/// The encode atlas: per-atom certified charts plus the online certified-encode
2258/// driver (issue #1010).
2259#[derive(Debug, Clone)]
2260pub struct EncodeAtlas {
2261    pub atoms: Vec<AtomEncodeAtlas>,
2262    pub config: AtlasConfig,
2263}
2264
2265impl EncodeAtlas {
2266    /// Build the offline atlas over a frozen dictionary: for each atom, lay down
2267    /// chart centers on the atom's coordinate grid and certify a Newton radius
2268    /// from the Kantorovich inequality at the worst-case in-chart start.
2269    ///
2270    /// `amplitude_bound[k]` is the per-atom bound on `|z_k|` used to scale the
2271    /// reconstruction jets (the offline `L` must hold for the largest amplitude
2272    /// the encode can produce); `target_norm_bound` bounds `‖x‖` over the data.
2273    pub fn build(
2274        atoms: &[SaeManifoldAtom],
2275        amplitude_bound: &[f64],
2276        target_norm_bound: f64,
2277        config: AtlasConfig,
2278    ) -> Result<Self, String> {
2279        if amplitude_bound.len() != atoms.len() {
2280            return Err(format!(
2281                "EncodeAtlas::build: amplitude_bound length {} != atom count {}",
2282                amplitude_bound.len(),
2283                atoms.len()
2284            ));
2285        }
2286        let mut atom_atlases = Vec::with_capacity(atoms.len());
2287        for (k, atom) in atoms.iter().enumerate() {
2288            let atlas =
2289                Self::build_atom_atlas(k, atom, amplitude_bound[k], target_norm_bound, &config)?;
2290            atom_atlases.push(atlas);
2291        }
2292        Ok(Self {
2293            atoms: atom_atlases,
2294            config,
2295        })
2296    }
2297
2298    pub(crate) fn build_atom_atlas(
2299        atom_index: usize,
2300        atom: &SaeManifoldAtom,
2301        amplitude_bound: f64,
2302        target_norm_bound: f64,
2303        config: &AtlasConfig,
2304    ) -> Result<AtomEncodeAtlas, String> {
2305        let centers = chart_center_grid(atom, config.grid_resolution);
2306        // Half the inter-center spacing is the natural in-chart radius so the
2307        // charts tile the grid without gaps; refined below if the certificate
2308        // fails at that radius. One uniform radius for the regular grid.
2309        let nominal_radius = chart_nominal_radius(atom, config.grid_resolution);
2310        let radii = vec![nominal_radius; centers.nrows()];
2311        Self::build_atom_atlas_from_centers(
2312            atom_index,
2313            atom,
2314            centers.view(),
2315            &radii,
2316            amplitude_bound,
2317            target_norm_bound,
2318            config,
2319        )
2320    }
2321
2322    /// Build a per-atom atlas from EXPLICIT chart centers with a per-center
2323    /// nominal radius — the geometry-agnostic core shared by the regular-grid
2324    /// [`Self::build_atom_atlas`] and the data-driven [`Self::build_data_driven`].
2325    /// Every chart is certified identically (Kantorovich radius from the in-chart
2326    /// curvature at its center); only the center PLACEMENT and per-center radius
2327    /// differ. `radii[c]` is the nominal in-chart radius for `centers[c]`.
2328    pub(crate) fn build_atom_atlas_from_centers(
2329        atom_index: usize,
2330        atom: &SaeManifoldAtom,
2331        centers: ArrayView2<'_, f64>,
2332        radii: &[f64],
2333        amplitude_bound: f64,
2334        target_norm_bound: f64,
2335        config: &AtlasConfig,
2336    ) -> Result<AtomEncodeAtlas, String> {
2337        let d = atom.latent_dim();
2338        if centers.ncols() != d {
2339            return Err(format!(
2340                "build_atom_atlas_from_centers: centers have {} cols but atom latent_dim is {d}",
2341                centers.ncols()
2342            ));
2343        }
2344        if radii.len() != centers.nrows() {
2345            return Err(format!(
2346                "build_atom_atlas_from_centers: {} radii != {} centers",
2347                radii.len(),
2348                centers.nrows()
2349            ));
2350        }
2351        // Full-width frame (matches `family_jet_sups` / `reconstruction_jet_sups`):
2352        // the atlas's stored decoder scaling must pair with the full-width family
2353        // sups, so a #1117 rank-reduced atom contributes `Σ‖(Q B̃)_{m,:}‖`, not the
2354        // reduced-row sum. Identical for an un-reduced atom.
2355        let decoder_norm_sum = decoder_row_norm_sum(atom.full_width_decoder().view());
2356        let mut charts = Vec::with_capacity(centers.nrows());
2357        // HONEST REFUSAL for Duchon atoms (F2/F3): the closed-form Hessian-Lipschitz
2358        // bound available here (`family_jet_sups` Duchon arm) hard-codes cubic-r³
2359        // jets and a single origin center, but the real Duchon kernel is the
2360        // polyharmonic `c·r^(2m−d)` (with log variants) over data-placed centers.
2361        // That bound can UNDER-estimate L → a FALSE certificate (the module's own
2362        // warning; underestimating L is the dangerous direction). The atom does not
2363        // expose its real order/centers/scaling to this crate, so no sound bound is
2364        // available — refuse rather than fabricate. Every Duchon chart is emitted
2365        // UNCERTIFIED (`certified_radius = 0`, no amortized predictor), so routing
2366        // skips it and every Duchon row flags for the exact multi-start encode.
2367        let duchon_uncertifiable =
2368            matches!(atom.basis_kind(), crate::manifold::SaeAtomBasisKind::Duchon);
2369        for c in 0..centers.nrows() {
2370            let center = centers.row(c).to_owned();
2371            let nominal_radius = radii[c];
2372            let region = chart_region(atom, center.clone(), nominal_radius);
2373            if duchon_uncertifiable {
2374                charts.push(CertifiedChart {
2375                    region,
2376                    lipschitz: f64::INFINITY,
2377                    beta_center: f64::INFINITY,
2378                    certified_radius: 0.0,
2379                    amortized_jacobian: None,
2380                    recon_center: Array1::<f64>::zeros(atom.output_dim()),
2381                    amortized_base: None,
2382                });
2383                continue;
2384            }
2385            let sups = family_jet_sups(atom, &region)?;
2386            let recon_sups = reconstruction_jet_sups(atom, sups);
2387            let lipschitz =
2388                hessian_lipschitz_constant(recon_sups, amplitude_bound, target_norm_bound, 0.0);
2389            // β at the chart center bounds the worst-case in-chart curvature
2390            // (the Gauss-Newton Hessian is continuous; the certified radius is
2391            // solved so the certificate is robust to the start within the ball).
2392            let beta_center = match center_beta(atom, &center, config.ridge) {
2393                Some(b) => b,
2394                None => {
2395                    // Degenerate center curvature: no certifiable chart here, and
2396                    // no amortized Jacobian (the same singular Gauss–Newton block).
2397                    charts.push(CertifiedChart {
2398                        region,
2399                        lipschitz,
2400                        beta_center: f64::INFINITY,
2401                        certified_radius: 0.0,
2402                        amortized_jacobian: None,
2403                        recon_center: Array1::<f64>::zeros(atom.output_dim()),
2404                        amortized_base: None,
2405                    });
2406                    continue;
2407                }
2408            };
2409            // Distill the amortized-encoder Jacobian at this center (#1026 ladder
2410            // item 3): the IFT derivative of the encode map, precomputed offline
2411            // so the online encode is one mat-vec. A finite `beta_center` (above)
2412            // means the Gauss–Newton block is non-singular, so this succeeds
2413            // alongside it; the pair travels together on the chart.
2414            let (amortized_jacobian, recon_center) =
2415                match center_amortized_jacobian(atom, &center, config.ridge) {
2416                    Some((a1, m1)) => (Some(a1), m1),
2417                    None => (None, Array1::<f64>::zeros(atom.output_dim())),
2418                };
2419            // Certified radius from h = β·η·L ≤ ½ with η ≤ R (Newton step length
2420            // is bounded by the start distance to the root, itself ≤ chart
2421            // radius at worst): R_c = ½ / (β·L), capped at the nominal radius.
2422            let certified_radius = if lipschitz > 0.0 && beta_center.is_finite() {
2423                (0.5 / (beta_center * lipschitz)).min(region.radius)
2424            } else {
2425                region.radius
2426            };
2427            // Precompute the affine-predictor constant `base = t_c − A₁·m₁` (atom-
2428            // static), so the online encode is a single `base + (1/z)·A₁·x` mat-vec.
2429            let amortized_base = amortized_jacobian
2430                .as_ref()
2431                .map(|a1| &center - &a1.dot(&recon_center));
2432            charts.push(CertifiedChart {
2433                region,
2434                lipschitz,
2435                beta_center,
2436                certified_radius,
2437                amortized_jacobian,
2438                recon_center,
2439                amortized_base,
2440            });
2441        }
2442        Ok(AtomEncodeAtlas {
2443            atom_index,
2444            latent_dim: d,
2445            decoder_norm_sum,
2446            charts,
2447        })
2448    }
2449
2450    /// Build the atlas with DATA-DRIVEN chart placement: instead of a dense
2451    /// `resolution^d` product grid (exponential in latent dim `d`, so the regular
2452    /// [`Self::build`] is forced to coarse, poorly-certified charts for `d ≥ 3`),
2453    /// place a bounded number of charts AT the data's own latent coordinates. The
2454    /// chart count is then `O(max_charts)` regardless of `d`, and every chart sits
2455    /// where data actually lands (small in-chart residual → certifies), so
2456    /// higher-dimensional atoms — which reconstruct real activations far better per
2457    /// parameter — become affordable and well-covered.
2458    ///
2459    /// `coords[k]` is atom `k`'s `n × d_k` latent coordinates (the seed coords, or
2460    /// a previous encode's output). Charts are chosen by greedy farthest-point
2461    /// sampling over those coords (deterministic, coverage-maximizing), capped at
2462    /// `max_charts`. Each chart's nominal radius is half the distance to its
2463    /// nearest neighbor center, so the charts tile the local data density. The
2464    /// per-chart Kantorovich certification is IDENTICAL to the regular grid — only
2465    /// the center placement differs.
2466    pub fn build_data_driven(
2467        atoms: &[SaeManifoldAtom],
2468        coords: &[Array2<f64>],
2469        amplitude_bound: &[f64],
2470        target_norm_bound: f64,
2471        max_charts: usize,
2472        config: AtlasConfig,
2473    ) -> Result<Self, String> {
2474        if amplitude_bound.len() != atoms.len() || coords.len() != atoms.len() {
2475            return Err(format!(
2476                "build_data_driven: amplitude_bound {} / coords {} must match atom count {}",
2477                amplitude_bound.len(),
2478                coords.len(),
2479                atoms.len()
2480            ));
2481        }
2482        let mut atom_atlases = Vec::with_capacity(atoms.len());
2483        for (k, atom) in atoms.iter().enumerate() {
2484            let (centers, radii) =
2485                data_driven_chart_centers(atom, coords[k].view(), max_charts.max(1))?;
2486            let atlas = Self::build_atom_atlas_from_centers(
2487                k,
2488                atom,
2489                centers.view(),
2490                &radii,
2491                amplitude_bound[k],
2492                target_norm_bound,
2493                &config,
2494            )?;
2495            atom_atlases.push(atlas);
2496        }
2497        Ok(Self {
2498            atoms: atom_atlases,
2499            config,
2500        })
2501    }
2502
2503    fn refine_certified_encode_start(
2504        &self,
2505        atom: &SaeManifoldAtom,
2506        evaluator: &dyn SaeBasisEvaluator,
2507        chart: &CertifiedChart,
2508        t: Array1<f64>,
2509        x: ArrayView1<'_, f64>,
2510        amplitude: f64,
2511        objective: &EncodeObjective<'_>,
2512    ) -> Result<(Array1<f64>, RowCertificate), String> {
2513        // Certify from the warm start, navigating into the Kantorovich basin first
2514        // if the unit-amplitude start has h > ½ (see `certify_with_basin_warmup`).
2515        // The Lipschitz is the objective's EFFECTIVE bound (F3): the stored
2516        // Euclidean data-term `L` scaled by the metric operator-norm bound plus the
2517        // prior's third-derivative bound. Reduces to `chart.lipschitz` exactly for
2518        // the Euclidean objective, so the metric-free path is unchanged.
2519        let lipschitz = objective.effective_lipschitz(atom, chart.lipschitz);
2520        let Some(probe) = certify_with_basin_warmup(
2521            atom,
2522            evaluator,
2523            t,
2524            x,
2525            amplitude,
2526            lipschitz,
2527            self.config.newton_steps,
2528            chart.region.center.view(),
2529            chart.region.radius,
2530            objective,
2531        )?
2532        else {
2533            return Ok((
2534                Array1::<f64>::zeros(atom.latent_dim()),
2535                uncertified_certificate(chart.lipschitz),
2536            ));
2537        };
2538        // F5: pair the REFINED coordinate with the certificate evaluated AT that
2539        // refined landing point (`final_cert`), not the pre-refinement
2540        // `initial_cert`. Returning `initial_cert` describes a different (earlier)
2541        // iterate's β/η/h — its Kantorovich root-radius overstates the refined
2542        // point's distance to the root.
2543        Ok((probe.coord, probe.final_cert))
2544    }
2545
2546    /// Online certified encode of one target row `x` against one atom `k` with
2547    /// fixed amplitude `z`. Routes to the nearest chart, starts from that chart's
2548    /// distilled IFT warm start, runs `config.newton_steps` Newton steps, and
2549    /// returns the encoded coordinate with its certificate. An uncertified start
2550    /// (no chart, no distilled Jacobian, non-positive amplitude, or `h > ½`)
2551    /// flags the row for the exact multi-start caller.
2552    pub fn certified_encode_row(
2553        &self,
2554        atom: &SaeManifoldAtom,
2555        atom_index: usize,
2556        x: ArrayView1<'_, f64>,
2557        amplitude: f64,
2558    ) -> Result<(Array1<f64>, RowCertificate), String> {
2559        // The bare Euclidean, prior-free objective — bit-identical to the metric-
2560        // free certified encode.
2561        self.certified_encode_row_with_objective(
2562            atom,
2563            atom_index,
2564            x,
2565            amplitude,
2566            &EncodeObjective::euclidean(),
2567        )
2568    }
2569
2570    /// [`Self::certified_encode_row`] against the TRUE encode objective (F3): the
2571    /// Newton–Kantorovich certificate is computed under the fit's per-row output
2572    /// metric and latent coordinate prior ([`EncodeObjective`]), so the certified
2573    /// root is the minimizer of the SAME generalized-least-squares-plus-prior
2574    /// functional the fit used — not a bare Euclidean stand-in that certifies a
2575    /// different problem. The metric operator-norm bound scales the offline chart
2576    /// Lipschitz; the per-row metric and prior enter `β, η` and the candidate-
2577    /// ranking SSE guard online. `EncodeObjective::euclidean()` reproduces the
2578    /// metric-free path exactly.
2579    pub fn certified_encode_row_with_objective(
2580        &self,
2581        atom: &SaeManifoldAtom,
2582        atom_index: usize,
2583        x: ArrayView1<'_, f64>,
2584        amplitude: f64,
2585        objective: &EncodeObjective<'_>,
2586    ) -> Result<(Array1<f64>, RowCertificate), String> {
2587        let atom_atlas = self
2588            .atoms
2589            .get(atom_index)
2590            .ok_or_else(|| format!("certified_encode_row: atom {atom_index} not in atlas"))?;
2591        let d = atom.latent_dim();
2592        // A per-row metric factor `U` must be `p × rank` (`M = U Uᵀ` acts on the
2593        // p-dim output). A shape mismatch is a caller bug — surface it rather than
2594        // silently certifying a wrong (or panicking) whitening.
2595        if let Some(u) = objective.metric_factor {
2596            if u.nrows() != atom.output_dim() {
2597                return Err(format!(
2598                    "certified_encode_row_with_objective: metric factor has {} rows but atom output_dim is {}",
2599                    u.nrows(),
2600                    atom.output_dim()
2601                ));
2602            }
2603        }
2604        // A missing basis evaluator means the amortized/cold predictor cannot fire
2605        // for this atom (e.g. a frozen-baseline or first-build atom that never
2606        // attached a distilled evaluator). That is exactly the "cannot certify"
2607        // state — flag the row uncertified (zeros coords, ∞ certificate) so the
2608        // upstream exact multi-start solve owns it, never a hard error that aborts
2609        // the whole criterion. Mirrors the no-chart / singular-Jacobian branches.
2610        let Some(evaluator) = atom.basis_evaluator.as_ref().cloned() else {
2611            return Ok((
2612                Array1::<f64>::zeros(d),
2613                RowCertificate {
2614                    beta: f64::INFINITY,
2615                    eta: f64::INFINITY,
2616                    lipschitz: f64::INFINITY,
2617                    h: f64::INFINITY,
2618                },
2619            ));
2620        };
2621
2622        // Route to the nearest chart centers by AMBIENT reconstruction distance.
2623        // A single nearest chart is NOT globally sound on self-approaching atoms:
2624        // where the decoded manifold folds near itself (two distant latent points
2625        // map near the same output), the nearest-center chart can certify into the
2626        // locally-worse basin while another chart holds the GLOBAL minimum (both
2627        // branches' charts reconstruct near the crossing, so both are near in
2628        // ambient distance). The certificate is honest about LOCAL convergence but
2629        // cannot see the better far basin. So we refine in the top-K nearest charts
2630        // and keep the lowest-reconstruction-error CERTIFIED result. For a unimodal
2631        // atom every candidate chart converges to the same root, so this is a no-op
2632        // (first-wins tie → the nearest chart), preserving the existing behavior.
2633        let candidates = nearest_charts_topk(atom_atlas, x, amplitude, CERTIFIED_ROUTING_TOPK);
2634        if candidates.is_empty() {
2635            return Ok((
2636                Array1::<f64>::zeros(d),
2637                RowCertificate {
2638                    beta: f64::INFINITY,
2639                    eta: f64::INFINITY,
2640                    lipschitz: f64::INFINITY,
2641                    h: f64::INFINITY,
2642                },
2643            ));
2644        }
2645        // Best CERTIFIED result by reconstruction error, plus the nearest chart's
2646        // result as the uncertified fallback (preserving the prior return when no
2647        // candidate certifies — the nearest chart owns the flagged row).
2648        let mut best: Option<(Array1<f64>, RowCertificate, f64)> = None;
2649        let mut nearest_fallback: Option<(Array1<f64>, RowCertificate)> = None;
2650        for chart_idx in candidates {
2651            let chart = &atom_atlas.charts[chart_idx];
2652            let Some(t) = amortized_warm_start(chart, x, amplitude) else {
2653                if nearest_fallback.is_none() {
2654                    nearest_fallback = Some((
2655                        Array1::<f64>::zeros(d),
2656                        uncertified_certificate(chart.lipschitz),
2657                    ));
2658                }
2659                continue;
2660            };
2661            let (coord, cert) = self.refine_certified_encode_start(
2662                atom,
2663                evaluator.as_ref(),
2664                chart,
2665                t,
2666                x,
2667                amplitude,
2668                objective,
2669            )?;
2670            if nearest_fallback.is_none() {
2671                nearest_fallback = Some((coord.clone(), cert.clone()));
2672            }
2673            if cert.certified() {
2674                let err = encode_reconstruction_error_core(
2675                    atom,
2676                    evaluator.as_ref(),
2677                    coord.view(),
2678                    x,
2679                    amplitude,
2680                    objective,
2681                );
2682                if best.as_ref().map(|(_, _, e)| err < *e).unwrap_or(true) {
2683                    best = Some((coord, cert, err));
2684                }
2685                // Global-minimum short-circuit: reconstruction error ≥ 0, so a
2686                // certified candidate already at the ambient noise floor is provably
2687                // the global optimum over the remaining charts — stop refining them.
2688                if let Some((_, _, e)) = best.as_ref() {
2689                    if *e <= CERTIFIED_GLOBAL_MIN_RECON_FLOOR * (1.0 + x.dot(&x).sqrt()) {
2690                        break;
2691                    }
2692                }
2693            }
2694        }
2695        match best {
2696            Some((coord, cert, _)) => Ok((coord, cert)),
2697            None => Ok(nearest_fallback.unwrap_or_else(|| {
2698                (
2699                    Array1::<f64>::zeros(d),
2700                    RowCertificate {
2701                        beta: f64::INFINITY,
2702                        eta: f64::INFINITY,
2703                        lipschitz: f64::INFINITY,
2704                        h: f64::INFINITY,
2705                    },
2706                )
2707            })),
2708        }
2709    }
2710
2711    /// Amortized (distilled) encode of one target row `x` against one atom `k`
2712    /// with fixed amplitude `z` (#1026 ladder item 3).
2713    ///
2714    /// Routes to the nearest chart, then predicts the latent coordinate in CLOSED
2715    /// FORM from that chart's precomputed implicit-function-theorem Jacobian:
2716    ///
2717    /// ```text
2718    /// t̂ = t_c + (1/z) · A₁ · (x − z · m₁(t_c)),
2719    /// ```
2720    ///
2721    /// a single `O(d·p)` mat-vec — no per-row Hessian factorization or
2722    /// eigendecomposition, which is the amortization. The Kantorovich
2723    /// certificate is then evaluated AT the predicted start `t̂` with the chart's
2724    /// closed-form Lipschitz constant. A prediction is accepted only when that
2725    /// certificate holds, an independent cold chart-center probe also certifies,
2726    /// and the two refined coordinates agree within the two probes' final
2727    /// Kantorovich root-radius bounds. This keeps the distilled path honest
2728    /// without letting the exact probe reuse the distilled warm start it is
2729    /// auditing. A chart without a distilled Jacobian (singular Gauss–Newton
2730    /// block) flags the row.
2731    pub fn amortized_encode_row(
2732        &self,
2733        atom: &SaeManifoldAtom,
2734        atom_index: usize,
2735        x: ArrayView1<'_, f64>,
2736        amplitude: f64,
2737    ) -> Result<(Array1<f64>, RowCertificate), String> {
2738        // Euclidean, prior-free objective — bit-identical to the metric-free path.
2739        self.amortized_encode_row_with_objective(
2740            atom,
2741            atom_index,
2742            x,
2743            amplitude,
2744            &EncodeObjective::euclidean(),
2745        )
2746    }
2747
2748    /// [`Self::amortized_encode_row`] against the TRUE encode objective (F3): the
2749    /// distilled predictor's warm start is Euclidean (its `A₁` is the Euclidean
2750    /// Gauss–Newton block), but BOTH Kantorovich probes certify under the supplied
2751    /// metric + prior objective, with the chart Lipschitz taken as the objective's
2752    /// effective bound. So the fast path is preserved (one mat-vec warm start) while
2753    /// the certificate — and therefore the trust/fallback decision — is honest about
2754    /// the metric-and-prior objective the fit optimized. `EncodeObjective::euclidean`
2755    /// reproduces the metric-free distilled path exactly.
2756    pub fn amortized_encode_row_with_objective(
2757        &self,
2758        atom: &SaeManifoldAtom,
2759        atom_index: usize,
2760        x: ArrayView1<'_, f64>,
2761        amplitude: f64,
2762        objective: &EncodeObjective<'_>,
2763    ) -> Result<(Array1<f64>, RowCertificate), String> {
2764        let atom_atlas = self
2765            .atoms
2766            .get(atom_index)
2767            .ok_or_else(|| format!("amortized_encode_row: atom {atom_index} not in atlas"))?;
2768        let d = atom.latent_dim();
2769        let uncertified = || {
2770            (
2771                Array1::<f64>::zeros(d),
2772                RowCertificate {
2773                    beta: f64::INFINITY,
2774                    eta: f64::INFINITY,
2775                    lipschitz: f64::INFINITY,
2776                    h: f64::INFINITY,
2777                },
2778            )
2779        };
2780        // A missing basis evaluator means the distilled predictor cannot fire for
2781        // this atom — flag the row uncertified (the exact upstream solve owns it)
2782        // rather than erroring, exactly as the no-chart / singular-Jacobian /
2783        // non-positive-amplitude branches below do. Never a silent wrong encode,
2784        // never a hard abort of the criterion.
2785        let Some(evaluator) = atom.basis_evaluator.as_ref().cloned() else {
2786            return Ok(uncertified());
2787        };
2788        let Some((chart_idx, _)) = nearest_chart(atom_atlas, x, amplitude) else {
2789            return Ok(uncertified());
2790        };
2791        let chart = &atom_atlas.charts[chart_idx];
2792        // Closed-form predicted start t̂ = t_c + (1/z)·A₁·(x − z·m₁). `None` when
2793        // the chart's Gauss–Newton block was singular (no distilled Jacobian, so
2794        // the amortized predictor cannot fire) or the amplitude is not strictly
2795        // positive and finite (a near-inactive atom, where the amplitude-divided
2796        // map is undefined) — either way flag for the exact fallback, never a
2797        // silent wrong encode.
2798        let Some(t_hat) = amortized_warm_start(chart, x, amplitude) else {
2799            return Ok(uncertified());
2800        };
2801        // Effective Kantorovich Lipschitz for the TRUE objective (F3): the stored
2802        // Euclidean data-term bound scaled by the metric operator-norm bound plus
2803        // the prior's third-derivative bound. Reduces to `chart.lipschitz` exactly
2804        // for `EncodeObjective::euclidean`, so the metric-free distilled path is
2805        // unchanged.
2806        let lipschitz = objective.effective_lipschitz(atom, chart.lipschitz);
2807        // Evaluate the SAME Kantorovich certificate at the predicted start. The
2808        // amortized prediction is trusted only if this certificate holds AND an
2809        // independent cold chart-center probe certifies and agrees below the
2810        // two probes' final Kantorovich root-radius bounds. This avoids the
2811        // self-referential gate where the "exact" probe is warm-started by the
2812        // same distilled prediction it is supposed to audit.
2813        let Some(amortized_probe) = certify_with_basin_warmup(
2814            atom,
2815            evaluator.as_ref(),
2816            t_hat,
2817            x,
2818            amplitude,
2819            lipschitz,
2820            self.config.newton_steps,
2821            chart.region.center.view(),
2822            chart.region.radius,
2823            objective,
2824        )?
2825        else {
2826            return Ok((Array1::<f64>::zeros(d), uncertified_certificate(lipschitz)));
2827        };
2828
2829        let cold_start = chart.region.center.clone();
2830        let Some(cold_probe) = certify_with_basin_warmup(
2831            atom,
2832            evaluator.as_ref(),
2833            cold_start,
2834            x,
2835            amplitude,
2836            lipschitz,
2837            self.config.newton_steps,
2838            chart.region.center.view(),
2839            chart.region.radius,
2840            objective,
2841        )?
2842        else {
2843            return Ok((amortized_probe.coord, uncertified_certificate(lipschitz)));
2844        };
2845
2846        let gap =
2847            latent_coordinate_distance(atom, amortized_probe.coord.view(), cold_probe.coord.view());
2848        let tolerance = distilled_probe_tolerance(&amortized_probe, &cold_probe, amplitude, x);
2849        if !(gap.is_finite() && gap <= tolerance) {
2850            return Ok((amortized_probe.coord, uncertified_certificate(lipschitz)));
2851        }
2852        // F5: return the certificate at the refined landing coordinate
2853        // (`final_cert`), consistent with the coord actually returned and with the
2854        // `distilled_probe_tolerance` gate above (which already reads `final_cert`
2855        // via `kantorovich_root_radius`). `initial_cert` is a stale earlier iterate.
2856        Ok((amortized_probe.coord, amortized_probe.final_cert))
2857    }
2858
2859    /// Batched amortized (distilled) encode over many rows against one atom
2860    /// (#1026 ladder item 3, corpus-rate). Each row uses the closed-form
2861    /// per-chart Jacobian predictor and carries its own Kantorovich certificate;
2862    /// uncertified rows are flagged in [`EncodeResult::encode_uncertified_count`]
2863    /// for the exact multi-start fallback. Row-independent against the frozen
2864    /// dictionary, so the batch fans out over rows (deterministic row-order
2865    /// assembly, bit-identical run-to-run), staying sequential inside a rayon
2866    /// worker to avoid nested oversubscription.
2867    pub fn amortized_encode_batch(
2868        &self,
2869        atom: &SaeManifoldAtom,
2870        atom_index: usize,
2871        targets: ArrayView2<'_, f64>,
2872        amplitudes: ArrayView1<'_, f64>,
2873    ) -> Result<EncodeResult, String> {
2874        let n = targets.nrows();
2875        if amplitudes.len() != n {
2876            return Err(format!(
2877                "amortized_encode_batch: amplitudes len {} != rows {n}",
2878                amplitudes.len()
2879            ));
2880        }
2881        let d = atom.latent_dim();
2882        let encode_rows =
2883            |range: std::ops::Range<usize>| -> Result<Vec<(Array1<f64>, bool)>, String> {
2884                range
2885                    .map(|row| {
2886                        let (t, cert) = self.amortized_encode_row(
2887                            atom,
2888                            atom_index,
2889                            targets.row(row),
2890                            amplitudes[row],
2891                        )?;
2892                        Ok((t, cert.certified()))
2893                    })
2894                    .collect()
2895            };
2896        let rows: Vec<(Array1<f64>, bool)> =
2897            if n >= ENCODE_BATCH_PARALLEL_ROW_MIN && rayon::current_thread_index().is_none() {
2898                use rayon::prelude::*;
2899                const CHUNK: usize = 256;
2900                let n_chunks = n.div_ceil(CHUNK);
2901                let chunked: Vec<Vec<(Array1<f64>, bool)>> = (0..n_chunks)
2902                    .into_par_iter()
2903                    .map(|c| {
2904                        let start = c * CHUNK;
2905                        let end = (start + CHUNK).min(n);
2906                        encode_rows(start..end)
2907                    })
2908                    .collect::<Result<_, _>>()?;
2909                chunked.into_iter().flatten().collect()
2910            } else {
2911                encode_rows(0..n)?
2912            };
2913        let mut coords = Array2::<f64>::zeros((n, d));
2914        let mut certified = Vec::with_capacity(n);
2915        for (row, (t, cert)) in rows.into_iter().enumerate() {
2916            coords.row_mut(row).assign(&t);
2917            certified.push(cert);
2918        }
2919        Ok(EncodeResult::from_rows(coords, certified))
2920    }
2921
2922    /// Encode one atom's rows through the full three-tier fallback cascade and
2923    /// report the cost breakdown ([`FallbackTelemetry`], reviewer condition #3).
2924    ///
2925    /// Each row is tried cheapest-first: the amortized one-mat-vec predictor,
2926    /// then (if uncertified) the certified IFT-warm-start Newton encode, then (if
2927    /// still uncertified) it is counted for the exact multi-start solve. The
2928    /// returned coords carry the best CERTIFIED encode reached; a multi-start row
2929    /// keeps the Newton iterate as its (uncertified) coordinate so the caller can
2930    /// still decode it, exactly as [`super::SaeManifoldTerm::amortized_encode_target`]
2931    /// does — the honesty flag rides `certified`.
2932    ///
2933    /// This is the instrumented analogue of [`Self::amortized_encode_batch`] +
2934    /// the per-row Newton rescue: it does the SAME work, and additionally counts
2935    /// which tier certified each encode so the multi-start-fallback fraction (the
2936    /// encode-tax multiplier) is measurable.
2937    pub fn encode_atom_with_fallback_telemetry(
2938        &self,
2939        atom: &SaeManifoldAtom,
2940        atom_index: usize,
2941        targets: ArrayView2<'_, f64>,
2942        amplitudes: ArrayView1<'_, f64>,
2943    ) -> Result<(EncodeResult, FallbackTelemetry), String> {
2944        let n = targets.nrows();
2945        if amplitudes.len() != n {
2946            return Err(format!(
2947                "encode_atom_with_fallback_telemetry: amplitudes len {} != rows {n}",
2948                amplitudes.len()
2949            ));
2950        }
2951        let amortized = self.amortized_encode_batch(atom, atom_index, targets, amplitudes)?;
2952        let mut coords = amortized.coords;
2953        let mut certified = amortized.certified;
2954        let mut telemetry = FallbackTelemetry {
2955            n_rows: n,
2956            n_atoms: 1,
2957            ..FallbackTelemetry::default()
2958        };
2959        for row in 0..n {
2960            if certified[row] {
2961                telemetry.amortized_certified += 1;
2962                continue;
2963            }
2964            // The amortized predictor missed: try the certified Newton warm-start.
2965            let (t, cert) =
2966                self.certified_encode_row(atom, atom_index, targets.row(row), amplitudes[row])?;
2967            // Keep the Newton iterate regardless (it is a better start than the
2968            // amortized one even when uncertified, and a multi-start row still
2969            // needs a decodable coordinate).
2970            coords.row_mut(row).assign(&t);
2971            if cert.certified() {
2972                certified[row] = true;
2973                telemetry.newton_rescued += 1;
2974            } else {
2975                telemetry.multistart_fallback += 1;
2976            }
2977        }
2978        Ok((EncodeResult::from_rows(coords, certified), telemetry))
2979    }
2980
2981    /// Batched certified encode over many rows against one atom (the #988
2982    /// throughput consumer). Each row carries its own certificate; uncertified
2983    /// rows are flagged in [`EncodeResult::encode_uncertified_count`] for the
2984    /// exact multi-start fallback.
2985    pub fn certified_encode_batch(
2986        &self,
2987        atom: &SaeManifoldAtom,
2988        atom_index: usize,
2989        targets: ArrayView2<'_, f64>,
2990        amplitudes: ArrayView1<'_, f64>,
2991    ) -> Result<EncodeResult, String> {
2992        let n = targets.nrows();
2993        if amplitudes.len() != n {
2994            return Err(format!(
2995                "certified_encode_batch: amplitudes len {} != rows {n}",
2996                amplitudes.len()
2997            ));
2998        }
2999        let d = atom.latent_dim();
3000        // Per-row encode is independent against a frozen dictionary (#1010), so
3001        // the corpus-rate batch fans out over rows (#1026 amortized-encoder leg /
3002        // #977 Stage-3 corpus encode). Each row produces an owned `(t, certified)`
3003        // pair; results are assembled back in row order so the output is
3004        // bit-identical run-to-run regardless of thread scheduling. Stay
3005        // sequential inside a rayon worker (e.g. when an outer atom-level fan-out
3006        // owns the pool) to avoid nested oversubscription. The first row that
3007        // fails to encode propagates its error deterministically.
3008        let encode_rows =
3009            |range: std::ops::Range<usize>| -> Result<Vec<(Array1<f64>, bool)>, String> {
3010                range
3011                    .map(|row| {
3012                        let (t, cert) = self.certified_encode_row(
3013                            atom,
3014                            atom_index,
3015                            targets.row(row),
3016                            amplitudes[row],
3017                        )?;
3018                        Ok((t, cert.certified()))
3019                    })
3020                    .collect()
3021            };
3022        let rows: Vec<(Array1<f64>, bool)> =
3023            if n >= ENCODE_BATCH_PARALLEL_ROW_MIN && rayon::current_thread_index().is_none() {
3024                use rayon::prelude::*;
3025                const CHUNK: usize = 256;
3026                let n_chunks = n.div_ceil(CHUNK);
3027                let chunked: Vec<Vec<(Array1<f64>, bool)>> = (0..n_chunks)
3028                    .into_par_iter()
3029                    .map(|c| {
3030                        let start = c * CHUNK;
3031                        let end = (start + CHUNK).min(n);
3032                        encode_rows(start..end)
3033                    })
3034                    .collect::<Result<_, _>>()?;
3035                chunked.into_iter().flatten().collect()
3036            } else {
3037                encode_rows(0..n)?
3038            };
3039        let mut coords = Array2::<f64>::zeros((n, d));
3040        let mut certified = Vec::with_capacity(n);
3041        for (row, (t, cert)) in rows.into_iter().enumerate() {
3042            coords.row_mut(row).assign(&t);
3043            certified.push(cert);
3044        }
3045        Ok(EncodeResult::from_rows(coords, certified))
3046    }
3047
3048    /// Batched GEMM "fast" amortized encode — the traditional-encoder forward
3049    /// pass, WITH manifolds. For every row this applies the SAME closed-form
3050    /// affine predictor as [`amortized_warm_start`]
3051    /// (`t̂ = t_c + (1/z)·A₁·(x − z·m₁)`), but routed and applied as batched
3052    /// matrix products instead of a per-row loop wrapped in the Kantorovich
3053    /// certificate + basin warmup. NO per-row certificate is taken: this is the
3054    /// speed mode (the certified `*_encode_*` paths remain the accuracy mode).
3055    ///
3056    /// Cost is GEMM-bound: one `(n × p)·(p × d)` decode-distance product for
3057    /// nearest-chart routing (skipped for single-chart atoms) plus, per chart,
3058    /// one `(n_c × p)·(p × d)` predictor product — i.e. `≈ X·Wᵀ`, exactly a
3059    /// dense SAE encoder's forward map.
3060    ///
3061    /// Degenerate rows are handled exactly as `amortized_warm_start` flags them
3062    /// (returns `None` ⇒ zeroed coord here): a missing basis evaluator, a chart
3063    /// whose Gauss–Newton block was singular (`amortized_jacobian == None`), or a
3064    /// non-finite / non-positive amplitude. Those rows are zeroed (never a panic,
3065    /// never a silent wrong encode), and their indices are returned in the
3066    /// `valid` mask so the caller can route them to the exact path if desired.
3067    ///
3068    /// Returns `(coords, valid)` where `coords` is `n × d` and `valid[row]` is
3069    /// `true` iff the amortized predictor fired for that row.
3070    pub fn amortized_encode_batch_fast(
3071        &self,
3072        atom: &SaeManifoldAtom,
3073        atom_index: usize,
3074        x: ArrayView2<'_, f64>,
3075        amplitudes: ArrayView1<'_, f64>,
3076    ) -> Result<(Array2<f64>, Vec<bool>), String> {
3077        let n = x.nrows();
3078        let p = atom.output_dim();
3079        let d = atom.latent_dim();
3080        if x.ncols() != p {
3081            return Err(format!(
3082                "amortized_encode_batch_fast: x has {} cols but atom output dim is {p}",
3083                x.ncols()
3084            ));
3085        }
3086        if amplitudes.len() != n {
3087            return Err(format!(
3088                "amortized_encode_batch_fast: amplitudes len {} != rows {n}",
3089                amplitudes.len()
3090            ));
3091        }
3092        let atom_atlas = self.atoms.get(atom_index).ok_or_else(|| {
3093            format!("amortized_encode_batch_fast: atom {atom_index} not in atlas")
3094        })?;
3095        let mut coords = Array2::<f64>::zeros((n, d));
3096        let mut valid = vec![false; n];
3097
3098        // A missing basis evaluator means this atom never had a well-formed atlas
3099        // built here — treat every row as uncertified (zeroed), exactly like the
3100        // per-row `amortized_encode_row` no-evaluator branch. (The predictor below
3101        // uses only cached atlas data, so no evaluator call is made online.)
3102        if atom.basis_evaluator.is_none() {
3103            return Ok((coords, valid));
3104        }
3105
3106        // ── Routing recon-centers (cached, no online basis evaluation). ────────
3107        // Routing sends a row to the chart whose center reconstruction
3108        // `m(t_c) = BᵀΦ(t_c)` is closest in ‖·‖². Those center reconstructions are
3109        // OFFLINE-cached in `chart.recon_center` (bit-identical to re-evaluating the
3110        // basis at the fixed centers — same φ·decoder accumulation). Gather the cache
3111        // instead of calling `evaluator.evaluate` on every invocation: that per-call
3112        // chart-center evaluation was the dominant per-atom-group overhead at massive
3113        // K, where N rows scatter across many atoms into tiny groups so a fixed
3114        // per-call cost is amortized over only a handful of rows. This is what keeps
3115        // the fast index-routed encode near-flat as K grows.
3116        let valid_charts: Vec<usize> = (0..atom_atlas.charts.len())
3117            .filter(|&c| atom_atlas.charts[c].certified_radius > 0.0)
3118            .collect();
3119        if valid_charts.is_empty() {
3120            return Ok((coords, valid));
3121        }
3122        // recon_centers (C × p): the cached m(t_c) for each certifiable chart.
3123        let mut recon_centers = Array2::<f64>::zeros((valid_charts.len(), p));
3124        for (ci, &c) in valid_charts.iter().enumerate() {
3125            recon_centers
3126                .row_mut(ci)
3127                .assign(&atom_atlas.charts[c].recon_center);
3128        }
3129        // Per-chart routing key: route_idx[row] = argmin_c ‖x_row − z_row·r_c‖²
3130        // (F1: the TRUE objective at the row's amplitude `z_row`, where `r_c` is
3131        // the amplitude-1 center reconstruction). First chart wins on a tie (strict
3132        // `<`), matching `nearest_chart`. A non-finite amplitude routes to chart 0
3133        // (moot — the predictor below skips the row anyway).
3134        //
3135        // Score the DIRECT squared distance `Σ_j (z·r_cj − x_j)²`, accumulated in
3136        // the same element order as the per-row `nearest_chart`, NOT the algebraic
3137        // expansion `z²‖r_c‖² − 2z·(x·r_c)`. The two are equal in exact arithmetic,
3138        // but the expansion subtracts two O(‖x‖²) quantities and loses ~‖x‖²·ε of
3139        // precision to cancellation — enough to FLIP the argmin between two charts
3140        // whose reconstructions coincide to rounding (e.g. period-wrapped torus-seam
3141        // charts, whose latent centers differ by a full period yet reconstruct to
3142        // within 1e-14). On such a near-tie the expansion and `nearest_chart`
3143        // disagree, and the two seam charts predict coords a full period apart — so
3144        // the batched fast-encode would diverge from the per-row warm-start it must
3145        // reproduce bit-for-bit (the `fast_encode_matches_per_row_warm_start`
3146        // contract). Computing the same direct distance in the same order keeps this
3147        // path byte-identical to `nearest_chart`, so routing (and the tie-break)
3148        // agrees on every row. `recon_centers` is still the cached offline
3149        // `m₁(t_c)` — no basis re-evaluation, which was the fast path's real cost.
3150        let route_idx: Vec<usize> = if valid_charts.len() == 1 {
3151            vec![0usize; n]
3152        } else {
3153            (0..n)
3154                .map(|row| {
3155                    let z = amplitudes[row];
3156                    // F4 — ACTIVITY GATE BEFORE ROUTING: an inactive (z = 0) or
3157                    // non-finite-amplitude row is skipped by the predictor loop
3158                    // below (`amp.abs() > 0.0`), so its chart never matters. Routing
3159                    // it anyway is the dense `O(n·K·C·p)` waste this path incurs at
3160                    // massive K, where each atom is active on only a sparse handful of
3161                    // the N rows yet the routing scan still touches every (row, chart)
3162                    // pair. Gate the amplitude here so the `C·p` distance scan runs
3163                    // only for the atom's genuinely-active rows; the chart-0 sentinel
3164                    // is moot for the gated rows (they are dropped downstream).
3165                    if !(z.is_finite() && z.abs() > 0.0) {
3166                        return 0usize;
3167                    }
3168                    let x_row = x.row(row);
3169                    let mut best_c = 0usize;
3170                    let mut best_d = f64::INFINITY;
3171                    for c in 0..valid_charts.len() {
3172                        // Shared F1 metric — allocation-free (no per-row/chart Vec),
3173                        // so the massive-K fast route stays near-flat as K grows.
3174                        let dist = amplitude_scaled_center_dist(recon_centers.row(c), x_row, z);
3175                        if dist < best_d {
3176                            best_d = dist;
3177                            best_c = c;
3178                        }
3179                    }
3180                    best_c
3181                })
3182                .collect()
3183        };
3184
3185        // ── Per-chart batched affine predictor. ───────────────────────────────
3186        // For rows routed to chart `c` with finite jacobian `A₁` (d × p) and
3187        // center reconstruction `m₁` (= `chart.recon_center`), the predictor is
3188        //   t̂ = t_c − A₁·m₁ + (1/z)·(A₁·x).
3189        // `t_c − A₁·m₁` is a per-chart constant `base`; `A₁·x` is a d-vector of
3190        // per-row dot products. Instead of gathering routed rows into a fresh
3191        // `X_c` (n_c × p) buffer and running a GEMM into a second `U` (n_c × d)
3192        // buffer — two allocations plus a full copy of the routed rows, per chart —
3193        // fuse the gather straight into the multiply: stream each source row of `x`
3194        // once (it is contiguous) and dot it against `A₁`'s rows, writing the
3195        // predicted coord directly. Zero per-chart heap traffic; the inverse
3196        // amplitude is hoisted to one reciprocal per row.
3197        //
3198        // Precompute each valid chart's `(A₁, base)` once (charts with a singular
3199        // Gauss–Newton block carry no `A₁`, so their routed rows stay
3200        // zeroed/uncertified — same as `amortized_warm_start` returning `None`).
3201        struct ChartPredictor<'a> {
3202            a1: &'a Array2<f64>,
3203            base: &'a Array1<f64>,
3204        }
3205        let predictors: Vec<Option<ChartPredictor<'_>>> = valid_charts
3206            .iter()
3207            .map(|&c| {
3208                let chart = &atom_atlas.charts[c];
3209                // `base = t_c − A₁·m₁` is precomputed offline in the atlas; reuse it
3210                // (both are `Some` together — singular G-N block ⇒ both `None`).
3211                match (
3212                    chart.amortized_jacobian.as_ref(),
3213                    chart.amortized_base.as_ref(),
3214                ) {
3215                    (Some(a1), Some(base)) => Some(ChartPredictor { a1, base }),
3216                    _ => None,
3217                }
3218            })
3219            .collect();
3220
3221        for row in 0..n {
3222            let Some(pred) = predictors[route_idx[row]].as_ref() else {
3223                continue;
3224            };
3225            let amp = amplitudes[row];
3226            if !(amp.is_finite() && amp.abs() > 0.0) {
3227                continue;
3228            }
3229            let inv_z = 1.0 / amp;
3230            let x_row = x.row(row);
3231            let mut coord_row = coords.row_mut(row);
3232            for axis in 0..d {
3233                // (A₁·x)[axis] = A₁ row `axis` (contiguous, length p) · x_row.
3234                coord_row[axis] = pred.base[axis] + pred.a1.row(axis).dot(&x_row) * inv_z;
3235            }
3236            valid[row] = true;
3237        }
3238        Ok((coords, valid))
3239    }
3240
3241    /// Fast batched FULL forward pass against one atom: encode → decode, the
3242    /// manifold analogue of a traditional SAE's `x̂ = z·D` (decoder `D`, code `z`).
3243    ///
3244    /// A traditional SAE decodes with one GEMM. The manifold SAE's reconstruction
3245    /// is `m(t̂) = z·Φ(t̂)·B` (module header) — the SAME GEMM `Φ·B`, but the code
3246    /// `Φ(t̂)` is the curved chart basis evaluated at the encoded latent coordinate
3247    /// rather than a flat one-hot. So the fast forward is exactly:
3248    ///   1. [`amortized_encode_batch_fast`] → per-row latent coords `t̂` (one
3249    ///      routing GEMM + one affine GEMM per chart — a traditional `W·x+b`);
3250    ///   2. ONE batched basis evaluation `Φ(t̂)` (the manifold-curvature step a
3251    ///      flat SAE doesn't have — `n×m`);
3252    ///   3. ONE GEMM `recon = Φ(t̂)·B` (`(n×m)·(m×p)` — a traditional decoder
3253    ///      `z·D`), then the per-row amplitude scale `z`.
3254    ///
3255    /// Rows the encoder could not certify-predict (no evaluator / singular
3256    /// Gauss–Newton block / non-finite-or-zero amplitude) are returned as a ZERO
3257    /// reconstruction and flagged `false` in the valid-mask — never a silent wrong
3258    /// decode. The reconstruction of a valid row equals, bit-for-bit up to GEMM
3259    /// reassociation, `z·(Φ(t̂_row)·B)` with `t̂` from the per-row predictor.
3260    pub fn amortized_reconstruct_batch_fast(
3261        &self,
3262        atom: &SaeManifoldAtom,
3263        atom_index: usize,
3264        x: ArrayView2<'_, f64>,
3265        amplitudes: ArrayView1<'_, f64>,
3266    ) -> Result<(Array2<f64>, Vec<bool>), String> {
3267        let n = x.nrows();
3268        let p = atom.output_dim();
3269        // Step 1: batched encode → latent coords (reuses the fast routing+affine).
3270        let (coords, valid) = self.amortized_encode_batch_fast(atom, atom_index, x, amplitudes)?;
3271        let mut recon = Array2::<f64>::zeros((n, p));
3272        // A missing evaluator means no row could encode — every row is zeroed and
3273        // already flagged `false` by the encode; nothing to decode.
3274        let Some(evaluator) = atom.basis_evaluator.as_ref().cloned() else {
3275            return Ok((recon, valid));
3276        };
3277        // Step 2: ONE batched basis evaluation Φ(t̂) over all rows (n × m). Invalid
3278        // rows carry coords = 0 (the chart-origin); we still evaluate them in the
3279        // batch for a single GEMM, then zero their reconstruction below — the basis
3280        // is finite at the origin so this cannot poison the valid rows' GEMM.
3281        let (phi, _jet) = evaluator
3282            .evaluate(coords.view())
3283            .map_err(|err| format!("amortized_reconstruct_batch_fast: basis eval: {err}"))?;
3284        // Step 3: ONE GEMM recon = Φ·B (n × p), then per-row amplitude scale z.
3285        // m(t̂) = z·Φ(t̂)·B, matching the module header and `fill_decoded_row`'s
3286        // `Φ·decoder` accumulation (the amplitude is applied once here).
3287        let decoded = phi.dot(&atom.decoder_coefficients); // (n × p), amplitude-1
3288        for row in 0..n {
3289            if !valid[row] {
3290                continue; // stays zeroed — uncertified, like warm_start `None`.
3291            }
3292            let z = amplitudes[row];
3293            for col in 0..p {
3294                recon[[row, col]] = z * decoded[[row, col]];
3295            }
3296        }
3297        Ok((recon, valid))
3298    }
3299
3300    /// LSH-routed certified encode (issue #1010 step 2 + 3): for each target
3301    /// row, the existing [`SaeCandidateIndex`] (#985/#994) proposes the
3302    /// best-aligned atom by frame alignment to the row direction; the row is then
3303    /// encoded against THAT atom's certified chart atlas. This is the production
3304    /// routing path. Atom selection is EXACT (#1777): [`SaeCandidateIndex::route_exact`]
3305    /// returns the global argmax of the routing score (the universal-bound LSH fast
3306    /// path, else a full-scan fallback) — never a silently-missed ungathered atom —
3307    /// and the atlas does the in-atom nearest-chart routing and the per-row
3308    /// Kantorovich certificate.
3309    ///
3310    /// `atoms[id]` must be aligned with the atlas's `atoms[id]` (same dictionary
3311    /// order the atlas was built from and the sketch/index were built over).
3312    /// A row over an empty dictionary, or whose globally-best atom aligns below the
3313    /// fit-quality floor, is flagged uncertified — it routes to the exact
3314    /// multi-start fallback, never a silent wrong encode.
3315    pub fn certified_encode_with_index<S: AtomFrameSketch + Sync>(
3316        &self,
3317        atoms: &[SaeManifoldAtom],
3318        index: &SaeCandidateIndex,
3319        sketch: &S,
3320        targets: ArrayView2<'_, f64>,
3321        amplitudes: ArrayView1<'_, f64>,
3322        latent_dim: usize,
3323    ) -> Result<EncodeResult, String> {
3324        let n = targets.nrows();
3325        if amplitudes.len() != n {
3326            return Err(format!(
3327                "certified_encode_with_index: amplitudes len {} != rows {n}",
3328                amplitudes.len()
3329            ));
3330        }
3331        let budget = auto_candidate_budget(atoms.len().max(1));
3332        // LSH-routed per-row encode is independent across rows (sublinear atom
3333        // selection + frozen-dictionary in-atom Newton), so the corpus-rate batch
3334        // fans out over rows (#1026 amortized-encoder/routing leg / #977 Stage-3).
3335        // `None` coords (no LSH candidate) carry through as a zeroed row flagged
3336        // uncertified — identical to the sequential semantics. Results assemble
3337        // back in row order (bit-identical run-to-run); the first encode error
3338        // propagates deterministically. Stay sequential inside a rayon worker to
3339        // avoid nested oversubscription.
3340        let encode_rows =
3341            |range: std::ops::Range<usize>| -> Result<Vec<Option<(Array1<f64>, bool)>>, String> {
3342                range
3343                    .map(|row| {
3344                        // EXACT routing (#1777): pick the GLOBAL argmax of the
3345                        // routing score over the whole dictionary, not merely the
3346                        // best LSH-gathered candidate. `route_exact` certifies the
3347                        // sublinear gather against the universal `[0,1]` alignment
3348                        // bound and falls back to a full scan otherwise, so the
3349                        // returned atom is guaranteed to be the globally-best — no
3350                        // silently-missed ungathered atom.
3351                        let Some(route) =
3352                            index.route_exact(sketch, targets.row(row), budget, true)
3353                        else {
3354                            // Empty dictionary: flag for the exact fallback.
3355                            return Ok(None);
3356                        };
3357                        let best_atom = route.atom;
3358                        // Fit-quality floor: even the globally-best atom may align
3359                        // only weakly with this row (no atom fits it). A finite
3360                        // alignment below the floor — or a NaN, the zero-norm
3361                        // ‖d‖ = 0 row — flags for the exact multi-start fallback
3362                        // rather than encoding against a poorly-fitting atom. This is
3363                        // a quality gate, not a routing-correctness gate; routing is
3364                        // already exact. See CANDIDATE_ROUTING_MIN_ALIGNMENT.
3365                        if !route.alignment.is_finite()
3366                            || route.alignment < CANDIDATE_ROUTING_MIN_ALIGNMENT
3367                        {
3368                            return Ok(None);
3369                        }
3370                        let atom = atoms.get(best_atom).ok_or_else(|| {
3371                            format!(
3372                                "certified_encode_with_index: proposed atom {best_atom} out of range"
3373                            )
3374                        })?;
3375                        let (t, cert) = self.certified_encode_row(
3376                            atom,
3377                            best_atom,
3378                            targets.row(row),
3379                            amplitudes[row],
3380                        )?;
3381                        // Heterogeneous-atom dictionaries with different latent_dim
3382                        // per atom are not supported by the batched API: the caller
3383                        // declares one shared `latent_dim` for the output tensor.
3384                        // Silently zeroing the coord row while recording a
3385                        // certified=true flag would produce corrupted
3386                        // reconstructions downstream — error loudly instead.
3387                        if t.len() != latent_dim {
3388                            return Err(format!(
3389                                "certified_encode_with_index: atom {best_atom} returned t.len()={} \
3390                                 but declared latent_dim={latent_dim}; heterogeneous-dim \
3391                                 dictionaries are not supported by this batched encode path",
3392                                t.len()
3393                            ));
3394                        }
3395                        Ok(Some((t, cert.certified())))
3396                    })
3397                    .collect()
3398            };
3399        let rows: Vec<Option<(Array1<f64>, bool)>> =
3400            if n >= ENCODE_BATCH_PARALLEL_ROW_MIN && rayon::current_thread_index().is_none() {
3401                use rayon::prelude::*;
3402                const CHUNK: usize = 256;
3403                let n_chunks = n.div_ceil(CHUNK);
3404                let chunked: Vec<Vec<Option<(Array1<f64>, bool)>>> = (0..n_chunks)
3405                    .into_par_iter()
3406                    .map(|c| {
3407                        let start = c * CHUNK;
3408                        let end = (start + CHUNK).min(n);
3409                        encode_rows(start..end)
3410                    })
3411                    .collect::<Result<_, _>>()?;
3412                chunked.into_iter().flatten().collect()
3413            } else {
3414                encode_rows(0..n)?
3415            };
3416        let mut coords = Array2::<f64>::zeros((n, latent_dim));
3417        let mut certified = Vec::with_capacity(n);
3418        for (row, slot) in rows.into_iter().enumerate() {
3419            match slot {
3420                Some((t, cert)) => {
3421                    coords.row_mut(row).assign(&t);
3422                    certified.push(cert);
3423                }
3424                None => certified.push(false),
3425            }
3426        }
3427        Ok(EncodeResult::from_rows(coords, certified))
3428    }
3429
3430    /// LSH-routed AMORTIZED (distilled) encode — the production token-rate
3431    /// encoder of #1026 ladder item 3. Identical routing to
3432    /// [`Self::certified_encode_with_index`] (LSH proposes the best-aligned atom,
3433    /// the atlas routes to the in-atom nearest chart), but the in-atom encode is
3434    /// the closed-form per-chart Jacobian predictor + certificate gate of
3435    /// [`Self::amortized_encode_row`] rather than the certified Newton-refinement
3436    /// path.
3437    /// This is the deployment path: the distilled affine map produces the encode
3438    /// in one mat-vec, the Kantorovich certificate decides trust-or-fallback per
3439    /// row, and uncertified rows (the adversarial tail the thread expects to
3440    /// concentrate on rare tokens) are flagged for the exact multi-start solve —
3441    /// compute goes where the questions are. Row-independent against the frozen
3442    /// dictionary, so the batch fans out over rows with deterministic row-order
3443    /// assembly (bit-identical run-to-run).
3444    pub fn amortized_encode_with_index<S: AtomFrameSketch + Sync>(
3445        &self,
3446        atoms: &[SaeManifoldAtom],
3447        index: &SaeCandidateIndex,
3448        sketch: &S,
3449        targets: ArrayView2<'_, f64>,
3450        amplitudes: ArrayView1<'_, f64>,
3451        latent_dim: usize,
3452    ) -> Result<EncodeResult, String> {
3453        let n = targets.nrows();
3454        if amplitudes.len() != n {
3455            return Err(format!(
3456                "amortized_encode_with_index: amplitudes len {} != rows {n}",
3457                amplitudes.len()
3458            ));
3459        }
3460        let budget = auto_candidate_budget(atoms.len().max(1));
3461        let encode_rows =
3462            |range: std::ops::Range<usize>| -> Result<Vec<Option<(Array1<f64>, bool)>>, String> {
3463                range
3464                    .map(|row| {
3465                        // EXACT routing (#1777): global argmax of the routing score,
3466                        // not just the best LSH-gathered candidate (see
3467                        // certified_encode_with_index for the full rationale).
3468                        let Some(route) =
3469                            index.route_exact(sketch, targets.row(row), budget, true)
3470                        else {
3471                            return Ok(None);
3472                        };
3473                        let best_atom = route.atom;
3474                        // Fit-quality floor (not a routing-correctness gate; routing
3475                        // is exact): even the globally-best atom may fit a row poorly,
3476                        // and a NaN alignment is the zero-norm ‖d‖ = 0 row. Either way
3477                        // flag for the exact multi-start fallback. See
3478                        // CANDIDATE_ROUTING_MIN_ALIGNMENT.
3479                        if !route.alignment.is_finite()
3480                            || route.alignment < CANDIDATE_ROUTING_MIN_ALIGNMENT
3481                        {
3482                            return Ok(None);
3483                        }
3484                        let atom = atoms.get(best_atom).ok_or_else(|| {
3485                            format!(
3486                                "amortized_encode_with_index: proposed atom {best_atom} out of range"
3487                            )
3488                        })?;
3489                        let (t, cert) = self.amortized_encode_row(
3490                            atom,
3491                            best_atom,
3492                            targets.row(row),
3493                            amplitudes[row],
3494                        )?;
3495                        if t.len() != latent_dim {
3496                            return Err(format!(
3497                                "amortized_encode_with_index: atom {best_atom} returned t.len()={} \
3498                                 but declared latent_dim={latent_dim}; heterogeneous-dim \
3499                                 dictionaries are not supported by this batched encode path",
3500                                t.len()
3501                            ));
3502                        }
3503                        Ok(Some((t, cert.certified())))
3504                    })
3505                    .collect()
3506            };
3507        let rows: Vec<Option<(Array1<f64>, bool)>> =
3508            if n >= ENCODE_BATCH_PARALLEL_ROW_MIN && rayon::current_thread_index().is_none() {
3509                use rayon::prelude::*;
3510                const CHUNK: usize = 256;
3511                let n_chunks = n.div_ceil(CHUNK);
3512                let chunked: Vec<Vec<Option<(Array1<f64>, bool)>>> = (0..n_chunks)
3513                    .into_par_iter()
3514                    .map(|c| {
3515                        let start = c * CHUNK;
3516                        let end = (start + CHUNK).min(n);
3517                        encode_rows(start..end)
3518                    })
3519                    .collect::<Result<_, _>>()?;
3520                chunked.into_iter().flatten().collect()
3521            } else {
3522                encode_rows(0..n)?
3523            };
3524        let mut coords = Array2::<f64>::zeros((n, latent_dim));
3525        let mut certified = Vec::with_capacity(n);
3526        for (row, slot) in rows.into_iter().enumerate() {
3527            match slot {
3528                Some((t, cert)) => {
3529                    coords.row_mut(row).assign(&t);
3530                    certified.push(cert);
3531                }
3532                None => certified.push(false),
3533            }
3534        }
3535        Ok(EncodeResult::from_rows(coords, certified))
3536    }
3537
3538    /// LSH-routed FAST amortized encode over the WHOLE dictionary — the
3539    /// multi-atom, corpus-rate analogue of [`Self::amortized_encode_with_index`].
3540    ///
3541    /// `amortized_encode_with_index` routes per row, then runs the per-row
3542    /// closed-form predictor + Kantorovich certificate + cold cross-check on each
3543    /// row independently. This fast variant keeps the SAME per-row EXACT routing
3544    /// (`index.route_exact` + the fit-quality floor), but replaces the per-row
3545    /// predictor with the GEMM-batched [`Self::amortized_encode_batch_fast`]:
3546    /// it GROUPS rows by their global-argmax atom and runs one batched affine-
3547    /// predictor pass per atom-group (a routing GEMM + a predictor GEMM each),
3548    /// reproducing a traditional SAE's whole-dictionary `W·x+b` throughput. No
3549    /// per-row certificate — this is the speed mode validated as accuracy-parity
3550    /// with the certified solve (`fast_forward_is_accuracy_parity_with_certified`).
3551    ///
3552    /// Returns the per-row latent coords and a valid-mask: `false` for a row with
3553    /// an empty dictionary, a sub-threshold/NaN routing alignment, or one the batched
3554    /// predictor could not fire on (no evaluator / singular Gauss–Newton block /
3555    /// non-finite-or-zero amplitude). Each row is written exactly once (disjoint
3556    /// per-atom groups), so the result is independent of group iteration order.
3557    pub fn amortized_encode_with_index_fast<S: AtomFrameSketch + Sync>(
3558        &self,
3559        atoms: &[SaeManifoldAtom],
3560        index: &SaeCandidateIndex,
3561        sketch: &S,
3562        targets: ArrayView2<'_, f64>,
3563        amplitudes: ArrayView1<'_, f64>,
3564        latent_dim: usize,
3565    ) -> Result<(Array2<f64>, Vec<bool>), String> {
3566        let n = targets.nrows();
3567        if amplitudes.len() != n {
3568            return Err(format!(
3569                "amortized_encode_with_index_fast: amplitudes len {} != rows {n}",
3570                amplitudes.len()
3571            ));
3572        }
3573        let budget = auto_candidate_budget(atoms.len().max(1));
3574        let mut coords = Array2::<f64>::zeros((n, latent_dim));
3575        let mut valid = vec![false; n];
3576        // ── Single allocation-free pass: route each row, apply the CACHED predictor.
3577        //
3578        // Routing sublinearity (massive-K, K≈32k): the certified path uses
3579        // `route_exact`, whose universal-bound LSH certificate only fires at the
3580        // alignment ceiling (≈1.0); for any real dictionary (`alignment < 1`) it
3581        // falls back to `brute_force_best_atom` — an O(K) full scan PER ROW, making
3582        // the encode O(N·K). This SPEED path takes the LSH gather's best-aligned atom
3583        // directly (`propose` scores only ~budget candidates → O(log K)); a rare miss
3584        // is caught by the fit-quality floor + the downstream certificate/exact
3585        // fallback (the documented speed/accuracy tradeoff).
3586        //
3587        // Allocation: the predictor uses only OFFLINE-cached atlas data (per-chart
3588        // `recon_center` for routing + `amortized_jacobian`/`amortized_base` for the
3589        // `t̂ = base + (1/z)·A₁·x` mat-vec), so NO per-row or per-atom heap buffer is
3590        // allocated. This replaces the old per-atom-group GEMM sub-batch — which at
3591        // `K ≫ N` degenerated to ONE row per group, so its per-group buffers (x_sub,
3592        // recon-centers, predictors) dominated and made the "fast" path allocation-
3593        // bound. Now the only per-row allocation is inside `index.propose`.
3594        for row in 0..n {
3595            let dir = targets.row(row);
3596            let proposal = index.propose(sketch, dir, budget, true);
3597            let Some(&best_atom) = proposal.proposed.first() else {
3598                continue; // nothing gathered (empty dictionary / probe-dim mismatch)
3599            };
3600            // Fit-quality floor: the best gathered atom still fits this row poorly,
3601            // or the alignment is NaN (zero-norm row) — flag for the exact fallback.
3602            let alignment = sketch.alignment(best_atom, dir);
3603            if !alignment.is_finite() || alignment < CANDIDATE_ROUTING_MIN_ALIGNMENT {
3604                continue;
3605            }
3606            let atom = atoms.get(best_atom).ok_or_else(|| {
3607                format!("amortized_encode_with_index_fast: proposed atom {best_atom} out of range")
3608            })?;
3609            if atom.latent_dim() != latent_dim {
3610                return Err(format!(
3611                    "amortized_encode_with_index_fast: atom {best_atom} latent_dim {} != declared \
3612                     {latent_dim}; heterogeneous-dim dictionaries are not supported by this path",
3613                    atom.latent_dim()
3614                ));
3615            }
3616            let Some(atom_atlas) = self.atoms.get(best_atom) else {
3617                continue; // no atlas for this atom → predictor cannot fire (zeroed)
3618            };
3619            if amortized_predict_row(
3620                atom_atlas,
3621                dir,
3622                amplitudes[row],
3623                latent_dim,
3624                coords.row_mut(row),
3625            ) {
3626                valid[row] = true;
3627            }
3628        }
3629        Ok((coords, valid))
3630    }
3631
3632    /// LSH-routed FAST full forward over the WHOLE dictionary: encode → decode,
3633    /// the multi-atom analogue of [`Self::amortized_reconstruct_batch_fast`]. Same
3634    /// sublinear per-row routing + per-atom grouping as
3635    /// [`Self::amortized_encode_with_index_fast`], but each group is run through
3636    /// the batched reconstruct (`m(t̂) = z·Φ(t̂)·B`) so the result is the per-row
3637    /// reconstruction in the ambient space. Rows that do not route/predict decode
3638    /// to an exact zero reconstruction and are flagged `false`.
3639    pub fn amortized_reconstruct_with_index_fast<S: AtomFrameSketch + Sync>(
3640        &self,
3641        atoms: &[SaeManifoldAtom],
3642        index: &SaeCandidateIndex,
3643        sketch: &S,
3644        targets: ArrayView2<'_, f64>,
3645        amplitudes: ArrayView1<'_, f64>,
3646    ) -> Result<(Array2<f64>, Vec<bool>), String> {
3647        let n = targets.nrows();
3648        let p = targets.ncols();
3649        if amplitudes.len() != n {
3650            return Err(format!(
3651                "amortized_reconstruct_with_index_fast: amplitudes len {} != rows {n}",
3652                amplitudes.len()
3653            ));
3654        }
3655        let budget = auto_candidate_budget(atoms.len().max(1));
3656        // SUBLINEAR routing for the SPEED-mode full forward — mirror
3657        // `amortized_encode_with_index_fast`: take the LSH gather's best-aligned
3658        // atom (O(budget) candidates) instead of route_exact's O(K) full-scan
3659        // certification, keeping the whole fast encode→decode sublinear in K at
3660        // K=32k. The gather's best is the exact argmax on the vast majority of rows;
3661        // rare misses are caught by the fit-quality floor + downstream fallback.
3662        let mut groups: std::collections::HashMap<usize, Vec<usize>> =
3663            std::collections::HashMap::new();
3664        for row in 0..n {
3665            let dir = targets.row(row);
3666            let proposal = index.propose(sketch, dir, budget, true);
3667            let Some(&best_atom) = proposal.proposed.first() else {
3668                continue; // nothing gathered (empty dictionary / probe-dim mismatch)
3669            };
3670            let alignment = sketch.alignment(best_atom, dir);
3671            if !alignment.is_finite() || alignment < CANDIDATE_ROUTING_MIN_ALIGNMENT {
3672                continue;
3673            }
3674            groups.entry(best_atom).or_default().push(row);
3675        }
3676
3677        let mut recon = Array2::<f64>::zeros((n, p));
3678        let mut valid = vec![false; n];
3679        for (atom_idx, rows_here) in groups {
3680            let atom = atoms.get(atom_idx).ok_or_else(|| {
3681                format!(
3682                    "amortized_reconstruct_with_index_fast: proposed atom {atom_idx} out of range"
3683                )
3684            })?;
3685            if atom.output_dim() != p {
3686                return Err(format!(
3687                    "amortized_reconstruct_with_index_fast: atom {atom_idx} output_dim {} != target \
3688                     dim {p}",
3689                    atom.output_dim()
3690                ));
3691            }
3692            let mut x_sub = Array2::<f64>::zeros((rows_here.len(), p));
3693            let mut amp_sub = Array1::<f64>::zeros(rows_here.len());
3694            for (i, &row) in rows_here.iter().enumerate() {
3695                x_sub.row_mut(i).assign(&targets.row(row));
3696                amp_sub[i] = amplitudes[row];
3697            }
3698            let (sub_recon, sub_valid) = self.amortized_reconstruct_batch_fast(
3699                atom,
3700                atom_idx,
3701                x_sub.view(),
3702                amp_sub.view(),
3703            )?;
3704            for (i, &row) in rows_here.iter().enumerate() {
3705                if sub_valid[i] {
3706                    recon.row_mut(row).assign(&sub_recon.row(i));
3707                    valid[row] = true;
3708                }
3709            }
3710        }
3711        Ok((recon, valid))
3712    }
3713}
3714
3715/// Offline `β = 1/λ_min(H_GN)` at a chart center from the Gauss-Newton block
3716/// `H_GN = J_mᵀ J_m` (residual-free). The offline `β` bounds the curvature the
3717/// online certificate sees: charts are placed where the encode lands, so the
3718/// representative residual is small and `H_GN` is the dominant, residual-free
3719/// curvature estimate. (The online per-row certificate still uses the FULL
3720/// Hessian; this is only the offline radius-sizing curvature.) Returns `None`
3721/// for a degenerate center (`λ_min ≤ 0`), which marks an uncertifiable chart.
3722pub(crate) fn center_beta(atom: &SaeManifoldAtom, center: &Array1<f64>, ridge: f64) -> Option<f64> {
3723    let evaluator = atom.basis_evaluator.as_ref()?.clone();
3724    let d = atom.latent_dim();
3725    let p = atom.output_dim();
3726    let m = atom.basis_size();
3727    let coords = center.view().to_shape((1, d)).ok()?.to_owned();
3728    let (_phi, jet) = evaluator.evaluate(coords.view()).ok()?;
3729    let decoder = &atom.decoder_coefficients;
3730    // J_m[axis] = Bᵀ (∂Φ/∂t_axis) ∈ ℝᵖ (amplitude-1; curvature scales with z²
3731    // and is absorbed conservatively by the amplitude-bounded Lipschitz term).
3732    let mut jm = Array2::<f64>::zeros((d, p));
3733    for axis in 0..d {
3734        for basis_col in 0..m {
3735            let dphi = jet[[0, basis_col, axis]];
3736            if dphi == 0.0 {
3737                continue;
3738            }
3739            for out in 0..p {
3740                jm[[axis, out]] += dphi * decoder[[basis_col, out]];
3741            }
3742        }
3743    }
3744    let mut h = Array2::<f64>::zeros((d, d));
3745    for a in 0..d {
3746        for b in 0..d {
3747            h[[a, b]] = jm.row(a).dot(&jm.row(b));
3748        }
3749        h[[a, a]] += ridge;
3750    }
3751    let (vals, _vecs) = h.eigh(Side::Lower).ok()?;
3752    let lambda_min = vals.iter().cloned().fold(f64::INFINITY, f64::min);
3753    if lambda_min.is_finite() && lambda_min > 0.0 {
3754        Some(1.0 / lambda_min)
3755    } else {
3756        None
3757    }
3758}
3759
3760/// #1154 — the amortized encoder's closed-form warm-start coordinate for one
3761/// row `x` against one chart at amplitude `z`:
3762///
3763/// ```text
3764/// t̂ = t_c + (1/z) · A₁ · (x − z · m₁(t_c)),
3765/// ```
3766///
3767/// a single `O(d·p)` mat-vec from the chart's precomputed IFT Jacobian `A₁` and
3768/// center reconstruction `m₁`. Returns `None` when the chart carries no
3769/// distilled Jacobian (singular Gauss–Newton block) or the amplitude is not
3770/// strictly positive and finite (a near-inactive atom, where the
3771/// amplitude-divided map is undefined) — in those cases the caller starts from
3772/// the chart center instead. Shared by the amortized encode (where `t̂` is the
3773/// prediction) and the exact certified encode (where `t̂` is the Newton
3774/// warm-start that then refines to stationarity, Design A).
3775pub(crate) fn amortized_warm_start(
3776    chart: &CertifiedChart,
3777    x: ArrayView1<'_, f64>,
3778    amplitude: f64,
3779) -> Option<Array1<f64>> {
3780    let a1 = chart.amortized_jacobian.as_ref()?;
3781    if !(amplitude.is_finite() && amplitude.abs() > 0.0) {
3782        return None;
3783    }
3784    let d = a1.nrows();
3785    let mut t_hat = chart.region.center.clone();
3786    for (out_idx, &m1_out) in chart.recon_center.iter().enumerate().take(a1.ncols()) {
3787        let resid = x[out_idx] - amplitude * m1_out;
3788        for axis in 0..d {
3789            t_hat[axis] += a1[[axis, out_idx]] * resid / amplitude;
3790        }
3791    }
3792    Some(t_hat)
3793}
3794
3795/// Single-row amortized predictor against ONE atom's cached atlas, writing the
3796/// encoded latent coordinate DIRECTLY into `out` (length `d`) with NO heap
3797/// allocation. Routes to the nearest certifiable chart by cached center-
3798/// reconstruction distance `‖x − m(t_c)‖²` (matching `amortized_encode_batch_fast`'s
3799/// per-chart routing), then applies that chart's precomputed affine predictor
3800/// `t̂ = base + (1/z)·A₁·x` (`base = t_c − A₁·m₁` is `chart.amortized_base`).
3801///
3802/// Returns `false` — leaving `out` at its incoming (zeroed) value — for exactly the
3803/// rows `amortized_encode_batch_fast` would flag: no certifiable chart, a nearest
3804/// chart with no distilled predictor (singular Gauss–Newton block), or an unusable
3805/// amplitude. This is the per-row core of the allocation-free massive-K fast encode.
3806pub(crate) fn amortized_predict_row(
3807    atom_atlas: &AtomEncodeAtlas,
3808    x: ArrayView1<'_, f64>,
3809    amplitude: f64,
3810    d: usize,
3811    mut out: ndarray::ArrayViewMut1<'_, f64>,
3812) -> bool {
3813    if !(amplitude.is_finite() && amplitude.abs() > 0.0) {
3814        return false;
3815    }
3816    // Nearest certifiable chart by the TRUE objective ‖x − z·recon_center‖²
3817    // (F1; `z = amplitude`, finite and non-zero by the guard above). First-wins on
3818    // ties (strict `<`) — same amplitude-scaled argmin as
3819    // `amortized_encode_batch_fast`'s route_idx.
3820    let mut best_ci: Option<usize> = None;
3821    let mut best_dist = f64::INFINITY;
3822    for (ci, chart) in atom_atlas.charts.iter().enumerate() {
3823        if chart.certified_radius <= 0.0 {
3824            continue;
3825        }
3826        let dist = amplitude_scaled_center_dist(chart.recon_center.view(), x, amplitude);
3827        if dist < best_dist {
3828            best_dist = dist;
3829            best_ci = Some(ci);
3830        }
3831    }
3832    let Some(ci) = best_ci else {
3833        return false;
3834    };
3835    let chart = &atom_atlas.charts[ci];
3836    // The nearest chart must carry a distilled predictor; otherwise flag (zeroed),
3837    // exactly as the per-chart `None` branch of `amortized_encode_batch_fast`.
3838    let (Some(a1), Some(base)) = (
3839        chart.amortized_jacobian.as_ref(),
3840        chart.amortized_base.as_ref(),
3841    ) else {
3842        return false;
3843    };
3844    let inv_z = 1.0 / amplitude;
3845    for axis in 0..d {
3846        out[axis] = base[axis] + a1.row(axis).dot(&x) * inv_z;
3847    }
3848    true
3849}
3850
3851/// The amplitude-1 distilled amortized-encoder Jacobian at a chart center
3852/// (#1026 ladder item 3). Returns `(A₁, m₁)` where `m₁ = BᵀΦ(t_c) ∈ ℝᵖ` is the
3853/// amplitude-1 center reconstruction and `A₁ = (J₁ᵀJ₁ + ridge·I)⁻¹ J₁ ∈ ℝ^{d×p}`
3854/// is the implicit-function-theorem derivative of the encode map `x ↦ t`
3855/// (Gauss–Newton block — the residual-free, dominant curvature exactly as the
3856/// offline radius-sizing `β`). With these, the online encode of a row `x` at
3857/// amplitude `z` is the closed-form affine prediction
3858/// `t = t_c + (1/z)·A₁·(x − z·m₁)` — one mat-vec, no per-row factorization.
3859/// `None` when the basis has no jet or the Gauss–Newton block is singular (no
3860/// certifiable amortization), matching `center_beta`'s gate so a chart with a
3861/// finite `β` always carries a Jacobian and vice versa.
3862pub(crate) fn center_amortized_jacobian(
3863    atom: &SaeManifoldAtom,
3864    center: &Array1<f64>,
3865    ridge: f64,
3866) -> Option<(Array2<f64>, Array1<f64>)> {
3867    let evaluator = atom.basis_evaluator.as_ref()?.clone();
3868    let d = atom.latent_dim();
3869    let p = atom.output_dim();
3870    let m = atom.basis_size();
3871    let coords = center.view().to_shape((1, d)).ok()?.to_owned();
3872    let (phi, jet) = evaluator.evaluate(coords.view()).ok()?;
3873    let decoder = &atom.decoder_coefficients;
3874    // m₁(t_c) = BᵀΦ(t_c) ∈ ℝᵖ (amplitude-1 center reconstruction).
3875    let mut recon = Array1::<f64>::zeros(p);
3876    for basis_col in 0..m {
3877        let phi_v = phi[[0, basis_col]];
3878        if phi_v == 0.0 {
3879            continue;
3880        }
3881        for out in 0..p {
3882            recon[out] += phi_v * decoder[[basis_col, out]];
3883        }
3884    }
3885    // J₁[axis] = Bᵀ (∂Φ/∂t_axis) ∈ ℝᵖ (amplitude-1; z factors out analytically).
3886    let mut jm = Array2::<f64>::zeros((d, p));
3887    for axis in 0..d {
3888        for basis_col in 0..m {
3889            let dphi = jet[[0, basis_col, axis]];
3890            if dphi == 0.0 {
3891                continue;
3892            }
3893            for out in 0..p {
3894                jm[[axis, out]] += dphi * decoder[[basis_col, out]];
3895            }
3896        }
3897    }
3898    // H_GN = J₁ J₁ᵀ + ridge·I ∈ ℝ^{d×d}.
3899    let mut h = Array2::<f64>::zeros((d, d));
3900    for a in 0..d {
3901        for b in 0..d {
3902            h[[a, b]] = jm.row(a).dot(&jm.row(b));
3903        }
3904        h[[a, a]] += ridge;
3905    }
3906    let (vals, vecs) = h.eigh(Side::Lower).ok()?;
3907    let lambda_min = vals.iter().cloned().fold(f64::INFINITY, f64::min);
3908    if !(lambda_min.is_finite() && lambda_min > 0.0) {
3909        return None;
3910    }
3911    // A₁ = H_GN⁻¹ J₁ via the eigendecomposition: H⁻¹ = Σ_i (1/λᵢ) vᵢ vᵢᵀ, so
3912    // A₁[:, out] = Σ_i (vᵢ · J₁[:, out]) / λᵢ · vᵢ. Column-by-column keeps it the
3913    // d×p Jacobian (one SPD solve reused across all p output channels).
3914    let mut a1 = Array2::<f64>::zeros((d, p));
3915    for out in 0..p {
3916        let jcol = jm.column(out);
3917        for (i, &lam) in vals.iter().enumerate() {
3918            if !(lam.is_finite() && lam > 0.0) {
3919                return None;
3920            }
3921            let vi = vecs.column(i);
3922            let coeff = vi.dot(&jcol) / lam;
3923            for row in 0..d {
3924                a1[[row, out]] += coeff * vi[row];
3925            }
3926        }
3927    }
3928    Some((a1, recon))
3929}
3930
3931/// The single F1 routing metric: the amplitude-scaled center-reconstruction
3932/// distance `Σ_j (z·m₁_j − x_j)²`, where `m₁` is the amplitude-1 center
3933/// reconstruction `recon_center` and `z` is the row amplitude. Computed as the
3934/// DIRECT squared distance in element order — never the algebraically-equal but
3935/// cancellation-prone expansion `z²‖m₁‖² − 2z·(x·m₁)`, which loses ~‖x‖²·ε and can
3936/// FLIP the argmin between two charts whose reconstructions coincide to rounding
3937/// (period-wrapped seam charts). Every router — [`nearest_chart`],
3938/// [`select_nearest_charts_topk`], the batched `amortized_encode_batch_fast`
3939/// routing, and [`amortized_predict_row`] — scores through THIS function, so they
3940/// break near-ties identically. `#[inline]` keeps it zero-overhead on the
3941/// allocation-free massive-K fast-encode paths.
3942#[inline]
3943pub(crate) fn amplitude_scaled_center_dist(
3944    recon: ArrayView1<'_, f64>,
3945    x: ArrayView1<'_, f64>,
3946    amplitude: f64,
3947) -> f64 {
3948    let mut dist = 0.0;
3949    for (r, xv) in recon.iter().zip(x.iter()) {
3950        let diff = amplitude * r - xv;
3951        dist += diff * diff;
3952    }
3953    dist
3954}
3955
3956/// The shared chart-routing comparator: the SINGLE amplitude-gating + tie-break
3957/// decision every chart-routing top-k selection funnels through, so the CPU
3958/// atlas encode ([`nearest_chart`], [`nearest_charts_topk`]) and the GPU-host
3959/// resident encode
3960/// ([`crate::gpu_kernels::sae_encode_resident::nearest_charts_topk`]) can never
3961/// drift apart.
3962///
3963/// For each of `n_charts` charts (in index order), `recon_into(idx, out)` writes
3964/// the chart's amplitude-1 center reconstruction `m₁(t_idx)` into `out` and
3965/// returns whether the chart is certifiable (`certified_radius > 0`); a `false`
3966/// return skips it. The score is the TRUE encode objective at the row's
3967/// amplitude `z`, `‖x − z·m₁(t_c)‖²` — NOT the amplitude-1 `‖x − m₁(t_c)‖²`,
3968/// because the reconstruction actually compared against `x` is `z·m₁(t_c)` and
3969/// routing on amplitude-1 centers picks the wrong chart whenever `z ≠ 1` (a small
3970/// `z` makes a large-norm center reconstruct near a small `x`). The `k` charts of
3971/// smallest score are returned as `(index, distance)` sorted by `(distance,
3972/// index)` — a strict, deterministic first-wins tie rule.
3973///
3974/// The recon SOURCE is deliberately left to the caller: the CPU path copies the
3975/// distilled `recon_center` (precomputed once at build), while the GPU-host path
3976/// re-evaluates the basis at the chart center (mirroring exactly what the CUDA
3977/// kernel does, so the emulator stays its bit-parity oracle). Only the comparator
3978/// is shared.
3979pub(crate) fn select_nearest_charts_topk(
3980    n_charts: usize,
3981    x: ArrayView1<'_, f64>,
3982    amplitude: f64,
3983    k: usize,
3984    mut recon_into: impl FnMut(usize, &mut [f64]) -> bool,
3985) -> Vec<(usize, f64)> {
3986    if n_charts == 0 || k == 0 {
3987        return Vec::new();
3988    }
3989    let mut recon = vec![0.0_f64; x.len()];
3990    let mut scored: Vec<(usize, f64)> = Vec::new();
3991    for idx in 0..n_charts {
3992        if !recon_into(idx, &mut recon) {
3993            continue;
3994        }
3995        let dist = amplitude_scaled_center_dist(ArrayView1::from(recon.as_slice()), x, amplitude);
3996        scored.push((idx, dist));
3997    }
3998    // Sort by distance, then chart index for a deterministic, first-wins order.
3999    scored.sort_by(|a, b| {
4000        a.1.partial_cmp(&b.1)
4001            .unwrap_or(std::cmp::Ordering::Equal)
4002            .then(a.0.cmp(&b.0))
4003    });
4004    scored.truncate(k);
4005    scored
4006}
4007
4008/// Route a target row to the nearest chart of an atom by reconstruction
4009/// distance: the chart whose center reconstruction `m(t_c)` is closest to `x`.
4010/// Returns the chart index and the distance, or `None` when the atom has no
4011/// charts. Equivalent to `select_nearest_charts_topk(.., 1).first()`, routed
4012/// through the shared comparator.
4013pub(crate) fn nearest_chart(
4014    atom_atlas: &AtomEncodeAtlas,
4015    x: ArrayView1<'_, f64>,
4016    amplitude: f64,
4017) -> Option<(usize, f64)> {
4018    select_nearest_charts_topk(atom_atlas.charts.len(), x, amplitude, 1, |idx, out| {
4019        let chart = &atom_atlas.charts[idx];
4020        if chart.certified_radius <= 0.0 {
4021            return false;
4022        }
4023        for (o, r) in out.iter_mut().zip(chart.recon_center.iter()) {
4024            *o = *r;
4025        }
4026        true
4027    })
4028    .into_iter()
4029    .next()
4030}
4031
4032/// The `k` charts whose CENTER reconstruction `m(t_c)` is nearest to `x` in
4033/// ambient ‖·‖², returned as chart indices sorted by increasing distance (ties
4034/// broken by chart index — deterministic). Only certifiable charts
4035/// (`certified_radius > 0`) are considered, exactly like [`nearest_chart`], whose
4036/// single result is `nearest_charts_topk(.., 1)[0]`. Used by the certified encode
4037/// to refine the global basin on self-approaching atoms (see
4038/// [`CERTIFIED_ROUTING_TOPK`]).
4039pub(crate) fn nearest_charts_topk(
4040    atom_atlas: &AtomEncodeAtlas,
4041    x: ArrayView1<'_, f64>,
4042    amplitude: f64,
4043    k: usize,
4044) -> Vec<usize> {
4045    // `m₁(t_c) = BᵀΦ(t_c)` is an OFFLINE per-chart constant already distilled into
4046    // `chart.recon_center` at build time (bit-for-bit the same φ·decoder
4047    // accumulation this used to recompute). Reuse it instead of re-evaluating the
4048    // basis at a fixed center for every row — that re-eval was the encode's
4049    // dominant per-row cost. The amplitude-gating + tie-break comparator lives in
4050    // `select_nearest_charts_topk` (shared with the GPU-host path).
4051    select_nearest_charts_topk(atom_atlas.charts.len(), x, amplitude, k, |idx, out| {
4052        let chart = &atom_atlas.charts[idx];
4053        if chart.certified_radius <= 0.0 {
4054            return false;
4055        }
4056        for (o, r) in out.iter_mut().zip(chart.recon_center.iter()) {
4057            *o = *r;
4058        }
4059        true
4060    })
4061    .into_iter()
4062    .map(|(idx, _)| idx)
4063    .collect()
4064}
4065
4066/// Reconstruction error `‖x − z·m(t)‖` of an encoded coordinate `t` — the
4067/// criterion the certified encode minimizes over its candidate charts to pick the
4068/// GLOBAL basin. `m(t) = Bᵀ Φ(t)` is the amplitude-1 reconstruction; `z` is the
4069/// amplitude. A non-finite reconstruction returns `+∞` so it never wins.
4070pub fn encode_reconstruction_error(
4071    atom: &SaeManifoldAtom,
4072    evaluator: &dyn SaeBasisEvaluator,
4073    coord: ArrayView1<'_, f64>,
4074    x: ArrayView1<'_, f64>,
4075    amplitude: f64,
4076) -> f64 {
4077    // Bare Euclidean residual norm — bit-identical to the metric-free encode.
4078    encode_reconstruction_error_core(
4079        atom,
4080        evaluator,
4081        coord,
4082        x,
4083        amplitude,
4084        &EncodeObjective::euclidean(),
4085    )
4086}
4087
4088/// Objective-aware reconstruction error (F3): the WHITENED residual norm
4089/// `‖M^{1/2} r‖ = ‖Uᵀ r‖` when a metric is active, so the candidate-ranking /
4090/// warm-start SSE guard measures error in the SAME metric the certified objective
4091/// minimizes — an unwhitened `‖r‖₂` guard would rank candidates by a different
4092/// functional than the one being certified. With no metric this is `‖r‖₂`, exactly
4093/// the historical guard.
4094pub(crate) fn encode_reconstruction_error_core(
4095    atom: &SaeManifoldAtom,
4096    evaluator: &dyn SaeBasisEvaluator,
4097    coord: ArrayView1<'_, f64>,
4098    x: ArrayView1<'_, f64>,
4099    amplitude: f64,
4100    objective: &EncodeObjective<'_>,
4101) -> f64 {
4102    let d = atom.latent_dim();
4103    let p = atom.output_dim();
4104    let m = atom.basis_size();
4105    let coords = match coord.to_shape((1, d)) {
4106        Ok(c) => c.to_owned(),
4107        Err(_) => return f64::INFINITY,
4108    };
4109    let Ok((phi, _jet)) = evaluator.evaluate(coords.view()) else {
4110        return f64::INFINITY;
4111    };
4112    let mut residual = Array1::<f64>::zeros(p);
4113    for out in 0..p {
4114        let mut recon = 0.0;
4115        for basis_col in 0..m {
4116            recon += phi[[0, basis_col]] * atom.decoder_coefficients[[basis_col, out]];
4117        }
4118        residual[out] = x[out] - amplitude * recon;
4119    }
4120    // `½ rᵀ M r = ½‖Uᵀr‖²` under `M = U Uᵀ`; the guard reports the metric norm
4121    // `‖Uᵀr‖`. Euclidean (`None`) accumulates `Σ r²` in the SAME element order as
4122    // the historical loop, so the metric-free guard is bit-for-bit unchanged.
4123    let err2 = match objective.metric_factor {
4124        Some(u) => {
4125            let utr = u.t().dot(&residual);
4126            utr.dot(&utr)
4127        }
4128        None => {
4129            let mut e = 0.0;
4130            for out in 0..p {
4131                e += residual[out] * residual[out];
4132            }
4133            e
4134        }
4135    };
4136    if err2.is_finite() {
4137        err2.sqrt()
4138    } else {
4139        f64::INFINITY
4140    }
4141}
4142
4143/// Maximum number of chart centers laid down per atom (the SHAPE_BAND grid
4144/// point cap; mirrors `SHAPE_BAND_MAX_POINTS` in the atom band machinery).
4145pub(crate) const SHAPE_BAND_MAX_POINTS: usize = 512;
4146
4147/// Lay down chart centers on an atom's coordinate grid (the SHAPE_BAND grid
4148/// idiom): a regular grid spanning the compact latent domain for periodic /
4149/// sphere / torus atoms, and a strided cover of the latent axes for unbounded
4150/// (Duchon / Euclidean) atoms.
4151///
4152/// Periodic / torus latents are fractions of one period, so the per-axis grid
4153/// spans `[0, 1)`; the sphere chart spans `lat ∈ [−π/2, π/2]`, `lon ∈ [−π, π)`.
4154/// These conventions match the basis evaluators (the fraction-of-period circle
4155/// harmonic and the lat/lon sphere chart).
4156/// Squared coordinate distance between two latent points under the atom's chart
4157/// geometry: per-axis WRAPPED distance `min(|a−b|, period−|a−b|)` on periodic
4158/// (circle) axes — period 1 to match `chart_center_grid`'s `[0,1)` torus tiling
4159/// — and plain difference on line axes. Used to place + size data-driven charts.
4160pub(crate) fn coord_dist_sq(
4161    atom: &SaeManifoldAtom,
4162    a: ArrayView1<'_, f64>,
4163    b: ArrayView1<'_, f64>,
4164) -> f64 {
4165    // Per-axis period comes from the ONE canonical source `latent_axis_period` —
4166    // the SAME convention the certified-encode `in_chart` guard uses (via
4167    // `latent_coordinate_distance`): period-1 fraction axes for periodic/torus and
4168    // the cylinder angle, period-2π on the sphere LONGITUDE (axis 1), and
4169    // NON-periodic otherwise — including the sphere LATITUDE (axis 0), which ranges
4170    // over [−π/2, π/2] and must NOT wrap. A former local `periodic_axis` closure
4171    // wrapped BOTH sphere axes at period 1, so two genuinely-far radian longitudes
4172    // (e.g. 0 and 3 rad) collapsed to distance 0 — corrupting the farthest-point
4173    // placement AND the nearest-neighbour radii in `data_driven_chart_centers` for
4174    // sphere atoms, and disagreeing with the metric the encode's `in_chart`
4175    // soundness guard enforces. Delegating keeps the two metrics identical.
4176    let mut acc = 0.0;
4177    for axis in 0..a.len().min(b.len()) {
4178        let mut d = (a[axis] - b[axis]).abs();
4179        if let Some(period) = latent_axis_period(atom, axis) {
4180            let wrapped = d.rem_euclid(period);
4181            d = wrapped.min(period - wrapped);
4182        }
4183        acc += d * d;
4184    }
4185    acc
4186}
4187
4188/// Greedy farthest-point sampling of up to `max_charts` chart centers from the
4189/// atom's latent `coords` (n × d), with each center's nominal radius set to half
4190/// the distance to its nearest neighbor center (floored, so a singleton/coincident
4191/// cluster still gets a usable ball). Deterministic: seeds from row 0, then
4192/// repeatedly adds the coord maximally far (under [`coord_dist_sq`]) from the
4193/// chosen set — coverage-maximizing and reproducible run-to-run.
4194pub(crate) fn data_driven_chart_centers(
4195    atom: &SaeManifoldAtom,
4196    coords: ArrayView2<'_, f64>,
4197    max_charts: usize,
4198) -> Result<(Array2<f64>, Vec<f64>), String> {
4199    let n = coords.nrows();
4200    let d = coords.ncols();
4201    if d != atom.latent_dim() {
4202        return Err(format!(
4203            "data_driven_chart_centers: coords have {d} cols but atom latent_dim is {}",
4204            atom.latent_dim()
4205        ));
4206    }
4207    if n == 0 {
4208        return Ok((Array2::<f64>::zeros((0, d)), Vec::new()));
4209    }
4210    let k = max_charts.min(n);
4211    // Farthest-point sampling: maintain each row's distance to the nearest chosen
4212    // center, add the row with the maximum such distance each step.
4213    let mut chosen: Vec<usize> = Vec::with_capacity(k);
4214    chosen.push(0);
4215    let mut nearest_sq: Vec<f64> = (0..n)
4216        .map(|r| coord_dist_sq(atom, coords.row(r), coords.row(0)))
4217        .collect();
4218    while chosen.len() < k {
4219        // Pick the row farthest from the current center set (first-wins tie).
4220        let mut best = 0usize;
4221        let mut best_d = -1.0;
4222        for r in 0..n {
4223            if nearest_sq[r] > best_d {
4224                best_d = nearest_sq[r];
4225                best = r;
4226            }
4227        }
4228        if best_d <= 0.0 {
4229            break; // all remaining rows coincide with a chosen center.
4230        }
4231        chosen.push(best);
4232        for r in 0..n {
4233            let dr = coord_dist_sq(atom, coords.row(r), coords.row(best));
4234            if dr < nearest_sq[r] {
4235                nearest_sq[r] = dr;
4236            }
4237        }
4238    }
4239    let m = chosen.len();
4240    let mut centers = Array2::<f64>::zeros((m, d));
4241    for (i, &row) in chosen.iter().enumerate() {
4242        centers.row_mut(i).assign(&coords.row(row));
4243    }
4244    // Per-center radius = half the nearest-OTHER-center distance, floored so a
4245    // coincident pair still yields a positive ball, capped at 0.5 (the largest
4246    // meaningful half-period on a unit circle).
4247    let mut radii = vec![0.0_f64; m];
4248    for i in 0..m {
4249        let mut nn = f64::INFINITY;
4250        for j in 0..m {
4251            if i == j {
4252                continue;
4253            }
4254            let dsq = coord_dist_sq(atom, centers.row(i), centers.row(j));
4255            if dsq < nn {
4256                nn = dsq;
4257            }
4258        }
4259        let r = if nn.is_finite() { 0.5 * nn.sqrt() } else { 0.5 };
4260        radii[i] = r.max(1.0e-3).min(0.5);
4261    }
4262    Ok((centers, radii))
4263}
4264
4265pub(crate) fn chart_center_grid(atom: &SaeManifoldAtom, resolution: usize) -> Array2<f64> {
4266    use crate::manifold::SaeAtomBasisKind::*;
4267    let d = atom.latent_dim();
4268    match atom.basis_kind() {
4269        Periodic | Torus | KleinBottle => regular_product_grid(d, resolution, 0.0, 1.0, false),
4270        // Cylinder `S¹ × ℝ`: axis 0 is the periodic circle `[0, 1)` (no
4271        // endpoint, like the harmonic axes); axis 1 is the unbounded line,
4272        // covered by a strided unit box `[-0.5, 0.5]` about the origin (like the
4273        // Euclidean patch). The certified radius refines each chart; out-of-cover
4274        // line starts route to the exact fallback honestly.
4275        Cylinder if d == 2 => cylinder_chart_center_grid(resolution),
4276        Cylinder => regular_product_grid(d, resolution, -0.5, 0.5, true),
4277        Mobius if d == 2 => mobius_chart_center_grid(resolution),
4278        Mobius => regular_product_grid(d, resolution, -1.0, 1.0, true),
4279        Sphere | ProjectivePlane if d == 2 => sphere_latlon_grid(resolution),
4280        Linear | Sphere | ProjectivePlane | Duchon | EuclideanPatch | Poincare | Precomputed(_)
4281        | FiniteSet => {
4282            // Unbounded / non-compact latents (and the finite-set index axis): a
4283            // strided cover of a unit box about the origin per axis. The certified
4284            // radius refines each chart; out-of-cover starts route to the exact
4285            // fallback honestly.
4286            regular_product_grid(d, resolution, -0.5, 0.5, true)
4287        }
4288    }
4289}
4290
4291/// A regular `resolution`-per-axis product grid over `[lo, hi]^d`, capped at
4292/// [`SHAPE_BAND_MAX_POINTS`] total points (the per-axis resolution is reduced
4293/// until the product fits). When `include_endpoint` the last grid point sits at
4294/// `hi`; otherwise the axis is treated as periodic and stops one step short.
4295/// Per-axis resolution actually used by [`regular_product_grid`] after the
4296/// [`SHAPE_BAND_MAX_POINTS`] product cap. Chart radii must be derived from THIS
4297/// (not the raw `resolution`), otherwise for `resolution^d > SHAPE_BAND_MAX_POINTS`
4298/// the grid spacing is coarser than the radius and the charts leave gaps.
4299pub(crate) fn capped_per_axis(d: usize, resolution: usize) -> usize {
4300    let mut per_axis = resolution.max(2);
4301    while per_axis.saturating_pow(d as u32) > SHAPE_BAND_MAX_POINTS && per_axis > 2 {
4302        per_axis -= 1;
4303    }
4304    per_axis
4305}
4306
4307pub(crate) fn regular_product_grid(
4308    d: usize,
4309    resolution: usize,
4310    lo: f64,
4311    hi: f64,
4312    include_endpoint: bool,
4313) -> Array2<f64> {
4314    if d == 0 {
4315        return Array2::<f64>::zeros((1, 0));
4316    }
4317    let per_axis = capped_per_axis(d, resolution);
4318    let total = per_axis.saturating_pow(d as u32).max(1);
4319    let denom = if include_endpoint {
4320        (per_axis.max(2) - 1) as f64
4321    } else {
4322        per_axis as f64
4323    };
4324    let mut grid = Array2::<f64>::zeros((total, d));
4325    let mut idx = vec![0usize; d];
4326    for flat in 0..total {
4327        for axis in 0..d {
4328            let frac = idx[axis] as f64 / denom;
4329            grid[[flat, axis]] = lo + (hi - lo) * frac;
4330        }
4331        for axis in (0..d).rev() {
4332            idx[axis] += 1;
4333            if idx[axis] < per_axis {
4334                break;
4335            }
4336            idx[axis] = 0;
4337        }
4338    }
4339    grid
4340}
4341
4342/// Lat/lon sphere chart grid: `lat ∈ [−π/2, π/2]`, `lon ∈ [−π, π)`, matching
4343/// the [`crate::manifold::SphereChartEvaluator`] convention.
4344pub(crate) fn sphere_latlon_grid(resolution: usize) -> Array2<f64> {
4345    use std::f64::consts::PI;
4346    // Per-axis cap derived from the shared point budget rather than a hardcoded
4347    // literal (#2071): the largest r with r² ≤ SHAPE_BAND_MAX_POINTS. Equals 22
4348    // at the current budget (22²=484≤512), but now tracks the budget if it
4349    // changes instead of silently desyncing from the sibling grid arms.
4350    let r_cap = SHAPE_BAND_MAX_POINTS.isqrt();
4351    let r = resolution.max(2).min(r_cap);
4352    let mut grid = Array2::<f64>::zeros((r * r, 2));
4353    for i in 0..r {
4354        let lat = -PI / 2.0 + PI * (i as f64 + 0.5) / r as f64;
4355        for j in 0..r {
4356            let lon = -PI + 2.0 * PI * (j as f64) / r as f64;
4357            grid[[i * r + j, 0]] = lat;
4358            grid[[i * r + j, 1]] = lon;
4359        }
4360    }
4361    grid
4362}
4363
4364/// Cylinder `S¹ × ℝ` chart-center grid: axis 0 sweeps the periodic circle over
4365/// one period `[0, 1)` (no endpoint, matching the harmonic axis), axis 1 strides
4366/// a unit box `[−0.5, 0.5]` about the origin on the unbounded line (with
4367/// endpoint). Capped at [`SHAPE_BAND_MAX_POINTS`] total centers.
4368pub(crate) fn cylinder_chart_center_grid(resolution: usize) -> Array2<f64> {
4369    let mut per_axis = resolution.max(2);
4370    while per_axis * per_axis > SHAPE_BAND_MAX_POINTS && per_axis > 2 {
4371        per_axis -= 1;
4372    }
4373    let total = per_axis * per_axis;
4374    let line_denom = (per_axis.max(2) - 1) as f64;
4375    let mut grid = Array2::<f64>::zeros((total, 2));
4376    for i in 0..per_axis {
4377        // Periodic axis 0: stop one step short of the period.
4378        let circle = i as f64 / per_axis as f64;
4379        for j in 0..per_axis {
4380            // Line axis 1: include the endpoint of the unit box.
4381            let line = -0.5 + (j as f64) / line_denom;
4382            grid[[i * per_axis + j, 0]] = circle;
4383            grid[[i * per_axis + j, 1]] = line;
4384        }
4385    }
4386    grid
4387}
4388
4389/// Möbius double-cover chart grid: angle `s ∈ [0, 2)` and bounded width
4390/// `w ∈ [-1, 1]`. The deck identification is encoded by the basis, so the
4391/// atlas covers the ordinary cylindrical chart without duplicating a seam.
4392pub(crate) fn mobius_chart_center_grid(resolution: usize) -> Array2<f64> {
4393    let mut per_axis = resolution.max(2);
4394    while per_axis * per_axis > SHAPE_BAND_MAX_POINTS && per_axis > 2 {
4395        per_axis -= 1;
4396    }
4397    let mut grid = Array2::<f64>::zeros((per_axis * per_axis, 2));
4398    let width_denom = (per_axis - 1) as f64;
4399    for i in 0..per_axis {
4400        let angle = 2.0 * i as f64 / per_axis as f64;
4401        for j in 0..per_axis {
4402            let width = -1.0 + 2.0 * j as f64 / width_denom;
4403            grid[[i * per_axis + j, 0]] = angle;
4404            grid[[i * per_axis + j, 1]] = width;
4405        }
4406    }
4407    grid
4408}
4409
4410/// Nominal in-chart radius: half the inter-center grid spacing, so charts tile
4411/// the domain. For compact latents this is the grid step; for unbounded latents
4412/// a unit default that the certified radius refines.
4413pub(crate) fn chart_nominal_radius(atom: &SaeManifoldAtom, resolution: usize) -> f64 {
4414    use crate::manifold::SaeAtomBasisKind::*;
4415    match atom.basis_kind() {
4416        Periodic | Torus | KleinBottle => {
4417            0.5 / (capped_per_axis(atom.latent_dim(), resolution) as f64)
4418        }
4419        // Must use the SAME capped per-axis count `sphere_latlon_grid` lays the
4420        // centers on: the coarsest tiling step is the longitude half-spacing `π/r`
4421        // with `r` the grid's per-axis count. Deriving the radius from the RAW
4422        // resolution makes it smaller than the grid spacing once the grid caps,
4423        // leaving gaps between charts so rows in the gaps spuriously route to the
4424        // exact fallback (the hazard `capped_per_axis` documents for the regular
4425        // grid). The cap is DERIVED, not a literal (#2071): `sphere_latlon_grid`
4426        // uses `SHAPE_BAND_MAX_POINTS.isqrt()` — the largest `r` with `r² ≤`
4427        // the band-point budget — so the two stay in lockstep if the budget moves
4428        // (a hardcoded `22` here silently desyncs the moment the budget changes).
4429        Sphere | ProjectivePlane => {
4430            let r_cap = SHAPE_BAND_MAX_POINTS.isqrt();
4431            std::f64::consts::PI / (resolution.max(2).min(r_cap) as f64)
4432        }
4433        // Cylinder charts tile two heterogeneous axes (a `[0,1)` periodic step
4434        // and a unit-box line step); the chart radius is a single scalar, so we
4435        // take the tighter (periodic) step `0.5/res` to keep every chart valid
4436        // on both axes. The certified Kantorovich radius refines it per chart.
4437        Cylinder => 0.5 / (capped_per_axis(atom.latent_dim(), resolution) as f64),
4438        // Angle spacing is `2/r` and width spacing is `2/(r-1)`; their
4439        // half-spacings are `1/r` and `1/(r-1)`, so the angular axis is the
4440        // conservative scalar chart radius.
4441        Mobius => 1.0 / (capped_per_axis(atom.latent_dim(), resolution) as f64),
4442        Linear | Duchon | EuclideanPatch | Poincare | Precomputed(_) | FiniteSet => {
4443            1.0 / (resolution.max(2) as f64)
4444        }
4445    }
4446}
4447
4448/// Build the [`ChartRegion`] for a center, attaching the radial r_min / r_max
4449/// bracket for Duchon atoms (the chart's distance range to the kernel centers).
4450pub(crate) fn chart_region(
4451    atom: &SaeManifoldAtom,
4452    center: Array1<f64>,
4453    radius: f64,
4454) -> ChartRegion {
4455    use crate::manifold::SaeAtomBasisKind::*;
4456    let region = ChartRegion::new(center.clone(), radius);
4457    match atom.basis_kind() {
4458        Duchon => {
4459            // r ranges over [‖t_c‖ − radius, ‖t_c‖ + radius] about the single
4460            // origin-anchored center used by the conservative radial bound.
4461            //
4462            // The lower bound must be `max(0, center_norm − radius)` — NOT floored
4463            // at `radius`. When the chart contains the kernel center
4464            // (`center_norm < radius`, true r_min = 0), flooring at `radius`
4465            // would give a finite, NON-CONSERVATIVE `r_min`, causing the
4466            // hessian_sup / third_sup formulas (which divide by r_min) to
4467            // underestimate the Lipschitz constant and potentially grant a false
4468            // Kantorovich certificate. Flooring at `f64::MIN_POSITIVE` instead
4469            // correctly drives the formulas toward ∞, producing a very large L
4470            // that will NEVER certify (rows route to the exact multi-start
4471            // fallback) — conservative and sound.
4472            let center_norm = center.dot(&center).sqrt();
4473            let r_min = (center_norm - radius).max(f64::MIN_POSITIVE);
4474            let r_max = center_norm + radius;
4475            region.with_radial_bounds(r_min, r_max)
4476        }
4477        // Cylinder has no radial kernel block (it is a harmonic × polynomial
4478        // tensor, not a Duchon radial basis), so it needs no radial r_min/r_max.
4479        Periodic | Sphere | Torus | ProjectivePlane | KleinBottle | Cylinder | Mobius | Linear
4480        | EuclideanPatch | Poincare | Precomputed(_) | FiniteSet => region,
4481    }
4482}
4483
4484/// Per-atom ambient tangents at the given coords: for atom `k` and row `i`, the
4485/// `d_k × p` matrix whose axis-`a` row is `∂m_k/∂t_a = (∂Φ/∂t_a)·B_k`, the image
4486/// tangent the joint Hessian couples through. `None` for an atom with no basis
4487/// evaluator (its coordinate is not differentiable, so it carries no coupling).
4488fn atom_row_tangents(
4489    atom: &SaeManifoldAtom,
4490    coords: ArrayView2<'_, f64>,
4491) -> Result<Option<Vec<Array2<f64>>>, String> {
4492    let Some(evaluator) = atom.basis_evaluator.as_ref() else {
4493        return Ok(None);
4494    };
4495    let n = coords.nrows();
4496    let d = atom.latent_dim();
4497    let p = atom.output_dim();
4498    let (_phi, jet) = evaluator.evaluate(coords)?; // jet: (n, M, d)
4499    let m = jet.shape()[1];
4500    let b = &atom.decoder_coefficients; // (M, p)
4501    let mut out = Vec::with_capacity(n);
4502    for row in 0..n {
4503        let mut tan = Array2::<f64>::zeros((d, p));
4504        for axis in 0..d {
4505            for out_col in 0..p {
4506                let mut acc = 0.0;
4507                for basis_col in 0..m {
4508                    acc += jet[[row, basis_col, axis]] * b[[basis_col, out_col]];
4509                }
4510                tan[[axis, out_col]] = acc;
4511            }
4512        }
4513        out.push(tan);
4514    }
4515    Ok(Some(out))
4516}
4517
4518/// Smallest eigenvalue of the symmetric `d × d` Gauss–Newton curvature block
4519/// `z² · T Tᵀ` (`T` is `d × p`). `d = 1` is the scalar fast path; general `d`
4520/// uses a symmetric eigensolve.
4521fn min_curvature_eigenvalue(tan: &Array2<f64>, z: f64) -> Result<f64, String> {
4522    let d = tan.nrows();
4523    if d == 0 {
4524        return Ok(0.0);
4525    }
4526    let z2 = z * z;
4527    if d == 1 {
4528        let row = tan.row(0);
4529        return Ok(z2 * row.dot(&row));
4530    }
4531    let mut gram = Array2::<f64>::zeros((d, d));
4532    for a in 0..d {
4533        for bx in 0..d {
4534            gram[[a, bx]] = z2 * tan.row(a).dot(&tan.row(bx));
4535        }
4536    }
4537    let (evals, _vecs) = gram
4538        .eigh(faer::Side::Lower)
4539        .map_err(|e| format!("min_curvature_eigenvalue: eigh failed: {e:?}"))?;
4540    Ok(evals.iter().copied().fold(f64::INFINITY, f64::min))
4541}
4542
4543/// Frobenius norm of the cross-coupling block `z_k z_j · T_k T_jᵀ`
4544/// (`T_k` is `d_k × p`, `T_j` is `d_j × p`). Frobenius upper-bounds the operator
4545/// norm, so a dominance decision made against it is SOUND.
4546fn cross_block_frobenius(tan_k: &Array2<f64>, tan_j: &Array2<f64>, zk: f64, zj: f64) -> f64 {
4547    let mut acc = 0.0;
4548    for a in 0..tan_k.nrows() {
4549        for bx in 0..tan_j.nrows() {
4550            let dot = tan_k.row(a).dot(&tan_j.row(bx));
4551            acc += dot * dot;
4552        }
4553    }
4554    (zk * zj).abs() * acc.sqrt()
4555}
4556
4557/// The JOINT (multi-atom) encode-fallback fraction: the share of rows whose
4558/// per-row joint reconstruction problem across CO-ACTIVE atoms is NOT covered by
4559/// the composition of the per-atom certificates, so the row genuinely needs the
4560/// exact multi-start solve (reviewer condition #3 — the honest encode-tax cost
4561/// multiplier at scale).
4562///
4563/// The per-atom Kantorovich certificate certifies each atom's coordinate encode
4564/// IN ISOLATION — a block-diagonal view of the joint Hessian. The joint problem
4565/// couples co-active atoms through the off-diagonal blocks
4566/// `H_kj = z_k z_j J_k(t_k)ᵀ J_j(t_j)` (tangent-image inner products). When an
4567/// atom's own curvature block fails to dominate its coupling to the rest —
4568/// Gershgorin: `λ_min(H_kk) ≤ Σ_{j≠k} ‖H_kj‖` — the block-diagonal certificate
4569/// no longer implies a joint root, a second basin can open, and the row must go
4570/// to multi-start. This fraction GROWS with atom-image similarity and
4571/// co-activation: no per-atom certificate covers the joint problem.
4572///
4573/// The curvature block uses the Gauss–Newton form `z_k² J_kᵀ J_k` (exact for
4574/// flat/linear atoms, where the residual-curvature term vanishes identically);
4575/// the off-diagonal is measured in Frobenius norm, which upper-bounds the
4576/// operator norm, so a row DECLARED dominant is genuinely dominant and the
4577/// fraction never under-reports the multi-start need. Rows with fewer than two
4578/// co-active atoms have no cross blocks and are never counted as fallbacks.
4579///
4580/// `amplitude_floor` is the mass above which an atom counts as co-active; pass a
4581/// small positive value (a domain threshold on the assignment mass, not a solver
4582/// knob).
4583pub fn joint_encode_fallback_fraction(
4584    atoms: &[SaeManifoldAtom],
4585    coords: &[Array2<f64>],
4586    amplitudes: ArrayView2<'_, f64>,
4587    amplitude_floor: f64,
4588) -> Result<f64, String> {
4589    let k_atoms = atoms.len();
4590    let (n, amp_k) = amplitudes.dim();
4591    if amp_k != k_atoms {
4592        return Err(format!(
4593            "joint_encode_fallback_fraction: amplitudes have {amp_k} cols but {k_atoms} atoms"
4594        ));
4595    }
4596    if coords.len() != k_atoms {
4597        return Err(format!(
4598            "joint_encode_fallback_fraction: {} coord blocks but {k_atoms} atoms",
4599            coords.len()
4600        ));
4601    }
4602    if n == 0 || k_atoms == 0 {
4603        return Ok(0.0);
4604    }
4605    // F5 — FILTER BEFORE MATERIALIZE: the dense form built the full `n × K`
4606    // tangent tensor (`atom_row_tangents` over every atom, all N rows) BEFORE the
4607    // per-row activity filter, which OOMs at `K = 32k` even though each row couples
4608    // only through its co-active atoms. Cross-coupling exists only among a row's
4609    // co-active, differentiable atoms (`z > floor`), so materialize just that row's
4610    // active tangents, lazily, per row. `atom_row_tangents` is row-wise, so the
4611    // single-row slice is bit-identical to the batched evaluate the dense path
4612    // indexed — the fallback fraction is unchanged; only the peak allocation drops
4613    // from `O(n·K·d·p)` to the max active-set size per row.
4614    for (atom_idx, coord) in coords.iter().enumerate() {
4615        if coord.nrows() != n {
4616            return Err(format!(
4617                "joint_encode_fallback_fraction: coord block {atom_idx} has {} rows, expected {n}",
4618                coord.nrows()
4619            ));
4620        }
4621    }
4622    let mut fallback_rows = 0usize;
4623    for row in 0..n {
4624        // Gather this row's co-active, differentiable atoms (evaluator-less atoms
4625        // carry no coupling — same `is_some()` predicate the dense path applied via
4626        // `tangents[k].is_some()`).
4627        let active: Vec<usize> = (0..k_atoms)
4628            .filter(|&k| {
4629                amplitudes[[row, k]] > amplitude_floor && atoms[k].basis_evaluator.is_some()
4630            })
4631            .collect();
4632        if active.len() < 2 {
4633            continue; // no cross blocks: the per-atom certificate composes trivially
4634        }
4635        // Materialize ONLY this row's active tangents (one `d × p` block per active
4636        // atom), never the dense tensor. An atom that unexpectedly yields no tangent
4637        // (evaluator present but non-differentiable) simply drops out of the coupling,
4638        // exactly as a `None` block did in the dense path.
4639        let mut tans: Vec<Array2<f64>> = Vec::with_capacity(active.len());
4640        let mut zs: Vec<f64> = Vec::with_capacity(active.len());
4641        for &k in &active {
4642            let coord_row = coords[k].row(row).insert_axis(Axis(0)); // (1, d)
4643            if let Some(mut block) = atom_row_tangents(&atoms[k], coord_row)? {
4644                tans.push(block.pop().expect("single-row tangents carry one block"));
4645                zs.push(amplitudes[[row, k]]);
4646            }
4647        }
4648        if tans.len() < 2 {
4649            continue;
4650        }
4651        let mut row_needs_multistart = false;
4652        for (ki, tan_k) in tans.iter().enumerate() {
4653            let zk = zs[ki];
4654            let lam_min = min_curvature_eigenvalue(tan_k, zk)?;
4655            let mut coupling = 0.0;
4656            for (ji, tan_j) in tans.iter().enumerate() {
4657                if ji == ki {
4658                    continue;
4659                }
4660                coupling += cross_block_frobenius(tan_k, tan_j, zk, zs[ji]);
4661            }
4662            if lam_min <= coupling {
4663                row_needs_multistart = true;
4664                break;
4665            }
4666        }
4667        if row_needs_multistart {
4668            fallback_rows += 1;
4669        }
4670    }
4671    Ok(fallback_rows as f64 / n as f64)
4672}
4673
4674#[cfg(test)]
4675mod encode_fix_tests {
4676    //! Unit tests for the two `certify_with_basin_warmup` fixes:
4677    //!   FIX #3 — wrap-aware chart containment (`in_chart` now measures latent
4678    //!            distance on the atom's periodic geometry via
4679    //!            [`latent_coordinate_distance`]).
4680    //!   FIX #4 — multiplicative sufficient-decrease bound on the warm-up loop
4681    //!            ([`warmup_progress_sufficient`]).
4682    use super::*;
4683    use crate::manifold::SaeAtomBasisKind;
4684    use ndarray::{Array1, Array2, Array3};
4685
4686    /// Minimal atom carrying only a `basis_kind`; the wrap-aware metric reads no
4687    /// other field, so a tiny well-formed basis/decoder/penalty suffices.
4688    fn tiny_atom(kind: SaeAtomBasisKind, latent_dim: usize) -> SaeManifoldAtom {
4689        let m = 2usize;
4690        let phi = Array2::<f64>::eye(m);
4691        let jet = Array3::<f64>::zeros((m, m, latent_dim));
4692        let dec = Array2::<f64>::from_elem((m, 1), 0.5);
4693        let smooth = Array2::<f64>::eye(m);
4694        SaeManifoldAtom::new_with_provided_function_gram(
4695            "tiny", kind, latent_dim, phi, jet, dec, smooth,
4696        )
4697        .expect("tiny atom builds")
4698    }
4699
4700    #[test]
4701    fn joint_normal_equations_use_the_shared_multi_atom_residual() {
4702        // u1=(1,0), u2=(1,1), x=(2,1). At t=(0,0), the shared residual is -x.
4703        // The joint normal equations recover the unique coefficients (1,1);
4704        // independent projections would instead produce (2,1.5).
4705        let jac = ndarray::array![[1.0_f64, 0.0], [1.0, 1.0]];
4706        let residual = ndarray::array![-2.0_f64, -1.0];
4707        let (_value, grad, hess) = joint_data_value_grad_hess(jac.view(), residual.view(), None);
4708        let step = joint_encode_damped_step(hess.view(), grad.view(), 1.0e-15)
4709            .expect("joint system factors")
4710            .expect("joint system is positive definite");
4711        assert!(
4712            (step[0] - 1.0).abs() < 1.0e-12,
4713            "first coefficient={}",
4714            step[0]
4715        );
4716        assert!(
4717            (step[1] - 1.0).abs() < 1.0e-12,
4718            "second coefficient={}",
4719            step[1]
4720        );
4721        let recon0 = step[0] + step[1];
4722        let recon1 = step[1];
4723        assert!((recon0 - 2.0).abs() < 1.0e-12 && (recon1 - 1.0).abs() < 1.0e-12);
4724    }
4725
4726    /// FIX #3: a point on the far side of the wrap seam of a periodic axis is now
4727    /// IN-CHART under the wrap-aware metric, where the old raw-Euclidean sum used
4728    /// by `in_chart` rejected it. This is the exact predicate `in_chart` evaluates
4729    /// (`latent_coordinate_distance(atom, t, center) <= radius`).
4730    #[test]
4731    fn wrap_aware_containment_accepts_seam_point() {
4732        let atom = tiny_atom(SaeAtomBasisKind::Periodic, 1);
4733        let t = Array1::from(vec![0.99f64]);
4734        let center = Array1::from(vec![0.01f64]);
4735        let radius = 0.05;
4736
4737        // Precondition: the OLD raw-Euclidean latent distance is 0.98 — far
4738        // outside the 0.05 ball, so the old `in_chart` REJECTED this iterate.
4739        let raw = (t[0] - center[0]).abs();
4740        assert!(
4741            raw > radius,
4742            "precondition: raw Euclidean distance {raw} must exceed the chart radius {radius}"
4743        );
4744
4745        // The wrap-aware metric sees the true circle distance 0.02 (period 1.0),
4746        // so the seam point is now correctly IN-CHART.
4747        let d = latent_coordinate_distance(&atom, t.view(), center.view());
4748        assert!(
4749            (d - 0.02).abs() < 1e-12,
4750            "wrap-aware distance across the seam must be 0.02, got {d}"
4751        );
4752        assert!(
4753            d <= radius,
4754            "wrap-aware seam point must now be in-chart (d={d} <= r={radius})"
4755        );
4756    }
4757
4758    /// Soundness guard for FIX #3: on a NON-periodic axis the metric must NOT
4759    /// wrap — the same coordinates stay at their full Euclidean separation and
4760    /// (correctly) remain OUT of the small ball. This preserves the
4761    /// never-issue-a-false-certificate invariant for flat-patch families.
4762    #[test]
4763    fn wrap_aware_containment_does_not_wrap_flat_axis() {
4764        let atom = tiny_atom(SaeAtomBasisKind::EuclideanPatch, 1);
4765        let t = Array1::from(vec![0.99f64]);
4766        let center = Array1::from(vec![0.01f64]);
4767        let d = latent_coordinate_distance(&atom, t.view(), center.view());
4768        assert!(
4769            (d - 0.98).abs() < 1e-12,
4770            "a flat (non-periodic) axis must keep the full 0.98 distance, got {d}"
4771        );
4772        assert!(
4773            d > 0.05,
4774            "flat-axis point correctly stays out of a 0.05 ball"
4775        );
4776    }
4777
4778    /// FIX #4: a genuinely converging (Kantorovich-quadratic) `h`-sequence is
4779    /// untouched — every step clears the sufficient-decrease bar, so no
4780    /// converging row is regressed to the fallback.
4781    #[test]
4782    fn converging_h_sequence_is_never_flagged() {
4783        // Quadratic Newton contraction h_{k+1} = h_k^2 from an uncertified start.
4784        let mut h = 0.9f64;
4785        while h > KANTOROVICH_THRESHOLD {
4786            let h_next = h * h;
4787            assert!(
4788                warmup_progress_sufficient(h_next, h),
4789                "converging step {h} -> {h_next} must be accepted (no regression)"
4790            );
4791            h = h_next;
4792        }
4793    }
4794
4795    /// FIX #4: a monotone `h`-sequence decreasing toward a limit ABOVE ½ (the
4796    /// pathological plateau that the old strict-decrease rule spun on until the
4797    /// increments fell below one ulp) now terminates in a BOUNDED, small number
4798    /// of `warmup_progress_sufficient` steps — flag-to-fallback rather than loop.
4799    #[test]
4800    fn plateau_h_sequence_terminates_bounded() {
4801        let limit = 0.65f64; // plateau limit strictly above ½: never certifies
4802        let ratio = 0.8f64; // geometric approach; ratio -> 1 as h -> limit
4803        let mut h = 2.0f64; // uncertified start
4804        let start = h;
4805        let mut accepted = 0usize;
4806        let mut iters = 0usize;
4807        loop {
4808            iters += 1;
4809            // Hard ceiling: proves boundedness. The OLD strict-decrease rule
4810            // would run ~1e15 steps here (h strictly decreases every step toward
4811            // 0.65 and only stalls below one ulp), so tripping this assert would
4812            // signal a regression to the unbounded behavior.
4813            assert!(
4814                iters < 10_000,
4815                "warm-up must terminate in a bounded number of steps, not loop"
4816            );
4817            let h_next = limit + (h - limit) * ratio; // monotone decreasing > limit
4818            if !warmup_progress_sufficient(h_next, h) {
4819                break; // flag to the exact fallback
4820            }
4821            accepted += 1;
4822            h = h_next;
4823        }
4824        // It made real progress before flagging (a warm-up, not an instant bail)…
4825        assert!(
4826            accepted >= 1,
4827            "expected the warm-up to accept at least one contracting step first"
4828        );
4829        // …and it flagged while still strictly above the plateau limit — exactly
4830        // where the OLD rule would have kept spinning (h is still shrinking).
4831        assert!(
4832            h > limit && h < start,
4833            "flagged mid-descent (limit < h={h} < start={start}); old rule would not stop here"
4834        );
4835        // Termination bound was tiny, not astronomical.
4836        assert!(iters < 200, "plateau flagged in {iters} steps (bounded)");
4837    }
4838
4839    /// FIX #4: non-finite `h` (indefinite / blown-up Hessian) is treated as
4840    /// insufficient progress — the row flags rather than being accepted.
4841    #[test]
4842    fn non_finite_h_flags() {
4843        assert!(!warmup_progress_sufficient(f64::NAN, 0.9));
4844        assert!(!warmup_progress_sufficient(f64::INFINITY, 0.9));
4845        assert!(!warmup_progress_sufficient(0.5, f64::NAN));
4846    }
4847
4848    /// BUG (sphere data-driven placement): `coord_dist_sq` must measure sphere
4849    /// coordinates in RADIANS via the canonical `latent_axis_period` — longitude
4850    /// (axis 1) wraps at 2π, latitude (axis 0) does NOT wrap — not at unit period
4851    /// on both axes. The old period-1 wrap collapsed genuinely-far longitudes to
4852    /// distance 0, corrupting `data_driven_chart_centers` for sphere atoms.
4853    #[test]
4854    fn coord_dist_sq_sphere_uses_radian_metric_not_unit_period() {
4855        let atom = tiny_atom(SaeAtomBasisKind::Sphere, 2);
4856        // Longitude differs by 3.0 rad: circle distance min(3, 2π−3)=3
4857        // (2π−3 ≈ 3.283) ⇒ dist² = 9. The old period-1 wrap read 3 mod 1 = 0.
4858        let a = Array1::from(vec![0.0f64, 0.0]);
4859        let b = Array1::from(vec![0.0f64, 3.0]);
4860        let dsq = coord_dist_sq(&atom, a.view(), b.view());
4861        assert!(
4862            (dsq - 9.0).abs() < 1e-9,
4863            "sphere longitude dist² must be 3²=9 (2π radian period), got {dsq}"
4864        );
4865        // Latitude (axis 0) must NOT wrap: 1.2 rad apart ⇒ dist² = 1.44.
4866        let c = Array1::from(vec![0.0f64, 0.0]);
4867        let e = Array1::from(vec![1.2f64, 0.0]);
4868        let dsq_lat = coord_dist_sq(&atom, c.view(), e.view());
4869        assert!(
4870            (dsq_lat - 1.44).abs() < 1e-9,
4871            "sphere latitude must not wrap; 1.2²=1.44, got {dsq_lat}"
4872        );
4873        // Must agree with the certified-encode metric (single source of truth).
4874        let dc = latent_coordinate_distance(&atom, a.view(), b.view());
4875        assert!(
4876            (dc * dc - dsq).abs() < 1e-9,
4877            "coord_dist_sq must equal latent_coordinate_distance² ({} vs {dsq})",
4878            dc * dc
4879        );
4880    }
4881
4882    /// No-regression: periodic / torus axes still wrap at unit period.
4883    #[test]
4884    fn coord_dist_sq_torus_still_wraps_unit_period() {
4885        let atom = tiny_atom(SaeAtomBasisKind::Torus, 1);
4886        let a = Array1::from(vec![0.02f64]);
4887        let b = Array1::from(vec![0.98f64]);
4888        // Wrapped circle distance min(0.96, 0.04) = 0.04 ⇒ dist² = 0.0016.
4889        let dsq = coord_dist_sq(&atom, a.view(), b.view());
4890        assert!(
4891            (dsq - 0.0016).abs() < 1e-12,
4892            "torus unit-period wrap; got {dsq}"
4893        );
4894    }
4895
4896    /// BUG (sphere chart tiling): `chart_nominal_radius` for the sphere must use
4897    /// the SAME capped per-axis count `SHAPE_BAND_MAX_POINTS.isqrt()` as
4898    /// `sphere_latlon_grid`, so the radius covers the (capped) longitude
4899    /// half-spacing `π/r_cap` and the charts tile without gaps for
4900    /// `resolution > r_cap` (raw π/40 would leave gaps). `r_cap` is derived from
4901    /// the band-point budget (#2071), so this test tracks it rather than pinning 22.
4902    #[test]
4903    fn chart_nominal_radius_sphere_covers_capped_grid_spacing() {
4904        let atom = tiny_atom(SaeAtomBasisKind::Sphere, 2);
4905        let r_cap = SHAPE_BAND_MAX_POINTS.isqrt(); // 22 at the current 512-point budget
4906        let resolution = r_cap * 2; // any resolution past the cap exercises the gap hazard
4907        let lon_half_spacing = std::f64::consts::PI / r_cap as f64;
4908        let r = chart_nominal_radius(&atom, resolution);
4909        assert!(
4910            r >= lon_half_spacing - 1e-12,
4911            "sphere radius {r} must cover the capped lon half-spacing {lon_half_spacing} \
4912             (no gaps for resolution>r_cap); raw π/{resolution}={} would gap",
4913            std::f64::consts::PI / resolution as f64
4914        );
4915    }
4916
4917    // ---------------------------------------------------------------------
4918    // F1: amplitude-aware chart routing.
4919    // ---------------------------------------------------------------------
4920
4921    /// A single certifiable chart with the given amplitude-1 center reconstruction.
4922    fn chart_with_recon(recon: Vec<f64>) -> CertifiedChart {
4923        CertifiedChart {
4924            region: ChartRegion::new(Array1::zeros(1), 1.0),
4925            lipschitz: 1.0,
4926            beta_center: 1.0,
4927            certified_radius: 1.0,
4928            amortized_jacobian: None,
4929            recon_center: Array1::from(recon),
4930            amortized_base: None,
4931        }
4932    }
4933
4934    fn atlas_two_charts(m1: f64, m2: f64) -> AtomEncodeAtlas {
4935        AtomEncodeAtlas {
4936            atom_index: 0,
4937            latent_dim: 1,
4938            decoder_norm_sum: 1.0,
4939            charts: vec![chart_with_recon(vec![m1]), chart_with_recon(vec![m2])],
4940        }
4941    }
4942
4943    /// F1 counterexample (module review): chart centers reconstruct (amplitude 1)
4944    /// to `m₁ = 1` and `m₂ = 10`. A row `x = 1` at amplitude `z = 0.1`
4945    /// reconstructs as `z·m`, so chart 2 is EXACT (`0.1·10 = 1 = x`) and chart 1 is
4946    /// wrong (`0.1·1 = 0.1`, error `0.9`). Amplitude-blind routing on the
4947    /// amplitude-1 centers picks chart 1 (`|1−1| = 0 < |10−1| = 9`); the
4948    /// amplitude-aware fix picks chart 2.
4949    #[test]
4950    fn f1_routing_scores_amplitude_scaled_reconstruction() {
4951        let atlas = atlas_two_charts(1.0, 10.0);
4952        let x = Array1::from(vec![1.0]);
4953
4954        let (idx, _) = nearest_chart(&atlas, x.view(), 0.1).expect("routes");
4955        assert_eq!(
4956            idx, 1,
4957            "z=0.1: must route to the m=10 chart (z·m=1 exact), not m=1"
4958        );
4959
4960        let ranked = nearest_charts_topk(&atlas, x.view(), 0.1, 2);
4961        assert_eq!(ranked[0], 1, "z=0.1: nearest chart is the m=10 chart");
4962
4963        // Negative amplitude: z=-0.1 makes z·m₂ = -1 (error 2) and z·m₁ = -0.1
4964        // (error 1.1), so chart 1 is now nearer — the sign is respected.
4965        let (idxn, _) = nearest_chart(&atlas, x.view(), -0.1).expect("routes");
4966        assert_eq!(idxn, 0, "z=-0.1: sign-aware routing prefers the m=1 chart");
4967
4968        // No-regression at z=1: recovers the amplitude-1 argmin (m=1 chart exact).
4969        let (idx1, _) = nearest_chart(&atlas, x.view(), 1.0).expect("routes");
4970        assert_eq!(idx1, 0, "z=1 recovers the amplitude-1 nearest (m=1) chart");
4971        assert_eq!(nearest_charts_topk(&atlas, x.view(), 1.0, 1)[0], 0);
4972    }
4973
4974    // ---------------------------------------------------------------------
4975    // F2: the certificate uses the TRUE (un-ridged) Hessian.
4976    // ---------------------------------------------------------------------
4977
4978    /// A basis with constant `Φ`, zero Jacobian, and zero second jet: the
4979    /// reconstruction is locally CONSTANT, so the true encode Hessian is `0`
4980    /// (singular) and the coordinate is non-unique.
4981    #[derive(Debug)]
4982    struct ConstantPhi {
4983        m: usize,
4984        d: usize,
4985    }
4986    impl SaeBasisEvaluator for ConstantPhi {
4987        fn evaluate(
4988            &self,
4989            coords: ndarray::ArrayView2<'_, f64>,
4990        ) -> Result<(Array2<f64>, Array3<f64>), String> {
4991            let n = coords.nrows();
4992            Ok((
4993                Array2::ones((n, self.m)),
4994                Array3::zeros((n, self.m, self.d)),
4995            ))
4996        }
4997        fn second_jet_dyn(
4998            &self,
4999            coords: ndarray::ArrayView2<'_, f64>,
5000        ) -> Option<Result<ndarray::Array4<f64>, String>> {
5001            let n = coords.nrows();
5002            Some(Ok(ndarray::Array4::zeros((n, self.m, self.d, self.d))))
5003        }
5004        fn third_jet_dyn(
5005            &self,
5006            coords: ndarray::ArrayView2<'_, f64>,
5007        ) -> Option<Result<ndarray::Array5<f64>, String>> {
5008            if coords.ncols() != self.d {
5009                return Some(Err(format!(
5010                    "ConstantPhi::third_jet_dyn: expected d = {}, got {} coords",
5011                    self.d,
5012                    coords.ncols()
5013                )));
5014            }
5015            None
5016        }
5017    }
5018
5019    /// F2: a locally-constant reconstruction has a SINGULAR true Hessian (`H = 0`),
5020    /// so the point is NOT a genuine isolated minimum and must NOT be certified.
5021    /// The old code ridged the certified Hessian (`H → ridge·I`, PD), faking
5022    /// `β = 1/ridge`, `η = 0`, `h = 0 ≤ ½` — a FALSE certificate. With the true
5023    /// Hessian (`encode_grad_hess` no longer adds ridge) `beta_eta_newton` sees
5024    /// `λ_min = 0` and refuses.
5025    #[test]
5026    fn f2_certificate_uses_true_hessian_refuses_singular_field() {
5027        let atom = tiny_atom(SaeAtomBasisKind::EuclideanPatch, 1);
5028        let eval = ConstantPhi {
5029            m: atom.basis_size(),
5030            d: 1,
5031        };
5032        let t0 = Array1::from(vec![0.0]);
5033        let x = Array1::from(vec![0.5]);
5034
5035        let (g, h) = encode_grad_hess(&atom, &eval, t0.view(), x.view(), 1.0)
5036            .expect("encode_grad_hess runs")
5037            .expect("second jet present ⇒ Some");
5038        assert!(
5039            h.iter().all(|&v| v == 0.0),
5040            "the TRUE Hessian of a constant reconstruction is 0 — no ridge is added \
5041             to the certified field; got {h:?}"
5042        );
5043        assert!(
5044            g.iter().all(|&v| v == 0.0),
5045            "gradient is 0 at a flat reconstruction"
5046        );
5047
5048        let (cert, _) = row_certificate(&atom, &eval, t0.view(), x.view(), 1.0, 1.0)
5049            .expect("row_certificate runs");
5050        assert!(
5051            !cert.certified(),
5052            "a singular true Hessian must NOT be certified (the old ridged H falsely did)"
5053        );
5054        assert!(
5055            !cert.beta.is_finite(),
5056            "β must be ∞ (uncertifiable), never the ridge-faked 1/ridge; got {}",
5057            cert.beta
5058        );
5059    }
5060
5061    // ---------------------------------------------------------------------
5062    // F3: Duchon atoms are refused (honest refusal, never a fabricated bound).
5063    // ---------------------------------------------------------------------
5064
5065    /// F3: `build_atom_atlas_from_centers` must emit ONLY uncertified charts for a
5066    /// Duchon atom — the closed-form bound available here would fabricate cubic-r³
5067    /// jets and an origin center for a polyharmonic `c·r^(2m−d)` kernel over
5068    /// data-placed centers, risking an under-estimate of `L` (false certificate).
5069    /// The refusal routes every Duchon row to the exact multi-start encode. A
5070    /// non-Duchon control atom is NOT auto-zeroed by this guard.
5071    #[test]
5072    fn f3_duchon_atoms_are_uncertifiable() {
5073        let atom = tiny_atom(SaeAtomBasisKind::Duchon, 1);
5074        let centers = ndarray::array![[0.0_f64], [0.3], [0.7]];
5075        let radii = vec![0.1_f64, 0.1, 0.1];
5076        let atlas = EncodeAtlas::build_atom_atlas_from_centers(
5077            0,
5078            &atom,
5079            centers.view(),
5080            &radii,
5081            1.0,
5082            1.0,
5083            &AtlasConfig::default(),
5084        )
5085        .expect("duchon atlas builds (uncertified)");
5086        assert_eq!(atlas.charts.len(), 3, "one chart per center");
5087        for (i, chart) in atlas.charts.iter().enumerate() {
5088            assert_eq!(
5089                chart.certified_radius, 0.0,
5090                "duchon chart {i} must be uncertified (refused), got r={}",
5091                chart.certified_radius
5092            );
5093            assert!(
5094                chart.amortized_jacobian.is_none(),
5095                "duchon chart {i} must carry no amortized predictor"
5096            );
5097        }
5098    }
5099
5100    // ---------------------------------------------------------------------
5101    // F6: an already-certified warm-up step is never rejected by the progress rule.
5102    // ---------------------------------------------------------------------
5103
5104    /// F6: a step that just crossed into the certified region (`h ≤ ½`) is accepted
5105    /// even when its multiplicative decrease was below the progress floor
5106    /// (`0.501 → 0.499`). Only a STILL-uncertified plateau step is rejected. The
5107    /// old unconditional progress test rejected the `0.501 → 0.499` cross — a false
5108    /// negative that forced an unnecessary exact-solve fallback.
5109    #[test]
5110    fn f6_certified_step_not_rejected_by_progress_rule() {
5111        // 0.501 → 0.499: below the 1/64 multiplicative floor, and NOT the quadratic
5112        // path, so `warmup_progress_sufficient` is false…
5113        assert!(
5114            !warmup_progress_sufficient(0.499, 0.501),
5115            "precondition: this tiny decrease fails the progress bar"
5116        );
5117        // …but because the step is now CERTIFIED it must NOT be rejected (F6).
5118        assert!(
5119            !warmup_should_reject(true, 0.499, 0.501),
5120            "a certified step must be accepted regardless of the progress floor"
5121        );
5122        // A still-uncertified plateau step IS rejected (the guard still bites).
5123        assert!(
5124            warmup_should_reject(false, 0.499, 0.501),
5125            "an uncertified sub-floor step is a plateau and must flag to fallback"
5126        );
5127        // A certified step that ALSO made big progress is accepted (sanity).
5128        assert!(!warmup_should_reject(true, 0.1, 0.9));
5129        // A genuinely-contracting uncertified step is accepted (no regression).
5130        assert!(!warmup_should_reject(false, 0.4, 0.9));
5131    }
5132
5133    // A singular Hessian is not invertible as the derivative F'(t). Replacing
5134    // its null eigenvalue by an arbitrary stiffness describes a different map,
5135    // so it must never produce an ordinary Kantorovich certificate.
5136    #[test]
5137    fn beta_eta_newton_refuses_rank1_null_2x2() {
5138        let h = ndarray::array![[4.0_f64, 0.0], [0.0, 0.0]];
5139        let g = Array1::from(vec![4.0_f64, 0.0]);
5140        assert!(beta_eta_newton(h.view(), g.view()).expect("runs").is_none());
5141    }
5142
5143    /// Refusal is independent of whether the observed gradient happens to have a
5144    /// small projection onto the null direction: that observation cannot turn a
5145    /// singular derivative into the derivative required by the theorem.
5146    #[test]
5147    fn beta_eta_newton_refuses_null_with_projected_gradient() {
5148        let h = ndarray::array![[4.0_f64, 0.0], [0.0, 0.0]];
5149        let g = Array1::from(vec![4.0_f64, 1.0e-3]);
5150        assert!(beta_eta_newton(h.view(), g.view()).expect("runs").is_none());
5151    }
5152
5153    /// A genuinely INDEFINITE 2×2 (one negative eigenvalue) is STILL refused —
5154    /// deflation must not manufacture a certificate at a saddle/max of the encode
5155    /// objective (the pre-existing negative-curvature guard is preserved).
5156    #[test]
5157    fn beta_eta_newton_refuses_genuine_negative_curvature_2x2() {
5158        let h = ndarray::array![[4.0_f64, 0.0], [0.0, -2.0]];
5159        let g = Array1::from(vec![1.0_f64, 1.0]);
5160        let out = beta_eta_newton(h.view(), g.view()).expect("runs");
5161        assert!(
5162            out.is_none(),
5163            "a negative-curvature (indefinite) start must NOT certify — it is at/past a \
5164             basin boundary; deflating it to +1 would be a false certificate"
5165        );
5166    }
5167
5168    /// The general eigen path applies the same refusal to a rank-deficient PSD
5169    /// block instead of silently changing its spectrum.
5170    #[test]
5171    fn beta_eta_newton_refuses_rank_deficient_3x3() {
5172        let h = ndarray::array![[4.0_f64, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 0.0]];
5173        let g = Array1::from(vec![4.0_f64, 1.0, 0.0]);
5174        assert!(beta_eta_newton(h.view(), g.view()).expect("runs").is_none());
5175    }
5176
5177    /// A healthy positive-definite 2×2 is UNCHANGED by the fix: it keeps the
5178    /// closed-form fast path, β = 1/λ_min, and the Newton step solves H·δ = −g.
5179    #[test]
5180    fn beta_eta_newton_healthy_pd_unchanged() {
5181        let h = ndarray::array![[4.0_f64, 1.0], [1.0, 3.0]];
5182        let g = Array1::from(vec![2.0_f64, -1.0]);
5183        let (beta, eta, delta) = beta_eta_newton(h.view(), g.view())
5184            .expect("runs")
5185            .expect("a PD block certifies");
5186        // λ_min = ½(7 − √5); β = 1/λ_min.
5187        let lambda_min = 0.5 * (7.0 - 5.0_f64.sqrt());
5188        assert!((beta - 1.0 / lambda_min).abs() < 1e-9, "β={beta}");
5189        // Newton normal equations: H·δ = −g.
5190        let hd0 = h[[0, 0]] * delta[0] + h[[0, 1]] * delta[1];
5191        let hd1 = h[[1, 0]] * delta[0] + h[[1, 1]] * delta[1];
5192        assert!((hd0 + g[0]).abs() < 1e-9 && (hd1 + g[1]).abs() < 1e-9);
5193        assert!((eta - delta.dot(&delta).sqrt()).abs() < 1e-12);
5194    }
5195}
5196
5197#[cfg(test)]
5198mod joint_fallback_tests {
5199    //! Reviewer condition #3 — the multi-start-fallback fraction is an honest
5200    //! encode-tax number that GROWS with atom-image similarity / co-activation
5201    //! interference. These tests drive `joint_encode_fallback_fraction` over a
5202    //! similarity sweep on flat (linear) atoms, where the Gauss–Newton curvature
5203    //! block is EXACT (no residual-curvature term), so the Gershgorin dominance
5204    //! decision is the true joint certificate and the curve is analytic.
5205    use super::*;
5206    use crate::manifold::SaeAtomBasisKind;
5207    use ndarray::Array2;
5208    use std::sync::Arc;
5209
5210    /// Deterministic LCG (no `rand` dependency) for reproducible amplitudes.
5211    struct Lcg(u64);
5212    impl Lcg {
5213        fn unit(&mut self) -> f64 {
5214            self.0 = self.0.wrapping_mul(6364136223846793005).wrapping_add(1);
5215            ((self.0 >> 11) as f64) / ((1u64 << 53) as f64)
5216        }
5217    }
5218
5219    /// Build `k` degree-1 (flat) atoms in `R^p` whose image TANGENT directions
5220    /// have common pairwise cosine `rho`: `dir_k = √rho · e0 + √(1−rho) · e_{k+1}`
5221    /// with `{e0, e1, …}` orthonormal. Requires `p ≥ k + 1`. Each atom's decoder
5222    /// puts the tangent on the degree-1 monomial row, so `∂m/∂t = dir_k` exactly.
5223    fn similar_linear_atoms(k: usize, p: usize, rho: f64) -> Vec<SaeManifoldAtom> {
5224        assert!(p >= k + 1, "need p >= k+1 orthonormal directions");
5225        let evaluator = Arc::new(EuclideanPatchEvaluator::new(1, 1).expect("degree-1 patch"));
5226        // A single coordinate row is enough to pin the (constant) tangent; the
5227        // diagnostic re-evaluates the jet at whatever coords it is given.
5228        let coord = Array2::<f64>::zeros((1, 1));
5229        let (phi, jet) = evaluator.evaluate(coord.view()).expect("evaluate");
5230        let m = phi.ncols();
5231        (0..k)
5232            .map(|atom_idx| {
5233                let mut dec = Array2::<f64>::zeros((m, p));
5234                // Row 1 is the degree-1 monomial (`∂φ/∂t = 1` there); place the
5235                // unit tangent direction on it.
5236                dec[[1, 0]] = rho.sqrt();
5237                dec[[1, atom_idx + 1]] = (1.0 - rho).sqrt();
5238                SaeManifoldAtom::new_with_provided_function_gram(
5239                    "lin",
5240                    SaeAtomBasisKind::EuclideanPatch,
5241                    1,
5242                    phi.clone(),
5243                    jet.clone(),
5244                    dec,
5245                    Array2::<f64>::eye(m),
5246                )
5247                .expect("atom builds")
5248                .with_basis_second_jet(evaluator.clone())
5249            })
5250            .collect()
5251    }
5252
5253    /// The joint multi-start-fallback fraction is MONOTONE NON-DECREASING in
5254    /// atom-image similarity, is exactly zero when the atoms are orthogonal, and
5255    /// is strictly positive once the images are strongly aligned — the honest
5256    /// encode-tax cost multiplier the reviewer asks for.
5257    #[test]
5258    fn joint_fallback_fraction_rises_with_atom_similarity() {
5259        let k = 4usize;
5260        let p = 8usize;
5261        let n = 400usize;
5262        // Per-row amplitudes: a spread of masses so that at intermediate
5263        // similarity SOME rows (those with an atom whose mass is dominated by its
5264        // co-active neighbours) tip out of Gershgorin dominance while others stay
5265        // in — a smooth curve rather than a step.
5266        let mut rng = Lcg(42);
5267        let amplitudes = Array2::from_shape_fn((n, k), |_| 0.2 + 1.3 * rng.unit());
5268        // Coordinates are irrelevant for flat atoms (constant tangent), but the
5269        // diagnostic still evaluates the jet at them.
5270        let coords: Vec<Array2<f64>> = (0..k).map(|_| Array2::<f64>::zeros((n, 1))).collect();
5271        let floor = 1.0e-9;
5272
5273        let sweep = [0.0_f64, 0.2, 0.4, 0.6, 0.8, 0.95];
5274        let mut fractions = Vec::new();
5275        for &rho in &sweep {
5276            let atoms = similar_linear_atoms(k, p, rho);
5277            let frac = joint_encode_fallback_fraction(&atoms, &coords, amplitudes.view(), floor)
5278                .expect("joint fallback fraction computes");
5279            assert!(
5280                (0.0..=1.0).contains(&frac),
5281                "fallback fraction must be a probability, got {frac} at rho={rho}"
5282            );
5283            fractions.push(frac);
5284        }
5285        eprintln!(
5286            "[ENCODE-FALLBACK-SWEEP] similarity rho={:?} -> joint multistart fraction={:?}",
5287            sweep, fractions
5288        );
5289        // Orthogonal atoms: the block-diagonal per-atom certificates compose, no
5290        // row needs multi-start.
5291        assert!(
5292            fractions[0] == 0.0,
5293            "orthogonal atoms must need no multi-start fallback, got {}",
5294            fractions[0]
5295        );
5296        // Monotone non-decreasing across the similarity sweep.
5297        for w in fractions.windows(2) {
5298            assert!(
5299                w[1] >= w[0] - 1.0e-12,
5300                "fallback fraction must not DECREASE as similarity rises: {:?}",
5301                fractions
5302            );
5303        }
5304        // Strongly-aligned images force a materially higher fallback fraction —
5305        // the effect is real, not a rounding wobble.
5306        assert!(
5307            *fractions.last().unwrap() > 0.25,
5308            "strong atom similarity must drive a substantial multi-start tail; curve={fractions:?}"
5309        );
5310    }
5311
5312    /// A single co-active atom per row has no cross blocks, so the joint fallback
5313    /// fraction is zero regardless of how curved or ill-conditioned the atom is —
5314    /// the joint tax is purely a CO-ACTIVATION phenomenon.
5315    #[test]
5316    fn joint_fallback_zero_without_coactivation() {
5317        let k = 3usize;
5318        let p = 8usize;
5319        let n = 50usize;
5320        let atoms = similar_linear_atoms(k, p, 0.9); // highly similar, but…
5321        // …only ONE atom active per row (block-diagonal one-hot amplitudes).
5322        let mut amplitudes = Array2::<f64>::zeros((n, k));
5323        for row in 0..n {
5324            amplitudes[[row, row % k]] = 1.0;
5325        }
5326        let coords: Vec<Array2<f64>> = (0..k).map(|_| Array2::<f64>::zeros((n, 1))).collect();
5327        let frac = joint_encode_fallback_fraction(&atoms, &coords, amplitudes.view(), 1.0e-9)
5328            .expect("computes");
5329        assert!(
5330            frac == 0.0,
5331            "no co-activation ⇒ no joint fallback, got {frac}"
5332        );
5333    }
5334
5335    /// The per-atom fallback tiers PARTITION the (row, atom) grid and the tier
5336    /// fractions are consistent probabilities; `accumulate` folds per-atom
5337    /// telemetry into a dictionary-wide breakdown that preserves the partition.
5338    #[test]
5339    fn fallback_telemetry_tiers_partition_and_accumulate() {
5340        let a = FallbackTelemetry {
5341            n_rows: 100,
5342            n_atoms: 1,
5343            amortized_certified: 70,
5344            newton_rescued: 20,
5345            multistart_fallback: 10,
5346        };
5347        assert_eq!(
5348            a.amortized_certified + a.newton_rescued + a.multistart_fallback,
5349            a.total(),
5350            "the three tiers must partition the (row, atom) grid"
5351        );
5352        assert!((a.amortized_fraction() - 0.70).abs() < 1e-12);
5353        assert!((a.newton_fraction() - 0.20).abs() < 1e-12);
5354        assert!((a.multistart_fraction() - 0.10).abs() < 1e-12);
5355        assert!(
5356            (a.amortized_fraction() + a.newton_fraction() + a.multistart_fraction() - 1.0).abs()
5357                < 1e-12,
5358            "the tier fractions must sum to one"
5359        );
5360
5361        let b = FallbackTelemetry {
5362            n_rows: 100,
5363            n_atoms: 1,
5364            amortized_certified: 40,
5365            newton_rescued: 30,
5366            multistart_fallback: 30,
5367        };
5368        let mut agg = a.clone();
5369        agg.accumulate(&b);
5370        assert_eq!(agg.n_rows, 100, "n_rows is shared across atoms");
5371        assert_eq!(agg.n_atoms, 2, "accumulate sums the atom count");
5372        assert_eq!(agg.multistart_fallback, 40);
5373        assert_eq!(
5374            agg.amortized_certified + agg.newton_rescued + agg.multistart_fallback,
5375            agg.total(),
5376            "the partition is preserved under accumulation"
5377        );
5378    }
5379}