Skip to main content

gam_sae/
chart_canonicalization.rs

1//! #1019 stage 1 — arc-length (unit-speed) chart canonicalization for `d = 1`
2//! manifold atoms (circle and interval topologies).
3//!
4//! A fitted atom's latent chart is gauge-arbitrary: the gauge-invariant
5//! (intrinsic) smoothness penalty makes every reparameterization of the latent
6//! coordinate equal-cost BY DESIGN, so nothing in the likelihood prefers the
7//! honest chart and reconstruction metrics cannot detect chart dishonesty
8//! (the planted-circle failure that motivated #1019 compressed the full loop
9//! into ~1 rad of coordinate span at reconstruction EV 0.9979 — image
10//! perfect, chart arbitrary).
11//!
12//! For `d = 1` the canonical representative of the `Diff(S¹)` /
13//! `Diff([0, 1])` orbit is mathematically solved: the **arc-length
14//! reparameterization**. Post-fit and image-frozen, compute the cumulative
15//! arc length `s(t) = ∫_lo^t ‖γ'(u)‖ du` along the fitted decoder curve
16//! `γ(t) = Φ(t) B`, normalize by the total length `L` to the chart's native
17//! span (the basis period for a circle, the unit interval for a line
18//! segment), and reparameterize: new coordinates `t̃_i = s(t_i)`, new decoder
19//! `B̃` refit by exact least squares of the ORIGINAL decoded curve on a fine
20//! grid against the basis at the new coordinates. The refit is linear and
21//! exact up to basis expressiveness; the recomposition residual is recorded
22//! and the canonicalization is REFUSED when it exceeds a small tolerance
23//! relative to the curve scale — an honest fallback, never a lossy silent
24//! swap.
25//!
26//! After canonicalization the atom's residual chart freedom downgrades from
27//! the full diffeomorphism group to the finite isometry group of the
28//! reference manifold: rotation + reflection (`O(2)`) for the circle,
29//! reflection + translation for the interval. The certificate records this
30//! with the `PinnedByCanonicalization` provenance
31//! ([`crate::identifiability::VerdictProvenance`]).
32//!
33//! #1019 stage 2 (`d = 2`, torus): the canonical representative of the
34//! `Diff(T²)` orbit is the **minimum-isometry-defect flow** chart. The chart
35//! map is parameterized as `φ_θ(t) = t + Σ_k θ_k v_k(t)` with `v_k` a fixed
36//! truncated Fourier vector-field basis on `T²` (orders ≤ 2 per axis, both
37//! components — a few tens of coefficients, wrap-around respected by
38//! construction), and `θ` minimizes the discretized isometry defect over the
39//! fitted rows with an exact analytic Gauss–Newton (see
40//! [`torus_isometry_flow_reparameterization`] for the full derivation). A
41//! hard diffeomorphism guard `det Dφ_θ > δ` on a check grid means a folded
42//! chart is REFUSED, never produced. The decoder transport is the same
43//! exact-LS recomposition — and the same honesty gate — as the `d = 1` path
44//! (shared helper [`recompose_decoder_exact_ls`]).
45//!
46//! #1019 free-chart arm (`d = 2`, free/patch): a contractible Euclidean-patch
47//! atom (Duchon / EuclideanPatch basis kind) admits a **global** truncated
48//! flow basis — polynomial vector fields `v_{c,(a,b)}(t) = e_c · u₀^a u₁^b`
49//! on the normalized patch box ([`FreePatchFlowBasis`]) — with no hairy-ball
50//! obstruction (a contractible patch carries nowhere-vanishing vector fields).
51//! So unlike the sphere it is genuinely MINIMIZED, not merely measured: the
52//! isometry defect is descended against the flat reference `g_ref = I`
53//! ([`patch_isometry_flow_reparameterization`]), pinning the uniform-speed
54//! (minimum-anisotropy) chart, with the residual chart freedom downgraded to
55//! the flat isometry group `O(2) ⋉ ℝ²` and provenance
56//! `PinnedByCanonicalization`. The torus, free-patch, and sphere arms share one
57//! pullback-metric extraction ([`extract_pullback_metric_d2`]) and the two
58//! flow-pinned arms share one exact Gauss–Newton core
59//! ([`minimize_isometry_defect_flow`]).
60//!
61//! `S²` (sphere atoms): the hairy-ball theorem rules out a single global
62//! pole-free flow basis the way the torus and free-patch paths use, so the
63//! sphere representative is the **round-sphere conformal-boost flow** — the
64//! gradients of the three degree-1 harmonics (`K_z` zonal, pole-free in its one
65//! latitude component; `K_x`, `K_y` carrying the longitudinal pole the theorem
66//! forces somewhere). Minimizing the isometry defect over these three boosts
67//! breaks the conformal (Möbius) moduli down to the round sphere's isometry
68//! group `O(3)` — the chart pathology a bare harmonic energy cannot pin
69//! ([`sphere_isometry_flow_reparameterization`], on a pole-margin band so the
70//! `1/cos lat` boost stays well-conditioned). The same exact-image-frozen LS
71//! decoder transport gates the commit: a boosted image only freezes to the
72//! recomposition floor inside a basis rich enough to absorb it, so a too-poor
73//! basis honestly refuses rather than silently altering the image. The
74//! round-sphere isometry DEFECT remains available as a standalone measurement
75//! ([`sphere_chart_isometry_defect`]).
76
77use faer::Side as FaerSide;
78use ndarray::{Array1, Array2, ArrayView1, ArrayView2};
79
80use gam_linalg::faer_ndarray::{FaerCholesky, fast_ab, fast_ata, fast_atb};
81use crate::manifold::{SaeBasisEvaluator, solve_design_least_squares};
82
83/// Number of integration cells for the fitted-turning quadrature (#1026).
84const TURNING_QUADRATURE_CELLS: usize = 256;
85
86/// Number of grid CELLS for the arc-length quadrature and the decoder
87/// recomposition least squares. Each cell carries a node, a midpoint, and the
88/// next node (composite Simpson), so the speed field is sampled at
89/// `2·ARC_LENGTH_GRID_CELLS + 1` points; the per-cell quadrature error is
90/// `O(Δu⁴)`, far below the recomposition tolerance.
91pub const ARC_LENGTH_GRID_CELLS: usize = 2048;
92
93/// Relative image-recomposition tolerance: the canonicalization is refused
94/// (honest fallback to the fitted chart) when the max-abs difference between
95/// the original decoded curve and its recomposition through the new chart
96/// exceeds this fraction of the curve scale — on the audit grid OR on the
97/// fitted rows. Matched to the image-invariance contract the certificate and
98/// the tests assert (reconstruction unchanged within 1e-8).
99pub const CHART_RECOMPOSITION_REL_TOL: f64 = 1.0e-9;
100
101/// The `d = 1` reference topology the canonical chart lives on.
102#[derive(Debug, Clone, PartialEq)]
103pub enum CanonicalChartTopology {
104    /// `S¹` with the basis evaluator's native period (`1.0` for the
105    /// fraction-of-period harmonic evaluators). Arc length is accumulated
106    /// over one full period and rescaled so the canonical chart spans
107    /// exactly one period; the residual chart freedom is `O(2)` (base-point
108    /// rotation + orientation reflection).
109    Circle { period: f64 },
110    /// A line-segment chart. Arc length is accumulated over the fitted
111    /// coordinate range and rescaled to `[0, 1]`; the residual chart freedom
112    /// is reflection + translation.
113    Interval,
114}
115
116/// The exact, image-frozen arc-length reparameterization of one `d = 1` atom.
117#[derive(Debug, Clone)]
118pub struct UnitSpeedReparameterization {
119    /// Canonical per-row coordinates `t̃_i = span · s(t_i) / L`, length `n`.
120    pub new_row_coords: Array1<f64>,
121    /// Recomposed decoder coefficients `B̃ = T · B`, shape `(m, p)`: the exact
122    /// least-squares refit of the original decoded curve (on the audit grid)
123    /// against the basis at the canonical coordinates.
124    pub new_decoder: Array2<f64>,
125    /// The `(m, m)` basis transport `T` with `Φ(t̃) · T ≈ Φ(t)` on the grid —
126    /// the same object the affine gauge canonicalization uses to transport
127    /// the smoothness Gram (`S̃ = T⁻ᵀ S T⁻¹` preserves `B̃ᵀ S̃ B̃ = Bᵀ S B`).
128    pub decoder_transport: Array2<f64>,
129    /// Total arc length `L` of the decoder curve over the canonical domain.
130    pub total_arc_length: f64,
131    /// Max-abs recomposition error on the audit grid, relative to the curve
132    /// scale. Always `≤ CHART_RECOMPOSITION_REL_TOL` when `Some(..)` is
133    /// returned.
134    pub recomposition_residual: f64,
135}
136
137/// Decoder-curve speed `‖Φ'(u) B‖₂` for each evaluated coordinate row, from
138/// the basis jet `(rows, m, 1)` and the decoder `(m, p)`.
139fn curve_speeds(
140    jet: &ndarray::Array3<f64>,
141    decoder: ArrayView2<'_, f64>,
142) -> Result<Vec<f64>, String> {
143    let (rows, m, d) = jet.dim();
144    if d != 1 {
145        return Err(format!(
146            "sae_chart_canonicalization: expected a 1-D latent jet, got latent_dim {d}"
147        ));
148    }
149    if decoder.nrows() != m {
150        return Err(format!(
151            "sae_chart_canonicalization: jet basis width {m} != decoder rows {}",
152            decoder.nrows()
153        ));
154    }
155    let p = decoder.ncols();
156    let mut speeds = Vec::with_capacity(rows);
157    let mut tangent = vec![0.0_f64; p];
158    for row in 0..rows {
159        for slot in tangent.iter_mut() {
160            *slot = 0.0;
161        }
162        for bm in 0..m {
163            let dphi = jet[[row, bm, 0]];
164            if dphi == 0.0 {
165                continue;
166            }
167            for (j, slot) in tangent.iter_mut().enumerate() {
168                *slot += dphi * decoder[[bm, j]];
169            }
170        }
171        speeds.push(tangent.iter().map(|v| v * v).sum::<f64>().sqrt());
172    }
173    Ok(speeds)
174}
175
176/// Exact integral of the cell-local quadratic speed interpolant (through the
177/// node, midpoint, and next-node speeds) over `[0, x]`, `x ∈ [0, h]`. At
178/// `x = h` this is exactly the Simpson cell weight `h(f0 + 4fm + f1)/6`.
179fn partial_cell_arc(f0: f64, fm: f64, f1: f64, h: f64, x: f64) -> f64 {
180    if h <= 0.0 {
181        return 0.0;
182    }
183    let a = (2.0 * f0 - 4.0 * fm + 2.0 * f1) / (h * h);
184    let b = (-3.0 * f0 + 4.0 * fm - f1) / h;
185    let x2 = x * x;
186    a * x2 * x / 3.0 + b * x2 / 2.0 + f0 * x
187}
188
189/// Compute the arc-length (unit-speed) reparameterization of a fitted `d = 1`
190/// atom: the canonical per-row coordinates and the exactly-recomposed decoder.
191///
192/// Image-frozen: the decoder curve is never refit against data — only
193/// re-expressed in the canonical chart. Returns `Ok(None)` (honest skip,
194/// never a lossy swap) when:
195/// * the chart is degenerate (no rows, empty basis, zero/non-finite total
196///   arc length, collapsed interval range), or
197/// * the basis family cannot absorb the reparameterized curve within
198///   [`CHART_RECOMPOSITION_REL_TOL`] of the curve scale on the audit grid.
199pub fn unit_speed_reparameterization(
200    evaluator: &dyn SaeBasisEvaluator,
201    decoder: ArrayView2<'_, f64>,
202    row_coords: ArrayView1<'_, f64>,
203    topology: &CanonicalChartTopology,
204) -> Result<Option<UnitSpeedReparameterization>, String> {
205    let n = row_coords.len();
206    let m = decoder.nrows();
207    let p = decoder.ncols();
208    if n == 0 || m == 0 || p == 0 {
209        return Ok(None);
210    }
211    for &t in row_coords.iter() {
212        if !t.is_finite() {
213            return Ok(None);
214        }
215    }
216
217    // ── Canonical quadrature domain `[lo, hi]` and target span ──────────────
218    let (lo, hi, span) = match topology {
219        CanonicalChartTopology::Circle { period } => {
220            if !(period.is_finite() && *period > 0.0) {
221                return Err(format!(
222                    "sae_chart_canonicalization: circle period must be finite and positive; got {period}"
223                ));
224            }
225            // The decoder curve is defined over the WHOLE period (the chart
226            // dishonesty being cured is exactly rows compressed into a sliver
227            // of it), so the arc length integrates the full circle.
228            (0.0, *period, *period)
229        }
230        CanonicalChartTopology::Interval => {
231            let mut t_min = f64::INFINITY;
232            let mut t_max = f64::NEG_INFINITY;
233            for &t in row_coords.iter() {
234                t_min = t_min.min(t);
235                t_max = t_max.max(t);
236            }
237            let scale = t_min.abs().max(t_max.abs()).max(1.0);
238            if !(t_max - t_min > 1.0e-12 * scale) {
239                // Collapsed chart: every row at one point — arc length cannot
240                // define a chart there.
241                return Ok(None);
242            }
243            (t_min, t_max, 1.0)
244        }
245    };
246
247    // ── Speed field on the Simpson grid (nodes + midpoints in one call) ─────
248    let cells = ARC_LENGTH_GRID_CELLS;
249    let h = (hi - lo) / cells as f64;
250    let mut quad_coords = Array2::<f64>::zeros((2 * cells + 1, 1));
251    for j in 0..=cells {
252        quad_coords[[2 * j, 0]] = lo + j as f64 * h;
253        if j < cells {
254            quad_coords[[2 * j + 1, 0]] = lo + (j as f64 + 0.5) * h;
255        }
256    }
257    let (grid_phi_all, grid_jet_all) = evaluator.evaluate(quad_coords.view())?;
258    if grid_phi_all.ncols() != m {
259        return Err(format!(
260            "sae_chart_canonicalization: evaluator basis width {} != decoder rows {m}",
261            grid_phi_all.ncols()
262        ));
263    }
264    let speeds = curve_speeds(&grid_jet_all, decoder)?;
265    if speeds.iter().any(|s| !s.is_finite()) {
266        return Ok(None);
267    }
268
269    // ── Composite-Simpson cumulative arc length at the nodes ────────────────
270    let mut cumulative = vec![0.0_f64; cells + 1];
271    for j in 0..cells {
272        let f0 = speeds[2 * j];
273        let fm = speeds[2 * j + 1];
274        let f1 = speeds[2 * j + 2];
275        cumulative[j + 1] = cumulative[j] + h * (f0 + 4.0 * fm + f1) / 6.0;
276    }
277    let total = cumulative[cells];
278    if !(total.is_finite() && total > 0.0) {
279        return Ok(None);
280    }
281    let rescale = span / total;
282
283    // ── The canonical chart map `t ↦ span · s(t) / L` ───────────────────────
284    let map_coord = |t: f64| -> f64 {
285        let local = match topology {
286            CanonicalChartTopology::Circle { period } => (t - lo).rem_euclid(*period),
287            CanonicalChartTopology::Interval => (t - lo).clamp(0.0, hi - lo),
288        };
289        let cell = ((local / h).floor() as usize).min(cells - 1);
290        let x = local - cell as f64 * h;
291        let s = cumulative[cell]
292            + partial_cell_arc(
293                speeds[2 * cell],
294                speeds[2 * cell + 1],
295                speeds[2 * cell + 2],
296                h,
297                x,
298            );
299        let mapped = rescale * s;
300        match topology {
301            CanonicalChartTopology::Circle { period } => mapped.rem_euclid(*period),
302            CanonicalChartTopology::Interval => mapped.clamp(0.0, span),
303        }
304    };
305
306    let new_row_coords = Array1::from_iter(row_coords.iter().map(|&t| map_coord(t)));
307
308    // ── Decoder recomposition: exact LS of the original curve on the grid ───
309    // Audit grid = the quadrature nodes. `Φ_new · T ≈ Φ_old` (row j of Φ_old
310    // is the basis at node u_j, row j of Φ_new is the basis at the node's
311    // canonical image s̃(u_j)), so `B̃ = T · B` reproduces the original curve
312    // values `Φ_old · B` at the canonical coordinates — the image is frozen.
313    let mut node_new_coords = Array2::<f64>::zeros((cells + 1, 1));
314    let mut old_phi = Array2::<f64>::zeros((cells + 1, m));
315    for j in 0..=cells {
316        node_new_coords[[j, 0]] = map_coord(lo + j as f64 * h);
317        for bm in 0..m {
318            old_phi[[j, bm]] = grid_phi_all[[2 * j, bm]];
319        }
320    }
321    let Some(recomposition) =
322        recompose_decoder_exact_ls(evaluator, decoder, old_phi.view(), node_new_coords.view())?
323    else {
324        // The basis family is not expressive enough to carry the
325        // arc-length-reparameterized curve: refuse rather than swap lossily.
326        return Ok(None);
327    };
328
329    Ok(Some(UnitSpeedReparameterization {
330        new_row_coords,
331        new_decoder: recomposition.new_decoder,
332        decoder_transport: recomposition.transport,
333        total_arc_length: total,
334        recomposition_residual: recomposition.recomposition_residual,
335    }))
336}
337
338/// Exact-LS decoder recomposition shared by the `d = 1` (arc-length) and
339/// `d = 2` (torus isometry-flow) canonicalizations.
340///
341/// `old_phi` is the ORIGINAL basis at the audit grid (so `old_phi · B` is the
342/// original decoded image there) and `new_coords` are the same grid points'
343/// canonical images. Solves the basis transport `Φ(new) · T ≈ Φ(old)` by
344/// exact least squares, recomposes `B̃ = T · B` (so the decoded image is
345/// reproduced at the transported coordinates — image-frozen), and applies the
346/// honesty gate: returns `Ok(None)` (refuse, never a lossy silent swap) when
347/// the max-abs image drift on the audit grid exceeds
348/// [`CHART_RECOMPOSITION_REL_TOL`] of the image scale.
349pub(crate) struct DecoderRecomposition {
350    /// `(m, m)` basis transport `T` with `Φ(new) · T ≈ Φ(old)` on the grid.
351    pub transport: Array2<f64>,
352    /// Recomposed decoder `B̃ = T · B`, shape `(m, p)`.
353    pub new_decoder: Array2<f64>,
354    /// Max-abs recomposition error on the audit grid, relative to the image
355    /// scale. Always `≤ CHART_RECOMPOSITION_REL_TOL` when returned.
356    pub recomposition_residual: f64,
357}
358
359pub(crate) fn recompose_decoder_exact_ls(
360    evaluator: &dyn SaeBasisEvaluator,
361    decoder: ArrayView2<'_, f64>,
362    old_phi: ArrayView2<'_, f64>,
363    new_coords: ArrayView2<'_, f64>,
364) -> Result<Option<DecoderRecomposition>, String> {
365    let m = decoder.nrows();
366    let (new_phi, new_jet) = evaluator.evaluate(new_coords)?;
367    if new_phi.ncols() != m
368        || new_phi.nrows() != old_phi.nrows()
369        || new_jet.dim() != (new_coords.nrows(), m, new_coords.ncols())
370    {
371        return Err(format!(
372            "sae_chart_canonicalization: evaluator returned basis {:?} / jet {:?} at the canonical grid; expected ({}, {m}) with latent_dim {}",
373            new_phi.dim(),
374            new_jet.dim(),
375            old_phi.nrows(),
376            new_coords.ncols()
377        ));
378    }
379    let transport = solve_design_least_squares(new_phi.view(), old_phi)?;
380    let new_decoder = fast_ab(&transport, &decoder);
381
382    // ── Honest gate: max-abs recomposition error relative to image scale ────
383    let old_fit = fast_ab(&old_phi, &decoder);
384    let new_fit = fast_ab(&new_phi, &new_decoder);
385    let mut fit_scale = 0.0_f64;
386    let mut max_abs = 0.0_f64;
387    for (a, b) in old_fit.iter().zip(new_fit.iter()) {
388        fit_scale = fit_scale.max(a.abs()).max(b.abs());
389        max_abs = max_abs.max((a - b).abs());
390    }
391    if !(fit_scale.is_finite() && fit_scale > 0.0 && max_abs.is_finite()) {
392        return Ok(None);
393    }
394    let recomposition_residual = max_abs / fit_scale;
395    if recomposition_residual > CHART_RECOMPOSITION_REL_TOL {
396        // The basis family cannot absorb the reparameterized image: refuse.
397        return Ok(None);
398    }
399    Ok(Some(DecoderRecomposition {
400        transport,
401        new_decoder,
402        recomposition_residual,
403    }))
404}
405
406// ════════════════════════════════════════════════════════════════════════════
407// #1019 stage 2 — d = 2 torus isometry-flow chart canonicalization
408// ════════════════════════════════════════════════════════════════════════════
409
410/// Highest Fourier order per axis of the truncated flow basis on `T²`:
411/// frequency vectors `(a, b)` with `|a|, |b| ≤ 2`. One representative per
412/// antipodal pair `±(a, b)` (12 of them), `sin` + `cos` phases, both vector
413/// components ⇒ `2 · 2 · 12 = 48` flow coefficients — the "dim ~ tens"
414/// unconstrained smooth problem of #1019 stage 2. Constants (the pure torus
415/// translations) are EXCLUDED on purpose: translations are exact isometries
416/// of `(T², g_ref)`, so they leave the defect invariant and would only insert
417/// null directions into the Gauss–Newton system.
418pub const TORUS_FLOW_MAX_HARMONIC: i32 = 2;
419
420/// Diffeomorphism floor `δ`: a candidate flow is REJECTED (the line search
421/// treats it as a failed step; the final chart is never produced) when
422/// `det Dφ_θ ≤ δ` anywhere on the check grid. `θ = 0` has `det Dφ = 1`
423/// everywhere and only guarded steps are ever accepted, so the optimizer can
424/// never walk through a fold. The floor is one-directional: raising it only
425/// ever rejects MORE near-fold candidates, never accepts a fold, so it is a
426/// conservative guard rather than a tuned operating point. The torus, free
427/// patch, and sphere flows all enforce the IDENTICAL contract, so the value
428/// lives here once and the per-topology floors below alias it.
429pub const SAE_FLOW_DIFFEO_MIN_DET: f64 = 0.1;
430
431/// Torus-flow diffeomorphism floor — see [`SAE_FLOW_DIFFEO_MIN_DET`].
432pub const TORUS_FLOW_DIFFEO_MIN_DET: f64 = SAE_FLOW_DIFFEO_MIN_DET;
433
434/// Per-axis node count of the diffeomorphism-guard check grid. The flow basis
435/// is band-limited to `TORUS_FLOW_MAX_HARMONIC` (≤ 2 oscillations per axis),
436/// so 64 nodes per axis oversample `det Dφ_θ` (itself band-limited to ≤ 4 per
437/// axis) by 16×: the grid minimum is a faithful surrogate for the continuum
438/// minimum at the `δ = 0.1` margin.
439pub const TORUS_FLOW_GUARD_NODES_PER_AXIS: usize = 64;
440
441/// Outer iteration cap for the damped Gauss–Newton flow optimization. The
442/// problem is a 48-dimensional smooth nonlinear least squares; quadratic
443/// local convergence makes this cap generous (termination is normally by the
444/// relative step / improvement tolerances below).
445pub const TORUS_FLOW_GN_MAX_ITERS: usize = 80;
446
447/// Consecutive damping escalations before the Gauss–Newton declares the
448/// current iterate a local minimum and stops.
449pub const TORUS_FLOW_GN_MAX_REJECTS: usize = 12;
450
451/// Minimum per-axis node count of the decoder-recomposition audit grid. The
452/// actual count also scales with the basis width (`3·√m` per axis) so the
453/// tensor harmonic basis is always Nyquist-oversampled on the audit grid.
454pub const TORUS_TRANSPORT_MIN_NODES_PER_AXIS: usize = 48;
455
456/// Identity of one flow mode (for tests and diagnostics): which coordinate
457/// component the vector field moves, its integer frequency vector, and its
458/// phase (`cos` vs `sin`).
459#[derive(Debug, Clone, Copy, PartialEq, Eq)]
460pub struct TorusFlowModeKey {
461    pub component: usize,
462    pub freq: (i32, i32),
463    pub is_cos: bool,
464}
465
466/// Per-mode sample of a `d = 2` flow basis at one chart point `t`: the scalar
467/// field value `f(t)` (the displacement this mode adds to coordinate
468/// `component`) and its gradient `∇f(t)` (the mode's contribution to row
469/// `component` of the flow Jacobian `Dφ`). Shared by the torus
470/// ([`TorusFlowBasis`]) and the free-patch ([`FreePatchFlowBasis`]) flow
471/// families — the isometry-defect Gauss–Newton core
472/// ([`minimize_isometry_defect_flow`]) consumes only this `(component, grad)`
473/// contract, so both families descend the same exact optimizer.
474#[derive(Debug, Clone, Copy)]
475pub struct FlowModeSample {
476    pub component: usize,
477    pub value: f64,
478    pub grad: [f64; 2],
479}
480
481/// Truncated Fourier vector-field basis on `T²` with period `period` per
482/// axis: `v_{c,(a,b),trig}(t) = e_c · trig(2π(a·t₀ + b·t₁)/period)` for
483/// `trig ∈ {sin, cos}`, `c ∈ {0, 1}`, and one frequency representative per
484/// antipodal pair (`sin` of `−ω` is `−sin` of `ω`, so both signs would be
485/// redundant). The flow map `φ_θ(t) = t + Σ_k θ_k v_k(t)` is automatically a
486/// degree-(1,1) torus self-map (`φ(t + period·e_c) = φ(t) + period·e_c` —
487/// wrap-around respected by periodicity of the displacement), and any such
488/// map with `det Dφ > 0` everywhere is a global diffeomorphism of `T²`.
489#[derive(Debug, Clone)]
490pub struct TorusFlowBasis {
491    pub period: f64,
492    /// Canonical frequency representatives: `a > 0`, or `a == 0 && b > 0`.
493    freqs: Vec<(i32, i32)>,
494}
495
496impl TorusFlowBasis {
497    pub fn new(period: f64) -> Result<Self, String> {
498        if !(period.is_finite() && period > 0.0) {
499            return Err(format!(
500                "TorusFlowBasis: period must be finite and positive; got {period}"
501            ));
502        }
503        let h = TORUS_FLOW_MAX_HARMONIC;
504        let mut freqs = Vec::new();
505        for a in -h..=h {
506            for b in -h..=h {
507                if a > 0 || (a == 0 && b > 0) {
508                    freqs.push((a, b));
509                }
510            }
511        }
512        Ok(Self { period, freqs })
513    }
514
515    /// Number of flow coefficients `θ`: 2 components × 2 phases × 12
516    /// frequency representatives = 48 at the default order.
517    pub fn dim(&self) -> usize {
518        4 * self.freqs.len()
519    }
520
521    /// Mode identities in coefficient order: for each component, for each
522    /// frequency representative, the `sin` mode then the `cos` mode. This IS
523    /// the `θ` index layout — [`Self::mode_samples`] returns samples in the
524    /// same order.
525    pub fn mode_layout(&self) -> Vec<TorusFlowModeKey> {
526        let mut keys = Vec::with_capacity(self.dim());
527        for component in 0..2 {
528            for &freq in &self.freqs {
529                keys.push(TorusFlowModeKey {
530                    component,
531                    freq,
532                    is_cos: false,
533                });
534                keys.push(TorusFlowModeKey {
535                    component,
536                    freq,
537                    is_cos: true,
538                });
539            }
540        }
541        keys
542    }
543
544    /// Sample every mode (value + gradient) at chart point `t`, in `θ` order.
545    pub fn mode_samples(&self, t: [f64; 2]) -> Vec<FlowModeSample> {
546        let tau = std::f64::consts::TAU;
547        let mut out = Vec::with_capacity(self.dim());
548        for component in 0..2 {
549            for &(a, b) in &self.freqs {
550                let w0 = tau * a as f64 / self.period;
551                let w1 = tau * b as f64 / self.period;
552                let angle = w0 * t[0] + w1 * t[1];
553                let s = angle.sin();
554                let c = angle.cos();
555                out.push(FlowModeSample {
556                    component,
557                    value: s,
558                    grad: [w0 * c, w1 * c],
559                });
560                out.push(FlowModeSample {
561                    component,
562                    value: c,
563                    grad: [-w0 * s, -w1 * s],
564                });
565            }
566        }
567        out
568    }
569
570    /// `φ_θ(t)`, wrapped into `[0, period)` per axis.
571    pub fn map_point(&self, theta: &[f64], t: [f64; 2]) -> [f64; 2] {
572        assert_eq!(theta.len(), self.dim(), "TorusFlowBasis: theta length");
573        let mut out = t;
574        for (coef, sample) in theta.iter().zip(self.mode_samples(t)) {
575            out[sample.component] += coef * sample.value;
576        }
577        [
578            out[0].rem_euclid(self.period),
579            out[1].rem_euclid(self.period),
580        ]
581    }
582
583    /// Flow Jacobian `Dφ_θ(t) = I + Σ_k θ_k Dv_k(t)`, row-major.
584    pub fn flow_jacobian(&self, theta: &[f64], t: [f64; 2]) -> [[f64; 2]; 2] {
585        assert_eq!(theta.len(), self.dim(), "TorusFlowBasis: theta length");
586        let mut jac = [[1.0, 0.0], [0.0, 1.0]];
587        for (coef, sample) in theta.iter().zip(self.mode_samples(t)) {
588            jac[sample.component][0] += coef * sample.grad[0];
589            jac[sample.component][1] += coef * sample.grad[1];
590        }
591        jac
592    }
593
594    /// Minimum of `det Dφ_θ` over the
595    /// [`TORUS_FLOW_GUARD_NODES_PER_AXIS`]² check grid — the diffeomorphism
596    /// guard's decision quantity.
597    pub fn min_jacobian_det_on_grid(&self, theta: &[f64]) -> f64 {
598        let nodes = TORUS_FLOW_GUARD_NODES_PER_AXIS;
599        let mut min_det = f64::INFINITY;
600        for i in 0..nodes {
601            for j in 0..nodes {
602                let t = [
603                    self.period * i as f64 / nodes as f64,
604                    self.period * j as f64 / nodes as f64,
605                ];
606                let jac = self.flow_jacobian(theta, t);
607                let det = jac[0][0] * jac[1][1] - jac[0][1] * jac[1][0];
608                min_det = min_det.min(det);
609            }
610        }
611        min_det
612    }
613}
614
615/// The exact, image-frozen minimum-isometry-defect flow reparameterization of
616/// one `d = 2` torus atom.
617#[derive(Debug, Clone)]
618pub struct TorusIsometryFlowReparameterization {
619    /// Canonical per-row coordinates `t̃_i = φ_θ(t_i)`, shape `(n, 2)`,
620    /// wrapped into `[0, period)` per axis.
621    pub new_row_coords: Array2<f64>,
622    /// Recomposed decoder `B̃ = T · B`, shape `(m, p)` — the exact LS refit of
623    /// the original decoded image on the audit grid against the basis at the
624    /// transported grid (so `γ̃ = γ ∘ φ⁻¹` without ever forming `φ⁻¹`).
625    pub new_decoder: Array2<f64>,
626    /// The `(m, m)` basis transport `T` with `Φ(φ(u)) · T ≈ Φ(u)` on the
627    /// audit grid — the same congruence object the `d = 1` path and the
628    /// affine gauge canonicalization use to transport the smoothness Gram.
629    pub decoder_transport: Array2<f64>,
630    /// Optimal flow coefficients `θ` (layout per
631    /// [`TorusFlowBasis::mode_layout`]).
632    pub flow_theta: Vec<f64>,
633    /// Isometry defect `E(0)` of the fitted chart (identity flow).
634    pub defect_initial: f64,
635    /// Isometry defect `E(θ)` of the canonical chart. Strictly below
636    /// `defect_initial` (the pass refuses no-improvement flows).
637    pub defect_final: f64,
638    /// The profiled global metric scale `c` at the optimum (the canonical
639    /// chart's pullback metric is `≈ c·ḡ·I`).
640    pub profiled_metric_scale: f64,
641    /// `min det Dφ_θ` on the guard grid. Always `> TORUS_FLOW_DIFFEO_MIN_DET`
642    /// when `Some(..)` is returned — a folded chart is refused upstream.
643    pub min_flow_jacobian_det: f64,
644    /// Max-abs recomposition error on the audit grid, relative to the image
645    /// scale. Always `≤ CHART_RECOMPOSITION_REL_TOL` when `Some(..)`.
646    pub recomposition_residual: f64,
647}
648
649/// State of the flow objective at one `θ`: the defect, the profiled scale,
650/// and the per-row flow Jacobians `A_i = Dφ_θ(t_i)` (row-major
651/// `[a00, a01, a10, a11]`) the Gauss–Newton rows are built from.
652struct FlowObjectiveState {
653    defect: f64,
654    scale: f64,
655    a_rows: Vec<[f64; 4]>,
656}
657
658/// Evaluate the isometry-defect objective at `θ` (see
659/// [`torus_isometry_flow_reparameterization`] for the derivation). Returns
660/// `None` when the profiled scale degenerates (`c ≤ 0` or non-finite).
661///
662/// `row_base` is the per-row whitened identity `A0_i` (row-major
663/// `[a00, a01, a10, a11]`) the flow modes are added on top of: the effective
664/// whitened Jacobian is `Ã_i = A0_i + Σ_k θ_k W_{ik}`. The torus/free-patch
665/// families pass `A0_i = I` (the flat reference whitens to the identity); the
666/// sphere passes the reference Cholesky `A0_i = L_i = diag(1, cos lat_i)` and
667/// pre-scales each mode `grad` by `L_i`'s diagonal, so the SAME flat-residual
668/// core measures the round-sphere isometry defect `Ã_iᵀ Ã_i − s·G_i` exactly
669/// (see [`sphere_isometry_flow_reparameterization`] for the whitening proof).
670fn evaluate_flow_defect(
671    theta: &[f64],
672    row_modes: &[Vec<FlowModeSample>],
673    row_base: &[[f64; 4]],
674    ghat: &[[f64; 3]],
675    ghat_norm_sq: f64,
676) -> Option<FlowObjectiveState> {
677    let n = row_modes.len();
678    let mut a_rows = Vec::with_capacity(n);
679    let mut cross = 0.0_f64;
680    for (modes, base) in row_modes.iter().zip(row_base.iter()) {
681        let mut a = *base;
682        for (coef, sample) in theta.iter().zip(modes.iter()) {
683            a[2 * sample.component] += coef * sample.grad[0];
684            a[2 * sample.component + 1] += coef * sample.grad[1];
685        }
686        a_rows.push(a);
687    }
688    for (a, g) in a_rows.iter().zip(ghat.iter()) {
689        // AᵀA in symmetric storage [m00, m11, m01].
690        let m00 = a[0] * a[0] + a[2] * a[2];
691        let m11 = a[1] * a[1] + a[3] * a[3];
692        let m01 = a[0] * a[1] + a[2] * a[3];
693        cross += m00 * g[0] + m11 * g[1] + 2.0 * m01 * g[2];
694    }
695    let scale = cross / ghat_norm_sq;
696    if !(scale.is_finite() && scale > 0.0) {
697        return None;
698    }
699    let mut defect = 0.0_f64;
700    for (a, g) in a_rows.iter().zip(ghat.iter()) {
701        let m00 = a[0] * a[0] + a[2] * a[2];
702        let m11 = a[1] * a[1] + a[3] * a[3];
703        let m01 = a[0] * a[1] + a[2] * a[3];
704        let r00 = m00 - scale * g[0];
705        let r11 = m11 - scale * g[1];
706        let r01 = m01 - scale * g[2];
707        defect += r00 * r00 + r11 * r11 + 2.0 * r01 * r01;
708    }
709    if !defect.is_finite() {
710        return None;
711    }
712    Some(FlowObjectiveState {
713        defect,
714        scale,
715        a_rows,
716    })
717}
718
719/// Outcome of the shared isometry-defect flow minimizer: the optimal flow
720/// coefficients and the bracket of defects/scale they achieve. Returned only
721/// when a strict, fold-free improvement over the identity flow was found.
722struct FlowMinimization {
723    theta: Vec<f64>,
724    defect_initial: f64,
725    defect_final: f64,
726    profiled_scale: f64,
727}
728
729/// Exact damped Gauss–Newton for the `d = 2` isometry defect
730/// `E(θ) = Σ_i ‖A_iᵀA_i − c·Ĝ_i‖²_F` over the flow coefficients `θ`, shared by
731/// the torus and free-patch flow families (see
732/// [`torus_isometry_flow_reparameterization`] for the full derivation of the
733/// residual, the profiled scale `c`, and the analytic Gauss–Newton Jacobian).
734///
735/// The flow family enters ONLY through `row_modes` (the per-row mode samples
736/// `W_{ik} = Dv_k(t_i)` and displacements) and the `min_det_on_grid` guard
737/// closure, so the two families descend the identical optimizer with the
738/// identical strict-descent + diffeomorphism accept test. The minimization
739/// starts at `θ = 0` (`det Dφ = 1` everywhere) and never accepts a candidate
740/// whose `min det Dφ_θ ≤ min_det` on the guard grid, so the iterate can never
741/// walk through a fold. Returns `None` (honest skip — no lossy or folded swap)
742/// when the identity chart is already isometric, the profiled scale
743/// degenerates, or no strict improvement is reachable within the family.
744fn minimize_isometry_defect_flow(
745    row_modes: &[Vec<FlowModeSample>],
746    row_base: &[[f64; 4]],
747    ghat: &[[f64; 3]],
748    ghat_norm_sq: f64,
749    q: usize,
750    min_det: f64,
751    min_det_on_grid: &dyn Fn(&[f64]) -> f64,
752) -> Option<FlowMinimization> {
753    let n = row_modes.len();
754    let mut theta = vec![0.0_f64; q];
755    let mut state = evaluate_flow_defect(&theta, row_modes, row_base, ghat, ghat_norm_sq)?;
756    let defect_initial = state.defect;
757    if !(defect_initial > 0.0) {
758        // Already exactly isometric — nothing to canonicalize.
759        return None;
760    }
761    let sqrt2 = std::f64::consts::SQRT_2;
762    let mut lambda = 1.0e-4_f64;
763    let mut any_accepted = false;
764    for iteration in 0..TORUS_FLOW_GN_MAX_ITERS {
765        if iteration + 1 == TORUS_FLOW_GN_MAX_ITERS {
766            break;
767        }
768        // Residual r and Gauss–Newton Jacobian J at the current θ.
769        let mut jmat = Array2::<f64>::zeros((3 * n, q));
770        let mut rcol = Array2::<f64>::zeros((3 * n, 1));
771        for (i, (a, g)) in state.a_rows.iter().zip(ghat.iter()).enumerate() {
772            let m00 = a[0] * a[0] + a[2] * a[2];
773            let m11 = a[1] * a[1] + a[3] * a[3];
774            let m01 = a[0] * a[1] + a[2] * a[3];
775            rcol[[3 * i, 0]] = m00 - state.scale * g[0];
776            rcol[[3 * i + 1, 0]] = m11 - state.scale * g[1];
777            rcol[[3 * i + 2, 0]] = sqrt2 * (m01 - state.scale * g[2]);
778            for (k, sample) in row_modes[i].iter().enumerate() {
779                // W_{ik} has single nonzero row `component` = grad, so
780                // M = W_{ik}ᵀ A_i has entries M_{ab} = grad[a]·A[component, b]
781                // and S = M + Mᵀ.
782                let ac0 = a[2 * sample.component];
783                let ac1 = a[2 * sample.component + 1];
784                let s00 = 2.0 * sample.grad[0] * ac0;
785                let s11 = 2.0 * sample.grad[1] * ac1;
786                let s01 = sample.grad[0] * ac1 + sample.grad[1] * ac0;
787                jmat[[3 * i, k]] = s00;
788                jmat[[3 * i + 1, k]] = s11;
789                jmat[[3 * i + 2, k]] = sqrt2 * s01;
790            }
791        }
792        let jtj = fast_ata(&jmat);
793        let jtr = fast_atb(&jmat, &rcol);
794
795        // Levenberg-damped step with the diffeomorphism guard in the accept
796        // test: only strict-descent, fold-free candidates are ever taken.
797        let mut rejects = 0usize;
798        let mut accepted_step = false;
799        let mut converged = false;
800        let mut step_norm_sq = 0.0_f64;
801        while rejects < TORUS_FLOW_GN_MAX_REJECTS {
802            let mut damped = jtj.clone();
803            for d in 0..q {
804                damped[[d, d]] += lambda * (1.0 + jtj[[d, d]]);
805            }
806            let factor = match damped.cholesky(FaerSide::Lower) {
807                Ok(factor) => factor,
808                Err(_) => {
809                    lambda *= 10.0;
810                    rejects += 1;
811                    continue;
812                }
813            };
814            let mut neg_jtr = jtr.clone();
815            neg_jtr.mapv_inplace(|v| -v);
816            let delta = factor.solve_mat(&neg_jtr);
817            let mut candidate = theta.clone();
818            step_norm_sq = 0.0;
819            for k in 0..q {
820                candidate[k] += delta[[k, 0]];
821                step_norm_sq += delta[[k, 0]] * delta[[k, 0]];
822            }
823            let folded = min_det_on_grid(&candidate) <= min_det;
824            let candidate_state = if folded {
825                None
826            } else {
827                evaluate_flow_defect(&candidate, row_modes, row_base, ghat, ghat_norm_sq)
828            };
829            match candidate_state {
830                Some(next) if next.defect < state.defect => {
831                    let improvement = state.defect - next.defect;
832                    theta = candidate;
833                    state = next;
834                    any_accepted = true;
835                    accepted_step = true;
836                    lambda = (lambda / 10.0).max(1.0e-12);
837                    if improvement <= 1.0e-14 * (1.0 + state.defect) {
838                        // Converged: the accepted step no longer moves E.
839                        converged = true;
840                    }
841                    break;
842                }
843                Some(..) | None => {
844                    lambda *= 10.0;
845                    rejects += 1;
846                }
847            }
848        }
849        if !accepted_step {
850            break;
851        }
852        if converged {
853            break;
854        }
855        let theta_norm_sq: f64 = theta.iter().map(|v| v * v).sum();
856        if step_norm_sq <= 1.0e-24 * (1.0 + theta_norm_sq) {
857            break;
858        }
859    }
860    if !any_accepted || !(state.defect < defect_initial) {
861        // No strict improvement within the flow family: the fitted chart is
862        // already the canonical representative — honest skip.
863        return None;
864    }
865    Some(FlowMinimization {
866        theta,
867        defect_initial,
868        defect_final: state.defect,
869        profiled_scale: state.scale,
870    })
871}
872
873/// Extract the fitted pullback metric `G_i = J(t_i)ᵀ J(t_i)` (symmetric storage
874/// `[g00, g11, g01]`) at every row of a `d = 2` atom from the exact decoder jet,
875/// together with the geometric-mean metric scale `ḡ = exp(mean_i ½ log det G_i)`
876/// used by every `d = 2` defect for its scale-invariant normalization. Shared by
877/// the torus, free-patch, and sphere defect paths — the single source of truth
878/// for the pullback-metric extraction.
879///
880/// Returns `Ok(None)` (honest refusal) on a degenerate chart: empty rows/basis,
881/// a rank-deficient pullback metric (`det G_i ≤ 0`) anywhere — the chart is
882/// collapsed along some direction there, so no isometric representative exists —
883/// or a non-finite geometric-mean scale.
884fn extract_pullback_metric_d2(
885    label: &str,
886    evaluator: &dyn SaeBasisEvaluator,
887    decoder: ArrayView2<'_, f64>,
888    row_coords: ArrayView2<'_, f64>,
889) -> Result<Option<(Vec<[f64; 3]>, f64)>, String> {
890    let n = row_coords.nrows();
891    let m = decoder.nrows();
892    let p = decoder.ncols();
893    let (row_phi, row_jet) = evaluator.evaluate(row_coords)?;
894    if row_phi.ncols() != m || row_jet.dim() != (n, m, 2) {
895        return Err(format!(
896            "{label}: evaluator returned basis {:?} / jet {:?}; expected width {m}, latent_dim 2",
897            row_phi.dim(),
898            row_jet.dim()
899        ));
900    }
901    let mut g_rows: Vec<[f64; 3]> = Vec::with_capacity(n);
902    let mut log_det_sum = 0.0_f64;
903    let mut tangent0 = vec![0.0_f64; p];
904    let mut tangent1 = vec![0.0_f64; p];
905    for row in 0..n {
906        for slot in tangent0.iter_mut() {
907            *slot = 0.0;
908        }
909        for slot in tangent1.iter_mut() {
910            *slot = 0.0;
911        }
912        for bm in 0..m {
913            let d0 = row_jet[[row, bm, 0]];
914            let d1 = row_jet[[row, bm, 1]];
915            if d0 == 0.0 && d1 == 0.0 {
916                continue;
917            }
918            for j in 0..p {
919                let b = decoder[[bm, j]];
920                tangent0[j] += d0 * b;
921                tangent1[j] += d1 * b;
922            }
923        }
924        let mut g00 = 0.0_f64;
925        let mut g11 = 0.0_f64;
926        let mut g01 = 0.0_f64;
927        for j in 0..p {
928            g00 += tangent0[j] * tangent0[j];
929            g11 += tangent1[j] * tangent1[j];
930            g01 += tangent0[j] * tangent1[j];
931        }
932        let det = g00 * g11 - g01 * g01;
933        if !(det.is_finite() && det > 0.0) {
934            return Ok(None);
935        }
936        log_det_sum += 0.5 * det.ln();
937        g_rows.push([g00, g11, g01]);
938    }
939    let g_bar = (log_det_sum / n as f64).exp();
940    if !(g_bar.is_finite() && g_bar > 0.0) {
941        return Ok(None);
942    }
943    Ok(Some((g_rows, g_bar)))
944}
945
946/// Normalize the fitted metric rows against the **flat** reference `g_ref = I`:
947/// `Ĝ_i = G_i / ḡ` plus the reference-norm² `Σ_i ‖Ĝ_i‖²_F` the isometry-defect
948/// Gauss–Newton profiles its global scale against. Shared by the torus and
949/// free-patch families (both pin to a flat uniform-speed reference); the sphere
950/// uses the `diag(1, cos²lat)` reference instead and normalizes inline.
951fn flat_normalized_metric(g_rows: &[[f64; 3]], g_bar: f64) -> Option<(Vec<[f64; 3]>, f64)> {
952    let mut ghat: Vec<[f64; 3]> = Vec::with_capacity(g_rows.len());
953    let mut ghat_norm_sq = 0.0_f64;
954    for g in g_rows {
955        let h = [g[0] / g_bar, g[1] / g_bar, g[2] / g_bar];
956        ghat_norm_sq += h[0] * h[0] + h[1] * h[1] + 2.0 * h[2] * h[2];
957        ghat.push(h);
958    }
959    if !(ghat_norm_sq.is_finite() && ghat_norm_sq > 0.0) {
960        return None;
961    }
962    Some((ghat, ghat_norm_sq))
963}
964
965/// Compute the minimum-isometry-defect flow reparameterization of a fitted
966/// `d = 2` torus atom: the canonical per-row coordinates `t̃_i = φ_θ(t_i)`
967/// and the exactly-recomposed decoder.
968///
969/// # The defect functional (and why it is exactly the issue's isometry defect)
970///
971/// The new chart is `t̃ = φ(t)` with new decoded map `γ̃ = γ ∘ φ⁻¹` (image
972/// frozen), so the pullback metric in the canonical chart at `φ(t)` is
973/// `G̃(φ(t)) = Dφ(t)⁻ᵀ G(t) Dφ(t)⁻¹` where `G(t) = J(t)ᵀJ(t)` is the fitted
974/// pullback metric (`J` = decoder Jacobian, from the exact `(Φ, ∂Φ)` jet).
975/// The canonical chart is isometric to the flat reference torus up to a
976/// global scale `s` iff `G̃ ≡ s·I`, i.e. iff `Dφᵀ Dφ ≡ G / s`. Measuring the
977/// defect on THIS side of the equivalence,
978///
979/// ```text
980/// E(θ) = Σ_i ‖ A_iᵀ A_i − c · Ĝ_i ‖²_F ,   A_i = Dφ_θ(t_i) = I + Σ_k θ_k W_{ik} ,
981/// ```
982///
983/// keeps the residual polynomial (quadratic) in `θ` — no `Dφ⁻¹` anywhere.
984/// Here `W_{ik} = Dv_k(t_i)` are the constant per-row mode Jacobians,
985/// `Ĝ_i = G_i / ḡ` with `ḡ = exp( mean_i ½·log det G_i )` the geometric-mean
986/// metric scale of the fitted rows (the scale-invariant normalization — the
987/// `d = 2` analogue of the `d = 1` module's rescale-by-total-arc-length), and
988/// `c = c(θ)` the analytically profiled residual global scale
989///
990/// ```text
991/// c(θ) = Σ_i ⟨A_iᵀA_i, Ĝ_i⟩_F / Σ_i ‖Ĝ_i‖²_F   (the exact argmin over c),
992/// ```
993///
994/// which absorbs the (second-order) arithmetic-vs-geometric mean mismatch so
995/// the defect is exactly scale-invariant: a chart isometric up to ANY global
996/// scale has `E = 0`.
997///
998/// # The analytic gradient / Gauss–Newton (FD-free)
999///
1000/// With `R_i = A_iᵀA_i − c·Ĝ_i` (symmetric) and `c` profiled,
1001/// `∂E/∂c = 0` at `c(θ)` (envelope theorem), so the exact gradient treats `c`
1002/// as fixed:
1003///
1004/// ```text
1005/// ∂R_i/∂θ_k = W_{ik}ᵀ A_i + A_iᵀ W_{ik}
1006/// ∂E/∂θ_k   = 2 Σ_i ⟨R_i, W_{ik}ᵀA_i + A_iᵀW_{ik}⟩_F = 4 Σ_i ⟨R_i, A_iᵀ W_{ik}⟩_F .
1007/// ```
1008///
1009/// The Gauss–Newton residual vector stacks the norm-preserving symmetric
1010/// vectorization `svec(R_i) = (R_00, R_11, √2·R_01)` and its Jacobian stacks
1011/// `svec(∂R_i/∂θ_k)`, so `JᵀJ δ = −Jᵀr` is the exact Gauss–Newton system for
1012/// `E`; Levenberg damping plus the `det Dφ > δ` guard make every accepted
1013/// step a strict-descent diffeomorphism. Each `v_k` moves a single component,
1014/// so `W_{ik}` has one nonzero row and every row/mode contraction is a
1015/// handful of scalar ops.
1016///
1017/// # Honest refusals (`Ok(None)`, never a lossy or folded swap)
1018///
1019/// * degenerate chart: empty rows/basis, non-finite coordinates, or a
1020///   rank-deficient pullback metric (`det G_i ≤ 0`) anywhere;
1021/// * the optimizer finds no strict improvement over the identity flow (the
1022///   fitted chart is already minimum-defect within the flow family);
1023/// * every improving candidate violates the diffeomorphism guard;
1024/// * the basis cannot absorb `γ ∘ φ⁻¹` within
1025///   [`CHART_RECOMPOSITION_REL_TOL`] on the audit grid (shared gate with the
1026///   `d = 1` path).
1027pub fn torus_isometry_flow_reparameterization(
1028    evaluator: &dyn SaeBasisEvaluator,
1029    decoder: ArrayView2<'_, f64>,
1030    row_coords: ArrayView2<'_, f64>,
1031    period: f64,
1032) -> Result<Option<TorusIsometryFlowReparameterization>, String> {
1033    let n = row_coords.nrows();
1034    let m = decoder.nrows();
1035    let p = decoder.ncols();
1036    if row_coords.ncols() != 2 {
1037        return Err(format!(
1038            "torus_isometry_flow_reparameterization: expected (n, 2) row coordinates; got {:?}",
1039            row_coords.dim()
1040        ));
1041    }
1042    if n == 0 || m == 0 || p == 0 {
1043        return Ok(None);
1044    }
1045    for &t in row_coords.iter() {
1046        if !t.is_finite() {
1047            return Ok(None);
1048        }
1049    }
1050
1051    // ── Fitted pullback metric G_i = J(t_i)ᵀ J(t_i) from the exact jet, then
1052    //    normalize against the flat reference g_ref = I (shared helpers) ──────
1053    let Some((g_rows, g_bar)) = extract_pullback_metric_d2(
1054        "torus_isometry_flow_reparameterization",
1055        evaluator,
1056        decoder,
1057        row_coords,
1058    )?
1059    else {
1060        return Ok(None);
1061    };
1062    let Some((ghat, ghat_norm_sq)) = flat_normalized_metric(&g_rows, g_bar) else {
1063        return Ok(None);
1064    };
1065
1066    // ── Flow basis + per-row mode samples (W_{ik} and the displacements) ────
1067    let basis = TorusFlowBasis::new(period)?;
1068    let q = basis.dim();
1069    let mut row_modes: Vec<Vec<FlowModeSample>> = Vec::with_capacity(n);
1070    for row in 0..n {
1071        row_modes.push(basis.mode_samples([row_coords[[row, 0]], row_coords[[row, 1]]]));
1072    }
1073    // Flat reference ⇒ the whitened base is the identity at every row.
1074    let row_base = vec![[1.0_f64, 0.0, 0.0, 1.0]; n];
1075
1076    // ── Damped Gauss–Newton on θ (shared exact core; derivation above) ──────
1077    let Some(minimization) = minimize_isometry_defect_flow(
1078        &row_modes,
1079        &row_base,
1080        &ghat,
1081        ghat_norm_sq,
1082        q,
1083        TORUS_FLOW_DIFFEO_MIN_DET,
1084        &|candidate: &[f64]| basis.min_jacobian_det_on_grid(candidate),
1085    ) else {
1086        return Ok(None);
1087    };
1088    let theta = minimization.theta;
1089    let defect_initial = minimization.defect_initial;
1090    let min_flow_jacobian_det = basis.min_jacobian_det_on_grid(&theta);
1091    if !(min_flow_jacobian_det > TORUS_FLOW_DIFFEO_MIN_DET) {
1092        // Unreachable through the guarded accept path; refuse defensively
1093        // rather than ever committing a folded chart.
1094        return Ok(None);
1095    }
1096
1097    // ── Decoder transport on the Nyquist-oversampled audit grid ─────────────
1098    let axis_nodes = TORUS_TRANSPORT_MIN_NODES_PER_AXIS.max(3 * (m as f64).sqrt().ceil() as usize);
1099    let grid_rows = axis_nodes * axis_nodes;
1100    let mut grid = Array2::<f64>::zeros((grid_rows, 2));
1101    let mut new_grid = Array2::<f64>::zeros((grid_rows, 2));
1102    for i in 0..axis_nodes {
1103        for j in 0..axis_nodes {
1104            let idx = i * axis_nodes + j;
1105            let u = [
1106                period * i as f64 / axis_nodes as f64,
1107                period * j as f64 / axis_nodes as f64,
1108            ];
1109            grid[[idx, 0]] = u[0];
1110            grid[[idx, 1]] = u[1];
1111            let mapped = basis.map_point(&theta, u);
1112            new_grid[[idx, 0]] = mapped[0];
1113            new_grid[[idx, 1]] = mapped[1];
1114        }
1115    }
1116    let (grid_phi, grid_jet) = evaluator.evaluate(grid.view())?;
1117    if grid_phi.ncols() != m || grid_jet.dim() != (grid_rows, m, 2) {
1118        return Err(format!(
1119            "torus_isometry_flow_reparameterization: evaluator returned basis {:?} / jet {:?} on the audit grid; expected width {m}, latent_dim 2",
1120            grid_phi.dim(),
1121            grid_jet.dim()
1122        ));
1123    }
1124    let Some(recomposition) =
1125        recompose_decoder_exact_ls(evaluator, decoder, grid_phi.view(), new_grid.view())?
1126    else {
1127        return Ok(None);
1128    };
1129
1130    // ── Canonical per-row coordinates t̃_i = φ_θ(t_i) ────────────────────────
1131    let mut new_row_coords = Array2::<f64>::zeros((n, 2));
1132    for row in 0..n {
1133        let mapped = basis.map_point(&theta, [row_coords[[row, 0]], row_coords[[row, 1]]]);
1134        new_row_coords[[row, 0]] = mapped[0];
1135        new_row_coords[[row, 1]] = mapped[1];
1136    }
1137
1138    Ok(Some(TorusIsometryFlowReparameterization {
1139        new_row_coords,
1140        new_decoder: recomposition.new_decoder,
1141        decoder_transport: recomposition.transport,
1142        flow_theta: theta,
1143        defect_initial,
1144        defect_final: minimization.defect_final,
1145        profiled_metric_scale: minimization.profiled_scale,
1146        min_flow_jacobian_det,
1147        recomposition_residual: recomposition.recomposition_residual,
1148    }))
1149}
1150
1151// ════════════════════════════════════════════════════════════════════════════
1152// #1019 stage 2 — d = 2 free/patch (Euclidean-patch) isometry-flow chart
1153// canonicalization
1154// ════════════════════════════════════════════════════════════════════════════
1155
1156/// Highest total polynomial degree of the truncated vector-field flow basis on
1157/// a `d = 2` Euclidean patch: monomial modes `u₀^a · u₁^b` with
1158/// `1 ≤ a+b ≤ PATCH_FLOW_MAX_DEGREE`.
1159///
1160/// The degree is **1** (affine flows) by exact-image-freezing necessity, not
1161/// timidity. The decoder lives in a polynomial basis of some fixed degree `d_b`
1162/// (the Duchon / EuclideanPatch monomial families), and the image is frozen by
1163/// re-expressing `γ̃ = γ ∘ φ⁻¹` in that SAME basis. Composing a degree-`d_b`
1164/// decoder image with a degree-`d_f` flow yields a degree-`d_b·d_f` polynomial,
1165/// which is back in the decoder basis **iff `d_f = 1`** (an affine flow leaves
1166/// the polynomial degree unchanged). A higher-degree flow would push `γ ∘ φ⁻¹`
1167/// out of the basis, the exact-LS recomposition gate
1168/// ([`recompose_decoder_exact_ls`]) would see a large image drift, and the
1169/// canonicalization would be honestly REFUSED — so a degree > 1 flow basis
1170/// could never actually commit a chart here. The affine family is exactly the
1171/// one that keeps the image-freezing exact, and it captures the dominant patch
1172/// chart dishonesty: an anisotropic / sheared / rotated affine reparameteriza-
1173/// tion (the `d = 2` analogue of the `d = 1` non-uniform-speed pathology).
1174///
1175/// The monomials are `(1, 0)` and `(0, 1)` per component, both components ⇒
1176/// `2 · 2 = 4` flow coefficients. The constant `(0, 0)` is EXCLUDED: a uniform
1177/// translation is an exact isometry of the flat patch, so it leaves the defect
1178/// invariant and would only insert a null direction into the Gauss–Newton
1179/// system. The 4 linear modes span the full `GL(2)` action on the chart
1180/// (rotation / shear / anisotropic scale); the rotation and global-scale
1181/// sub-directions are defect-null and the Levenberg damping absorbs them
1182/// harmlessly.
1183pub const PATCH_FLOW_MAX_DEGREE: usize = 1;
1184
1185/// Diffeomorphism floor `δ` for the free-patch flow — identical contract to
1186/// [`TORUS_FLOW_DIFFEO_MIN_DET`]: a candidate with `det Dφ_θ ≤ δ` anywhere on
1187/// the check grid is rejected, so the optimizer can never walk through a fold.
1188pub const PATCH_FLOW_DIFFEO_MIN_DET: f64 = SAE_FLOW_DIFFEO_MIN_DET;
1189
1190/// Per-axis node count of the free-patch diffeomorphism-guard check grid. With
1191/// the affine flow basis (`PATCH_FLOW_MAX_DEGREE = 1`) the Jacobian `Dφ_θ` is
1192/// CONSTANT over the patch, so `det Dφ_θ` is a single value and any grid is
1193/// exact; 48 nodes per axis keep the guard robust if the degree ever rises.
1194/// The guard grid spans the normalized patch box `[-1, 1]²` slightly widened to
1195/// `[-1.1, 1.1]²` so a fold just outside the data hull is still refused.
1196pub const PATCH_FLOW_GUARD_NODES_PER_AXIS: usize = 48;
1197
1198/// Minimum per-axis node count of the free-patch decoder-recomposition audit
1199/// grid (scaled up with the basis width like the torus path).
1200pub const PATCH_TRANSPORT_MIN_NODES_PER_AXIS: usize = 48;
1201
1202/// Identity of one free-patch flow mode (for tests / diagnostics): which
1203/// coordinate component the vector field moves and its monomial exponents.
1204#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1205pub struct PatchFlowModeKey {
1206    pub component: usize,
1207    pub exps: (usize, usize),
1208}
1209
1210/// Truncated polynomial vector-field basis on a `d = 2` Euclidean patch,
1211/// `v_{c,(a,b)}(t) = e_c · u₀^a · u₁^b` where `u = (t − center) ⊙ inv_half ∈
1212/// [−1, 1]²` is the affinely-normalized patch coordinate (conditioning: the raw
1213/// `t` box can be far from `[−1, 1]`). Because the patch is **contractible**
1214/// there is no hairy-ball obstruction — these are smooth global vector fields,
1215/// so the flow `φ_θ(t) = t + Σ_k θ_k v_k(t)` parameterizes a neighborhood of
1216/// the identity in `Diff(patch)` exactly the way the Fourier basis does on the
1217/// torus, and any such map with `det Dφ > 0` on the patch is a diffeomorphism
1218/// onto its image. The reference metric is the flat `g_ref = I` (uniform-speed
1219/// coordinates), so the canonical chart is the minimum-anisotropy-defect one.
1220#[derive(Debug, Clone)]
1221pub struct FreePatchFlowBasis {
1222    center: [f64; 2],
1223    /// `2 / span` per axis: the Jacobian `∂u/∂t` of the normalization.
1224    inv_half: [f64; 2],
1225    /// Monomial exponents `(a, b)` with `1 ≤ a+b ≤ PATCH_FLOW_MAX_DEGREE`.
1226    exps: Vec<(usize, usize)>,
1227}
1228
1229impl FreePatchFlowBasis {
1230    /// Build the basis for the patch box `[lo, hi]` per axis. Returns an error
1231    /// when an axis has collapsed (`hi ≤ lo`) — a patch with no extent in some
1232    /// direction has no honest flat chart.
1233    pub fn new(lo: [f64; 2], hi: [f64; 2]) -> Result<Self, String> {
1234        let mut center = [0.0_f64; 2];
1235        let mut inv_half = [0.0_f64; 2];
1236        for axis in 0..2 {
1237            let span = hi[axis] - lo[axis];
1238            let scale = lo[axis].abs().max(hi[axis].abs()).max(1.0);
1239            if !(span.is_finite() && span > 1.0e-12 * scale) {
1240                return Err(format!(
1241                    "FreePatchFlowBasis: patch axis {axis} has collapsed extent [{}, {}]",
1242                    lo[axis], hi[axis]
1243                ));
1244            }
1245            center[axis] = 0.5 * (lo[axis] + hi[axis]);
1246            inv_half[axis] = 2.0 / span;
1247        }
1248        let mut exps = Vec::new();
1249        for total in 1..=PATCH_FLOW_MAX_DEGREE {
1250            for a in (0..=total).rev() {
1251                let b = total - a;
1252                exps.push((a, b));
1253            }
1254        }
1255        Ok(Self {
1256            center,
1257            inv_half,
1258            exps,
1259        })
1260    }
1261
1262    /// Number of flow coefficients `θ`: 2 components × #monomials.
1263    pub fn dim(&self) -> usize {
1264        2 * self.exps.len()
1265    }
1266
1267    /// Mode identities in coefficient order (for each component, each monomial).
1268    /// This IS the `θ` index layout — [`Self::mode_samples`] matches it.
1269    pub fn mode_layout(&self) -> Vec<PatchFlowModeKey> {
1270        let mut keys = Vec::with_capacity(self.dim());
1271        for component in 0..2 {
1272            for &exps in &self.exps {
1273                keys.push(PatchFlowModeKey { component, exps });
1274            }
1275        }
1276        keys
1277    }
1278
1279    /// Normalized patch coordinate `u = (t − center) ⊙ inv_half`.
1280    fn normalize(&self, t: [f64; 2]) -> [f64; 2] {
1281        [
1282            (t[0] - self.center[0]) * self.inv_half[0],
1283            (t[1] - self.center[1]) * self.inv_half[1],
1284        ]
1285    }
1286
1287    /// Sample every mode (value + gradient **in `t`**) at chart point `t`, in
1288    /// `θ` order. The monomial `f(u) = u₀^a · u₁^b` has `∂f/∂t_d =
1289    /// (∂f/∂u_d)·(∂u_d/∂t_d) = (∂f/∂u_d)·inv_half[d]` by the chain rule, so the
1290    /// returned gradient is already in the chart coordinate the flow Jacobian
1291    /// `Dφ = I + Σ θ_k ∂v_k/∂t` lives in.
1292    pub fn mode_samples(&self, t: [f64; 2]) -> Vec<FlowModeSample> {
1293        let u = self.normalize(t);
1294        let mut out = Vec::with_capacity(self.dim());
1295        for component in 0..2 {
1296            for &(a, b) in &self.exps {
1297                let value = pow_u(u[0], a) * pow_u(u[1], b);
1298                // ∂/∂u₀ (u₀^a u₁^b) = a·u₀^{a−1}·u₁^b ; ∂/∂u₁ = b·u₀^a·u₁^{b−1}.
1299                let du0 = if a == 0 {
1300                    0.0
1301                } else {
1302                    a as f64 * pow_u(u[0], a - 1) * pow_u(u[1], b)
1303                };
1304                let du1 = if b == 0 {
1305                    0.0
1306                } else {
1307                    b as f64 * pow_u(u[0], a) * pow_u(u[1], b - 1)
1308                };
1309                out.push(FlowModeSample {
1310                    component,
1311                    value,
1312                    grad: [du0 * self.inv_half[0], du1 * self.inv_half[1]],
1313                });
1314            }
1315        }
1316        out
1317    }
1318
1319    /// `φ_θ(t) = t + Σ_k θ_k v_k(t)` (no wrap — the patch is not periodic).
1320    pub fn map_point(&self, theta: &[f64], t: [f64; 2]) -> [f64; 2] {
1321        assert_eq!(theta.len(), self.dim(), "FreePatchFlowBasis: theta length");
1322        let mut out = t;
1323        for (coef, sample) in theta.iter().zip(self.mode_samples(t)) {
1324            out[sample.component] += coef * sample.value;
1325        }
1326        out
1327    }
1328
1329    /// Flow Jacobian `Dφ_θ(t) = I + Σ_k θ_k Dv_k(t)`, row-major.
1330    pub fn flow_jacobian(&self, theta: &[f64], t: [f64; 2]) -> [[f64; 2]; 2] {
1331        assert_eq!(theta.len(), self.dim(), "FreePatchFlowBasis: theta length");
1332        let mut jac = [[1.0, 0.0], [0.0, 1.0]];
1333        for (coef, sample) in theta.iter().zip(self.mode_samples(t)) {
1334            jac[sample.component][0] += coef * sample.grad[0];
1335            jac[sample.component][1] += coef * sample.grad[1];
1336        }
1337        jac
1338    }
1339
1340    /// Minimum of `det Dφ_θ` over the widened normalized check grid `[−1.1,
1341    /// 1.1]²` (in `t` coordinates) — the diffeomorphism guard's decision
1342    /// quantity. The grid is built in `t` by inverting the normalization.
1343    pub fn min_jacobian_det_on_grid(&self, theta: &[f64]) -> f64 {
1344        let nodes = PATCH_FLOW_GUARD_NODES_PER_AXIS;
1345        let mut min_det = f64::INFINITY;
1346        for i in 0..nodes {
1347            for j in 0..nodes {
1348                // u ∈ [−1.1, 1.1] per axis; t = center + u / inv_half.
1349                let u0 = -1.1 + 2.2 * i as f64 / (nodes - 1) as f64;
1350                let u1 = -1.1 + 2.2 * j as f64 / (nodes - 1) as f64;
1351                let t = [
1352                    self.center[0] + u0 / self.inv_half[0],
1353                    self.center[1] + u1 / self.inv_half[1],
1354                ];
1355                let jac = self.flow_jacobian(theta, t);
1356                let det = jac[0][0] * jac[1][1] - jac[0][1] * jac[1][0];
1357                min_det = min_det.min(det);
1358            }
1359        }
1360        min_det
1361    }
1362}
1363
1364/// `u^k` for small non-negative integer `k` (avoids `f64::powi`'s branch on the
1365/// hot mode-sampling path; `k ≤ PATCH_FLOW_MAX_DEGREE`).
1366fn pow_u(u: f64, k: usize) -> f64 {
1367    let mut acc = 1.0_f64;
1368    for _ in 0..k {
1369        acc *= u;
1370    }
1371    acc
1372}
1373
1374/// The exact, image-frozen minimum-isometry-defect flow reparameterization of
1375/// one `d = 2` **free/patch** (Euclidean-patch) atom.
1376#[derive(Debug, Clone)]
1377pub struct PatchIsometryFlowReparameterization {
1378    /// Canonical per-row coordinates `t̃_i = φ_θ(t_i)`, shape `(n, 2)`.
1379    pub new_row_coords: Array2<f64>,
1380    /// Recomposed decoder `B̃ = T · B`, shape `(m, p)` — exact LS refit of the
1381    /// original decoded image on the audit grid against the basis at the
1382    /// transported grid (so `γ̃ = γ ∘ φ⁻¹` without ever forming `φ⁻¹`).
1383    pub new_decoder: Array2<f64>,
1384    /// The `(m, m)` basis transport `T` with `Φ(φ(u)) · T ≈ Φ(u)` on the audit
1385    /// grid — the congruence object that transports the smoothness Gram.
1386    pub decoder_transport: Array2<f64>,
1387    /// Optimal flow coefficients `θ` (layout per
1388    /// [`FreePatchFlowBasis::mode_layout`]).
1389    pub flow_theta: Vec<f64>,
1390    /// Isometry defect `E(0)` of the fitted chart (identity flow).
1391    pub defect_initial: f64,
1392    /// Isometry defect `E(θ)` of the canonical chart. Strictly below
1393    /// `defect_initial` (the pass refuses no-improvement flows).
1394    pub defect_final: f64,
1395    /// The profiled global metric scale `c` at the optimum (the canonical
1396    /// chart's pullback metric is `≈ c·ḡ·I` — flat, uniform-speed).
1397    pub profiled_metric_scale: f64,
1398    /// `min det Dφ_θ` on the guard grid. Always `> PATCH_FLOW_DIFFEO_MIN_DET`
1399    /// when `Some(..)` is returned — a folded chart is refused upstream.
1400    pub min_flow_jacobian_det: f64,
1401    /// Max-abs recomposition error on the audit grid, relative to the image
1402    /// scale. Always `≤ CHART_RECOMPOSITION_REL_TOL` when `Some(..)`.
1403    pub recomposition_residual: f64,
1404}
1405
1406/// Compute the minimum-isometry-defect flow reparameterization of a fitted
1407/// `d = 2` **free/patch** atom: the canonical per-row coordinates
1408/// `t̃_i = φ_θ(t_i)` and the exactly-recomposed decoder.
1409///
1410/// This is the #1019 unblocked free-chart case the issue charter calls out: for
1411/// a manifold patch (a contractible Euclidean-patch atom) a **global** truncated
1412/// flow basis DOES exist (no hairy-ball obstruction — [`FreePatchFlowBasis`]),
1413/// so the defect is genuinely MINIMIZED (not merely measured as on the sphere).
1414/// The reference metric is the flat `g_ref = I`, so the canonical chart is the
1415/// uniform-speed (minimum-anisotropy) one. Everything else — the scale-invariant
1416/// isometry defect `E(θ) = Σ_i ‖A_iᵀA_i − c·Ĝ_i‖²_F`, the analytic profiled scale
1417/// `c`, the exact Gauss–Newton, the `det Dφ > δ` diffeomorphism guard, and the
1418/// exact-LS decoder transport with the [`CHART_RECOMPOSITION_REL_TOL`] honesty
1419/// gate — is the SHARED machinery the torus path uses
1420/// ([`minimize_isometry_defect_flow`], [`recompose_decoder_exact_ls`]); see
1421/// [`torus_isometry_flow_reparameterization`] for the full derivation.
1422///
1423/// The residual chart freedom after pinning is the finite isometry group of the
1424/// flat patch with the reference uniform metric: `O(2) ⋉ ℝ²` (rotation +
1425/// reflection + translation) — reported on the certificate as the
1426/// `PinnedByCanonicalization` residual gauge.
1427///
1428/// # Honest refusals (`Ok(None)`, never a lossy or folded swap)
1429///
1430/// * degenerate chart: empty rows/basis, non-finite coordinates, a collapsed
1431///   coordinate box on some axis, or a rank-deficient pullback metric anywhere;
1432/// * the optimizer finds no strict improvement over the identity flow (the
1433///   fitted chart is already minimum-defect within the flow family);
1434/// * every improving candidate violates the diffeomorphism guard;
1435/// * the basis cannot absorb `γ ∘ φ⁻¹` within
1436///   [`CHART_RECOMPOSITION_REL_TOL`] on the audit grid (shared gate).
1437pub fn patch_isometry_flow_reparameterization(
1438    evaluator: &dyn SaeBasisEvaluator,
1439    decoder: ArrayView2<'_, f64>,
1440    row_coords: ArrayView2<'_, f64>,
1441) -> Result<Option<PatchIsometryFlowReparameterization>, String> {
1442    let n = row_coords.nrows();
1443    let m = decoder.nrows();
1444    let p = decoder.ncols();
1445    if row_coords.ncols() != 2 {
1446        return Err(format!(
1447            "patch_isometry_flow_reparameterization: expected (n, 2) row coordinates; got {:?}",
1448            row_coords.dim()
1449        ));
1450    }
1451    if n == 0 || m == 0 || p == 0 {
1452        return Ok(None);
1453    }
1454    let mut lo = [f64::INFINITY; 2];
1455    let mut hi = [f64::NEG_INFINITY; 2];
1456    for row in 0..n {
1457        for axis in 0..2 {
1458            let t = row_coords[[row, axis]];
1459            if !t.is_finite() {
1460                return Ok(None);
1461            }
1462            lo[axis] = lo[axis].min(t);
1463            hi[axis] = hi[axis].max(t);
1464        }
1465    }
1466
1467    // ── Fitted pullback metric, normalized against the flat reference I ──────
1468    let Some((g_rows, g_bar)) = extract_pullback_metric_d2(
1469        "patch_isometry_flow_reparameterization",
1470        evaluator,
1471        decoder,
1472        row_coords,
1473    )?
1474    else {
1475        return Ok(None);
1476    };
1477    let Some((ghat, ghat_norm_sq)) = flat_normalized_metric(&g_rows, g_bar) else {
1478        return Ok(None);
1479    };
1480
1481    // ── Flow basis + per-row mode samples ───────────────────────────────────
1482    let basis = match FreePatchFlowBasis::new(lo, hi) {
1483        Ok(basis) => basis,
1484        // A collapsed patch axis: no honest flat chart — refuse, don't error.
1485        Err(_) => return Ok(None),
1486    };
1487    let q = basis.dim();
1488    let mut row_modes: Vec<Vec<FlowModeSample>> = Vec::with_capacity(n);
1489    for row in 0..n {
1490        row_modes.push(basis.mode_samples([row_coords[[row, 0]], row_coords[[row, 1]]]));
1491    }
1492    // Flat reference ⇒ the whitened base is the identity at every row.
1493    let row_base = vec![[1.0_f64, 0.0, 0.0, 1.0]; n];
1494
1495    // ── Damped Gauss–Newton on θ (shared exact core) ────────────────────────
1496    let Some(minimization) = minimize_isometry_defect_flow(
1497        &row_modes,
1498        &row_base,
1499        &ghat,
1500        ghat_norm_sq,
1501        q,
1502        PATCH_FLOW_DIFFEO_MIN_DET,
1503        &|candidate: &[f64]| basis.min_jacobian_det_on_grid(candidate),
1504    ) else {
1505        return Ok(None);
1506    };
1507    let theta = minimization.theta;
1508    let defect_initial = minimization.defect_initial;
1509    let min_flow_jacobian_det = basis.min_jacobian_det_on_grid(&theta);
1510    if !(min_flow_jacobian_det > PATCH_FLOW_DIFFEO_MIN_DET) {
1511        return Ok(None);
1512    }
1513
1514    // ── Decoder transport on the audit grid spanning the patch box ──────────
1515    let axis_nodes = PATCH_TRANSPORT_MIN_NODES_PER_AXIS.max(3 * (m as f64).sqrt().ceil() as usize);
1516    let grid_rows = axis_nodes * axis_nodes;
1517    let mut grid = Array2::<f64>::zeros((grid_rows, 2));
1518    let mut new_grid = Array2::<f64>::zeros((grid_rows, 2));
1519    for i in 0..axis_nodes {
1520        for j in 0..axis_nodes {
1521            let idx = i * axis_nodes + j;
1522            let u = [
1523                lo[0] + (hi[0] - lo[0]) * i as f64 / (axis_nodes - 1) as f64,
1524                lo[1] + (hi[1] - lo[1]) * j as f64 / (axis_nodes - 1) as f64,
1525            ];
1526            grid[[idx, 0]] = u[0];
1527            grid[[idx, 1]] = u[1];
1528            let mapped = basis.map_point(&theta, u);
1529            new_grid[[idx, 0]] = mapped[0];
1530            new_grid[[idx, 1]] = mapped[1];
1531        }
1532    }
1533    let (grid_phi, grid_jet) = evaluator.evaluate(grid.view())?;
1534    if grid_phi.ncols() != m || grid_jet.dim() != (grid_rows, m, 2) {
1535        return Err(format!(
1536            "patch_isometry_flow_reparameterization: evaluator returned basis {:?} / jet {:?} on the audit grid; expected width {m}, latent_dim 2",
1537            grid_phi.dim(),
1538            grid_jet.dim()
1539        ));
1540    }
1541    let Some(recomposition) =
1542        recompose_decoder_exact_ls(evaluator, decoder, grid_phi.view(), new_grid.view())?
1543    else {
1544        return Ok(None);
1545    };
1546
1547    // ── Canonical per-row coordinates t̃_i = φ_θ(t_i) ────────────────────────
1548    let mut new_row_coords = Array2::<f64>::zeros((n, 2));
1549    for row in 0..n {
1550        let mapped = basis.map_point(&theta, [row_coords[[row, 0]], row_coords[[row, 1]]]);
1551        new_row_coords[[row, 0]] = mapped[0];
1552        new_row_coords[[row, 1]] = mapped[1];
1553    }
1554
1555    Ok(Some(PatchIsometryFlowReparameterization {
1556        new_row_coords,
1557        new_decoder: recomposition.new_decoder,
1558        decoder_transport: recomposition.transport,
1559        flow_theta: theta,
1560        defect_initial,
1561        defect_final: minimization.defect_final,
1562        profiled_metric_scale: minimization.profiled_scale,
1563        min_flow_jacobian_det,
1564        recomposition_residual: recomposition.recomposition_residual,
1565    }))
1566}
1567
1568// ════════════════════════════════════════════════════════════════════════════
1569// #1019 sphere arm — d = 2 sphere (S²) isometry-flow chart canonicalization
1570// ════════════════════════════════════════════════════════════════════════════
1571
1572/// Diffeomorphism floor `δ` for the sphere conformal-boost flow: a candidate
1573/// flow with `det Dφ_θ ≤ δ` anywhere on the data band is rejected, so the
1574/// optimizer never walks through a fold (identical contract to the torus /
1575/// patch floors).
1576pub const SPHERE_FLOW_DIFFEO_MIN_DET: f64 = SAE_FLOW_DIFFEO_MIN_DET;
1577
1578/// Latitude band margin (radians) from each pole inside which the sphere
1579/// conformal-boost flow is well-conditioned. The off-meridian boost generators
1580/// carry a `1/cos lat` longitudinal component, so a chart whose data reaches
1581/// within this margin of a pole (`|lat| > π/2 − margin`) is honestly REFUSED
1582/// rather than pinned with a near-singular flow — the genuine residue of the
1583/// hairy-ball obstruction, scoped to exactly where it bites.
1584pub const SPHERE_FLOW_POLE_MARGIN: f64 = 0.20;
1585
1586/// Identity of one sphere conformal-boost flow mode (for tests / diagnostics):
1587/// which round-sphere axis the boost points along.
1588#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1589pub enum SphereBoostAxis {
1590    /// Zonal boost toward the poles, `cos(lat) ∂_lat` — pole-free in its single
1591    /// (latitude) component, the dominant sphere chart pathology.
1592    Z,
1593    /// Boost toward the `x = (lat 0, lon 0)` point.
1594    X,
1595    /// Boost toward the `y = (lat 0, lon π/2)` point.
1596    Y,
1597}
1598
1599/// The three conformal-boost vector fields on the round `S²` in `(lat, lon)`
1600/// coordinates — the **non-isometric** part of the conformal group, i.e. the
1601/// gradient fields of the degree-1 spherical harmonics `z, x, y`.
1602///
1603/// # Why these three modes, and why they are the right (and complete) pin
1604///
1605/// After the gauge-invariant fit the sphere chart's residual freedom is the
1606/// conformal group of `(S², g_round)`, which is `PSL(2, ℂ)` (6-real-dim): the
1607/// three **rotation** Killing fields (the isometries `O(3)`) plus the three
1608/// **boosts**. The isometry defect `‖g_φ − g_ref‖²` is null on the rotations
1609/// (true isometries) and strictly positive on the boosts (a boost is a
1610/// non-isometric conformal map: it scales the metric by a non-constant
1611/// conformal factor `μ(x) ≠ 1`, compressing one hemisphere into a cap — the
1612/// `d = 2` analogue of the `d = 1` circle-into-`1.0`-rad pathology). So
1613/// minimizing the defect over the **boost** subspace breaks the conformal
1614/// moduli down to the residual isometry group `O(3)` — exactly the finite-dim
1615/// residual the certificate reports. The rotation modes are deliberately
1616/// EXCLUDED: they are defect-null and would only insert null directions into
1617/// the Gauss–Newton system (the same reason torus translations / patch
1618/// rotations are excluded from their flow bases).
1619///
1620/// In `(lat, lon)` the boost fields are (with `s = sin lat`, `c = cos lat`):
1621///
1622/// ```text
1623/// K_z = ( c,                0                 )      (gradient of z = sin lat)
1624/// K_x = ( s·cos lon,       −sin lon / c       )      (gradient of x)
1625/// K_y = ( s·sin lon,        cos lon / c       )      (gradient of y)
1626/// ```
1627///
1628/// `K_z` is globally smooth (pole-free); `K_x, K_y` carry the `1/c`
1629/// longitudinal pole the hairy-ball theorem guarantees somewhere. The flow is
1630/// the same Euler convention as the torus / patch families,
1631/// `φ_θ(t) = t + Σ_k θ_k v_k(t)`, and the band guard ([`SPHERE_FLOW_POLE_MARGIN`])
1632/// keeps every evaluated row away from the `1/c` singularity, so the three
1633/// fields are smooth and bounded on the data and the pin is well-posed.
1634#[derive(Debug, Clone)]
1635pub struct SphereBoostFlowBasis;
1636
1637impl SphereBoostFlowBasis {
1638    /// The three boost modes are always present; the dimension is fixed at 3.
1639    pub fn dim(&self) -> usize {
1640        3
1641    }
1642
1643    /// Mode identities in coefficient order: `[Z, X, Y]`. This IS the `θ`
1644    /// index layout — [`Self::mode_samples`] returns samples in the same order.
1645    pub fn mode_layout(&self) -> [SphereBoostAxis; 3] {
1646        [SphereBoostAxis::Z, SphereBoostAxis::X, SphereBoostAxis::Y]
1647    }
1648
1649    /// The displacement `v_k(t)` of each boost mode at chart point `t`.
1650    fn mode_displacements(t: [f64; 2]) -> [[f64; 2]; 3] {
1651        let (lat, lon) = (t[0], t[1]);
1652        let (s, c) = (lat.sin(), lat.cos());
1653        let (cl, sl) = (lon.cos(), lon.sin());
1654        [
1655            [c, 0.0],          // K_z
1656            [s * cl, -sl / c], // K_x
1657            [s * sl, cl / c],  // K_y
1658        ]
1659    }
1660
1661    /// Jacobian `Dv_k(t)` (`[[∂v0/∂lat, ∂v0/∂lon], [∂v1/∂lat, ∂v1/∂lon]]`) of
1662    /// each boost mode at `t`. With `s = sin lat`, `c = cos lat`,
1663    /// `∂(1/c)/∂lat = s/c²`.
1664    fn mode_jacobians(t: [f64; 2]) -> [[[f64; 2]; 2]; 3] {
1665        let (lat, lon) = (t[0], t[1]);
1666        let (s, c) = (lat.sin(), lat.cos());
1667        let (cl, sl) = (lon.cos(), lon.sin());
1668        let c2 = c * c;
1669        [
1670            // K_z = (c, 0)
1671            [[-s, 0.0], [0.0, 0.0]],
1672            // K_x = (s·cl, −sl/c)
1673            [[c * cl, -s * sl], [-sl * s / c2, -cl / c]],
1674            // K_y = (s·sl, cl/c)
1675            [[c * sl, s * cl], [cl * s / c2, -sl / c]],
1676        ]
1677    }
1678
1679    /// Min `det Dφ_θ` over a band-restricted check grid for the
1680    /// diffeomorphism guard. `φ_θ(t) = t + Σ_k θ_k v_k(t)`, so
1681    /// `Dφ_θ = I + Σ_k θ_k Dv_k`. The grid spans the data latitude band
1682    /// `[lat_lo, lat_hi]` (kept `SPHERE_FLOW_POLE_MARGIN` off each pole) and a
1683    /// full longitude turn.
1684    fn min_jacobian_det_on_band(theta: &[f64], lat_lo: f64, lat_hi: f64) -> f64 {
1685        let nodes = 48usize;
1686        let mut min_det = f64::INFINITY;
1687        for i in 0..nodes {
1688            let lat = lat_lo + (lat_hi - lat_lo) * i as f64 / (nodes - 1) as f64;
1689            for j in 0..nodes {
1690                let lon = -std::f64::consts::PI + std::f64::consts::TAU * j as f64 / nodes as f64;
1691                let jac = Self::mode_jacobians([lat, lon]);
1692                let mut a = [[1.0_f64, 0.0], [0.0, 1.0]];
1693                for (k, dv) in jac.iter().enumerate() {
1694                    a[0][0] += theta[k] * dv[0][0];
1695                    a[0][1] += theta[k] * dv[0][1];
1696                    a[1][0] += theta[k] * dv[1][0];
1697                    a[1][1] += theta[k] * dv[1][1];
1698                }
1699                let det = a[0][0] * a[1][1] - a[0][1] * a[1][0];
1700                min_det = min_det.min(det);
1701            }
1702        }
1703        min_det
1704    }
1705
1706    /// Apply the flow `φ_θ(t) = t + Σ_k θ_k v_k(t)` to one chart point.
1707    fn map_point(theta: &[f64], t: [f64; 2]) -> [f64; 2] {
1708        let disp = Self::mode_displacements(t);
1709        let mut out = t;
1710        for (k, v) in disp.iter().enumerate() {
1711            out[0] += theta[k] * v[0];
1712            out[1] += theta[k] * v[1];
1713        }
1714        out
1715    }
1716}
1717
1718/// The exact, image-frozen conformal-boost flow reparameterization of one
1719/// `d = 2` **sphere** atom: the canonical per-row `(lat, lon)` and the exactly
1720/// recomposed decoder.
1721#[derive(Debug, Clone)]
1722pub struct SphereIsometryFlowReparameterization {
1723    /// Canonical per-row coordinates `t̃_i = φ_θ(t_i)`, shape `(n, 2)`.
1724    pub new_row_coords: Array2<f64>,
1725    /// Recomposed decoder `B̃ = T · B`, shape `(m, p)` — the exact LS refit of
1726    /// the original decoded image on the audit grid against the basis at the
1727    /// transported grid (so `γ̃ = γ ∘ φ⁻¹` without ever forming `φ⁻¹`).
1728    pub new_decoder: Array2<f64>,
1729    /// The `(m, m)` basis transport `T` with `Φ(φ(u)) · T ≈ Φ(u)` on the audit
1730    /// grid — the congruence object the smoothness Gram is transported by.
1731    pub decoder_transport: Array2<f64>,
1732    /// Optimal boost coefficients `θ` (layout `[Z, X, Y]`).
1733    pub flow_theta: Vec<f64>,
1734    /// Round-sphere isometry defect `E(0)` of the fitted chart (identity flow).
1735    pub defect_initial: f64,
1736    /// Round-sphere isometry defect `E(θ)` of the canonical chart. Strictly
1737    /// below `defect_initial` (the pass refuses no-improvement flows).
1738    pub defect_final: f64,
1739    /// `min det Dφ_θ` on the band guard grid. Always `>
1740    /// SPHERE_FLOW_DIFFEO_MIN_DET` when `Some(..)` is returned.
1741    pub min_flow_jacobian_det: f64,
1742    /// Max-abs recomposition error on the audit grid, relative to the image
1743    /// scale. Always `≤ CHART_RECOMPOSITION_REL_TOL` when `Some(..)`.
1744    pub recomposition_residual: f64,
1745}
1746
1747/// Compute the minimum-isometry-defect conformal-boost flow reparameterization
1748/// of a fitted `d = 2` sphere atom against the round-sphere reference metric.
1749///
1750/// # The defect, the reference-whitening reduction, and why the shared core fits
1751///
1752/// The canonical sphere chart is `t̃ = φ(t)` with `γ̃ = γ ∘ φ⁻¹` (image frozen),
1753/// and is round-isometric up to a global scale iff its pullback metric equals
1754/// `c · g_ref(lat) = c · diag(1, cos²lat)`. Measuring on the `φ` side (no
1755/// `Dφ⁻¹`), the chart is canonical iff `Dφ(t)ᵀ g_ref(φ(t)) Dφ(t) ≡ s · G(t)`
1756/// for some global `s` (`G = JᵀJ` the fitted pullback metric). The round
1757/// reference is diagonal, `g_ref(lat) = L(lat)ᵀ L(lat)` with
1758/// `L(lat) = diag(1, cos lat)`, so writing the **whitened** flow Jacobian
1759/// `Ã_i = L(φ(t_i)) · Dφ(t_i)`,
1760///
1761/// ```text
1762/// Dφ_iᵀ g_ref,i Dφ_i = Ã_iᵀ Ã_i ,
1763/// ```
1764///
1765/// the residual `R_i = Ã_iᵀ Ã_i − s · Ĝ_i` is **exactly the flat-reference
1766/// residual** the torus / patch Gauss–Newton core already minimizes — only the
1767/// per-row base changes. At the identity flow `Dφ = I`, `Ã_i = L(lat_i) =
1768/// diag(1, cos lat_i)`, so the shared core is driven with the per-row base
1769/// `A0_i = diag(1, cos lat_i)` (flattened `[1, 0, 0, cos lat_i]`) and the boost
1770/// modes pre-scaled by `L`: a boost displacement adding `δ` to coordinate
1771/// component `a` contributes `L_i[a]·δ` to row `a` of `Ã`. Both boost
1772/// components of each `θ_k` are folded into the base+mode accumulation, so the
1773/// sphere assembles its own per-row `A_i` and Gauss–Newton system here (the
1774/// three boosts each move BOTH components, unlike the single-component
1775/// `FlowModeSample` contract), reusing the residual algebra and the damped,
1776/// fold-guarded accept test in spirit; the profiled scale `s` and the analytic
1777/// `svec` Gauss–Newton are identical to the torus derivation.
1778///
1779/// Whitening by `L(φ(t_i))` (the reference at the MOVED point) rather than
1780/// `L(lat_i)` keeps the measured object the true round-sphere defect of the new
1781/// chart; to first order in `θ` the two agree, and the post-hoc defect
1782/// re-measurement on the committed chart ([`sphere_chart_isometry_defect`])
1783/// plus the strict-improvement gate make the commit exact-or-refused.
1784///
1785/// # Honest refusals (`Ok(None)`)
1786///
1787/// * degenerate chart (empty / non-finite, rank-deficient pullback metric);
1788/// * data within [`SPHERE_FLOW_POLE_MARGIN`] of a pole (the `1/cos lat` boost
1789///   singularity — the scoped hairy-ball residue);
1790/// * the fitted chart is already minimum-defect (no strict improvement);
1791/// * every improving candidate violates the diffeomorphism guard;
1792/// * the basis cannot absorb `γ ∘ φ⁻¹` within [`CHART_RECOMPOSITION_REL_TOL`].
1793pub fn sphere_isometry_flow_reparameterization(
1794    evaluator: &dyn SaeBasisEvaluator,
1795    decoder: ArrayView2<'_, f64>,
1796    row_coords: ArrayView2<'_, f64>,
1797) -> Result<Option<SphereIsometryFlowReparameterization>, String> {
1798    let n = row_coords.nrows();
1799    let m = decoder.nrows();
1800    let p = decoder.ncols();
1801    if row_coords.ncols() != 2 {
1802        return Err(format!(
1803            "sphere_isometry_flow_reparameterization: expected (n, 2) row coordinates; got {:?}",
1804            row_coords.dim()
1805        ));
1806    }
1807    if n == 0 || m == 0 || p == 0 {
1808        return Ok(None);
1809    }
1810    for &t in row_coords.iter() {
1811        if !t.is_finite() {
1812            return Ok(None);
1813        }
1814    }
1815
1816    // ── Pole-band guard: refuse charts reaching into the 1/cos lat singularity ─
1817    let mut lat_lo = f64::INFINITY;
1818    let mut lat_hi = f64::NEG_INFINITY;
1819    for row in 0..n {
1820        let lat = row_coords[[row, 0]];
1821        lat_lo = lat_lo.min(lat);
1822        lat_hi = lat_hi.max(lat);
1823    }
1824    let pole = std::f64::consts::FRAC_PI_2 - SPHERE_FLOW_POLE_MARGIN;
1825    if !(lat_lo > -pole && lat_hi < pole) {
1826        return Ok(None);
1827    }
1828
1829    // ── Fitted pullback metric G_i = J(t_i)ᵀ J(t_i) (raw, NOT flat-normalized:
1830    //    the sphere target is g_ref, not I), with the geometric-mean scale ḡ ───
1831    let Some((g_rows, g_bar)) = extract_pullback_metric_d2(
1832        "sphere_isometry_flow_reparameterization",
1833        evaluator,
1834        decoder,
1835        row_coords,
1836    )?
1837    else {
1838        return Ok(None);
1839    };
1840    // ĝ_i = G_i / ḡ; the round reference target enters per row via the whitened
1841    // base below, so the GN `ghat` here is the scale-normalized fitted metric.
1842    let mut ghat: Vec<[f64; 3]> = Vec::with_capacity(n);
1843    let mut ghat_norm_sq = 0.0_f64;
1844    for g in &g_rows {
1845        let h = [g[0] / g_bar, g[1] / g_bar, g[2] / g_bar];
1846        ghat_norm_sq += h[0] * h[0] + h[1] * h[1] + 2.0 * h[2] * h[2];
1847        ghat.push(h);
1848    }
1849    if !(ghat_norm_sq.is_finite() && ghat_norm_sq > 0.0) {
1850        return Ok(None);
1851    }
1852
1853    // ── Damped Gauss–Newton over the 3 boost coefficients ───────────────────
1854    let q = 3usize;
1855    let Some(minimization) =
1856        sphere_minimize_boost_defect(&ghat, ghat_norm_sq, row_coords, q, lat_lo, lat_hi)
1857    else {
1858        return Ok(None);
1859    };
1860    let theta = minimization.theta;
1861    let defect_initial = minimization.defect_initial;
1862    let min_flow_jacobian_det =
1863        SphereBoostFlowBasis::min_jacobian_det_on_band(&theta, lat_lo, lat_hi);
1864    if !(min_flow_jacobian_det > SPHERE_FLOW_DIFFEO_MIN_DET) {
1865        return Ok(None);
1866    }
1867
1868    // ── Decoder transport on an audit grid spanning the data band ───────────
1869    let axis_nodes = TORUS_TRANSPORT_MIN_NODES_PER_AXIS.max(3 * (m as f64).sqrt().ceil() as usize);
1870    let grid_rows = axis_nodes * axis_nodes;
1871    let mut grid = Array2::<f64>::zeros((grid_rows, 2));
1872    let mut new_grid = Array2::<f64>::zeros((grid_rows, 2));
1873    for i in 0..axis_nodes {
1874        for j in 0..axis_nodes {
1875            let idx = i * axis_nodes + j;
1876            let lat = lat_lo + (lat_hi - lat_lo) * i as f64 / (axis_nodes - 1) as f64;
1877            let lon = -std::f64::consts::PI + std::f64::consts::TAU * j as f64 / axis_nodes as f64;
1878            grid[[idx, 0]] = lat;
1879            grid[[idx, 1]] = lon;
1880            let mapped = SphereBoostFlowBasis::map_point(&theta, [lat, lon]);
1881            new_grid[[idx, 0]] = mapped[0];
1882            new_grid[[idx, 1]] = mapped[1];
1883        }
1884    }
1885    let (grid_phi, grid_jet) = evaluator.evaluate(grid.view())?;
1886    if grid_phi.ncols() != m || grid_jet.dim() != (grid_rows, m, 2) {
1887        return Err(format!(
1888            "sphere_isometry_flow_reparameterization: evaluator returned basis {:?} / jet {:?} on the audit grid; expected width {m}, latent_dim 2",
1889            grid_phi.dim(),
1890            grid_jet.dim()
1891        ));
1892    }
1893    let Some(recomposition) =
1894        recompose_decoder_exact_ls(evaluator, decoder, grid_phi.view(), new_grid.view())?
1895    else {
1896        return Ok(None);
1897    };
1898
1899    // ── Canonical per-row coordinates t̃_i = φ_θ(t_i) ────────────────────────
1900    let mut new_row_coords = Array2::<f64>::zeros((n, 2));
1901    for row in 0..n {
1902        let mapped =
1903            SphereBoostFlowBasis::map_point(&theta, [row_coords[[row, 0]], row_coords[[row, 1]]]);
1904        new_row_coords[[row, 0]] = mapped[0];
1905        new_row_coords[[row, 1]] = mapped[1];
1906    }
1907
1908    Ok(Some(SphereIsometryFlowReparameterization {
1909        new_row_coords,
1910        new_decoder: recomposition.new_decoder,
1911        decoder_transport: recomposition.transport,
1912        flow_theta: theta,
1913        defect_initial,
1914        defect_final: minimization.defect_final,
1915        min_flow_jacobian_det,
1916        recomposition_residual: recomposition.recomposition_residual,
1917    }))
1918}
1919
1920/// Outcome of the sphere boost-defect minimizer.
1921struct SphereFlowMinimization {
1922    theta: Vec<f64>,
1923    defect_initial: f64,
1924    defect_final: f64,
1925}
1926
1927/// Per-row whitened flow Jacobian `Ã_i = L(φ(t_i)) · Dφ_θ(t_i)` (row-major
1928/// `[a00, a01, a10, a11]`) and the round-sphere defect at `θ`.
1929///
1930/// `Dφ_θ(t) = I + Σ_k θ_k Dv_k(t)`; whitening by `L(lat̃) = diag(1, cos lat̃)`
1931/// at the moved latitude `lat̃ = φ_θ(t)[0]` realizes the reference metric (see
1932/// [`sphere_isometry_flow_reparameterization`] for the reduction). Returns
1933/// `None` when the profiled scale degenerates or any whitened latitude leaves
1934/// the valid band (`cos lat̃ ≤ 0`).
1935fn sphere_eval_boost_defect(
1936    theta: &[f64],
1937    row_coords: ArrayView2<'_, f64>,
1938    ghat: &[[f64; 3]],
1939    ghat_norm_sq: f64,
1940) -> Option<FlowObjectiveState> {
1941    let n = row_coords.nrows();
1942    let mut a_rows: Vec<[f64; 4]> = Vec::with_capacity(n);
1943    let mut cross = 0.0_f64;
1944    for row in 0..n {
1945        let t = [row_coords[[row, 0]], row_coords[[row, 1]]];
1946        let jac = SphereBoostFlowBasis::mode_jacobians(t);
1947        // Dφ = I + Σ θ_k Dv_k.
1948        let mut dphi = [[1.0_f64, 0.0], [0.0, 1.0]];
1949        for (k, dv) in jac.iter().enumerate() {
1950            dphi[0][0] += theta[k] * dv[0][0];
1951            dphi[0][1] += theta[k] * dv[0][1];
1952            dphi[1][0] += theta[k] * dv[1][0];
1953            dphi[1][1] += theta[k] * dv[1][1];
1954        }
1955        // Whiten by L(lat̃) at the moved latitude.
1956        let mapped = SphereBoostFlowBasis::map_point(theta, t);
1957        let cos_lat_new = mapped[0].cos();
1958        // Guard against the `1/cos lat` singularity in the lon component of the
1959        // whitened Jacobian `Ã = L(lat̃)·Dφ`.  `cos(π/2)` in f64 is ~6.1e-17, not
1960        // exactly 0, so a bare `> 0.0` lets a pole-adjacent row through and then
1961        // multiplies `dphi[1, .]` by an effectively-zero factor, corrupting `Ã` and
1962        // the GN system.  Mirror the floor used in `sphere_chart_isometry_defect`
1963        // (`POLE_COS2_FLOOR = 1e-12` on cos²lat ↔ `|cos lat| > 1e-6`).
1964        const SPHERE_EVAL_COS_FLOOR: f64 = 1.0e-6;
1965        if !(cos_lat_new.is_finite() && cos_lat_new > SPHERE_EVAL_COS_FLOOR) {
1966            return None;
1967        }
1968        let a = [
1969            dphi[0][0],
1970            dphi[0][1],
1971            cos_lat_new * dphi[1][0],
1972            cos_lat_new * dphi[1][1],
1973        ];
1974        a_rows.push(a);
1975    }
1976    for (a, g) in a_rows.iter().zip(ghat.iter()) {
1977        let m00 = a[0] * a[0] + a[2] * a[2];
1978        let m11 = a[1] * a[1] + a[3] * a[3];
1979        let m01 = a[0] * a[1] + a[2] * a[3];
1980        cross += m00 * g[0] + m11 * g[1] + 2.0 * m01 * g[2];
1981    }
1982    let scale = cross / ghat_norm_sq;
1983    if !(scale.is_finite() && scale > 0.0) {
1984        return None;
1985    }
1986    let mut defect = 0.0_f64;
1987    for (a, g) in a_rows.iter().zip(ghat.iter()) {
1988        let m00 = a[0] * a[0] + a[2] * a[2];
1989        let m11 = a[1] * a[1] + a[3] * a[3];
1990        let m01 = a[0] * a[1] + a[2] * a[3];
1991        let r00 = m00 - scale * g[0];
1992        let r11 = m11 - scale * g[1];
1993        let r01 = m01 - scale * g[2];
1994        defect += r00 * r00 + r11 * r11 + 2.0 * r01 * r01;
1995    }
1996    if !defect.is_finite() {
1997        return None;
1998    }
1999    Some(FlowObjectiveState {
2000        defect,
2001        scale,
2002        a_rows,
2003    })
2004}
2005
2006/// Damped Gauss–Newton on the 3 sphere conformal-boost coefficients. The
2007/// residual `svec(Ã_iᵀÃ_i − s·Ĝ_i)` and its `θ`-Jacobian are formed by central
2008/// finite differences of the whitened per-row `Ã_i` (the whitening composes the
2009/// boost Jacobian with the moved-latitude `cos`, so an analytic `∂Ã/∂θ` is a
2010/// chain rule the FD evaluates exactly to step order) — the SAME Levenberg
2011/// damping + strict-descent + diffeomorphism accept test as the torus / patch
2012/// core. Starts at `θ = 0` (`Dφ = I`); only fold-free strict-descent candidates
2013/// are accepted. Returns `None` (honest skip) when the identity chart is already
2014/// round-isometric or no strict improvement is reachable.
2015fn sphere_minimize_boost_defect(
2016    ghat: &[[f64; 3]],
2017    ghat_norm_sq: f64,
2018    row_coords: ArrayView2<'_, f64>,
2019    q: usize,
2020    lat_lo: f64,
2021    lat_hi: f64,
2022) -> Option<SphereFlowMinimization> {
2023    let n = row_coords.nrows();
2024    let mut theta = vec![0.0_f64; q];
2025    let mut state = sphere_eval_boost_defect(&theta, row_coords, ghat, ghat_norm_sq)?;
2026    let defect_initial = state.defect;
2027    if !(defect_initial > 0.0) {
2028        return None;
2029    }
2030    let sqrt2 = std::f64::consts::SQRT_2;
2031    // FD-OK: FD-audit certificate of the analytic chart-mode Jacobian (central-difference residual Jacobian for the GN flow)
2032    let fd_h = 1.0e-6_f64; // fd-ok: FD Jacobian for sphere-boost Gauss-Newton; analytic Jacobian requires per-row product differentials, FD bounded by convergence guard
2033    let mut lambda = 1.0e-4_f64;
2034    let mut any_accepted = false;
2035    for iteration in 0..TORUS_FLOW_GN_MAX_ITERS {
2036        if iteration + 1 == TORUS_FLOW_GN_MAX_ITERS {
2037            break;
2038        }
2039        // Residual r(θ) = svec(Ã_iᵀÃ_i − s·Ĝ_i), and its θ-Jacobian by central
2040        // FD of the whitened per-row residual (scale s held at its profiled
2041        // value — envelope theorem, exactly as the analytic torus core).
2042        let mut jmat = Array2::<f64>::zeros((3 * n, q));
2043        let mut rcol = Array2::<f64>::zeros((3 * n, 1));
2044        let scale = state.scale;
2045        for (i, (a, g)) in state.a_rows.iter().zip(ghat.iter()).enumerate() {
2046            let m00 = a[0] * a[0] + a[2] * a[2];
2047            let m11 = a[1] * a[1] + a[3] * a[3];
2048            let m01 = a[0] * a[1] + a[2] * a[3];
2049            rcol[[3 * i, 0]] = m00 - scale * g[0];
2050            rcol[[3 * i + 1, 0]] = m11 - scale * g[1];
2051            rcol[[3 * i + 2, 0]] = sqrt2 * (m01 - scale * g[2]);
2052        }
2053        for k in 0..q {
2054            let mut tp = theta.clone();
2055            let mut tm = theta.clone();
2056            tp[k] += fd_h; // fd-ok: FD Jacobian for sphere-boost Gauss-Newton; analytic Jacobian requires per-row product differentials, FD bounded by convergence guard
2057            tm[k] -= fd_h; // fd-ok: FD Jacobian for sphere-boost Gauss-Newton; analytic Jacobian requires per-row product differentials, FD bounded by convergence guard
2058            let sp = sphere_eval_boost_defect(&tp, row_coords, ghat, ghat_norm_sq);
2059            let sm = sphere_eval_boost_defect(&tm, row_coords, ghat, ghat_norm_sq);
2060            let (Some(sp), Some(sm)) = (sp, sm) else {
2061                // A perturbation left the valid band — abandon this GN step.
2062                return if any_accepted {
2063                    Some(SphereFlowMinimization {
2064                        theta,
2065                        defect_initial,
2066                        defect_final: state.defect,
2067                    })
2068                } else {
2069                    None
2070                };
2071            };
2072            for (i, (ap, am)) in sp.a_rows.iter().zip(sm.a_rows.iter()).enumerate() {
2073                let mp00 = ap[0] * ap[0] + ap[2] * ap[2] - scale * ghat[i][0];
2074                let mp11 = ap[1] * ap[1] + ap[3] * ap[3] - scale * ghat[i][1];
2075                let mp01 = ap[0] * ap[1] + ap[2] * ap[3] - scale * ghat[i][2];
2076                let mm00 = am[0] * am[0] + am[2] * am[2] - scale * ghat[i][0];
2077                let mm11 = am[1] * am[1] + am[3] * am[3] - scale * ghat[i][1];
2078                let mm01 = am[0] * am[1] + am[2] * am[3] - scale * ghat[i][2];
2079                jmat[[3 * i, k]] = (mp00 - mm00) / (2.0 * fd_h); // fd-ok: FD Jacobian for sphere-boost Gauss-Newton; analytic Jacobian requires per-row product differentials, FD bounded by convergence guard
2080                jmat[[3 * i + 1, k]] = (mp11 - mm11) / (2.0 * fd_h); // fd-ok: FD Jacobian for sphere-boost Gauss-Newton; analytic Jacobian requires per-row product differentials, FD bounded by convergence guard
2081                jmat[[3 * i + 2, k]] = sqrt2 * (mp01 - mm01) / (2.0 * fd_h); // fd-ok: FD Jacobian for sphere-boost Gauss-Newton; analytic Jacobian requires per-row product differentials, FD bounded by convergence guard
2082            }
2083        }
2084        // END-FD-OK
2085        let jtj = fast_ata(&jmat);
2086        let jtr = fast_atb(&jmat, &rcol);
2087
2088        let mut rejects = 0usize;
2089        let mut accepted_step = false;
2090        let mut converged = false;
2091        let mut step_norm_sq = 0.0_f64;
2092        while rejects < TORUS_FLOW_GN_MAX_REJECTS {
2093            let mut damped = jtj.clone();
2094            for d in 0..q {
2095                damped[[d, d]] += lambda * (1.0 + jtj[[d, d]]);
2096            }
2097            let factor = match damped.cholesky(FaerSide::Lower) {
2098                Ok(factor) => factor,
2099                Err(_) => {
2100                    lambda *= 10.0;
2101                    rejects += 1;
2102                    continue;
2103                }
2104            };
2105            let mut neg_jtr = jtr.clone();
2106            neg_jtr.mapv_inplace(|v| -v);
2107            let delta = factor.solve_mat(&neg_jtr);
2108            let mut candidate = theta.clone();
2109            step_norm_sq = 0.0;
2110            for k in 0..q {
2111                candidate[k] += delta[[k, 0]];
2112                step_norm_sq += delta[[k, 0]] * delta[[k, 0]];
2113            }
2114            let folded = SphereBoostFlowBasis::min_jacobian_det_on_band(&candidate, lat_lo, lat_hi)
2115                <= SPHERE_FLOW_DIFFEO_MIN_DET;
2116            let candidate_state = if folded {
2117                None
2118            } else {
2119                sphere_eval_boost_defect(&candidate, row_coords, ghat, ghat_norm_sq)
2120            };
2121            match candidate_state {
2122                Some(next) if next.defect < state.defect => {
2123                    let improvement = state.defect - next.defect;
2124                    theta = candidate;
2125                    state = next;
2126                    any_accepted = true;
2127                    accepted_step = true;
2128                    lambda = (lambda / 10.0).max(1.0e-12);
2129                    if improvement <= 1.0e-14 * (1.0 + state.defect) {
2130                        converged = true;
2131                    }
2132                    break;
2133                }
2134                Some(..) | None => {
2135                    lambda *= 10.0;
2136                    rejects += 1;
2137                }
2138            }
2139        }
2140        if !accepted_step || converged {
2141            break;
2142        }
2143        let theta_norm_sq: f64 = theta.iter().map(|v| v * v).sum();
2144        if step_norm_sq <= 1.0e-24 * (1.0 + theta_norm_sq) {
2145            break;
2146        }
2147    }
2148    if !any_accepted || !(state.defect < defect_initial) {
2149        return None;
2150    }
2151    Some(SphereFlowMinimization {
2152        theta,
2153        defect_initial,
2154        defect_final: state.defect,
2155    })
2156}
2157
2158/// Scale-invariant isometry defect of a fitted `d = 2` **sphere** atom's
2159/// `(lat, lon)` chart against the round-sphere reference metric (#1019 stage 2,
2160/// sphere arm).
2161///
2162/// This is the certified objective the sphere flow-pin
2163/// ([`sphere_isometry_flow_reparameterization`]) descends; it is also exposed
2164/// on its own as the read-only acceptance measurement (the issue's "defect
2165/// within 10% of optimum" quantity), so a chart that is already round-isometric
2166/// (a true `O(3)` representative) scores `≈ 0` and a warped chart scores large.
2167///
2168/// # The defect functional
2169///
2170/// For the round sphere, the `(lat, lon)` chart's reference first fundamental
2171/// form is `g_ref(lat) = diag(1, cos²lat)` (a unit-radius sphere; the lon
2172/// circumference shrinks as `cos lat`). The fitted decoder's pullback metric at
2173/// row `i` is `G_i = J(t_i)ᵀ J(t_i)` from the exact `(Φ, ∂Φ)` jet, exactly as
2174/// in the torus path. The chart is isometric to the round sphere up to a global
2175/// scale `c` iff `G_i ≡ c · g_ref(lat_i)` for all `i`. Measuring the residual
2176/// with `c` analytically profiled (the exact argmin over the global scale),
2177///
2178/// ```text
2179/// E = Σ_i ‖ Ĝ_i − c · ĝ_ref,i ‖²_F ,
2180/// c = Σ_i ⟨Ĝ_i, ĝ_ref,i⟩_F / Σ_i ‖ĝ_ref,i‖²_F ,
2181/// ```
2182///
2183/// where both metrics are normalized by the geometric-mean fitted metric scale
2184/// `ḡ = exp(mean_i ½ log det G_i)` so the defect is scale-invariant (a chart
2185/// isometric up to ANY global scale scores 0). The Frobenius norm on symmetric
2186/// `2×2` matrices uses the `[m00, m11, m01]` storage with the off-diagonal
2187/// weighted by 2 (matching the torus path).
2188///
2189/// Returns `None` on a degenerate chart (empty/non-finite, rank-deficient
2190/// pullback metric anywhere, or a degenerate profiled scale) — an honest
2191/// refusal, never a fabricated zero. The returned defect is the scale-invariant
2192/// `E` above; `0` means the fitted chart is already a round-isometric `O(3)`
2193/// representative.
2194pub fn sphere_chart_isometry_defect(
2195    evaluator: &dyn SaeBasisEvaluator,
2196    decoder: ArrayView2<'_, f64>,
2197    row_coords: ArrayView2<'_, f64>,
2198) -> Result<Option<f64>, String> {
2199    let n = row_coords.nrows();
2200    let m = decoder.nrows();
2201    let p = decoder.ncols();
2202    if row_coords.ncols() != 2 {
2203        return Err(format!(
2204            "sphere_chart_isometry_defect: expected (n, 2) row coordinates; got {:?}",
2205            row_coords.dim()
2206        ));
2207    }
2208    if n == 0 || m == 0 || p == 0 {
2209        return Ok(None);
2210    }
2211    for &t in row_coords.iter() {
2212        if !t.is_finite() {
2213            return Ok(None);
2214        }
2215    }
2216
2217    // Fitted pullback metric G_i = J(t_i)ᵀJ(t_i) from the exact jet — identical
2218    // extraction to the torus / patch paths (axis 0 = lat, axis 1 = lon), via
2219    // the shared helper. The sphere reference below differs (diag(1, cos²lat)
2220    // vs flat I), so only the extraction is shared, not the normalization.
2221    let Some((g_rows, g_bar)) = extract_pullback_metric_d2(
2222        "sphere_chart_isometry_defect",
2223        evaluator,
2224        decoder,
2225        row_coords,
2226    )?
2227    else {
2228        return Ok(None);
2229    };
2230
2231    // Reference metric ĝ_ref,i = diag(1, cos²lat_i) (round sphere, lat = axis 0).
2232    // Both G and g_ref are normalized by ḡ; g_ref carries no fitted scale so the
2233    // profiled `c` absorbs the absolute size. The reference's own determinant is
2234    // cos²lat, which can vanish near the poles — guard it so a pole-adjacent row
2235    // does not inject a degenerate reference direction.
2236    let mut ghat: Vec<[f64; 3]> = Vec::with_capacity(n);
2237    let mut gref: Vec<[f64; 3]> = Vec::with_capacity(n);
2238    let mut gref_norm_sq = 0.0_f64;
2239    let mut cross = 0.0_f64;
2240    for (row, g) in g_rows.iter().enumerate() {
2241        let lat = row_coords[[row, 0]];
2242        let cos_lat = lat.cos();
2243        let r11 = cos_lat * cos_lat;
2244        // A pole-adjacent reference column (cos lat → 0) carries no transverse
2245        // metric content; treating it as part of the defect would falsely
2246        // reward squeezing the lon direction. Refuse charts sitting on the
2247        // pole singularity rather than fabricate a defect there.
2248        //
2249        // NB: `cos(π/2)` is ~6.1e-17 in f64, not exactly 0, so an exactly-on-pole
2250        // row gives `r11 = cos²lat ≈ 3.7e-33` — finite and strictly positive. A
2251        // bare `r11 > 0.0` therefore lets it through; floor against `POLE_COS2_FLOOR`
2252        // (cos lat within ~1e-6 of a pole) so the singular row is honestly refused.
2253        const POLE_COS2_FLOOR: f64 = 1e-12;
2254        if !(r11.is_finite() && r11 > POLE_COS2_FLOOR) {
2255            return Ok(None);
2256        }
2257        let h = [g[0] / g_bar, g[1] / g_bar, g[2] / g_bar];
2258        let r = [1.0_f64, r11, 0.0_f64];
2259        cross += h[0] * r[0] + h[1] * r[1] + 2.0 * h[2] * r[2];
2260        gref_norm_sq += r[0] * r[0] + r[1] * r[1] + 2.0 * r[2] * r[2];
2261        ghat.push(h);
2262        gref.push(r);
2263    }
2264    if !(gref_norm_sq.is_finite() && gref_norm_sq > 0.0) {
2265        return Ok(None);
2266    }
2267    let c = cross / gref_norm_sq;
2268    if !(c.is_finite() && c > 0.0) {
2269        return Ok(None);
2270    }
2271    let mut defect = 0.0_f64;
2272    for (h, r) in ghat.iter().zip(gref.iter()) {
2273        let r00 = h[0] - c * r[0];
2274        let r11 = h[1] - c * r[1];
2275        let r01 = h[2] - c * r[2];
2276        defect += r00 * r00 + r11 * r11 + 2.0 * r01 * r01;
2277    }
2278    if !defect.is_finite() {
2279        return Ok(None);
2280    }
2281    Ok(Some(defect))
2282}
2283
2284/// Total fitted turning `Θ = ∫ κ ds` of a `d = 1` atom's decoded curve (#1026).
2285///
2286/// # Why this is the discriminating measurement
2287///
2288/// The hybrid-vs-shatter question — does a curved atom genuinely earn its
2289/// curvature, or is it just more linear directions in disguise — is answered by
2290/// pairing each atom's EV contribution with its fitted **turning** `Θ`
2291/// (integrated curvature). A linear SAE shatters a curved feature of total
2292/// turning `Θ` into `N(ε) ≈ Θ/(2√(2ε))` rank-1 atoms at relative error `ε`
2293/// (radius cancels — relative error is scale-free), so the curved win is
2294/// concentrated on high-`Θ` features and vanishes as `Θ → 0`. A near-linear
2295/// atom (`Θ ≈ 0`) contributing EV is a linear direction wearing a curved basis;
2296/// a high-`Θ` atom contributing EV is a genuine curved family. Reporting EV-per-
2297/// atom vs `Θ` (not EV vs K) directly shows which.
2298///
2299/// # The functional
2300///
2301/// For the decoded curve `γ(t) = Φ(t)·B` the unsigned total curvature is
2302///
2303/// ```text
2304/// Θ = ∫ κ(t) ‖γ'(t)‖ dt ,   κ = ‖γ'(t) ∧ γ''(t)‖ / ‖γ'(t)‖³ ,
2305/// ```
2306///
2307/// so `Θ = ∫ ‖γ' ∧ γ''‖ / ‖γ'‖² dt`, where the wedge norm in `ℝ^p` is the
2308/// parallelogram area `‖γ'∧γ''‖ = √(‖γ'‖²‖γ''‖² − ⟨γ',γ''⟩²)` (Lagrange). `Θ`
2309/// is reparameterization-invariant (it is an integral of `κ ds`), so it is the
2310/// honest chart-free geometric content. Integrated by Simpson's rule over a
2311/// uniform grid spanning the fitted coordinate range from the exact `(γ', γ'')`
2312/// jets. Units: radians of total turning (a full hue circle ≈ `2π`).
2313///
2314/// Returns `None` on a degenerate atom (no rows, no second jet, a collapsed
2315/// coordinate range, or a non-finite integrand) — an honest refusal, never a
2316/// fabricated number.
2317pub fn d1_atom_fitted_turning(
2318    evaluator: &dyn SaeBasisEvaluator,
2319    decoder: ArrayView2<'_, f64>,
2320    row_coords: ArrayView1<'_, f64>,
2321) -> Result<Option<f64>, String> {
2322    let m = decoder.nrows();
2323    let p = decoder.ncols();
2324    if m == 0 || p == 0 || row_coords.is_empty() {
2325        return Ok(None);
2326    }
2327    let mut lo = f64::INFINITY;
2328    let mut hi = f64::NEG_INFINITY;
2329    for &t in row_coords.iter() {
2330        if !t.is_finite() {
2331            return Ok(None);
2332        }
2333        lo = lo.min(t);
2334        hi = hi.max(t);
2335    }
2336    if !(hi > lo) {
2337        return Ok(None);
2338    }
2339    let cells = TURNING_QUADRATURE_CELLS;
2340    let nodes = 2 * cells + 1; // node, midpoint, node per cell (Simpson)
2341    let h = (hi - lo) / (nodes - 1) as f64;
2342    let mut grid = Array2::<f64>::zeros((nodes, 1));
2343    for (i, mut row) in grid.outer_iter_mut().enumerate() {
2344        row[0] = lo + h * i as f64;
2345    }
2346    let (_phi, jet) = evaluator.evaluate(grid.view())?;
2347    if jet.dim() != (nodes, m, 1) {
2348        return Err(format!(
2349            "d1_atom_fitted_turning: evaluator returned jet {:?}; expected ({nodes}, {m}, 1)",
2350            jet.dim()
2351        ));
2352    }
2353    let Some(hess_result) = evaluator.second_jet_dyn(grid.view()) else {
2354        // No analytic second jet for this basis family → no honest turning.
2355        return Ok(None);
2356    };
2357    let hess = hess_result?;
2358    if hess.dim() != (nodes, m, 1, 1) {
2359        return Err(format!(
2360            "d1_atom_fitted_turning: second_jet returned {:?}; expected ({nodes}, {m}, 1, 1)",
2361            hess.dim()
2362        ));
2363    }
2364    // Per-node curvature integrand f(t) = ‖γ'∧γ''‖/‖γ'‖² (= κ·‖γ'‖).
2365    let mut integrand = vec![0.0_f64; nodes];
2366    // Distinguish a GLOBALLY constant image (every node stationary → a degenerate
2367    // point with total turning exactly 0) from a curve that is stationary at only
2368    // SOME nodes (a genuine cusp whose turning is ill-defined there, refused).
2369    let mut any_moving = false;
2370    let mut any_stationary = false;
2371    let mut g1 = vec![0.0_f64; p];
2372    let mut g2 = vec![0.0_f64; p];
2373    for node in 0..nodes {
2374        for slot in g1.iter_mut() {
2375            *slot = 0.0;
2376        }
2377        for slot in g2.iter_mut() {
2378            *slot = 0.0;
2379        }
2380        for bm in 0..m {
2381            let d1 = jet[[node, bm, 0]];
2382            let d2 = hess[[node, bm, 0, 0]];
2383            if d1 == 0.0 && d2 == 0.0 {
2384                continue;
2385            }
2386            for j in 0..p {
2387                let b = decoder[[bm, j]];
2388                g1[j] += d1 * b;
2389                g2[j] += d2 * b;
2390            }
2391        }
2392        let mut n1 = 0.0_f64; // ‖γ'‖²
2393        let mut n2 = 0.0_f64; // ‖γ''‖²
2394        let mut dot = 0.0_f64; // ⟨γ',γ''⟩
2395        for j in 0..p {
2396            n1 += g1[j] * g1[j];
2397            n2 += g2[j] * g2[j];
2398            dot += g1[j] * g2[j];
2399        }
2400        if !(n1 > 0.0) {
2401            // Zero speed at this node: the arc-length measure `ds = ‖γ'‖dt`
2402            // vanishes here, so this node contributes zero turning regardless of
2403            // κ. Record it and continue; the all-stationary (constant image) vs
2404            // mixed (cusp) cases are resolved after the loop.
2405            any_stationary = true;
2406            integrand[node] = 0.0;
2407            continue;
2408        }
2409        any_moving = true;
2410        // Wedge norm² = ‖γ'‖²‖γ''‖² − ⟨γ',γ''⟩² (Lagrange identity); clamp tiny
2411        // negative round-off to 0 before the sqrt.
2412        let raw_wedge_sq = n1 * n2 - dot * dot;
2413        let roundoff_floor = 64.0 * f64::EPSILON * (n1 * n2).abs().max(dot.abs() * dot.abs());
2414        let wedge_sq = if raw_wedge_sq <= roundoff_floor {
2415            0.0
2416        } else {
2417            raw_wedge_sq
2418        };
2419        integrand[node] = wedge_sq.sqrt() / n1;
2420        if !integrand[node].is_finite() {
2421            return Ok(None);
2422        }
2423    }
2424    if !any_moving {
2425        // The decoded image never moves over the coordinate span: a degenerate
2426        // single point (e.g. a `d = 1` atom straightened to its constant DC
2427        // component). A point has no arc to turn through, so its total turning is
2428        // exactly 0 — the ultimate linear-tail signature — not "undefined".
2429        return Ok(Some(0.0));
2430    }
2431    if any_stationary {
2432        // Stationary at SOME nodes but moving at others: a cusp where the turning
2433        // integrand is genuinely ill-defined. Refuse rather than under-count the
2434        // sharp turn (the historical conservative behavior for a partial cusp).
2435        return Ok(None);
2436    }
2437    // Composite Simpson over `cells` cells: each cell [2i, 2i+1, 2i+2] gets
2438    // (h/3)(f0 + 4 f_mid + f1).
2439    let mut theta = 0.0_f64;
2440    for cell in 0..cells {
2441        let f0 = integrand[2 * cell];
2442        let fm = integrand[2 * cell + 1];
2443        let f1 = integrand[2 * cell + 2];
2444        theta += h / 3.0 * (f0 + 4.0 * fm + f1);
2445    }
2446    if !(theta.is_finite() && theta >= 0.0) {
2447        return Ok(None);
2448    }
2449    Ok(Some(theta))
2450}
2451
2452#[cfg(test)]
2453mod patch_flow_tests {
2454    use super::*;
2455    use ndarray::{Array2, Array3, ArrayView2};
2456
2457    /// Mock free-patch evaluator with the affine basis `Φ(t) = [1, t₀, t₁]`
2458    /// (`m = 3`) and its exact jet. The decoder plants the affine decoded image
2459    /// `γ(t) = M·t` (a constant anisotropic warp), so the fitted pullback metric
2460    /// is the constant `G = MᵀM` everywhere — an anisotropically warped flat
2461    /// chart. The affine basis is closed under affine reparameterization, so the
2462    /// linear flow modes can transport the image exactly (the recomposition gate
2463    /// passes), and the minimizer recovers `Dφ ≈ M⁻¹` (up to a flat O(2)
2464    /// isometry), driving the anisotropy defect to ≈ 0.
2465    #[derive(Debug)]
2466    struct MockPatchEvaluator;
2467
2468    impl SaeBasisEvaluator for MockPatchEvaluator {
2469        fn evaluate(
2470            &self,
2471            coords: ArrayView2<'_, f64>,
2472        ) -> Result<(Array2<f64>, Array3<f64>), String> {
2473            let n = coords.nrows();
2474            let mut phi = Array2::<f64>::zeros((n, 3));
2475            let mut jet = Array3::<f64>::zeros((n, 3, 2));
2476            for row in 0..n {
2477                let t0 = coords[[row, 0]];
2478                let t1 = coords[[row, 1]];
2479                phi[[row, 0]] = 1.0;
2480                phi[[row, 1]] = t0;
2481                phi[[row, 2]] = t1;
2482                // ∂Φ/∂t₀ = [0, 1, 0]; ∂Φ/∂t₁ = [0, 0, 1].
2483                jet[[row, 1, 0]] = 1.0;
2484                jet[[row, 2, 1]] = 1.0;
2485            }
2486            Ok((phi, jet))
2487        }
2488
2489        fn second_jet_dyn(
2490            &self,
2491            coords: ArrayView2<'_, f64>,
2492        ) -> Option<Result<ndarray::Array4<f64>, String>> {
2493            // Affine basis: second jet is exactly zero. Return it honestly
2494            // (shape (n, 3, 2, 2)) after validating the coordinate width.
2495            if coords.ncols() != 2 {
2496                return Some(Err(format!(
2497                    "MockPatchEvaluator::second_jet_dyn: expected 2 cols, got {}",
2498                    coords.ncols()
2499                )));
2500            }
2501            Some(Ok(ndarray::Array4::<f64>::zeros((coords.nrows(), 3, 2, 2))))
2502        }
2503
2504        fn third_jet_dyn(
2505            &self,
2506            coords: ArrayView2<'_, f64>,
2507        ) -> Option<Result<ndarray::Array5<f64>, String>> {
2508            if coords.ncols() != 2 {
2509                return Some(Err(format!(
2510                    "MockPatchEvaluator::third_jet_dyn: expected 2 cols, got {}",
2511                    coords.ncols()
2512                )));
2513            }
2514            Some(Ok(ndarray::Array5::<f64>::zeros((
2515                coords.nrows(),
2516                3,
2517                2,
2518                2,
2519                2,
2520            ))))
2521        }
2522    }
2523
2524    /// Decoder for `γ(t) = M·t`: basis `[1, t₀, t₁]` ↦ outputs `(γ₀, γ₁)`.
2525    /// Row 0 (const) = 0; row 1 (t₀) = (M₀₀, M₁₀); row 2 (t₁) = (M₀₁, M₁₁).
2526    fn warp_decoder(m: [[f64; 2]; 2]) -> Array2<f64> {
2527        let mut b = Array2::<f64>::zeros((3, 2));
2528        b[[1, 0]] = m[0][0];
2529        b[[1, 1]] = m[1][0];
2530        b[[2, 0]] = m[0][1];
2531        b[[2, 1]] = m[1][1];
2532        b
2533    }
2534
2535    /// A grid of fitted coordinates over the patch box `[0, 1]²`.
2536    fn patch_coords() -> Array2<f64> {
2537        let g = 9usize;
2538        let mut c = Array2::<f64>::zeros((g * g, 2));
2539        for i in 0..g {
2540            for j in 0..g {
2541                let row = i * g + j;
2542                c[[row, 0]] = i as f64 / (g - 1) as f64;
2543                c[[row, 1]] = j as f64 / (g - 1) as f64;
2544            }
2545        }
2546        c
2547    }
2548
2549    /// Scale-invariant anisotropy defect of a constant pullback metric `G`
2550    /// against the flat reference — the optimum the minimizer chases is 0.
2551    fn flat_defect_of_constant_metric(g: [f64; 3], n: usize) -> f64 {
2552        // ḡ = sqrt(det G); Ĝ = G/ḡ; profile c against I; sum over n identical
2553        // rows. With one constant metric the per-row defect is identical.
2554        let det = g[0] * g[1] - g[2] * g[2];
2555        let g_bar = det.sqrt();
2556        let h = [g[0] / g_bar, g[1] / g_bar, g[2] / g_bar];
2557        // c = <Ĝ, I> / <I, I> = (h00 + h11) / 2 ; residual = Ĝ − c·I.
2558        let c = 0.5 * (h[0] + h[1]);
2559        let r00 = h[0] - c;
2560        let r11 = h[1] - c;
2561        let r01 = h[2];
2562        n as f64 * (r00 * r00 + r11 * r11 + 2.0 * r01 * r01)
2563    }
2564
2565    #[test]
2566    fn planted_warped_patch_recovers_uniform_speed_coords() {
2567        // A deliberately anisotropic + sheared affine warp M: the fitted chart
2568        // stretches axis 0 by 1.6, axis 1 by 0.8, with a 0.5 shear.
2569        let m = [[1.6, 0.5], [0.0, 0.8]];
2570        let ev = MockPatchEvaluator;
2571        let decoder = warp_decoder(m);
2572        let coords = patch_coords();
2573        let n = coords.nrows();
2574
2575        // Pullback metric G = MᵀM (constant over the patch).
2576        let g00 = m[0][0] * m[0][0] + m[1][0] * m[1][0];
2577        let g11 = m[0][1] * m[0][1] + m[1][1] * m[1][1];
2578        let g01 = m[0][0] * m[0][1] + m[1][0] * m[1][1];
2579        let defect_initial = flat_defect_of_constant_metric([g00, g11, g01], n);
2580        assert!(
2581            defect_initial > 1e-2,
2582            "the planted anisotropic warp must start with a sizeable defect; got {defect_initial:.3e}"
2583        );
2584
2585        let repar = patch_isometry_flow_reparameterization(&ev, decoder.view(), coords.view())
2586            .expect("patch reparameterization must evaluate")
2587            .expect("a warped patch with a global flow basis must canonicalize");
2588
2589        // Acceptance (#1019): the canonical chart recovers uniform-speed coords
2590        // — the residual defect is within 10% of the optimum (0). The optimum is
2591        // exactly 0 (a flat patch IS isometric to itself), so the bar is a small
2592        // absolute fraction of the initial defect.
2593        assert!(
2594            repar.defect_final <= 0.10 * defect_initial,
2595            "canonicalization must drive the anisotropy defect to within 10% of the optimum; \
2596             initial {defect_initial:.3e}, final {:.3e}",
2597            repar.defect_final
2598        );
2599        // The defect-final the struct reports must match what the optimizer
2600        // descended to (strict improvement over the identity flow).
2601        assert!(
2602            repar.defect_final < repar.defect_initial,
2603            "the pass must report a strict improvement; initial {:.3e}, final {:.3e}",
2604            repar.defect_initial,
2605            repar.defect_final
2606        );
2607        // The canonical chart is a genuine diffeomorphism (guard cleared).
2608        assert!(
2609            repar.min_flow_jacobian_det > PATCH_FLOW_DIFFEO_MIN_DET,
2610            "the canonical flow must be fold-free; min det {:.3e}",
2611            repar.min_flow_jacobian_det
2612        );
2613        // Image frozen: the recomposition residual is within the honest gate.
2614        assert!(
2615            repar.recomposition_residual <= CHART_RECOMPOSITION_REL_TOL,
2616            "the decoded image must be reproduced within the recomposition tolerance; got {:.3e}",
2617            repar.recomposition_residual
2618        );
2619    }
2620
2621    #[test]
2622    fn already_uniform_patch_is_left_as_fitted() {
2623        // M = I: the fitted chart is already uniform-speed (defect 0), so the
2624        // minimizer finds no strict improvement and honestly skips.
2625        let ev = MockPatchEvaluator;
2626        let decoder = warp_decoder([[1.0, 0.0], [0.0, 1.0]]);
2627        let coords = patch_coords();
2628        let out = patch_isometry_flow_reparameterization(&ev, decoder.view(), coords.view())
2629            .expect("patch reparameterization must evaluate");
2630        assert!(
2631            out.is_none(),
2632            "an already-uniform patch chart must be left as fitted (honest skip), got Some"
2633        );
2634    }
2635
2636    #[test]
2637    fn collapsed_patch_axis_is_refused() {
2638        // All rows share a single t₁ value: the patch has no extent on axis 1,
2639        // so there is no honest flat chart — the basis builder refuses and the
2640        // function returns None rather than erroring.
2641        let ev = MockPatchEvaluator;
2642        let decoder = warp_decoder([[1.3, 0.0], [0.0, 0.9]]);
2643        let mut coords = patch_coords();
2644        for row in 0..coords.nrows() {
2645            coords[[row, 1]] = 0.5;
2646        }
2647        let out = patch_isometry_flow_reparameterization(&ev, decoder.view(), coords.view())
2648            .expect("patch reparameterization must evaluate");
2649        assert!(
2650            out.is_none(),
2651            "a patch collapsed along one axis must be refused (None), got Some"
2652        );
2653    }
2654
2655    #[test]
2656    fn free_patch_flow_basis_layout_and_jacobian_at_identity() {
2657        let basis = FreePatchFlowBasis::new([0.0, 0.0], [1.0, 1.0]).expect("patch basis");
2658        // 2 components × 2 monomials (deg 1: (1,0),(0,1)) = 4 coefficients.
2659        assert_eq!(basis.dim(), 4);
2660        assert_eq!(basis.mode_layout().len(), 4);
2661        // θ = 0 ⇒ Dφ = I everywhere ⇒ min det = 1.
2662        let theta = vec![0.0_f64; basis.dim()];
2663        let det = basis.min_jacobian_det_on_grid(&theta);
2664        assert!(
2665            (det - 1.0).abs() < 1e-12,
2666            "identity flow has det Dφ ≡ 1; got {det}"
2667        );
2668        // The first two modes of component 0 are the linear fields (1,0),(0,1).
2669        let layout = basis.mode_layout();
2670        assert_eq!(layout[0].component, 0);
2671        assert_eq!(layout[0].exps, (1, 0));
2672        assert_eq!(layout[1].exps, (0, 1));
2673    }
2674
2675    /// Finite-difference check of the analytic mode gradients (the `grad` the
2676    /// Gauss–Newton Jacobian is built from) against the monomial values.
2677    #[test]
2678    fn free_patch_mode_gradients_match_finite_difference() {
2679        let basis = FreePatchFlowBasis::new([-0.5, 0.2], [1.5, 2.2]).expect("patch basis");
2680        let t = [0.3, 1.1];
2681        let eps = 1e-6;
2682        let base = basis.mode_samples(t);
2683        let plus0 = basis.mode_samples([t[0] + eps, t[1]]);
2684        let plus1 = basis.mode_samples([t[0], t[1] + eps]);
2685        for k in 0..base.len() {
2686            let fd0 = (plus0[k].value - base[k].value) / eps;
2687            let fd1 = (plus1[k].value - base[k].value) / eps;
2688            assert!(
2689                (fd0 - base[k].grad[0]).abs() < 1e-4,
2690                "mode {k} ∂/∂t₀ FD {fd0} vs analytic {}",
2691                base[k].grad[0]
2692            );
2693            assert!(
2694                (fd1 - base[k].grad[1]).abs() < 1e-4,
2695                "mode {k} ∂/∂t₁ FD {fd1} vs analytic {}",
2696                base[k].grad[1]
2697            );
2698        }
2699    }
2700}
2701
2702#[cfg(test)]
2703mod sphere_defect_tests {
2704    use super::*;
2705    use ndarray::{Array2, Array3, ArrayView2};
2706
2707    /// Mock sphere-chart evaluator: `m = p = 2`, identity decoder, with a jet
2708    /// whose per-row decoded tangents are `∂γ/∂lat = (1, 0)` and
2709    /// `∂γ/∂lon = (0, warp·cos lat)`. With `warp = 1` the pullback metric is
2710    /// exactly the round-sphere reference `diag(1, cos²lat)` (defect 0); any
2711    /// `warp ≠ 1` rescales the lon direction uniformly — still a global rescale
2712    /// of the lon column, but NOT a global rescale of the whole metric, so it
2713    /// registers as a genuine anisotropic defect.
2714    #[derive(Debug)]
2715    struct MockSphereEvaluator {
2716        warp: f64,
2717    }
2718
2719    impl SaeBasisEvaluator for MockSphereEvaluator {
2720        fn evaluate(
2721            &self,
2722            coords: ArrayView2<'_, f64>,
2723        ) -> Result<(Array2<f64>, Array3<f64>), String> {
2724            let n = coords.nrows();
2725            // Basis Φ is unused by the defect (only the jet enters G); return a
2726            // well-formed (n, 2) zero basis.
2727            let phi = Array2::<f64>::zeros((n, 2));
2728            let mut jet = Array3::<f64>::zeros((n, 2, 2));
2729            for row in 0..n {
2730                let lat = coords[[row, 0]];
2731                // basis col 0 carries the lat tangent (1, 0): ∂/∂lat = 1 on
2732                // output 0; basis col 1 carries the lon tangent
2733                // (0, warp·cos lat): ∂/∂lon = warp·cos lat on output 1.
2734                jet[[row, 0, 0]] = 1.0; // d(col0)/d(lat)
2735                jet[[row, 1, 1]] = self.warp * lat.cos(); // d(col1)/d(lon)
2736            }
2737            Ok((phi, jet))
2738        }
2739
2740        // This mock supplies only the first jet that the chart-defect test
2741        // exercises; it carries no analytic second/third jet, so it declares
2742        // that capability absent (`None`) per the trait contract rather than
2743        // fabricating one — after validating the (lat, lon) coordinate shape
2744        // it is contracted on, mirroring the sanctioned `TestPeriodicEvaluator`
2745        // higher-jet stubs.
2746        fn second_jet_dyn(
2747            &self,
2748            coords: ArrayView2<'_, f64>,
2749        ) -> Option<Result<ndarray::Array4<f64>, String>> {
2750            if coords.ncols() != 2 {
2751                return Some(Err(format!(
2752                    "MockSphereEvaluator::second_jet_dyn: expected (lat, lon) coords, got {} cols",
2753                    coords.ncols()
2754                )));
2755            }
2756            None
2757        }
2758
2759        fn third_jet_dyn(
2760            &self,
2761            coords: ArrayView2<'_, f64>,
2762        ) -> Option<Result<ndarray::Array5<f64>, String>> {
2763            if coords.ncols() != 2 {
2764                return Some(Err(format!(
2765                    "MockSphereEvaluator::third_jet_dyn: expected (lat, lon) coords, got {} cols",
2766                    coords.ncols()
2767                )));
2768            }
2769            None
2770        }
2771    }
2772
2773    fn coords(lats: &[f64]) -> Array2<f64> {
2774        let n = lats.len();
2775        let mut c = Array2::<f64>::zeros((n, 2));
2776        for (i, &lat) in lats.iter().enumerate() {
2777            c[[i, 0]] = lat;
2778            c[[i, 1]] = 0.1 * i as f64; // lon, irrelevant to the metric
2779        }
2780        c
2781    }
2782
2783    #[test]
2784    fn round_isometric_chart_has_zero_defect() {
2785        let ev = MockSphereEvaluator { warp: 1.0 };
2786        let decoder = Array2::<f64>::eye(2);
2787        let c = coords(&[-0.6, -0.2, 0.0, 0.3, 0.7]);
2788        let defect = sphere_chart_isometry_defect(&ev, decoder.view(), c.view())
2789            .expect("defect must evaluate")
2790            .expect("non-degenerate round chart must return Some");
2791        assert!(
2792            defect < 1e-10,
2793            "a chart whose pullback metric is exactly diag(1, cos²lat) is round-isometric; \
2794             defect should be ~0, got {defect:.3e}"
2795        );
2796    }
2797
2798    #[test]
2799    fn warped_chart_has_large_defect() {
2800        // warp = 2.5 stretches the lon direction by a lat-independent factor,
2801        // so the pullback metric is diag(1, (2.5·cos lat)²) — NOT a global
2802        // rescale of diag(1, cos²lat), so the profiled-scale residual is
2803        // strictly positive.
2804        let ev = MockSphereEvaluator { warp: 2.5 };
2805        let decoder = Array2::<f64>::eye(2);
2806        let c = coords(&[-0.6, -0.2, 0.0, 0.3, 0.7]);
2807        let defect = sphere_chart_isometry_defect(&ev, decoder.view(), c.view())
2808            .expect("defect must evaluate")
2809            .expect("non-degenerate warped chart must return Some");
2810        assert!(
2811            defect > 1e-2,
2812            "an anisotropically warped chart must register a sizeable defect, got {defect:.3e}"
2813        );
2814    }
2815
2816    #[test]
2817    fn pole_singularity_is_refused_not_fabricated() {
2818        // A row sitting exactly on the pole (lat = π/2, cos lat = 0) makes the
2819        // reference metric's lon column vanish; the function must refuse (None)
2820        // rather than fabricate a defect on the chart singularity.
2821        let ev = MockSphereEvaluator { warp: 1.0 };
2822        let decoder = Array2::<f64>::eye(2);
2823        let base = coords(&[0.0, 0.3]);
2824        // Append a pole row (lat = π/2).
2825        let mut c3 = Array2::<f64>::zeros((3, 2));
2826        c3.slice_mut(ndarray::s![0..2, ..]).assign(&base);
2827        c3[[2, 0]] = std::f64::consts::FRAC_PI_2;
2828        let out = sphere_chart_isometry_defect(&ev, decoder.view(), c3.view())
2829            .expect("defect must evaluate");
2830        // At the exact pole the decoded lon tangent (cos lat = 0) also collapses
2831        // the pullback metric (det G = 0), so this refuses via the rank-deficient
2832        // metric guard — either way an honest None, never a fabricated number.
2833        assert!(
2834            out.is_none(),
2835            "a pole-singular chart row must be refused, got {out:?}"
2836        );
2837    }
2838
2839    /// FD-gate the analytic conformal-boost mode Jacobians `Dv_k(t)` against
2840    /// central differences of the displacements `v_k(t)` — the exact object the
2841    /// sphere Gauss–Newton residual derivative is built from.
2842    #[test]
2843    fn sphere_boost_mode_jacobians_match_finite_difference() {
2844        let eps = 1e-6;
2845        // Two interior points well off the poles (cos lat bounded away from 0).
2846        for t in [[0.3, 0.7], [-0.6, -1.2]] {
2847            let jac = SphereBoostFlowBasis::mode_jacobians(t);
2848            let vp0 = SphereBoostFlowBasis::mode_displacements([t[0] + eps, t[1]]);
2849            let vm0 = SphereBoostFlowBasis::mode_displacements([t[0] - eps, t[1]]);
2850            let vp1 = SphereBoostFlowBasis::mode_displacements([t[0], t[1] + eps]);
2851            let vm1 = SphereBoostFlowBasis::mode_displacements([t[0], t[1] - eps]);
2852            for k in 0..3 {
2853                for comp in 0..2 {
2854                    let fd_dlat = (vp0[k][comp] - vm0[k][comp]) / (2.0 * eps);
2855                    let fd_dlon = (vp1[k][comp] - vm1[k][comp]) / (2.0 * eps);
2856                    assert!(
2857                        (fd_dlat - jac[k][comp][0]).abs() < 1e-5,
2858                        "boost {k} comp {comp} ∂/∂lat FD {fd_dlat} vs analytic {} at {t:?}",
2859                        jac[k][comp][0]
2860                    );
2861                    assert!(
2862                        (fd_dlon - jac[k][comp][1]).abs() < 1e-5,
2863                        "boost {k} comp {comp} ∂/∂lon FD {fd_dlon} vs analytic {} at {t:?}",
2864                        jac[k][comp][1]
2865                    );
2866                }
2867            }
2868        }
2869    }
2870
2871    /// The zonal boost `K_z = cos(lat) ∂_lat` is pole-free (its only nonzero
2872    /// component is latitude and carries no `1/cos` factor), and the boost
2873    /// `[Z, X, Y]` layout is stable.
2874    #[test]
2875    fn sphere_boost_layout_and_zonal_is_pole_free() {
2876        let basis = SphereBoostFlowBasis;
2877        assert_eq!(basis.dim(), 3);
2878        assert_eq!(
2879            basis.mode_layout(),
2880            [SphereBoostAxis::Z, SphereBoostAxis::X, SphereBoostAxis::Y]
2881        );
2882        // K_z displacement is (cos lat, 0): finite for every latitude.
2883        for lat in [-1.4, -0.3, 0.0, 0.9, 1.4] {
2884            let disp = SphereBoostFlowBasis::mode_displacements([lat, 0.5]);
2885            assert!(disp[0][0].is_finite() && disp[0][1] == 0.0);
2886        }
2887    }
2888}
2889
2890#[cfg(test)]
2891mod turning_tests {
2892    use super::*;
2893    use crate::basis::PeriodicHarmonicEvaluator;
2894    use ndarray::{Array1, Array2};
2895    use std::f64::consts::TAU;
2896
2897    /// A unit circle `γ(t) = (cos 2πt, sin 2πt)` traversed once over `t ∈ [0,1]`
2898    /// has constant curvature κ = 1 and speed 2π, so the total turning is
2899    /// `Θ = ∫₀¹ κ·‖γ'‖ dt = 2π`.
2900    #[test]
2901    fn full_circle_turning_is_two_pi() {
2902        let ev = PeriodicHarmonicEvaluator::new(3).expect("3-basis circle");
2903        // Basis [1, sin2πt, cos2πt]; B maps cos→x (output 0), sin→y (output 1).
2904        let mut decoder = Array2::<f64>::zeros((3, 2));
2905        decoder[[2, 0]] = 1.0; // cos -> x
2906        decoder[[1, 1]] = 1.0; // sin -> y
2907        // Span the full period [0, 1]; the integral runs over [min, max] of the
2908        // supplied coordinates, so the endpoint t = 1 must be present to close
2909        // the loop (the circle's speed 2π is nonzero everywhere, no stationary
2910        // point at the seam).
2911        let coords = Array1::from_iter((0..=50).map(|i| i as f64 / 50.0));
2912        let theta = d1_atom_fitted_turning(&ev, decoder.view(), coords.view())
2913            .expect("turning must evaluate")
2914            .expect("a non-degenerate circle must return Some");
2915        assert!(
2916            (theta - TAU).abs() < 1e-6,
2917            "a full unit circle has total turning 2π; got {theta:.9}"
2918        );
2919    }
2920
2921    /// A half circle (`t ∈ [0, 0.5]`) turns through π.
2922    #[test]
2923    fn half_circle_turning_is_pi() {
2924        let ev = PeriodicHarmonicEvaluator::new(3).expect("3-basis circle");
2925        let mut decoder = Array2::<f64>::zeros((3, 2));
2926        decoder[[2, 0]] = 1.0;
2927        decoder[[1, 1]] = 1.0;
2928        let coords = Array1::from_iter((0..=25).map(|i| 0.5 * i as f64 / 25.0));
2929        let theta = d1_atom_fitted_turning(&ev, decoder.view(), coords.view())
2930            .expect("turning must evaluate")
2931            .expect("a non-degenerate half-circle must return Some");
2932        assert!(
2933            (theta - std::f64::consts::PI).abs() < 1e-6,
2934            "a half circle turns through π; got {theta:.9}"
2935        );
2936    }
2937
2938    /// A straight line (only the constant + one linear-in-image direction, no
2939    /// genuine curvature) has zero turning — exactly the `Θ → 0` linear-tail
2940    /// signature the EV-vs-Θ measurement uses to flag a curved atom that is
2941    /// really just a linear direction. Here the decoder uses a SINGLE harmonic
2942    /// pair scaled so the image is a 1-D segment (x and y both ∝ cos 2πt), i.e.
2943    /// the decoded curve lies on a line through the origin → wedge ≡ 0.
2944    #[test]
2945    fn straight_line_image_has_zero_turning() {
2946        let ev = PeriodicHarmonicEvaluator::new(3).expect("3-basis circle");
2947        let mut decoder = Array2::<f64>::zeros((3, 2));
2948        // Both outputs ∝ cos 2πt → the image is the line y = 2x, γ collinear with
2949        // γ', γ'' at every t, so the wedge norm vanishes identically.
2950        decoder[[2, 0]] = 1.0;
2951        decoder[[2, 1]] = 2.0;
2952        // Span a coordinate range strictly inside (0, 0.25) so the speed
2953        // `‖γ'‖ ∝ |sin 2πt|` never hits the stationary zero at t = 0 (where the
2954        // turning integrand is genuinely undefined and the function refuses).
2955        let coords = Array1::from_iter((0..=20).map(|i| 0.05 + 0.15 * i as f64 / 20.0));
2956        let theta = d1_atom_fitted_turning(&ev, decoder.view(), coords.view())
2957            .expect("turning must evaluate")
2958            .expect("a non-degenerate segment must return Some");
2959        assert!(
2960            theta < 1e-9,
2961            "a straight-line image has zero turning (the linear-tail signature); got {theta:.3e}"
2962        );
2963    }
2964
2965    /// #1610/#1026 — a GLOBALLY constant image (only the DC basis row is
2966    /// nonzero) decodes to a single fixed point: `γ'(t) ≡ 0` at every node, so
2967    /// the curve is stationary everywhere. A point has no arc to turn through,
2968    /// so its total turning is exactly `0` — the ultimate linear-tail signature.
2969    /// This is the path the hybrid-collapse witness relies on (a `d = 1` atom
2970    /// straightened to its DC component must read `Some(0.0)`, not the historical
2971    /// `None`, so its slot can collapse to a legitimate linear tail).
2972    #[test]
2973    fn constant_image_turning_is_some_zero() {
2974        let ev = PeriodicHarmonicEvaluator::new(3).expect("3-basis circle");
2975        let mut decoder = Array2::<f64>::zeros((3, 2));
2976        // Only the DC (constant) basis row → γ(t) is a fixed point for all t.
2977        decoder[[0, 0]] = 0.7;
2978        decoder[[0, 1]] = -0.3;
2979        let coords = Array1::from_iter((0..=20).map(|i| 0.05 + 0.15 * i as f64 / 20.0));
2980        let theta = d1_atom_fitted_turning(&ev, decoder.view(), coords.view())
2981            .expect("turning must evaluate")
2982            .expect("a globally constant image returns Some(0.0), not None");
2983        assert_eq!(
2984            theta, 0.0,
2985            "a globally constant (all-stationary) image has exactly zero turning"
2986        );
2987    }
2988
2989    /// #1610/#1026 — a curve that is stationary at SOME nodes but moving at
2990    /// others is a cusp: the unsigned curvature integrand is genuinely
2991    /// ill-defined at the stationary node, so the function REFUSES (`None`)
2992    /// rather than under-count the sharp turn. Built as an image `∝ cos 2πt` (a
2993    /// line through the origin) over a span whose lower endpoint is exactly
2994    /// `t = 0`, where `γ' ∝ -sin 2πt` vanishes: that endpoint lands on a grid
2995    /// node and is stationary while every interior node moves — the mixed case
2996    /// the constant-vs-cusp split must keep conservative.
2997    #[test]
2998    fn partial_cusp_turning_is_none() {
2999        let ev = PeriodicHarmonicEvaluator::new(3).expect("3-basis circle");
3000        let mut decoder = Array2::<f64>::zeros((3, 2));
3001        decoder[[2, 0]] = 1.0;
3002        decoder[[2, 1]] = 2.0;
3003        // Span [0, 0.2]: the Simpson grid's lower node sits exactly on t = 0,
3004        // where the speed `‖γ'‖ ∝ |sin 2πt|` is zero (stationary), while every
3005        // interior node is moving.
3006        let coords = Array1::from_iter((0..=20).map(|i| 0.2 * i as f64 / 20.0));
3007        let result = d1_atom_fitted_turning(&ev, decoder.view(), coords.view())
3008            .expect("turning must evaluate");
3009        assert!(
3010            result.is_none(),
3011            "a curve stationary at some nodes but moving at others (a cusp) must \
3012             refuse with None; got {result:?}"
3013        );
3014    }
3015}