Skip to main content

gam_sae/inference/
steering.rs

1//! `steer_delta` — the **steering primitive with output dosimetry**: the
2//! actionable LLM payload of the SAE-manifold machine.
3//!
4//! # What this computes
5//!
6//! Given a fitted [`SaeManifoldTerm`] and the per-row output-Fisher
7//! [`RowMetric`], a *steering move* is "drive atom `k`'s latent coordinate from
8//! `t_from` to `t_to`". The atom's decoder curve `g_k(t) = Φ_k(t) B_k` maps that
9//! latent move to an **activation-space delta** — the actual vector you add to
10//! the residual stream / reconstruction to realize the move *on the manifold*.
11//! Here `g_k(t) = Phi_k^eta(t) B_k` is the fitted physical decoder, including
12//! the curvature-homotopy state:
13//!
14//! ```text
15//! delta = a · ( g_k(t_to) - g_k(t_from) )      (the on-manifold move)
16//! ```
17//!
18//! where `a` is the atom's amplitude (how loudly the atom is expressed). This is
19//! the thing a downstream consumer adds to a hidden state.
20//!
21//! # Dosimetry — how big is this push, in nats?
22//!
23//! The headline number is the **predicted output effect**: how much behavioral
24//! change (in nats of KL on the model's output distribution) the exact applied
25//! activation move induces. For a locally-quadratic output readout the KL of a
26//! move `delta` is `0.5 * delta^T F delta`, with `F` the output-Fisher
27//! information — exactly the inner product [`RowMetric`] carries:
28//!
29//! ```text
30//! predicted_nats = 0.5 * delta^T M_metric_row delta
31//! ```
32//!
33//! This endpoint quadratic form is the single canonical nats prediction because
34//! it prices the same `delta` a patched forward pass applies. Arc energy and
35//! tangent-only surrogates are deliberately not exposed as alternate nats lanes:
36//! they price different objects and therefore cannot be calibrated against the
37//! patched-forward endpoint KL by construction (#2249).
38//!
39//! # Validity radius — where local linearization stops being trusted
40//!
41//! A consumer must know *how far* the move can be trusted as a linear push. The
42//! **validity radius** is the latent step size at which the exact chord dose
43//! diverges from the initial-tangent quadratic prediction by more than
44//! [`VALIDITY_DIVERGENCE_FRACTION`]. Beyond it the surface has curved enough that
45//! the endpoint chord no longer represents the move. We **report** it; we do not
46//! silently clip to it.
47//!
48//! # Off-manifold guard
49//!
50//! `δ` is, by construction, a chord of the decoder curve, so it should lie in the
51//! atom's local tangent/frame at `t_from` (up to second-order curvature). The
52//! **off-manifold norm** projects `δ` onto the span of the local decoder tangents
53//! `∂g_k/∂t` at `t_from` and reports the residual norm — a self-check that the
54//! steering move stays on the learned surface. It is `≈ 0` for small steps and
55//! grows with arc curvature; a large value means the requested move left the
56//! manifold and the dose number is not to be trusted.
57//!
58//! # Read-only / no loss contact
59//!
60//! This module is a **pure read** over the fitted term and the metric. It calls
61//! only `g_k(t)` evaluation ([`SaeManifoldAtom`]'s decoder + installed
62//! [`SaeBasisEvaluator`]) and the criterion-facing
63//! [`RowMetric::fisher_mass`] / [`RowMetric::pullback`]. It never mutates the
64//! model, never touches a likelihood / criterion / penalty, and the solver floor
65//! `δ` of [`RowMetric`] never enters any number it reports (the fisher-mass /
66//! pullback face is `δ`-free, #747).
67
68use ndarray::{Array1, Array2, ArrayView1};
69
70use crate::encode::EncodeAtlas;
71use crate::manifold::{SaeManifoldAtom, SaeManifoldTerm};
72use gam_problem::{FisherFactorKind, MetricProvenance, RowMetric};
73
74/// Number of sub-steps the latent path `[t_from, t_to]` is integrated over for
75/// the dosimetry path integral. The decoder curve is smooth, so a modest
76/// midpoint-rule grid resolves the arc; fixed (no clock / no adaptivity) so the
77/// reported dose is deterministic.
78const STEER_VALIDITY_STEPS: usize = 64;
79
80/// The fraction by which the exact chord dose may diverge from the
81/// initial-tangent quadratic prediction before the move is declared past its
82/// validity radius.
83const VALIDITY_DIVERGENCE_FRACTION: f64 = 0.1;
84
85/// Scientific status of the quadratic dose relative to the full output Fisher.
86#[derive(Clone, Copy, Debug, PartialEq, Eq)]
87pub enum FisherDoseKind {
88    /// Euclidean/no-behavior metric: no nats dose exists.
89    Unavailable,
90    /// The supplied factor exactly represents the complete local Fisher.
91    ExactFull,
92    /// The producer certified the retained PSD operator as a lower bound.
93    CertifiedPsdLowerBound,
94    /// The factor is randomized, stochastic, truncated, or otherwise lacks an
95    /// operator-order certificate.
96    UncertifiedApproximation,
97}
98
99impl FisherDoseKind {
100    pub const fn as_str(self) -> &'static str {
101        match self {
102            Self::Unavailable => "unavailable",
103            Self::ExactFull => "exact_full",
104            Self::CertifiedPsdLowerBound => "certified_psd_lower_bound",
105            Self::UncertifiedApproximation => "uncertified_approximation",
106        }
107    }
108}
109
110/// The actionable output of a steering query over one atom.
111#[derive(Clone, Debug, PartialEq)]
112pub struct SteerPlan {
113    /// Which atom was steered (index into [`SaeManifoldTerm::atoms`]).
114    pub atom: usize,
115    /// The atom's name (mirrors [`crate::manifold::SaeManifoldAtom::name`]).
116    pub atom_name: String,
117    /// The source latent coordinate `t_from` (length = atom's `latent_dim`).
118    pub t_from: Vec<f64>,
119    /// The target latent coordinate `t_to` (length = atom's `latent_dim`).
120    pub t_to: Vec<f64>,
121    /// The exact amplitude `a` the caller applied to the on-manifold move.
122    pub amplitude: f64,
123    /// The exact row whose output-Fisher metric prices the applied move.
124    pub metric_row: usize,
125    /// **The activation-space delta**: `δ = a · (g_k(t_to) − g_k(t_from))`, a
126    /// length-`p` vector in the reconstruction/output space — the actual move to
127    /// add to a hidden state.
128    pub delta: Array1<f64>,
129    /// **DOSIMETRY**: predicted output effect of the exact applied move in
130    /// **nats** of KL, `0.5 * delta^T M_metric_row delta`.
131    /// `None` when the metric carries no behavioral information (Euclidean
132    /// provenance) — the dose is *not available*, not zero.
133    pub predicted_nats: Option<f64>,
134    /// Mathematical status of the factor used for `predicted_nats`.
135    pub predicted_nats_kind: FisherDoseKind,
136    /// Captured trace `tr(U_n U_n^T)` at `metric_row`, when behavior is present.
137    pub fisher_mass_captured: Option<f64>,
138    /// Non-negative omitted Fisher trace supplied by the harvest, when audited.
139    pub fisher_mass_residual: Option<f64>,
140    /// `residual / (captured + residual)`, when audited.
141    pub fisher_mass_residual_fraction: Option<f64>,
142    /// **VALIDITY RADIUS**: the latent step size (Euclidean norm of the move from
143    /// `t_from`) at which the exact chord dose first diverges from the
144    /// initial-tangent quadratic prediction by more than
145    /// [`VALIDITY_DIVERGENCE_FRACTION`]. Equals the full move length when the
146    /// linearization is trusted all the way to `t_to`. `None` under a no-behavior
147    /// metric (there is no dose to validate).
148    pub validity_radius: Option<f64>,
149    /// **OFF-MANIFOLD GUARD**: the norm of `δ`'s component outside the span of
150    /// the atom's local decoder tangents `∂g_k/∂t` at `t_from`. `≈ 0` by
151    /// construction (the move is a chord of the curve); a large value flags a
152    /// move that left the learned surface.
153    pub off_manifold_norm: f64,
154    /// The provenance of the metric the dose was read through, echoed so a
155    /// consumer can certify *why* `predicted_nats` is `None` when it is.
156    pub metric_provenance: MetricProvenance,
157}
158
159/// Result of writing one certified chart coordinate into an activation row.
160///
161/// The edited row is always `x + δ`, where `δ` is the delta returned by
162/// [`steer_delta`] for the atom's current encoded coordinate and the requested
163/// target coordinate. Because only the on-manifold atom chord is added, every
164/// component of `x` outside this atom's chart residual is preserved exactly; this
165/// is the locality guarantee missing from whole-residual linear-steering
166/// baselines.
167#[derive(Clone, Debug)]
168pub struct CoordinateSetResult {
169    /// The edited activation/reconstruction row.
170    pub edited: Array1<f64>,
171    /// Certified coordinate read from the input row before the write.
172    pub t_from_certified: Array1<f64>,
173    /// Certificate attached to `t_from_certified`.
174    pub encode_certificate: crate::encode::RowCertificate,
175    /// Steering plan whose `delta` was added to the row.
176    pub steer: SteerPlan,
177}
178
179/// Write atom `atom_k`'s chart coordinate in row `x` to `t_to` by delta
180/// steering, preserving the row's off-atom/off-subspace residual exactly.
181///
182/// `amplitude` is the assignment/intensity with which the row expresses this
183/// atom; callers that have already separated existence/intensity/position should
184/// pass the intensity and only swap the position coordinate. The certified read
185/// uses [`EncodeAtlas::certified_encode_row`]; the write uses [`steer_delta`].
186pub fn set_coordinate(
187    model: &SaeManifoldTerm,
188    metric: &RowMetric,
189    atlas: &EncodeAtlas,
190    x: ArrayView1<'_, f64>,
191    atom_k: usize,
192    metric_row: usize,
193    amplitude: f64,
194    t_to: &[f64],
195) -> Result<CoordinateSetResult, String> {
196    let atom = model.atoms.get(atom_k).ok_or_else(|| {
197        format!(
198            "set_coordinate: atom index {atom_k} out of range (term has {} atoms)",
199            model.k_atoms()
200        )
201    })?;
202    if x.len() != atom.output_dim() {
203        return Err(format!(
204            "set_coordinate: input row has length {} but atom {atom_k} output_dim is {}",
205            x.len(),
206            atom.output_dim()
207        ));
208    }
209    let (t_from, cert) = atlas.certified_encode_row(atom, atom_k, x, amplitude)?;
210    let steer = steer_delta(
211        model,
212        metric,
213        atom_k,
214        metric_row,
215        amplitude,
216        t_from.as_slice().unwrap_or(&[]),
217        t_to,
218    )?;
219    let mut edited = x.to_owned();
220    if edited.len() != steer.delta.len() {
221        return Err(format!(
222            "set_coordinate: steering delta length {} does not match row length {}",
223            steer.delta.len(),
224            edited.len()
225        ));
226    }
227    for i in 0..edited.len() {
228        edited[i] += steer.delta[i];
229    }
230    Ok(CoordinateSetResult {
231        edited,
232        t_from_certified: t_from,
233        encode_certificate: cert,
234        steer,
235    })
236}
237
238/// Result of a coordinate interchange: donor position read from `x_source`, then
239/// written into `x_target` while preserving the target residual and intensity.
240#[derive(Clone, Debug)]
241pub struct InterchangeResult {
242    /// Target row after the donor coordinate has been delta-written into it.
243    pub edited_target: Array1<f64>,
244    /// Donor/source coordinate that was transplanted.
245    pub donor_t: Array1<f64>,
246    /// Target coordinate before the transplant.
247    pub target_t_before: Array1<f64>,
248    /// Target behavior coordinate after the transplant, re-read from the edit.
249    pub target_t_after: Array1<f64>,
250    /// Steering dose in nats, when a behavioral metric is available.
251    pub predicted_nats: Option<f64>,
252    /// Norm of the steering delta outside the local atom tangent frame.
253    pub off_manifold_norm: f64,
254    /// Reported steering validity radius.
255    pub validity_radius: Option<f64>,
256    /// Geodesic chart-coordinate landing error. Wrapped axes use their shortest
257    /// signed displacement. This is a descriptive reconstruction diagnostic,
258    /// not a p/e-value: no counterfactual null distribution is available here.
259    pub landing_error: f64,
260    /// Underlying coordinate-write plan.
261    pub set_result: CoordinateSetResult,
262}
263
264/// Interchange atom `atom_k`'s chart coordinate from `x_source` into `x_target`.
265///
266/// The source coordinate is certified with `source_amplitude`; the target write
267/// is performed with `target_amplitude`, so swapping a position coordinate cannot
268/// silently smuggle donor intensity into the target. The returned landing error
269/// is descriptive; statistical evidence requires an externally specified null
270/// experiment and is deliberately not fabricated from the error magnitude.
271pub fn interchange(
272    model: &SaeManifoldTerm,
273    metric: &RowMetric,
274    atlas: &EncodeAtlas,
275    x_target: ArrayView1<'_, f64>,
276    target_amplitude: f64,
277    x_source: ArrayView1<'_, f64>,
278    source_amplitude: f64,
279    atom_k: usize,
280    target_metric_row: usize,
281) -> Result<InterchangeResult, String> {
282    let atom = model.atoms.get(atom_k).ok_or_else(|| {
283        format!(
284            "interchange: atom index {atom_k} out of range (term has {} atoms)",
285            model.k_atoms()
286        )
287    })?;
288    let (donor_t, _donor_cert) =
289        atlas.certified_encode_row(atom, atom_k, x_source, source_amplitude)?;
290    let set = set_coordinate(
291        model,
292        metric,
293        atlas,
294        x_target,
295        atom_k,
296        target_metric_row,
297        target_amplitude,
298        donor_t.as_slice().unwrap_or(&[]),
299    )?;
300    let (target_t_after, _after_cert) =
301        atlas.certified_encode_row(atom, atom_k, set.edited.view(), target_amplitude)?;
302    let periods = model.assignment.coords[atom_k].effective_axis_periods();
303    let landing_error = coordinate_l2_distance(
304        donor_t.as_slice().unwrap_or(&[]),
305        target_t_after.as_slice().unwrap_or(&[]),
306        &periods,
307    )?;
308    Ok(InterchangeResult {
309        edited_target: set.edited.clone(),
310        donor_t,
311        target_t_before: set.t_from_certified.clone(),
312        target_t_after,
313        predicted_nats: set.steer.predicted_nats,
314        off_manifold_norm: set.steer.off_manifold_norm,
315        validity_radius: set.steer.validity_radius,
316        landing_error,
317        set_result: set,
318    })
319}
320
321fn shortest_coordinate_delta(
322    from: &[f64],
323    to: &[f64],
324    periods: &[Option<f64>],
325) -> Result<Vec<f64>, String> {
326    if from.len() != to.len() || from.len() != periods.len() {
327        return Err(format!(
328            "coordinate displacement length mismatch: from={}, to={}, periods={}",
329            from.len(),
330            to.len(),
331            periods.len()
332        ));
333    }
334    let mut delta = Vec::with_capacity(from.len());
335    for axis in 0..from.len() {
336        let mut d = to[axis] - from[axis];
337        if let Some(period) = periods[axis] {
338            if !(period.is_finite() && period > 0.0) {
339                return Err(format!(
340                    "coordinate axis {axis} has invalid period {period}"
341                ));
342            }
343            d -= period * (d / period).round();
344        }
345        delta.push(d);
346    }
347    Ok(delta)
348}
349
350fn coordinate_l2_distance(a: &[f64], b: &[f64], periods: &[Option<f64>]) -> Result<f64, String> {
351    Ok(shortest_coordinate_delta(a, b, periods)?
352        .iter()
353        .map(|d| d * d)
354        .sum::<f64>()
355        .sqrt())
356}
357
358fn path_coordinate(
359    from: &[f64],
360    delta: &[f64],
361    periods: &[Option<f64>],
362    fraction: f64,
363) -> Vec<f64> {
364    from.iter()
365        .zip(delta.iter())
366        .zip(periods.iter())
367        .map(|((&start, &step), &period)| {
368            let value = start + fraction * step;
369            period.map_or(value, |p| value.rem_euclid(p))
370        })
371        .collect()
372}
373
374/// Build a [`SteerPlan`] for driving atom `atom_k` from `t_from` to `t_to`.
375///
376/// `model` is the fitted term (read only); `metric` is the per-row output-Fisher
377/// inner product the dose is measured through (typically `model.row_metric()`'s
378/// own metric, or any metric whose row/output dims match the term). `t_from` and
379/// `t_to` are latent coordinates of length `atom.latent_dim`.
380///
381/// Errors when the atom index is out of range, the coordinate lengths do not
382/// match the atom's latent dimension, the atom has no installed
383/// [`crate::manifold::SaeBasisEvaluator`] (arbitrary-`t` evaluation
384/// requires one), or the metric dimensions do not match the term. Under a
385/// Euclidean (no-behavior) metric the geometry is still produced but
386/// `predicted_nats` / `validity_radius` degrade to `None`.
387pub fn steer_delta(
388    model: &SaeManifoldTerm,
389    metric: &RowMetric,
390    atom_k: usize,
391    metric_row: usize,
392    amplitude: f64,
393    t_from: &[f64],
394    t_to: &[f64],
395) -> Result<SteerPlan, String> {
396    if !(amplitude.is_finite() && amplitude > 0.0) {
397        return Err(format!(
398            "steer_delta: amplitude must be finite and positive, got {amplitude}"
399        ));
400    }
401    let k = model.k_atoms();
402    if atom_k >= k {
403        return Err(format!(
404            "steer_delta: atom index {atom_k} out of range (term has {k} atoms)"
405        ));
406    }
407    let atom = &model.atoms[atom_k];
408    let d = atom.latent_dim();
409    let p = atom.output_dim();
410    if t_from.len() != d || t_to.len() != d {
411        return Err(format!(
412            "steer_delta: t_from/t_to must have length latent_dim={d}; got {} and {}",
413            t_from.len(),
414            t_to.len()
415        ));
416    }
417    atom.basis_evaluator.as_ref().ok_or_else(|| {
418        format!(
419            "steer_delta: atom {atom_k} ('{}') has no installed basis evaluator; \
420             arbitrary-t decoder evaluation requires one",
421            atom.name
422        )
423    })?;
424    let periods = model.assignment.coords[atom_k].effective_axis_periods();
425    let coordinate_delta = shortest_coordinate_delta(t_from, t_to, &periods)?;
426
427    let n = model.n_obs();
428    if metric.n_rows() != n || metric.p_out() != p {
429        return Err(format!(
430            "steer_delta: metric shape ({}, {}) must equal fitted term shape ({n}, {p})",
431            metric.n_rows(),
432            metric.p_out()
433        ));
434    }
435    if metric_row >= n {
436        return Err(format!(
437            "steer_delta: metric_row={metric_row} out of range for {n} fitted rows"
438        ));
439    }
440
441    // --- the on-manifold activation-space delta -----------------------------
442    let tier0_scale = model.tier0_scale();
443    let g_from = decode_at(atom, t_from, tier0_scale)?;
444    let g_to = decode_at(atom, t_to, tier0_scale)?;
445    let mut delta = Array1::<f64>::zeros(p);
446    for i in 0..p {
447        delta[i] = amplitude * (g_to[i] - g_from[i]);
448    }
449
450    // Whether the metric can/does match this term and carries behavior.
451    let provenance = metric.provenance();
452    let behavior_available = metric_carries_behavior(provenance);
453    let fisher_mass_captured = behavior_available.then(|| metric.row_traces()[metric_row]);
454    let fisher_mass_residual = behavior_available
455        .then(|| metric.truncation_mass_residual(metric_row))
456        .flatten();
457    let fisher_mass_residual_fraction = behavior_available
458        .then(|| metric.truncation_mass_residual_fraction(metric_row))
459        .flatten();
460    let predicted_nats_kind = if !behavior_available {
461        FisherDoseKind::Unavailable
462    } else {
463        match metric.fisher_factor_kind() {
464            Some(FisherFactorKind::ExactFull) => FisherDoseKind::ExactFull,
465            Some(FisherFactorKind::CertifiedPsdLowerBound) => {
466                FisherDoseKind::CertifiedPsdLowerBound
467            }
468            Some(FisherFactorKind::UncertifiedApproximation) => {
469                FisherDoseKind::UncertifiedApproximation
470            }
471            None => {
472                return Err(format!(
473                    "steer_delta: behavioral metric provenance {provenance:?} has no explicit Fisher factor status"
474                ));
475            }
476        }
477    };
478
479    // --- off-manifold guard -------------------------------------------------
480    // Project δ onto the span of the local decoder tangents ∂g_k/∂t and report
481    // the residual norm. The tangents are evaluated at the move's MIDPOINT, not
482    // at t_from: the chord of a curve is symmetric about its midpoint, so its
483    // component transverse to the midpoint tangent is the true second-order
484    // sagitta (`O(‖Δt‖²)`), whereas the endpoint tangent differs from the chord
485    // direction already at first order. Measuring against the midpoint frame is
486    // therefore the honest "did the move stay on the surface" self-check: it is
487    // `≈ 0` for an on-manifold move and grows only with genuine arc curvature.
488    let mut t_mid = vec![0.0_f64; d];
489    for a in 0..d {
490        t_mid[a] = t_from[a] + 0.5 * coordinate_delta[a];
491        if let Some(period) = periods[a] {
492            t_mid[a] = t_mid[a].rem_euclid(period);
493        }
494    }
495    let tangents = decode_tangents_at(atom, &t_mid, tier0_scale)?;
496    let off_manifold_norm = off_manifold_residual_norm(&tangents, delta.view());
497
498    // --- dosimetry: exact applied-delta Fisher endpoint KL ------------------
499    let (predicted_nats, validity_radius) = if !behavior_available {
500        (None, None)
501    } else {
502        let ctx = SteerContext {
503            atom,
504            scale: tier0_scale,
505            metric,
506            row: metric_row,
507            p,
508            d,
509            amplitude,
510            coordinate_delta: &coordinate_delta,
511            periods: &periods,
512        };
513        let dose = 0.5 * metric.fisher_mass(metric_row, delta.view());
514        let radius = validity_radius(&ctx, t_from)?;
515        (Some(dose), Some(radius))
516    };
517
518    Ok(SteerPlan {
519        atom: atom_k,
520        atom_name: atom.name.clone(),
521        t_from: t_from.to_vec(),
522        t_to: t_to.to_vec(),
523        amplitude,
524        metric_row,
525        delta,
526        predicted_nats,
527        predicted_nats_kind,
528        fisher_mass_captured,
529        fisher_mass_residual,
530        fisher_mass_residual_fraction,
531        validity_radius,
532        off_manifold_norm,
533        metric_provenance: provenance,
534    })
535}
536
537/// The model's predicted output-mean response to an applied activation push
538/// `δ`, under the LOCAL-LINEAR reading of its fitted surface: the projection
539/// of `δ` onto the span of atom `atom_k`'s decoder tangents `∂g_k/∂t` at the
540/// operating point `t_at`. A dictionary "predicts" exactly the component of a
541/// push it can carry along its learned surface; the transverse component is
542/// off-manifold and predicted to die (this is the same local model the
543/// off-manifold guard and the dosimetry chord trust, used in the same radius).
544///
545/// This is `μ(δ)` for the design loop of
546/// [`gam_terms::inference::structure_evidence`]: two structural hypotheses about
547/// the same activations (e.g. "one curved atom" vs "two flat atoms") are two
548/// fitted terms whose tangent spans differ, so they predict DIFFERENT
549/// responses to the same probe — and that disagreement, in the output-Fisher
550/// metric, is what `select_probe_by_expected_evidence` maximizes.
551pub fn predicted_response(
552    model: &SaeManifoldTerm,
553    atom_k: usize,
554    t_at: &[f64],
555    delta: ArrayView1<'_, f64>,
556) -> Result<Array1<f64>, String> {
557    let k = model.k_atoms();
558    if atom_k >= k {
559        return Err(format!(
560            "predicted_response: atom index {atom_k} out of range (term has {k} atoms)"
561        ));
562    }
563    let atom = &model.atoms[atom_k];
564    let d = atom.latent_dim();
565    let p = atom.output_dim();
566    if t_at.len() != d {
567        return Err(format!(
568            "predicted_response: t_at must have length latent_dim={d}; got {}",
569            t_at.len()
570        ));
571    }
572    if delta.len() != p {
573        return Err(format!(
574            "predicted_response: delta must have length output_dim={p}; got {}",
575            delta.len()
576        ));
577    }
578    atom.basis_evaluator.as_ref().ok_or_else(|| {
579        format!(
580            "predicted_response: atom {atom_k} ('{}') has no installed basis evaluator",
581            atom.name
582        )
583    })?;
584    let tangents = decode_tangents_at(atom, t_at, model.tier0_scale())?;
585    Ok(project_onto_tangent_span(&tangents, delta))
586}
587
588/// One model-in-the-loop observation of the exact [`SteerPlan`] supplied to an
589/// [`AppliedDoseProbe`]. The effective delta is the vector the downstream model
590/// actually received after its device/dtype conversion; it may differ from the
591/// f64 requested delta through quantization. `exact_directional_nats` is the
592/// full local-Fisher quadratic of that effective delta. `measured_nats` is the
593/// patched-forward `KL(p_base ‖ p_patched)` for the same effective delta.
594///
595/// Keeping all three values atomic prevents a caller from pricing one vector
596/// and measuring another, and keeps the resident [`RowMetric`] dose an explicit
597/// diagnostic rather than silently promoting an approximate operator (#2249).
598/// `certified_attainable_upper_nats`, when present, is a global upper bound on
599/// measured KL over every finite non-negative amplitude on this exact atom
600/// chord and downstream execution context. A point observation or an apparent
601/// plateau is not such a certificate.
602#[derive(Clone, Debug, PartialEq)]
603pub struct AppliedDoseObservation {
604    pub effective_delta: Array1<f64>,
605    pub exact_directional_nats: f64,
606    pub measured_nats: f64,
607    pub certified_attainable_upper_nats: Option<f64>,
608}
609
610/// Plan-aware external-model dose probe. The callback must execute the supplied
611/// plan; a scalar-amplitude callback cannot prove which activation delta it
612/// actually applied and is deliberately not part of the contract.
613pub type AppliedDoseProbe<'a> =
614    dyn FnMut(&SteerPlan) -> Result<AppliedDoseObservation, String> + 'a;
615
616/// Tuning for the closed-loop correction in [`steer_to_target_nats`].
617#[derive(Clone, Copy, Debug)]
618pub struct TargetDoseConfig {
619    /// Relative tolerance on measured KL vs the target that stops the loop.
620    pub tol_rel: f64,
621    /// Hard cap on patched-forward probes, including bracket construction.
622    pub max_iter: usize,
623    /// A probed amplitude counts as inside the readout-KL radius while its
624    /// measured KL matches the probe's exact directional local-Fisher dose
625    /// within this relative tolerance.
626    pub readout_tol_rel: f64,
627}
628
629impl Default for TargetDoseConfig {
630    fn default() -> Self {
631        Self {
632            tol_rel: 1.0e-2,
633            max_iter: 12,
634            readout_tol_rel: 1.0e-1,
635        }
636    }
637}
638
639/// One target-dose solve on a fixed atom chord.
640///
641/// The model and row metric remain explicit execution context; everything that
642/// identifies and tunes the requested dose is carried together so callers
643/// cannot accidentally reorder a train of homogeneous scalar/slice arguments.
644#[derive(Clone, Copy, Debug)]
645pub struct TargetDoseRequest<'a> {
646    /// The atom whose coordinate is being steered.
647    pub atom_k: usize,
648    /// Exact fitted row whose output-Fisher block prices the move.
649    pub metric_row: usize,
650    /// Source on-manifold coordinate.
651    pub t_from: &'a [f64],
652    /// Target on-manifold coordinate, which fixes the chord direction.
653    pub t_to: &'a [f64],
654    /// Requested output-KL dose in nats.
655    pub target_nats: f64,
656    /// Closed-loop correction tuning.
657    pub config: TargetDoseConfig,
658}
659
660/// A target output-KL dose on one atom's chord, returned atomically with the
661/// exact activation-space move the caller must apply (gh#2249/#2263).
662#[derive(Clone, Debug)]
663pub struct TargetDosePlan {
664    /// The requested dose in nats of KL.
665    pub target_nats: f64,
666    /// Closed-form first-order amplitude `a0 = sqrt(2 q* / (dgᵀ M dg))`, exact in
667    /// the quadratic/in-radius regime.
668    pub seed_amplitude: f64,
669    /// Exact applied move at the solved amplitude, including `delta`, predicted
670    /// dose, metric provenance, chart radius, and off-manifold audit.
671    pub steer: SteerPlan,
672    /// Exact directional local-Fisher dose, effective applied delta, and measured
673    /// patched-forward KL at [`SteerPlan::amplitude`]. `None` for the pure
674    /// closed-form seed. This never changes the resident metric or its status.
675    pub applied_probe: Option<AppliedDoseObservation>,
676    /// Number of patched-forward probes consumed (0 without a callback).
677    pub iterations: usize,
678    /// **READOUT-KL radius**: the largest probed amplitude whose measured KL still
679    /// matched its exact directional local-Fisher dose within `readout_tol_rel`
680    /// before the first probed failure. A later accidental match cannot extend
681    /// the radius past a failed point. `None` without a callback or when the
682    /// first probe failed.
683    pub readout_kl_radius: Option<f64>,
684    /// Tightest global attainable-dose upper bound certified by any applied
685    /// probe in this solve. `None` means no global envelope was certified.
686    pub certified_attainable_upper_nats: Option<f64>,
687}
688
689/// A measured target-dose solve either returns a certified plan or one of these
690/// explicit failure states. No unconverged iterate is representable as success.
691#[derive(Clone, Debug, PartialEq)]
692pub enum TargetDoseError {
693    InvalidRequest(String),
694    Steering(String),
695    Probe(String),
696    FactorNeedsAppliedDoseProbe {
697        kind: FisherDoseKind,
698    },
699    UnreachableTarget {
700        target_nats: f64,
701        certified_attainable_upper_nats: f64,
702    },
703    UnbracketedTarget {
704        target_nats: f64,
705        max_probed_amplitude: f64,
706        max_measured_nats: f64,
707        probes: usize,
708    },
709    BracketResolutionExhausted {
710        target_nats: f64,
711        lower_amplitude: f64,
712        lower_nats: f64,
713        upper_amplitude: f64,
714        upper_nats: f64,
715        probes: usize,
716    },
717}
718
719impl std::fmt::Display for TargetDoseError {
720    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
721        match self {
722            Self::InvalidRequest(message) | Self::Steering(message) | Self::Probe(message) => {
723                f.write_str(message)
724            }
725            Self::FactorNeedsAppliedDoseProbe { kind } => write!(
726                f,
727                "steer_to_target_nats: factor kind {} cannot solve a full-KL target without an applied-dose probe",
728                kind.as_str()
729            ),
730            Self::UnreachableTarget {
731                target_nats,
732                certified_attainable_upper_nats,
733            } => write!(
734                f,
735                "steer_to_target_nats: target {target_nats} nats is outside the certified \
736                 attainable envelope, whose global upper bound is \
737                 {certified_attainable_upper_nats} nats"
738            ),
739            Self::UnbracketedTarget {
740                target_nats,
741                max_probed_amplitude,
742                max_measured_nats,
743                probes,
744            } => write!(
745                f,
746                "steer_to_target_nats: could not bracket target {target_nats} nats after \
747                 {probes} probes through amplitude {max_probed_amplitude}; the largest \
748                 observed dose was {max_measured_nats} nats and no global attainable \
749                 envelope certified the target unreachable"
750            ),
751            Self::BracketResolutionExhausted {
752                target_nats,
753                lower_amplitude,
754                lower_nats,
755                upper_amplitude,
756                upper_nats,
757                probes,
758            } => write!(
759                f,
760                "steer_to_target_nats: exhausted {probes} probes before resolving target \
761                 {target_nats} nats inside measured bracket \
762                 ({lower_amplitude}, {lower_nats})..({upper_amplitude}, {upper_nats})"
763            ),
764        }
765    }
766}
767
768impl std::error::Error for TargetDoseError {}
769
770/// Execute and validate one model-in-the-loop observation. Validation lives in
771/// the Rust authority so every binding fails closed on malformed model data.
772fn probe_applied_dose(
773    probe: &mut AppliedDoseProbe<'_>,
774    plan: &SteerPlan,
775) -> Result<AppliedDoseObservation, TargetDoseError> {
776    let observation = probe(plan).map_err(TargetDoseError::Probe)?;
777    if observation.effective_delta.len() != plan.delta.len() {
778        return Err(TargetDoseError::Probe(format!(
779            "steer_to_target_nats: probe effective_delta length {} does not match plan delta length {}",
780            observation.effective_delta.len(),
781            plan.delta.len()
782        )));
783    }
784    if !observation
785        .effective_delta
786        .iter()
787        .all(|value| value.is_finite())
788    {
789        return Err(TargetDoseError::Probe(
790            "steer_to_target_nats: probe effective_delta must be finite".to_string(),
791        ));
792    }
793    if !(observation.exact_directional_nats.is_finite()
794        && observation.exact_directional_nats >= 0.0)
795    {
796        return Err(TargetDoseError::Probe(format!(
797            "steer_to_target_nats: probe exact_directional_nats must be finite and non-negative; got {}",
798            observation.exact_directional_nats
799        )));
800    }
801    if !(observation.measured_nats.is_finite() && observation.measured_nats >= 0.0) {
802        return Err(TargetDoseError::Probe(format!(
803            "steer_to_target_nats: probe measured_nats must be finite and non-negative; got {}",
804            observation.measured_nats
805        )));
806    }
807    if let Some(upper) = observation.certified_attainable_upper_nats {
808        if !(upper.is_finite() && upper >= 0.0) {
809            return Err(TargetDoseError::Probe(format!(
810                "steer_to_target_nats: probe certified_attainable_upper_nats must be finite \
811                 and non-negative when present; got {upper}"
812            )));
813        }
814        if observation.measured_nats > upper {
815            return Err(TargetDoseError::Probe(format!(
816                "steer_to_target_nats: measured dose {} exceeds the probe's certified \
817                 global attainable upper bound {upper}",
818                observation.measured_nats
819            )));
820        }
821    }
822    Ok(observation)
823}
824
825/// Merge one probe's optional global certificate into the solve-wide envelope.
826/// Every certificate must bound every observation in the same solve, not only
827/// the point at which it was returned.
828fn merge_attainable_envelope(
829    observation: &AppliedDoseObservation,
830    max_measured_nats: f64,
831    envelope: &mut Option<f64>,
832) -> Result<(), TargetDoseError> {
833    if let Some(upper) = observation.certified_attainable_upper_nats {
834        *envelope = Some(envelope.map_or(upper, |current| current.min(upper)));
835    }
836    if let Some(upper) = *envelope
837        && max_measured_nats > upper
838    {
839        return Err(TargetDoseError::Probe(format!(
840            "steer_to_target_nats: observed dose {max_measured_nats} exceeds an earlier \
841             certified global attainable upper bound {upper}"
842        )));
843    }
844    Ok(())
845}
846
847fn record_readout_probe(
848    amplitude: f64,
849    measured: f64,
850    predicted: f64,
851    tolerance: f64,
852    first_failure: &mut Option<f64>,
853    radius: &mut Option<f64>,
854) {
855    let agrees = predicted > 0.0 && (measured - predicted).abs() / predicted <= tolerance;
856    if agrees {
857        if (*first_failure).is_none_or(|failed| amplitude < failed) {
858            *radius = Some((*radius).map_or(amplitude, |current| current.max(amplitude)));
859        }
860    } else {
861        *first_failure = Some((*first_failure).map_or(amplitude, |failed| failed.min(amplitude)));
862        if (*radius).is_some_and(|current| current >= amplitude) {
863            *radius = None;
864        }
865    }
866}
867
868/// Solve for the amplitude that lands a target output-KL dose `target_nats` (in
869/// nats) on atom `atom_k`'s chord from `t_from` to `t_to` (gh#2263 target-dose
870/// surface — `amplitude = 1` has no universal meaning, the dose does).
871///
872/// The closed form `a0 = sqrt(2 q* / (dgᵀ M dg))` (`dg` the unit-amplitude chord,
873/// `M` the row output-Fisher) is exact in the quadratic/in-radius regime — and is
874/// correctly scaled only because the chord and the metric now share the raw
875/// activation frame (gh#2249, `ace3b9af3`; a Tier-0 σ-mis-scale on `dg` would
876/// have poisoned `a0`). Past the readout-KL radius the true KL saturates, so an
877/// optional `probe` (a patched forward) expands amplitudes in increasing order
878/// until two exact observations form a sign-change bracket, then solves that
879/// bracket with a safeguarded secant while recording the contiguous readout-KL
880/// radius. A local decrease is only another point observation: expansion keeps
881/// going. `UnreachableTarget` is possible only when a probe supplies a certified
882/// global attainable-dose upper bound below the requested tolerance band. With
883/// `probe = None` the result is the unvalidated closed-form seed: pure math and
884/// plumbing, no model in the loop.
885pub fn steer_to_target_nats(
886    model: &SaeManifoldTerm,
887    metric: &RowMetric,
888    request: TargetDoseRequest<'_>,
889    probe: Option<&mut AppliedDoseProbe<'_>>,
890) -> Result<TargetDosePlan, TargetDoseError> {
891    let TargetDoseRequest {
892        atom_k,
893        metric_row,
894        t_from,
895        t_to,
896        target_nats,
897        config,
898    } = request;
899    if !(target_nats.is_finite() && target_nats > 0.0) {
900        return Err(TargetDoseError::InvalidRequest(format!(
901            "steer_to_target_nats: target_nats must be finite and positive, got {target_nats}"
902        )));
903    }
904    if !(config.tol_rel.is_finite() && (0.0..1.0).contains(&config.tol_rel))
905        || config.max_iter == 0
906        || !(config.readout_tol_rel.is_finite() && (0.0..1.0).contains(&config.readout_tol_rel))
907    {
908        return Err(TargetDoseError::InvalidRequest(format!(
909            "steer_to_target_nats: config must have finite 0<=tol_rel<1, max_iter>0, \
910             finite 0<=readout_tol_rel<1; \
911             got {config:?}"
912        )));
913    }
914    // Unit-amplitude reference: predicted_nats(a) = a²·unit_nats, and the chart
915    // radius / provenance / dose kind are all amplitude-invariant.
916    let unit = steer_delta(model, metric, atom_k, metric_row, 1.0, t_from, t_to)
917        .map_err(TargetDoseError::Steering)?;
918    let unit_nats = unit.predicted_nats.ok_or_else(|| {
919        TargetDoseError::InvalidRequest(format!(
920            "steer_to_target_nats: atom {atom_k} has no behavioral (nats) metric \
921             (provenance {:?}); a target-nats dose is undefined",
922            unit.metric_provenance
923        ))
924    })?;
925    if !(unit_nats.is_finite() && unit_nats > 0.0) {
926        return Err(TargetDoseError::InvalidRequest(format!(
927            "steer_to_target_nats: unit-amplitude dose must be finite and positive; got \
928             {unit_nats} in metric row {metric_row}"
929        )));
930    }
931    if probe.is_none() && unit.predicted_nats_kind != FisherDoseKind::ExactFull {
932        return Err(TargetDoseError::FactorNeedsAppliedDoseProbe {
933            kind: unit.predicted_nats_kind,
934        });
935    }
936    // Closed-form first-order seed: a0²·unit_nats = q*.
937    let seed_amplitude = (target_nats / unit_nats).sqrt();
938    if !(seed_amplitude.is_finite() && seed_amplitude > 0.0) {
939        return Err(TargetDoseError::InvalidRequest(format!(
940            "steer_to_target_nats: target {target_nats} nats and unit dose {unit_nats} \
941             imply an unrepresentable amplitude {seed_amplitude}"
942        )));
943    }
944    let plan_at = |amplitude: f64| {
945        steer_delta(model, metric, atom_k, metric_row, amplitude, t_from, t_to)
946            .map_err(TargetDoseError::Steering)
947    };
948    let finish = |steer: SteerPlan,
949                  applied_probe: Option<AppliedDoseObservation>,
950                  iterations: usize,
951                  readout_kl_radius: Option<f64>,
952                  certified_attainable_upper_nats: Option<f64>|
953     -> Result<TargetDosePlan, TargetDoseError> {
954        Ok(TargetDosePlan {
955            target_nats,
956            seed_amplitude,
957            steer,
958            applied_probe,
959            iterations,
960            readout_kl_radius,
961            certified_attainable_upper_nats,
962        })
963    };
964
965    let probe = match probe {
966        Some(probe) => probe,
967        // No model in the loop: return the unvalidated closed-form seed.
968        None => return finish(plan_at(seed_amplitude)?, None, 0, None, None),
969    };
970
971    // Track a contiguous probed prefix of quadratic agreement. Once an amplitude
972    // fails the contract, no later probe at or beyond it can enlarge the radius.
973    let mut first_readout_failure: Option<f64> = None;
974    let mut readout_kl_radius: Option<f64> = None;
975
976    // Establish a genuine sign-change bracket [lo, hi], starting from the exact
977    // point KL(0)=0 and expanding the closed-form seed in amplitude order. No
978    // finite set of non-increasing observations proves a global plateau; only a
979    // callback-supplied global envelope can certify that the target is outside
980    // the attainable range.
981    let mut probes = 0usize;
982    let mut lo_a = 0.0_f64;
983    let mut lo_kl = 0.0_f64;
984    let mut hi_a = seed_amplitude;
985    let hi_plan = plan_at(hi_a)?;
986    let hi_probe = probe_applied_dose(probe, &hi_plan)?;
987    let mut hi_kl = hi_probe.measured_nats;
988    probes += 1;
989    let mut max_probed_amplitude = hi_a;
990    let mut max_measured_nats = hi_kl;
991    let mut certified_attainable_upper_nats = None;
992    merge_attainable_envelope(
993        &hi_probe,
994        max_measured_nats,
995        &mut certified_attainable_upper_nats,
996    )?;
997    record_readout_probe(
998        hi_a,
999        hi_kl,
1000        hi_probe.exact_directional_nats,
1001        config.readout_tol_rel,
1002        &mut first_readout_failure,
1003        &mut readout_kl_radius,
1004    );
1005    if (hi_kl - target_nats).abs() / target_nats <= config.tol_rel {
1006        return finish(
1007            hi_plan,
1008            Some(hi_probe),
1009            probes,
1010            readout_kl_radius,
1011            certified_attainable_upper_nats,
1012        );
1013    }
1014    let accepted_lower_nats = target_nats * (1.0 - config.tol_rel);
1015    if let Some(upper) = certified_attainable_upper_nats
1016        && upper < accepted_lower_nats
1017    {
1018        return Err(TargetDoseError::UnreachableTarget {
1019            target_nats,
1020            certified_attainable_upper_nats: upper,
1021        });
1022    }
1023    while hi_kl < target_nats {
1024        if probes >= config.max_iter {
1025            return Err(TargetDoseError::UnbracketedTarget {
1026                target_nats,
1027                max_probed_amplitude,
1028                max_measured_nats,
1029                probes,
1030            });
1031        }
1032        let next_a = hi_a * 2.0;
1033        if !(next_a.is_finite() && next_a > hi_a) {
1034            return Err(TargetDoseError::UnbracketedTarget {
1035                target_nats,
1036                max_probed_amplitude,
1037                max_measured_nats,
1038                probes,
1039            });
1040        }
1041        let next_plan = plan_at(next_a)?;
1042        let next_probe = probe_applied_dose(probe, &next_plan)?;
1043        let next_kl = next_probe.measured_nats;
1044        probes += 1;
1045        max_probed_amplitude = next_a;
1046        max_measured_nats = max_measured_nats.max(next_kl);
1047        merge_attainable_envelope(
1048            &next_probe,
1049            max_measured_nats,
1050            &mut certified_attainable_upper_nats,
1051        )?;
1052        record_readout_probe(
1053            next_a,
1054            next_kl,
1055            next_probe.exact_directional_nats,
1056            config.readout_tol_rel,
1057            &mut first_readout_failure,
1058            &mut readout_kl_radius,
1059        );
1060        if (next_kl - target_nats).abs() / target_nats <= config.tol_rel {
1061            return finish(
1062                next_plan,
1063                Some(next_probe),
1064                probes,
1065                readout_kl_radius,
1066                certified_attainable_upper_nats,
1067            );
1068        }
1069        if let Some(upper) = certified_attainable_upper_nats
1070            && upper < accepted_lower_nats
1071        {
1072            return Err(TargetDoseError::UnreachableTarget {
1073                target_nats,
1074                certified_attainable_upper_nats: upper,
1075            });
1076        }
1077        lo_a = hi_a;
1078        lo_kl = hi_kl;
1079        hi_a = next_a;
1080        hi_kl = next_kl;
1081    }
1082
1083    // Safeguarded secant inside the measured bracket. If roundoff puts the
1084    // secant outside the open bracket, bisection preserves monotone contraction.
1085    while probes < config.max_iter {
1086        let denominator = hi_kl - lo_kl;
1087        let secant = hi_a - (hi_kl - target_nats) * (hi_a - lo_a) / denominator;
1088        let candidate = if secant.is_finite() && secant > lo_a && secant < hi_a {
1089            secant
1090        } else {
1091            0.5 * (lo_a + hi_a)
1092        };
1093        let candidate_plan = plan_at(candidate)?;
1094        let candidate_probe = probe_applied_dose(probe, &candidate_plan)?;
1095        let measured = candidate_probe.measured_nats;
1096        probes += 1;
1097        max_measured_nats = max_measured_nats.max(measured);
1098        merge_attainable_envelope(
1099            &candidate_probe,
1100            max_measured_nats,
1101            &mut certified_attainable_upper_nats,
1102        )?;
1103        record_readout_probe(
1104            candidate,
1105            measured,
1106            candidate_probe.exact_directional_nats,
1107            config.readout_tol_rel,
1108            &mut first_readout_failure,
1109            &mut readout_kl_radius,
1110        );
1111        if (measured - target_nats).abs() / target_nats <= config.tol_rel {
1112            return finish(
1113                candidate_plan,
1114                Some(candidate_probe),
1115                probes,
1116                readout_kl_radius,
1117                certified_attainable_upper_nats,
1118            );
1119        }
1120        if measured < target_nats {
1121            lo_a = candidate;
1122            lo_kl = measured;
1123        } else {
1124            hi_a = candidate;
1125            hi_kl = measured;
1126        }
1127    }
1128    Err(TargetDoseError::BracketResolutionExhausted {
1129        target_nats,
1130        lower_amplitude: lo_a,
1131        lower_nats: lo_kl,
1132        upper_amplitude: hi_a,
1133        upper_nats: hi_kl,
1134        probes,
1135    })
1136}
1137
1138/// Does this provenance carry behavioral (output-Fisher) information? Euclidean
1139/// is the isotropic activation-only path and carries none; the factored
1140/// provenances do. (Mirrors `atom_lens::metric_carries_behavior`.)
1141fn metric_carries_behavior(p: MetricProvenance) -> bool {
1142    match p {
1143        MetricProvenance::Euclidean | MetricProvenance::WhitenedStructured { .. } => false,
1144        MetricProvenance::OutputFisher { .. }
1145        | MetricProvenance::OutputFisherDownstream { .. }
1146        | MetricProvenance::BehavioralFisher { .. } => true,
1147    }
1148}
1149
1150/// Evaluate the decoder output `g_k(t) = Φ_k(t) B_k ∈ ℝ^p` at an arbitrary
1151/// latent coordinate `t` (length `d`) via the atom's installed evaluator.
1152///
1153/// `scale` is the owning term's Tier-0 column scale (`term.tier0_scale()`):
1154/// under standardization/equilibration the fitted decoder lives in the
1155/// internal per-column frame `B_int[:,c] = B_raw[:,c]/σ_c`, while the row
1156/// metric `M = UUᵀ` is always built from raw activation-space probes. Every
1157/// steering quantity that meets the metric (chords, tangents, doses) must
1158/// therefore be mapped back to raw units, `g_raw[c] = σ_c·g_int[c]` (gh#2249
1159/// calibration confound; the Tier-0 MEAN cancels in every consumer here
1160/// because only chords/tangents are used, never absolute decodes).
1161fn decode_at(
1162    atom: &SaeManifoldAtom,
1163    t: &[f64],
1164    scale: Option<&Array1<f64>>,
1165) -> Result<Array1<f64>, String> {
1166    let d = t.len();
1167    let coords = Array2::from_shape_vec((1, d), t.to_vec())
1168        .map_err(|e| format!("steer_delta::decode_at: coord shape: {e}"))?;
1169    let mut out = atom.decode_at_coords(coords.view())?.row(0).to_owned();
1170    if let Some(scale) = scale {
1171        if scale.len() != out.len() {
1172            return Err(format!(
1173                "steer_delta::decode_at: tier0 scale length {} != output_dim {}",
1174                scale.len(),
1175                out.len()
1176            ));
1177        }
1178        for (v, &s) in out.iter_mut().zip(scale.iter()) {
1179            *v *= s;
1180        }
1181    }
1182    Ok(out)
1183}
1184
1185/// Evaluate the decoder tangents `∂g_k/∂t_a = Φ_k'(t) B_k ∈ ℝ^p`, one per latent
1186/// axis `a ∈ 0..d`, at an arbitrary latent coordinate `t`. Returned as a
1187/// `(p × d)` matrix whose column `a` is the tangent along axis `a`.
1188fn decode_tangents_at(
1189    atom: &SaeManifoldAtom,
1190    t: &[f64],
1191    scale: Option<&Array1<f64>>,
1192) -> Result<Array2<f64>, String> {
1193    let evaluator = atom.basis_evaluator.as_ref().ok_or_else(|| {
1194        "steer_delta::decode_tangents_at: atom has no installed basis evaluator".to_string()
1195    })?;
1196    let p = atom.output_dim();
1197    let d = atom.latent_dim();
1198    let coords = Array2::from_shape_vec((1, d), t.to_vec())
1199        .map_err(|e| format!("steer_delta::decode_tangents_at: coord shape: {e}"))?;
1200    let jet = if atom.homotopy_eta == 1.0 {
1201        evaluator.evaluate(coords.view())?.1
1202    } else {
1203        evaluator
1204            .evaluate_phi_eta(coords.view(), atom.homotopy_eta)?
1205            .jet
1206    };
1207    let decoder = &atom.decoder_coefficients;
1208    let m = decoder.nrows();
1209    if jet.dim() != (1, m, d) {
1210        return Err(format!(
1211            "steer_delta::decode_tangents_at: evaluator jet {:?} != (1, {m}, {d})",
1212            jet.dim()
1213        ));
1214    }
1215    let mut tang = Array2::<f64>::zeros((p, d));
1216    for axis in 0..d {
1217        for basis_col in 0..m {
1218            let dphi = jet[[0, basis_col, axis]];
1219            if dphi == 0.0 {
1220                continue;
1221            }
1222            for out_col in 0..p {
1223                tang[[out_col, axis]] += dphi * decoder[[basis_col, out_col]];
1224            }
1225        }
1226    }
1227    if let Some(scale) = scale {
1228        if scale.len() != p {
1229            return Err(format!(
1230                "steer_delta::decode_tangents_at: tier0 scale length {} != output_dim {p}",
1231                scale.len()
1232            ));
1233        }
1234        for (out_col, &s) in scale.iter().enumerate() {
1235            tang.row_mut(out_col).mapv_inplace(|v| v * s);
1236        }
1237    }
1238    Ok(tang)
1239}
1240
1241/// Least-squares projection of `δ` onto the span of the local tangents
1242/// (columns of `tangents`, shape `p × d`): `δ̂ = T (TᵀT)⁻¹ Tᵀ δ` via a small
1243/// `d × d` Gram solve (with a tiny diagonal jitter to absorb a rank-deficient
1244/// tangent frame; the jitter only shrinks the projection, never inflates it).
1245fn project_onto_tangent_span(tangents: &Array2<f64>, delta: ArrayView1<'_, f64>) -> Array1<f64> {
1246    let p = tangents.nrows();
1247    let d = tangents.ncols();
1248    if d == 0 {
1249        return Array1::<f64>::zeros(p);
1250    }
1251    // Gram = TᵀT (d × d) and rhs = Tᵀδ (d).
1252    let mut gram = Array2::<f64>::zeros((d, d));
1253    let mut rhs = Array1::<f64>::zeros(d);
1254    for a in 0..d {
1255        let mut r = 0.0_f64;
1256        for i in 0..p {
1257            r += tangents[[i, a]] * delta[i];
1258        }
1259        rhs[a] = r;
1260        for b in a..d {
1261            let mut acc = 0.0_f64;
1262            for i in 0..p {
1263                acc += tangents[[i, a]] * tangents[[i, b]];
1264            }
1265            gram[[a, b]] = acc;
1266            gram[[b, a]] = acc;
1267        }
1268    }
1269    let trace: f64 = (0..d).map(|a| gram[[a, a]]).sum();
1270    let jitter = if trace > 0.0 { 1e-12 * trace } else { 1e-12 };
1271    for a in 0..d {
1272        gram[[a, a]] += jitter;
1273    }
1274    let coeffs = solve_spd_small(&gram, &rhs);
1275    let mut proj = Array1::<f64>::zeros(p);
1276    for i in 0..p {
1277        for a in 0..d {
1278            proj[i] += tangents[[i, a]] * coeffs[a];
1279        }
1280    }
1281    proj
1282}
1283
1284/// Norm of `δ`'s component orthogonal to the span of the local tangents:
1285/// `‖δ − δ̂‖` with `δ̂` the [`project_onto_tangent_span`] projection.
1286fn off_manifold_residual_norm(tangents: &Array2<f64>, delta: ArrayView1<'_, f64>) -> f64 {
1287    let proj = project_onto_tangent_span(tangents, delta);
1288    let mut res_sq = 0.0_f64;
1289    for i in 0..delta.len() {
1290        let r = delta[i] - proj[i];
1291        res_sq += r * r;
1292    }
1293    res_sq.max(0.0).sqrt()
1294}
1295
1296/// Tiny symmetric-positive-definite solve via Cholesky for the `d × d` tangent
1297/// Gram (`d` is the atom's latent dim, typically 1–3). Falls back to the bare rhs
1298/// if the factorization fails (a fully degenerate frame), which only inflates the
1299/// reported off-manifold residual — never deflates it.
1300fn solve_spd_small(gram: &Array2<f64>, rhs: &Array1<f64>) -> Array1<f64> {
1301    let d = gram.nrows();
1302    // Cholesky L LᵀT = gram.
1303    let mut l = Array2::<f64>::zeros((d, d));
1304    for i in 0..d {
1305        for j in 0..=i {
1306            let mut sum = gram[[i, j]];
1307            for k in 0..j {
1308                sum -= l[[i, k]] * l[[j, k]];
1309            }
1310            if i == j {
1311                if sum <= 0.0 {
1312                    return Array1::<f64>::zeros(d);
1313                }
1314                l[[i, j]] = sum.sqrt();
1315            } else {
1316                l[[i, j]] = sum / l[[j, j]];
1317            }
1318        }
1319    }
1320    // Forward solve L y = rhs.
1321    let mut y = Array1::<f64>::zeros(d);
1322    for i in 0..d {
1323        let mut sum = rhs[i];
1324        for k in 0..i {
1325            sum -= l[[i, k]] * y[k];
1326        }
1327        y[i] = sum / l[[i, i]];
1328    }
1329    // Back solve Lᵀ x = y.
1330    let mut x = Array1::<f64>::zeros(d);
1331    for i in (0..d).rev() {
1332        let mut sum = y[i];
1333        for k in (i + 1)..d {
1334            sum -= l[[k, i]] * x[k];
1335        }
1336        x[i] = sum / l[[i, i]];
1337    }
1338    x
1339}
1340
1341/// The fixed geometry of one steering query, bundled so the dose integrator and
1342/// its helpers take a single context rather than a long argument list.
1343struct SteerContext<'a> {
1344    atom: &'a SaeManifoldAtom,
1345    /// Tier-0 column scale of the owning term, when standardization /
1346    /// equilibration is installed: decodes must be un-scaled back to raw
1347    /// activation units before they meet the (always raw-frame) row metric.
1348    scale: Option<&'a Array1<f64>>,
1349    metric: &'a RowMetric,
1350    /// The row whose per-row metric the dose is measured through.
1351    row: usize,
1352    /// Output dimension `p`.
1353    p: usize,
1354    /// Latent dimension `d`.
1355    d: usize,
1356    /// Amplitude `a` the move is scaled by.
1357    amplitude: f64,
1358    coordinate_delta: &'a [f64],
1359    periods: &'a [Option<f64>],
1360}
1361
1362/// The validity radius: the latent step length (Euclidean distance from
1363/// `t_from`) at which **local linearization stops being trusted**.
1364///
1365/// Linearizing the steering move means predicting the output effect of a prefix
1366/// step `τ·Δt` from the initial tangent alone: the first-order output move is
1367/// `δ_lin(τ) = a · (∂g/∂t|_{t_from}) · (τ Δt)`, whose output-Fisher KL is the
1368/// quadratic form `½ ‖δ_lin(τ)‖²_M = τ² · ½ a² ‖∂g/∂t·Δt‖²_M`. The **true**
1369/// effect of that prefix is the chord quadratic form of the *actual* curved
1370/// output move `½ a² ‖g(t_from + τΔt) − g(t_from)‖²_M`.
1371///
1372/// The radius is the chord length `τ* · ‖Δt‖` at the first prefix `τ*` where the
1373/// true chord KL diverges from the linear prediction by more than
1374/// [`VALIDITY_DIVERGENCE_FRACTION`] (relative to the linear prediction). This is
1375/// pure surface curvature: on a flat decoder the two agree for every `τ` and the
1376/// radius is the whole move. If the metric kills the tangent (no linear effect to
1377/// validate), the move is trusted to its full length.
1378fn validity_radius(ctx: &SteerContext<'_>, t_from: &[f64]) -> Result<f64, String> {
1379    let d = ctx.d;
1380    let p = ctx.p;
1381    let full_len: f64 = ctx
1382        .coordinate_delta
1383        .iter()
1384        .map(|d| d * d)
1385        .sum::<f64>()
1386        .sqrt();
1387    if full_len == 0.0 {
1388        return Ok(0.0);
1389    }
1390    let dt = ctx.coordinate_delta;
1391    let amp = ctx.amplitude;
1392
1393    // Initial-tangent linear output move per unit τ: v0 = (∂g/∂t|_{t_from}) Δt.
1394    let tang0 = decode_tangents_at(ctx.atom, t_from, ctx.scale)?;
1395    let mut v0 = Array1::<f64>::zeros(p);
1396    for i in 0..p {
1397        let mut acc = 0.0_f64;
1398        for a in 0..d {
1399            acc += tang0[[i, a]] * dt[a];
1400        }
1401        v0[i] = acc;
1402    }
1403    // ½ a² ‖v0‖²_M — the per-τ² linear KL coefficient.
1404    let lin_coeff = 0.5 * amp * amp * ctx.metric.fisher_mass(ctx.row, v0.view());
1405    // No linear effect to validate against ⇒ trust the full move.
1406    if !(lin_coeff > 0.0) {
1407        return Ok(full_len);
1408    }
1409
1410    let g_from = decode_at(ctx.atom, t_from, ctx.scale)?;
1411    let steps = STEER_VALIDITY_STEPS;
1412    for s in 0..steps {
1413        let tau = (s as f64 + 1.0) / steps as f64;
1414        let t_mid = path_coordinate(t_from, dt, ctx.periods, tau);
1415        let g_tau = decode_at(ctx.atom, &t_mid, ctx.scale)?;
1416        let mut chord = Array1::<f64>::zeros(p);
1417        for i in 0..p {
1418            chord[i] = amp * (g_tau[i] - g_from[i]);
1419        }
1420        // True chord KL of the prefix, and the linear prediction τ²·lin_coeff.
1421        let chord_kl = 0.5 * ctx.metric.fisher_mass(ctx.row, chord.view());
1422        let lin_kl = tau * tau * lin_coeff;
1423        let rel = (chord_kl - lin_kl).abs() / lin_kl;
1424        if rel > VALIDITY_DIVERGENCE_FRACTION {
1425            return Ok(tau * full_len);
1426        }
1427    }
1428    Ok(full_len)
1429}
1430
1431/// One dose sample on a collateral-damage curve (gam#2234 E2, the intrinsic
1432/// Rust-owned counterpart of the model-in-the-loop KL frontier): the on-target
1433/// effect and the off-target collateral of a single steering intervention,
1434/// measured in the fitted dictionary's own representation, with no LLM in the
1435/// loop.
1436#[derive(Clone, Debug, PartialEq, serde::Serialize)]
1437pub struct CollateralPoint {
1438    /// The chart-coordinate dose applied to the target atom's steered axis
1439    /// (radians / fraction-of-period, per the atom's manifold).
1440    pub dose: f64,
1441    /// RMS-over-rows on-target effect: `‖proj_{T_k} Δ‖`, the energy the
1442    /// intervention deposits into the TARGET atom's own local decode-tangent
1443    /// frame `T_k = ∂g_k/∂t` at each row's fitted operating point. This is the
1444    /// intended landing — how loudly the knob turned the feature it names.
1445    pub on_target_effect: f64,
1446    /// RMS-over-rows collateral: `‖Δ − proj_{T_k} Δ‖`, the energy the SAME
1447    /// intervention deposits OUTSIDE the target atom's own local frame — the total
1448    /// damage. The on-manifold move is a chord of atom `k`'s decoder curve, so its
1449    /// off-target component is only the second-order sagitta (`≈ 0`, growing with
1450    /// dose-curvature); a fixed flat direction is off the rotating target frame at
1451    /// most rows, so its off-target energy is immediate. This is the direct
1452    /// generalization of the single-move [`SteerPlan::off_manifold_norm`] guard to
1453    /// a swept intervention.
1454    pub collateral: f64,
1455    /// RMS-over-rows CROSS-FEATURE leakage: `sqrt(Σ_{j∈others} ‖proj_{T_j} Δ‖²)`,
1456    /// the part of the move that lands on OTHER named atoms' frames — the
1457    /// interpretable "steering feature `k` spuriously moved feature `j`" damage, a
1458    /// component of the total `collateral`.
1459    pub cross_feature: f64,
1460}
1461
1462/// One intervention family's swept collateral curve plus its aggregate
1463/// collateral efficiency (collateral energy spent per unit on-target effect).
1464#[derive(Clone, Debug, PartialEq, serde::Serialize)]
1465pub struct CollateralArm {
1466    /// Per-dose `(effect, collateral)` samples, in the order of the input doses.
1467    pub points: Vec<CollateralPoint>,
1468    /// Collateral energy per unit on-target effect over the swept doses:
1469    /// `sqrt(Σ collateral²) / sqrt(Σ effect²)`. Lower is a cleaner control knob.
1470    /// `NaN` when the arm achieves no on-target effect at any dose.
1471    pub efficiency: f64,
1472}
1473
1474/// The on-manifold-vs-flat collateral-damage comparison for one target atom
1475/// (gam#2234 E2 thesis, measured intrinsically — no model surgery, no outer-fit
1476/// convergence in the loop). The two arms move the SAME per-row ambient energy:
1477/// the flat arm applies it along a single fixed decoder direction (the flat-SAE
1478/// `x' = x + α·w` baseline), the manifold arm applies the chart-coordinate group
1479/// action `x' = x + a·(Φ_k(t⊕δ) − Φ_k(t))·B_k`, which rotates with each row's
1480/// coordinate to stay on the atom's decoded image. The thesis: at matched
1481/// per-row norm the manifold arm spends strictly less collateral per unit
1482/// on-target effect — curved features are the right control knobs.
1483#[derive(Clone, Debug, PartialEq, serde::Serialize)]
1484pub struct CollateralCurve {
1485    /// The steered (target) atom.
1486    pub atom: usize,
1487    /// The target atom's latent axis the dose is applied along.
1488    pub axis: usize,
1489    /// The atoms collateral is measured against (typically every `j ≠ atom`).
1490    pub others: Vec<usize>,
1491    /// The on-manifold group-action arm.
1492    pub manifold: CollateralArm,
1493    /// The matched-per-row-norm fixed-direction (flat-SAE) control arm.
1494    pub flat: CollateralArm,
1495    /// `true` when the on-manifold arm spends strictly less collateral per unit
1496    /// on-target effect than the flat arm (`manifold.efficiency < flat.efficiency`),
1497    /// with both efficiencies finite — the E2 dominance verdict, decided
1498    /// structurally in the SAE's own representation.
1499    pub manifold_is_cleaner: bool,
1500}
1501
1502/// Norm of `δ`'s component that lands inside the span of a local decode-tangent
1503/// frame — the energy the ambient move deposits into that atom's feature
1504/// direction at its current operating point.
1505fn frame_landed_norm(frame: &Array2<f64>, delta: ArrayView1<'_, f64>) -> f64 {
1506    let proj = project_onto_tangent_span(frame, delta);
1507    proj.iter().map(|&x| x * x).sum::<f64>().sqrt()
1508}
1509
1510/// Sweep the intrinsic collateral-damage curve for steering atom `atom_k` along
1511/// latent `axis` over `doses`, comparing the on-manifold group action against a
1512/// matched-per-row-norm flat-direction control (gam#2234 E2).
1513///
1514/// For each dose `δ` the on-manifold ambient move is the fitted group-action
1515/// delta [`SaeManifoldTerm::steer_rows`] (chart step `δ` on `axis`, gate held
1516/// fixed). The flat control replays the SAME per-row move NORM along one fixed
1517/// ambient direction `w` — the atom's mean decode-tangent direction along `axis`,
1518/// the manifold analog of a flat SAE's single decoder column. Each move is
1519/// decomposed against every atom's local decode-tangent frame at its fitted
1520/// coordinate: the projection onto the TARGET atom's frame is the on-target
1521/// effect, the projection onto the OTHER atoms' frames is the collateral.
1522///
1523/// This is a pure read over the fitted term (no criterion, no penalty, no outer
1524/// fit), so it runs on any fitted or hand-built term with installed evaluators —
1525/// it does not wait on outer-loop convergence, unlike the model-in-the-loop E1/E2
1526/// KL frontier it mirrors.
1527///
1528/// Errors when `atom_k`/`axis`/an `others` index is out of range, `doses` is
1529/// empty, or the target atom's mean tangent along `axis` vanishes (no fixed
1530/// direction to define the flat control against).
1531pub fn collateral_curve(
1532    model: &SaeManifoldTerm,
1533    atom_k: usize,
1534    axis: usize,
1535    others: &[usize],
1536    doses: &[f64],
1537) -> Result<CollateralCurve, String> {
1538    let k = model.k_atoms();
1539    if atom_k >= k {
1540        return Err(format!(
1541            "collateral_curve: atom index {atom_k} out of range (term has {k} atoms)"
1542        ));
1543    }
1544    let d_k = model.atoms[atom_k].latent_dim();
1545    if axis >= d_k {
1546        return Err(format!(
1547            "collateral_curve: axis {axis} out of range for atom {atom_k} latent_dim {d_k}"
1548        ));
1549    }
1550    if doses.is_empty() {
1551        return Err("collateral_curve: doses must be non-empty".to_string());
1552    }
1553    for &j in others {
1554        if j >= k {
1555            return Err(format!(
1556                "collateral_curve: other atom index {j} out of range (term has {k} atoms)"
1557            ));
1558        }
1559    }
1560    let n = model.n_obs();
1561    let p = model.output_dim();
1562    let rows: Vec<usize> = (0..n).collect();
1563
1564    // Per-row local decode-tangent frames at each atom's fitted operating point.
1565    // These are dose-independent (the fitted coordinates never move; steering is
1566    // the hypothetical move whose leakage we price against the CURRENT features).
1567    let frame_at = |atom_idx: usize| -> Result<Vec<Array2<f64>>, String> {
1568        let coords = model.assignment.coords[atom_idx].as_matrix();
1569        let mut frames = Vec::with_capacity(n);
1570        for row in 0..n {
1571            let t: Vec<f64> = coords.row(row).to_vec();
1572            frames.push(decode_tangents_at(
1573                &model.atoms[atom_idx],
1574                &t,
1575                model.tier0_scale(),
1576            )?);
1577        }
1578        Ok(frames)
1579    };
1580    let target_frames = frame_at(atom_k)?;
1581    let mut other_frames: Vec<Vec<Array2<f64>>> = Vec::with_capacity(others.len());
1582    for &j in others {
1583        other_frames.push(frame_at(j)?);
1584    }
1585
1586    // The fixed flat direction w: the dominant ambient direction the target atom
1587    // moves along `axis` — the top left singular vector of its per-row tangent
1588    // field, i.e. the leading eigenvector of `G = Σ_i g_i g_iᵀ` with
1589    // `g_i = ∂g_k/∂t_axis|_{t_i}`. This is the single best fixed decoder column a
1590    // flat SAE would steer this feature with (the mean tangent is not usable — it
1591    // averages to ≈0 over a full circle). Found by power iteration on the small
1592    // `p × p` Gram, which is exact for the leading direction.
1593    let mut gram = Array2::<f64>::zeros((p, p));
1594    for frame in &target_frames {
1595        for i in 0..p {
1596            let gi = frame[[i, axis]];
1597            if gi == 0.0 {
1598                continue;
1599            }
1600            for j in 0..p {
1601                gram[[i, j]] += gi * frame[[j, axis]];
1602            }
1603        }
1604    }
1605    let mut w = Array1::<f64>::from_elem(p, 1.0 / (p as f64).sqrt());
1606    for _ in 0..128 {
1607        let mut next = Array1::<f64>::zeros(p);
1608        for i in 0..p {
1609            let mut acc = 0.0_f64;
1610            for j in 0..p {
1611                acc += gram[[i, j]] * w[j];
1612            }
1613            next[i] = acc;
1614        }
1615        let norm = next.iter().map(|&x| x * x).sum::<f64>().sqrt();
1616        if !(norm > 0.0) {
1617            return Err(format!(
1618                "collateral_curve: atom {atom_k} has a vanishing tangent field along axis {axis}; \
1619                 no fixed direction to define the flat control"
1620            ));
1621        }
1622        next.mapv_inplace(|x| x / norm);
1623        w = next;
1624    }
1625
1626    // Decompose one per-row move field into (effect, off-target collateral,
1627    // cross-feature leakage) RMS over rows.
1628    let decompose = |field: &Array2<f64>| -> CollateralPoint {
1629        let mut eff_sq = 0.0_f64;
1630        let mut col_sq = 0.0_f64;
1631        let mut cross_sq = 0.0_f64;
1632        for row in 0..n {
1633            let delta = field.row(row);
1634            let on_target = project_onto_tangent_span(&target_frames[row], delta);
1635            let mut e = 0.0_f64;
1636            let mut c = 0.0_f64;
1637            for i in 0..p {
1638                e += on_target[i] * on_target[i];
1639                let residual = delta[i] - on_target[i];
1640                c += residual * residual;
1641            }
1642            eff_sq += e;
1643            col_sq += c;
1644            let mut cross = 0.0_f64;
1645            for frames in &other_frames {
1646                let l = frame_landed_norm(&frames[row], delta);
1647                cross += l * l;
1648            }
1649            cross_sq += cross;
1650        }
1651        let denom = n.max(1) as f64;
1652        CollateralPoint {
1653            dose: 0.0,
1654            on_target_effect: (eff_sq / denom).sqrt(),
1655            collateral: (col_sq / denom).sqrt(),
1656            cross_feature: (cross_sq / denom).sqrt(),
1657        }
1658    };
1659
1660    let mut manifold_pts = Vec::with_capacity(doses.len());
1661    let mut flat_pts = Vec::with_capacity(doses.len());
1662    for &dose in doses {
1663        let mut step = Array1::<f64>::zeros(d_k);
1664        step[axis] = dose;
1665        let on_field = model.steer_rows(atom_k, &rows, step.view())?;
1666
1667        // Matched control: same per-row move NORM, along the fixed direction w.
1668        let mut flat_field = Array2::<f64>::zeros((n, p));
1669        for row in 0..n {
1670            let norm = on_field.row(row).iter().map(|&x| x * x).sum::<f64>().sqrt();
1671            for i in 0..p {
1672                flat_field[[row, i]] = norm * w[i];
1673            }
1674        }
1675
1676        let mut m = decompose(&on_field);
1677        m.dose = dose;
1678        manifold_pts.push(m);
1679        let mut f = decompose(&flat_field);
1680        f.dose = dose;
1681        flat_pts.push(f);
1682    }
1683
1684    let efficiency = |pts: &[CollateralPoint]| -> f64 {
1685        let eff_sq: f64 = pts
1686            .iter()
1687            .map(|q| q.on_target_effect * q.on_target_effect)
1688            .sum();
1689        let col_sq: f64 = pts.iter().map(|q| q.collateral * q.collateral).sum();
1690        if eff_sq > 0.0 {
1691            (col_sq / eff_sq).sqrt()
1692        } else {
1693            f64::NAN
1694        }
1695    };
1696    let manifold = CollateralArm {
1697        efficiency: efficiency(&manifold_pts),
1698        points: manifold_pts,
1699    };
1700    let flat = CollateralArm {
1701        efficiency: efficiency(&flat_pts),
1702        points: flat_pts,
1703    };
1704    let manifold_is_cleaner = manifold.efficiency.is_finite()
1705        && flat.efficiency.is_finite()
1706        && manifold.efficiency < flat.efficiency;
1707
1708    Ok(CollateralCurve {
1709        atom: atom_k,
1710        axis,
1711        others: others.to_vec(),
1712        manifold,
1713        flat,
1714        manifold_is_cleaner,
1715    })
1716}
1717
1718#[cfg(test)]
1719mod tests {
1720    use super::*;
1721
1722    #[test]
1723    fn periodic_steering_uses_shortest_path_across_seam() {
1724        let periods = [Some(1.0)];
1725        let delta = shortest_coordinate_delta(&[0.99], &[0.01], &periods).unwrap();
1726        assert!((delta[0] - 0.02).abs() < 1e-12);
1727        let midpoint = path_coordinate(&[0.99], &delta, &periods, 0.5);
1728        assert!(midpoint[0].abs() < 1e-12 || (midpoint[0] - 1.0).abs() < 1e-12);
1729        let distance = coordinate_l2_distance(&[0.99], &[0.01], &periods).unwrap();
1730        assert!((distance - 0.02).abs() < 1e-12);
1731    }
1732}