1use ndarray::{Array1, Array2, ArrayView1};
69
70use crate::encode::EncodeAtlas;
71use crate::manifold::{SaeManifoldAtom, SaeManifoldTerm};
72use gam_problem::{FisherFactorKind, MetricProvenance, RowMetric};
73
74const STEER_VALIDITY_STEPS: usize = 64;
79
80const VALIDITY_DIVERGENCE_FRACTION: f64 = 0.1;
84
85#[derive(Clone, Copy, Debug, PartialEq, Eq)]
87pub enum FisherDoseKind {
88 Unavailable,
90 ExactFull,
92 CertifiedPsdLowerBound,
94 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#[derive(Clone, Debug, PartialEq)]
112pub struct SteerPlan {
113 pub atom: usize,
115 pub atom_name: String,
117 pub t_from: Vec<f64>,
119 pub t_to: Vec<f64>,
121 pub amplitude: f64,
123 pub metric_row: usize,
125 pub delta: Array1<f64>,
129 pub predicted_nats: Option<f64>,
134 pub predicted_nats_kind: FisherDoseKind,
136 pub fisher_mass_captured: Option<f64>,
138 pub fisher_mass_residual: Option<f64>,
140 pub fisher_mass_residual_fraction: Option<f64>,
142 pub validity_radius: Option<f64>,
149 pub off_manifold_norm: f64,
154 pub metric_provenance: MetricProvenance,
157}
158
159#[derive(Clone, Debug)]
168pub struct CoordinateSetResult {
169 pub edited: Array1<f64>,
171 pub t_from_certified: Array1<f64>,
173 pub encode_certificate: crate::encode::RowCertificate,
175 pub steer: SteerPlan,
177}
178
179pub 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#[derive(Clone, Debug)]
241pub struct InterchangeResult {
242 pub edited_target: Array1<f64>,
244 pub donor_t: Array1<f64>,
246 pub target_t_before: Array1<f64>,
248 pub target_t_after: Array1<f64>,
250 pub predicted_nats: Option<f64>,
252 pub off_manifold_norm: f64,
254 pub validity_radius: Option<f64>,
256 pub landing_error: f64,
260 pub set_result: CoordinateSetResult,
262}
263
264pub 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
374pub 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 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 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 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 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
537pub 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#[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
610pub type AppliedDoseProbe<'a> =
614 dyn FnMut(&SteerPlan) -> Result<AppliedDoseObservation, String> + 'a;
615
616#[derive(Clone, Copy, Debug)]
618pub struct TargetDoseConfig {
619 pub tol_rel: f64,
621 pub max_iter: usize,
623 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#[derive(Clone, Copy, Debug)]
645pub struct TargetDoseRequest<'a> {
646 pub atom_k: usize,
648 pub metric_row: usize,
650 pub t_from: &'a [f64],
652 pub t_to: &'a [f64],
654 pub target_nats: f64,
656 pub config: TargetDoseConfig,
658}
659
660#[derive(Clone, Debug)]
663pub struct TargetDosePlan {
664 pub target_nats: f64,
666 pub seed_amplitude: f64,
669 pub steer: SteerPlan,
672 pub applied_probe: Option<AppliedDoseObservation>,
676 pub iterations: usize,
678 pub readout_kl_radius: Option<f64>,
684 pub certified_attainable_upper_nats: Option<f64>,
687}
688
689#[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
770fn 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
825fn 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
868pub 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 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 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 None => return finish(plan_at(seed_amplitude)?, None, 0, None, None),
969 };
970
971 let mut first_readout_failure: Option<f64> = None;
974 let mut readout_kl_radius: Option<f64> = None;
975
976 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 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
1138fn 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
1150fn 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
1185fn 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
1241fn 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 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
1284fn 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
1296fn solve_spd_small(gram: &Array2<f64>, rhs: &Array1<f64>) -> Array1<f64> {
1301 let d = gram.nrows();
1302 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 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 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
1341struct SteerContext<'a> {
1344 atom: &'a SaeManifoldAtom,
1345 scale: Option<&'a Array1<f64>>,
1349 metric: &'a RowMetric,
1350 row: usize,
1352 p: usize,
1354 d: usize,
1356 amplitude: f64,
1358 coordinate_delta: &'a [f64],
1359 periods: &'a [Option<f64>],
1360}
1361
1362fn 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 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 let lin_coeff = 0.5 * amp * amp * ctx.metric.fisher_mass(ctx.row, v0.view());
1405 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 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#[derive(Clone, Debug, PartialEq, serde::Serialize)]
1437pub struct CollateralPoint {
1438 pub dose: f64,
1441 pub on_target_effect: f64,
1446 pub collateral: f64,
1455 pub cross_feature: f64,
1460}
1461
1462#[derive(Clone, Debug, PartialEq, serde::Serialize)]
1465pub struct CollateralArm {
1466 pub points: Vec<CollateralPoint>,
1468 pub efficiency: f64,
1472}
1473
1474#[derive(Clone, Debug, PartialEq, serde::Serialize)]
1484pub struct CollateralCurve {
1485 pub atom: usize,
1487 pub axis: usize,
1489 pub others: Vec<usize>,
1491 pub manifold: CollateralArm,
1493 pub flat: CollateralArm,
1495 pub manifold_is_cleaner: bool,
1500}
1501
1502fn 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
1510pub 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 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 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 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 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}