gam_solve/rho_optimizer/run.rs
1use super::*;
2use gam_problem::{StationarityRung, StationarityStandard};
3
4use super::asymptote_certificate::{
5 AsymptoteSample, AsymptoteSide, AsymptoteTolerances, AsymptoteVerdict, AsymptoteWindow,
6 MIN_TAIL_SAMPLES, assess_coordinate,
7};
8use super::rail_face::{
9 RailFaceLimit, RailFaceLimitOutcome, RailFaceProof, RailFaceVerdict, certify_rail_face,
10};
11
12pub(crate) const OPERATOR_TRUST_RESTART_RADIUS_FLOOR: f64 = 1.0e-6;
13
14/// Inner coefficient state bound to one exact outer seed.
15///
16/// A cached coefficient vector is only a valid initialization for the outer
17/// coordinate that produced it. Keeping the coordinate beside the vector
18/// prevents a multi-start run from silently reusing one basin's coefficients
19/// at a different seed.
20#[derive(Clone, Debug)]
21pub(crate) struct BoundInnerSeed {
22 pub(crate) theta: Array1<f64>,
23 pub(crate) beta: Array1<f64>,
24}
25
26pub(crate) fn outer_theta_bitwise_eq(left: &Array1<f64>, right: &Array1<f64>) -> bool {
27 left.len() == right.len()
28 && left
29 .iter()
30 .zip(right.iter())
31 .all(|(left, right)| left.to_bits() == right.to_bits())
32}
33
34/// Install cached inner state only at the exact outer coordinate that owns it.
35///
36/// This function is called after a seed-attempt reset and immediately before
37/// the literal seed can be evaluated. Generated multistart candidates never
38/// inherit coefficients from another outer point.
39pub(crate) fn install_matching_initial_inner_seed(
40 obj: &mut dyn OuterObjective,
41 config: &OuterConfig,
42 seed: &Array1<f64>,
43 context: &str,
44) -> Result<(), EstimationError> {
45 let Some(bound) = config.initial_inner_seed.as_ref() else {
46 return Ok(());
47 };
48 if !outer_theta_bitwise_eq(&bound.theta, seed) {
49 return Ok(());
50 }
51 match obj.seed_inner_state(&bound.beta)? {
52 SeedOutcome::Installed => log::info!(
53 "[CACHE] beta-warm context={} theta_dim={} beta_dim={} action=installed",
54 context,
55 bound.theta.len(),
56 bound.beta.len(),
57 ),
58 SeedOutcome::NoSlot => log::warn!(
59 "[CACHE] beta-warm context={} theta_dim={} beta_dim={} action=skip \
60 reason=objective_has_no_inner_beta_slot",
61 context,
62 bound.theta.len(),
63 bound.beta.len(),
64 ),
65 SeedOutcome::Incompatible => log::info!(
66 "[CACHE] beta-warm context={} theta_dim={} beta_dim={} action=rho-only \
67 reason=seed_beta_incompatible_with_inner_state",
68 context,
69 bound.theta.len(),
70 bound.beta.len(),
71 ),
72 }
73 Ok(())
74}
75
76/// Temporarily require a complete inner solve.
77///
78/// Search-time REML evaluations may deliberately cap P-IRLS, but a sample
79/// used as mathematical evidence about the true profiled objective cannot.
80/// Seed samples, terminal certificates, and final state installation therefore
81/// lift the cap for the duration of their evaluation and restore the scheduler's
82/// value afterward. Callers that may hold capped cached state reset the
83/// objective before making the full-fidelity request.
84pub(crate) struct FullFidelityInnerCapGuard<'a> {
85 cap: &'a AtomicUsize,
86 previous: usize,
87}
88
89impl<'a> FullFidelityInnerCapGuard<'a> {
90 pub(crate) fn lift(feedback: &'a InnerProgressFeedback) -> Self {
91 let cap = feedback.cap.as_ref();
92 let previous = cap.swap(0, Ordering::Relaxed);
93 Self { cap, previous }
94 }
95}
96
97impl Drop for FullFidelityInnerCapGuard<'_> {
98 fn drop(&mut self) {
99 self.cap.store(self.previous, Ordering::Relaxed);
100 }
101}
102
103/// Configuration for the outer optimization runner.
104#[derive(Clone, Debug)]
105pub(crate) struct OuterConfig {
106 pub(crate) tolerance: f64,
107 /// Optional override for the *relative-cost-decrease* convergence stop,
108 /// decoupled from `tolerance`. `outer_gradient_tolerance` normally derives
109 /// BOTH the absolute projected-gradient floor
110 /// (`max(tolerance, scale·√ε_machine)`)
111 /// AND the relative-cost stop (`rel_cost = tolerance`) from the single
112 /// `tolerance`. That conflation forces a caller who needs a *tight absolute
113 /// floor* (to resolve λ to the genuine REML optimum at large `n`, where the
114 /// floor is `scale·√ε_machine`) to also accept a *tight rel-cost stop*,
115 /// which on a flat REML ridge never trips and grinds the optimizer to `max_iter` —
116 /// dozens of surplus O(D·p³) Laplace-derivative outer iterations (the #1082
117 /// multinomial smooth-by-factor wall-clock blow-up). When `Some(r)`, the
118 /// rel-cost stop uses `r` while the absolute floor keeps using `tolerance`
119 /// via `objective_scale`, so accuracy (absolute floor) and perf (loose
120 /// rel-cost) are selected independently. `None` preserves the legacy coupling
121 /// (`rel_cost = tolerance`) for every existing path byte-for-byte.
122 pub(crate) rel_cost_tolerance: Option<f64>,
123 pub(crate) max_iter: usize,
124 /// The model's canonical feasible outer domain. Every stationarity
125 /// certificate and rail report reasons against this box.
126 pub(crate) model_domain_bounds: Option<(Array1<f64>, Array1<f64>)>,
127 /// A temporary algorithmic subspace used only by an active-set polish.
128 /// It may narrow the model domain but never changes the feasible cone that
129 /// screening or mint is allowed to certify.
130 pub(crate) search_bounds_override: Option<(Array1<f64>, Array1<f64>)>,
131 pub(crate) seed_config: gam_problem::SeedConfig,
132 pub(crate) rho_bound: f64,
133 pub(crate) heuristic_lambdas: Option<Vec<f64>>,
134 pub(crate) initial_rho: Option<Array1<f64>>,
135 /// Additional explicit, model-derived starts. Unlike the generic seed
136 /// lattice these are supplied by the objective owner and survive the
137 /// `max_seeds` truncation; the certified keep-best loop optimizes each one.
138 pub(crate) initial_rho_candidates: Vec<Array1<f64>>,
139 /// Seed points an EARLIER round of this same [`run_outer`] call already
140 /// started and whose certification it already refused (#2569).
141 ///
142 /// Set only by the certify-resume loop, from
143 /// [`OuterResult::refused_seed_points`]. The plan runner drops these from
144 /// its cascade (never the caller's own `initial_rho`, which is the reseed
145 /// point the resume exists to explore), because re-running them from the
146 /// reset state they were refused in reproduces the recorded verdict digit
147 /// for digit — the same argument #2080 makes for replaying a recorded
148 /// cold-entry-leg refusal. Empty on every non-resume path, so the ordinary
149 /// cascade is unchanged.
150 pub(crate) previously_refused_seed_points: Vec<Array1<f64>>,
151 pub(crate) initial_inner_seed: Option<BoundInnerSeed>,
152 pub(crate) fallback_policy: FallbackPolicy,
153 pub(crate) screening_cap: Option<Arc<AtomicUsize>>,
154 pub(crate) screen_initial_rho: bool,
155 /// `initial_rho` came from a PRIOR FIT'S TERMINAL CERTIFICATE, not from a
156 /// heuristic, a mid-run checkpoint, or a caller's guess.
157 ///
158 /// The distinction is not where the number was stored, it is what is known
159 /// about it: a terminal certificate is a rho a previous outer run already
160 /// certified as stationary, so re-deriving it can only move it.
161 pub(crate) initial_rho_is_prior_terminal_certificate: bool,
162 /// Outer-aware inner-PIRLS iteration cap (sibling of `screening_cap`).
163 /// When set, the BFGS bridge drives this atomic on every accepted
164 /// gradient eval to coarsen the inner Newton solve at early outer iters
165 /// (when ρ is far from converged) and lift it back to full as
166 /// convergence approaches. Distinct from `screening_cap` in that it
167 /// does NOT suppress cache writes / warm-start updates / KKT
168 /// enforcement; it is purely a budget. See
169 /// `RemlObjectiveState::outer_inner_cap` for dual-cap semantics.
170 pub(crate) outer_inner_cap: Option<InnerProgressFeedback>,
171 pub(crate) operator_initial_trust_radius: Option<f64>,
172 pub(crate) arc_initial_regularization: Option<f64>,
173 /// Optional scale factor for the objective's natural magnitude.
174 /// Used to widen the absolute gradient-norm floor on objectives whose
175 /// gradient lives on a non-unit scale (e.g. Gaussian-identity REML at
176 /// large `n`, whose ∂/∂logλ inherits the O(n) likelihood constant).
177 /// `None` falls back to the bare `tolerance` floor.
178 pub(crate) objective_scale: Option<f64>,
179 /// BFGS line-search infinity-norm cap applied to the leading `rho_dim`
180 /// outer parameters (log-λ axes). Documented natural step for
181 /// `log(lambda)` is ≈ 5 (`e^5 ≈ 148`-fold smoothing-parameter change
182 /// per accepted outer iter — matches typical quasi-Newton direction
183 /// magnitude on flat REML surfaces). Setting this `None` disables the
184 /// rho-axis cap entirely.
185 pub(crate) bfgs_step_cap: Option<f64>,
186 /// BFGS line-search infinity-norm cap applied to the trailing `psi_dim`
187 /// outer parameters (kappa / aniso-log-scale axes). Required because
188 /// the kernel scale axes need much tighter control (`e^1 ≈ 2.7`-fold
189 /// per iter is plenty) — using the rho-axis cap here lets the optimizer
190 /// jump kappa by orders of magnitude per step and oscillate. Setting
191 /// this `None` disables the psi-axis cap.
192 pub(crate) bfgs_step_cap_psi: Option<f64>,
193 /// Optional persistent-cache session. When `Some`, every finite objective
194 /// evaluation is written through to disk (rate-limited, atomic-rename)
195 /// and the best on-disk rho is prepended as a seed at the start of each
196 /// plan attempt. Defaulted off so test-only paths skip filesystem I/O.
197 pub(crate) cache_session: Option<Arc<CacheSession>>,
198 /// Optional mirror cache sessions. Checkpoints and successful finalize
199 /// writes are also written to each of these sessions (different keys,
200 /// shared store). Used for hierarchical broadcast: the current best ρ is
201 /// written to the exact-key (primary) AND the data-independent
202 /// seed-prefix key so the next fit with related structure can warm-start
203 /// from this one, even after an interrupted run.
204 pub(crate) cache_mirror_sessions: Vec<Arc<CacheSession>>,
205 pub(crate) rho_uncertainty_problem_size: crate::rho_uncertainty::RhoUncertaintyProblemSize,
206 /// Converged exact outer Hessian `H(θ̂)` transferred from a prior
207 /// structurally-matching fit via the persistent cache (a warm-start *hit*),
208 /// in the full θ layout. When present and SPD, the BFGS host path seeds its
209 /// iter-0 metric with `InitialMetric::DenseInverseHessian(H⁻¹)` so the first
210 /// outer step is quasi-Newton instead of unscaled steepest descent — the
211 /// dominant LOSO line-search-bracketing cost (each bracketing probe is a
212 /// full inner joint-Newton re-solve). Strictly stronger than the scalar
213 /// `1/‖g₀‖` metric: it carries the full anisotropic curvature, which across
214 /// folds (one held-out point) is nearly identical to this fold's. Never
215 /// changes the converged optimum — BFGS reaches `∇V=0` under any SPD initial
216 /// metric. `None` on every cold-start / no-cache / pre-Hessian-schema path,
217 /// which falls back to the scalar warm metric byte-for-byte.
218 pub(crate) warm_start_outer_hessian: Option<Array2<f64>>,
219 /// Per-ρ-coordinate structural keys, in the objective's NATIVE (formula)
220 /// coordinate order, used to make the outer smoothing-parameter search
221 /// invariant to the order the user wrote the smooth terms / tensor margins
222 /// (#1538/#1539).
223 ///
224 /// When `Some` and the keys induce a non-identity canonical permutation,
225 /// [`run_outer`] reorders the coordinate layout the optimizer sees into a
226 /// stable canonical order (derived purely from the keys, never from the
227 /// native position) before seeding/optimizing, and inverts the permutation
228 /// on the returned ρ / gradient / Hessian so the caller still receives the
229 /// native layout. Seeding, multistart and tie-breaking then all operate on
230 /// the identical canonical layout for every term order, so both orders
231 /// reach the same λ̂ and the same fitted surface. `None` (or an identity
232 /// permutation) leaves the legacy native-order path byte-for-byte unchanged.
233 pub(crate) rho_canonical_keys: Option<Vec<u64>>,
234 /// A CALLER'S absolute requirement on the projected outer gradient norm,
235 /// honoured by the search and not merely checked at the end (#2568).
236 ///
237 /// The engine's own stationarity bound is a function of the FIT, not a
238 /// constant of the engine: it is a widening ladder anchored on the
239 /// criterion's magnitude and the optimizer's terminal state. That is right
240 /// for minting a model, and it means two fits on the same data in the same
241 /// call can be certified against bounds four orders apart -- measured on
242 /// #2568 at `2.0708e-4` for one and **exactly `1.000e0`** for its
243 /// companion, the latter being the saturated score-relative flat-valley
244 /// value -- a rung since deleted (#2458), because it was a constant
245 /// selected by an exit reason and it won precisely where the probe-noise
246 /// MEASUREMENT had declined to license any bound. A consumer
247 /// whose accuracy requirement is stricter than whatever the ladder happens
248 /// to compute had no supported way to impose it.
249 ///
250 /// Reading `|Pg|` off the summary and thresholding it afterwards is not the
251 /// same thing, and is the shape SPEC-20 exists to prevent: it mints a
252 /// replayable, diagnostics-clean fit the consumer must then reject. Nothing
253 /// made the optimizer *work harder* to reach the stricter standard.
254 ///
255 /// So this number enters in BOTH places that decide the outcome:
256 ///
257 /// * [`outer_gradient_tolerance`] floors the solver's convergence band at
258 /// it, so the outer loop keeps going instead of stopping at a looser
259 /// sealed bound -- the load-bearing half;
260 /// * the certificate's bound ladder is CAPPED by it, so a point the engine
261 /// would have certified against a wider rung is refused, reporting
262 /// [`StationarityBoundSource::CallerRequirement`] and naming the engine's
263 /// own bound alongside it.
264 ///
265 /// The cap is applied to the ladder's TOP, after every widening rung, which
266 /// is why it cannot be defeated by a rung that fires later. It never
267 /// loosens: a requirement weaker than the engine's own bound leaves the
268 /// engine's in force, because a caller asking for less accuracy than the
269 /// engine already guarantees is not asking for anything.
270 ///
271 /// `None` reproduces today's behaviour byte-for-byte on every path.
272 pub(crate) required_projected_gradient_norm: Option<f64>,
273 /// Require the terminal mint to carry a measured, raw PSD Hessian.
274 ///
275 /// The general certificate may admit a tiny assembled negative direction
276 /// when the gradient-residue floor proves it unresolved. Profiled
277 /// nonconvex coefficient families cannot use that weaker answer: their
278 /// selected branch is a local minimum only when the analytic outer Hessian
279 /// itself is measured PSD. Declaring that requirement here lets the outer
280 /// recovery loop escape/re-optimize instead of contradicting the certificate
281 /// later during fit assembly.
282 pub(crate) require_measured_psd: bool,
283}
284
285impl Default for OuterConfig {
286 fn default() -> Self {
287 Self {
288 tolerance: 1e-5,
289 rel_cost_tolerance: None,
290 required_projected_gradient_norm: None,
291 require_measured_psd: false,
292 max_iter: 200,
293 model_domain_bounds: None,
294 search_bounds_override: None,
295 seed_config: gam_problem::SeedConfig::default(),
296 rho_bound: 30.0,
297 heuristic_lambdas: None,
298 initial_rho: None,
299 initial_rho_candidates: Vec::new(),
300 previously_refused_seed_points: Vec::new(),
301 initial_inner_seed: None,
302 fallback_policy: FallbackPolicy::Automatic,
303 screening_cap: None,
304 screen_initial_rho: false,
305 initial_rho_is_prior_terminal_certificate: false,
306 outer_inner_cap: None,
307 operator_initial_trust_radius: None,
308 arc_initial_regularization: None,
309 objective_scale: None,
310 bfgs_step_cap: None,
311 bfgs_step_cap_psi: None,
312 cache_session: None,
313 cache_mirror_sessions: Vec::new(),
314 rho_uncertainty_problem_size:
315 crate::rho_uncertainty::RhoUncertaintyProblemSize::default(),
316 warm_start_outer_hessian: None,
317 rho_canonical_keys: None,
318 }
319 }
320}
321
322// ─── OuterProblem builder ─────────────────────────────────────────────
323//
324// Declarative builder for outer optimization problems. Derives
325// OuterCapability flags from high-level inputs (gradient/hessian
326// availability, psi dimension, EFS eligibility) so call sites never
327// hand-copy capability flags.
328
329/// Declarative outer-problem builder. Produces both the
330/// [`OuterCapability`] (what the objective can provide) and the
331/// `OuterConfig` (how the runner should behave) from a small set
332/// of high-level declarations.
333pub struct OuterProblem {
334 n_params: usize,
335 gradient: Derivative,
336 hessian: DeclaredHessianForm,
337 prefer_gradient_only: bool,
338 disable_fixed_point: bool,
339 psi_dim: usize,
340 barrier_config: Option<BarrierConfig>,
341 tolerance: f64,
342 rel_cost_tolerance: Option<f64>,
343 /// See [`OuterConfig::required_projected_gradient_norm`] (#2568).
344 required_projected_gradient_norm: Option<f64>,
345 require_measured_psd: bool,
346 max_iter: usize,
347 bounds: Option<(Array1<f64>, Array1<f64>)>,
348 rho_bound: f64,
349 seed_config: gam_problem::SeedConfig,
350 heuristic_lambdas: Option<Vec<f64>>,
351 initial_rho: Option<Array1<f64>>,
352 initial_rho_candidates: Vec<Array1<f64>>,
353 fallback_policy: FallbackPolicy,
354 screening_cap: Option<Arc<AtomicUsize>>,
355 screen_initial_rho: bool,
356 outer_inner_cap: Option<InnerProgressFeedback>,
357 operator_initial_trust_radius: Option<f64>,
358 arc_initial_regularization: Option<f64>,
359 objective_scale: Option<f64>,
360 bfgs_step_cap: Option<f64>,
361 bfgs_step_cap_psi: Option<f64>,
362 cache_session: Option<Arc<CacheSession>>,
363 cache_mirror_sessions: Vec<Arc<CacheSession>>,
364 rho_uncertainty_problem_size: crate::rho_uncertainty::RhoUncertaintyProblemSize,
365 rho_canonical_keys: Option<Vec<u64>>,
366}
367
368impl OuterProblem {
369 pub fn new(n_params: usize) -> Self {
370 Self {
371 n_params,
372 gradient: Derivative::Unavailable,
373 hessian: DeclaredHessianForm::Unavailable,
374 // Closed-form/test objectives preserve explicit ARC availability;
375 // production family-ladder entry points opt into #2359's
376 // optimize-3/certify-4 protocol with
377 // `with_prefer_gradient_only(true)`.
378 prefer_gradient_only: false,
379 disable_fixed_point: false,
380 psi_dim: 0,
381 barrier_config: None,
382 tolerance: 1e-5,
383 rel_cost_tolerance: None,
384 required_projected_gradient_norm: None,
385 require_measured_psd: false,
386 max_iter: 200,
387 bounds: None,
388 rho_bound: 30.0,
389 seed_config: gam_problem::SeedConfig::default(),
390 heuristic_lambdas: None,
391 initial_rho: None,
392 initial_rho_candidates: Vec::new(),
393 fallback_policy: FallbackPolicy::Automatic,
394 screening_cap: None,
395 screen_initial_rho: false,
396 outer_inner_cap: None,
397 operator_initial_trust_radius: None,
398 arc_initial_regularization: None,
399 objective_scale: None,
400 bfgs_step_cap: None,
401 bfgs_step_cap_psi: None,
402 cache_session: None,
403 cache_mirror_sessions: Vec::new(),
404 rho_uncertainty_problem_size:
405 crate::rho_uncertainty::RhoUncertaintyProblemSize::default(),
406 rho_canonical_keys: None,
407 }
408 }
409
410 /// Supply per-ρ-coordinate structural keys (native/formula order) so the
411 /// outer search is canonicalized to be invariant to the order the smooth
412 /// terms / tensor margins were written (#1538/#1539). See
413 /// `OuterConfig::rho_canonical_keys`.
414 pub fn with_rho_canonical_keys(mut self, keys: Option<Vec<u64>>) -> Self {
415 self.rho_canonical_keys = keys;
416 self
417 }
418
419 pub fn with_gradient(mut self, d: Derivative) -> Self {
420 self.gradient = d;
421 self
422 }
423 pub fn with_hessian(mut self, form: DeclaredHessianForm) -> Self {
424 self.hessian = form;
425 self
426 }
427 /// Choose whether analytic Hessian work is reserved for terminal
428 /// certification (`true`, the generic derivative-ladder protocol) or may
429 /// also be consumed by the search (`false`, for closed-form objectives).
430 pub fn with_prefer_gradient_only(mut self, prefer_gradient_only: bool) -> Self {
431 self.prefer_gradient_only = prefer_gradient_only;
432 self
433 }
434 /// Forbid the planner from selecting EFS/HybridEfs, even when the
435 /// objective implements `eval_efs()` and the coordinate structure would
436 /// otherwise make pure/hybrid EFS eligible.
437 ///
438 /// Callers use this for families where the Wood-Fasiolo structural
439 /// property is known not to hold (e.g. GAMLSS/location-scale with
440 /// β-dependent joint Hessian), so EFS would stagnate and burn budget
441 /// before the automatic cascade falls back to gradient-based BFGS.
442 pub fn with_disable_fixed_point(mut self, disable: bool) -> Self {
443 self.disable_fixed_point = disable;
444 self
445 }
446 // MEASURE-JET ψ REGISTRATION: the engine below is already complete for a
447 // 3-coordinate measure-jet ψ group (s, α, ln τ) — `psi_dim` is generic,
448 // `with_bounds` carries the s ∈ (0, 2) box (the same convention matern κ
449 // uses for its log-κ window; no logistic reparameterization exists or is
450 // needed in-house), `with_bfgs_step_cap_psi` caps per-iteration ψ moves,
451 // and `DirectionalHyperParam::new_compact` (solver/reml/mod.rs) carries
452 // penalty-only first/second/cross jets with `is_penalty_like`
453 // auto-derived from the identically-zero design drift (∂X/∂ψ ≡ 0).
454 // Every remaining registration arm is formula-layer dispatch in
455 // src/terms/smooth.rs (eligibility in
456 // `spatial_term_supports_hyper_optimization`, dims in
457 // `spatial_dims_per_term`, seed/bounds/write-back on
458 // `SpatialLogKappaCoords`, the per-trial rebuild in
459 // `apply_log_kappa_to_term`, and the derivative bundle in
460 // `try_build_spatial_term_log_kappa_derivative`, which currently returns
461 // `Ok(None)` for `SmoothBasisSpec::MeasureJet`) plus the
462 // `build_measure_jet_basis_psi_derivatives` producer in
463 // src/terms/basis/measure_jet_smooth.rs; both are owned by the
464 // measure-jet terms actor. Registration stays gated on those arms — do
465 // NOT add measure-jet-specific branches to this engine.
466 pub fn with_psi_dim(mut self, dim: usize) -> Self {
467 self.psi_dim = dim;
468 self
469 }
470 pub fn with_barrier(mut self, cfg: Option<BarrierConfig>) -> Self {
471 self.barrier_config = cfg;
472 self
473 }
474 pub fn with_tolerance(mut self, tol: f64) -> Self {
475 self.tolerance = tol;
476 self
477 }
478 pub fn with_max_iter(mut self, n: usize) -> Self {
479 self.max_iter = n;
480 self
481 }
482 pub fn with_bounds(mut self, lo: Array1<f64>, hi: Array1<f64>) -> Self {
483 self.bounds = Some((lo, hi));
484 self
485 }
486 pub fn with_rho_bound(mut self, b: f64) -> Self {
487 self.rho_bound = b;
488 self
489 }
490 pub fn with_seed_config(mut self, sc: gam_problem::SeedConfig) -> Self {
491 self.seed_config = sc;
492 self
493 }
494 pub fn with_heuristic_lambdas(mut self, h: Vec<f64>) -> Self {
495 self.heuristic_lambdas = Some(h);
496 self
497 }
498 pub fn with_initial_rho(mut self, rho: Array1<f64>) -> Self {
499 self.initial_rho = Some(rho);
500 self
501 }
502 pub fn with_initial_rho_candidates(mut self, candidates: Vec<Array1<f64>>) -> Self {
503 self.initial_rho_candidates = candidates;
504 self
505 }
506 pub fn with_screening_cap(mut self, screening_cap: Arc<AtomicUsize>) -> Self {
507 self.screening_cap = Some(screening_cap);
508 self
509 }
510 /// Allow seed screening to rank the explicit initial rho against generated
511 /// candidates even when the effective seed budget is one. The default keeps
512 /// a user-provided initial point authoritative and avoids a separate
513 /// screening pass.
514 pub fn with_screen_initial_rho(mut self, screen_initial_rho: bool) -> Self {
515 self.screen_initial_rho = screen_initial_rho;
516 self
517 }
518 /// Wire the bidirectional inner-PIRLS feedback channel.
519 ///
520 /// The outer bridge writes a coarsened iteration cap into
521 /// `feedback.cap` on every accepted gradient/Hessian eval; the inner
522 /// solver writes back into `feedback.last_iters` /
523 /// `feedback.last_converged` after each non-screening solve so the
524 /// next outer iter's schedule can adapt to the inner solver's
525 /// actual convergence behavior. Typical caller passes
526 /// `InnerProgressFeedback {
527 /// cap: Arc::clone(&reml_state.outer_inner_cap),
528 /// last_iters: Arc::clone(&reml_state.last_inner_iters),
529 /// last_converged: Arc::clone(&reml_state.last_inner_converged),
530 /// }` so the inner and outer observe the same atomics.
531 pub fn with_outer_inner_cap(mut self, feedback: InnerProgressFeedback) -> Self {
532 self.outer_inner_cap = Some(feedback);
533 self
534 }
535
536 /// Wire a one-shot "re-evaluate the inner solve COLD" signal that the outer
537 /// cost-stall guard raises when it grants a STUCK-stall escape (#2349).
538 ///
539 /// A profiled objective whose inner solve is warm-started along the outer
540 /// trajectory can carry value HYSTERESIS on a near-flat inner ridge — the
541 /// multinomial simplex-boundary regime where the softmax Fisher weight
542 /// `diag(p) − ppᵀ` collapses is the motivating case: two warm starts
543 /// converge to different ridge points whose Laplace `½log|H(β)|`, hence the
544 /// profiled objective, differ by more than the outer descent resolution, so
545 /// the optimizer's step-acceptance cannot separate real descent from that
546 /// hysteresis and grinds to `max_iter` at a non-stationary point. Uncapping
547 /// the inner cycle budget does not cure it (a fully converged warm solve
548 /// still lands on the warm-biased ridge point); the objective must re-solve
549 /// COLD to see a consistent surface.
550 ///
551 /// The caller shares this `Arc<AtomicBool>` with its objective closure and
552 /// consults it there, re-solving the inner problem from a canonical seed
553 /// (dropping the warm cache) whenever the flag is raised. The signal rides
554 /// the internal inner-cap feedback channel, but its `cap` slot is a private
555 /// throwaway so wiring the signal never perturbs the caller's own inner-cap
556 /// scheduling (custom families hold their real inner cap separately).
557 /// Objectives that do not warm-start, or never near-separate, simply never
558 /// observe the flag raised.
559 pub fn with_stuck_stall_cold_reeval_signal(self, signal: Arc<AtomicBool>) -> Self {
560 self.with_outer_inner_cap(InnerProgressFeedback {
561 cap: Arc::new(AtomicUsize::new(0)),
562 accepted_iter: Arc::new(AtomicUsize::new(0)),
563 // `last_iters == 0` ⇒ `snapshot()` returns `None` ⇒ no cap-schedule
564 // adaptation is derived from this dummy; `last_converged == true`
565 // matches the `None` default of `inner_solve_converged`, so
566 // terminal-fidelity gating is byte-for-byte unchanged.
567 last_iters: Arc::new(AtomicUsize::new(0)),
568 last_converged: Arc::new(AtomicBool::new(true)),
569 ift_residual: Arc::new(AtomicU64::new(f64::NAN.to_bits())),
570 accept_rho: Arc::new(AtomicU64::new(f64::NAN.to_bits())),
571 force_cold: signal,
572 })
573 }
574
575 /// Set the objective's natural magnitude scale, used to derive an
576 /// `n`-aware absolute gradient-norm floor. When set to `Some(s)`,
577 /// the runner uses `abs_floor = max(tol, s * √ε_machine)` for the
578 /// projected-gradient convergence check.
579 ///
580 /// Rationale: a fixed `abs = tol` (e.g. 1e-6) is appropriate when the
581 /// objective and its gradient live on a unit scale, but Gaussian-
582 /// identity REML carries an O(n) likelihood constant that flows into
583 /// ∂/∂logλ. At large-scale n the floor becomes binding even when the
584 /// relative-from-seed component (`rel_initial_grad * ‖g0‖`) declared
585 /// convergence iters earlier — chasing sub-ULP changes in log-λ at
586 /// the cost of repeated k²·n·p² analytic-Hessian assemblies.
587 pub fn with_objective_scale(mut self, scale: Option<f64>) -> Self {
588 self.objective_scale = scale.filter(|v| v.is_finite() && *v > 0.0);
589 self
590 }
591
592 /// Decouple the *relative-cost-decrease* convergence stop from the
593 /// absolute projected-gradient floor. By default both are derived from the
594 /// single `with_tolerance` value (`abs = max(tol, scale·√ε_machine)`,
595 /// `rel_cost = tol`). Supplying `Some(r)` here makes the rel-cost stop use
596 /// `r` while the absolute floor keeps using `tolerance` (so a caller can
597 /// keep a tight absolute floor for accuracy at large `n` AND a loose
598 /// rel-cost stop for perf on a flat REML ridge — see #1082). `None` keeps
599 /// the legacy coupling.
600 pub fn with_rel_cost_tolerance(mut self, rel_cost: Option<f64>) -> Self {
601 self.rel_cost_tolerance = rel_cost.filter(|v| v.is_finite() && *v > 0.0);
602 self
603 }
604
605 /// Require the returned fit's projected outer gradient norm to satisfy
606 /// `|Pg| <= requirement`, and make the SEARCH pursue it (#2568).
607 ///
608 /// This is the caller-side floor the engine's own stationarity bound could
609 /// not previously express. Without it the engine's data-scaled ladder is the
610 /// only standard applied, and because that ladder is a function of the fit,
611 /// a fit can be certified at `|Pg| = 5.564e-1` against a bound that
612 /// saturated to exactly `1.000e0` while its companion on the same data in
613 /// the same call was held to `2.0708e-4`.
614 ///
615 /// Two consequences, and the first is the point:
616 ///
617 /// 1. the outer loop's convergence band is floored at `requirement`, so it
618 /// keeps optimizing rather than stopping at a looser sealed bound;
619 /// 2. the certificate cannot mint above `requirement` -- the bound ladder is
620 /// capped at it, so an unmet requirement is a typed refusal naming both
621 /// `requirement` and the engine's own bound.
622 ///
623 /// A refusal is the CORRECT outcome when the requirement is unreachable on
624 /// the design: `|Pg| = 5.564e-1` may be a genuine floor of the criterion
625 /// there, and saying so beats certifying against `1.0`. Callers who want the
626 /// engine's judgement unmodified pass `None`, which is the default and is
627 /// byte-for-byte today's behaviour.
628 ///
629 /// Non-finite and non-positive values are rejected rather than silently
630 /// clamped: a requirement of `0.0` or `NaN` is not a stricter standard, it
631 /// is an unsatisfiable one, and honouring it would grind the outer loop to
632 /// `max_iter` and then refuse every fit.
633 pub fn with_required_projected_gradient_norm(mut self, requirement: Option<f64>) -> Self {
634 self.required_projected_gradient_norm = requirement.filter(|v| v.is_finite() && *v > 0.0);
635 self
636 }
637
638 /// Cap the infinity-norm displacement of BFGS cost-only line-search probes
639 /// on the **rho axes** (the first `n_params - psi_dim` outer parameters,
640 /// = log-λ). Also scales the initial inverse metric so the first trial
641 /// direction respects the same local budget coordinate-wise. Documented
642 /// natural step on log-λ is ≈ 5; tighter values throttle BFGS and starve
643 /// convergence on flat REML valleys.
644 pub fn with_bfgs_step_cap(mut self, cap: Option<f64>) -> Self {
645 self.bfgs_step_cap = cap.filter(|v| v.is_finite() && *v > 0.0);
646 self
647 }
648
649 /// Cap the infinity-norm displacement of BFGS cost-only line-search probes
650 /// on the **psi axes** (the trailing `psi_dim` outer parameters, = kappa
651 /// or anisotropic log-scales). Mirrors [`Self::with_bfgs_step_cap`] but
652 /// scoped to kernel-scale parameters whose natural step is much smaller
653 /// than log-λ (≈ ln 2 per iter keeps kappa from oscillating). Without
654 /// this split, a uniform rho-scale cap lets psi explode while a uniform
655 /// psi-scale cap throttles rho — both fail the survival-marginal-slope
656 /// path at large scale, where rho needs |d|≈5 while psi wants |d|≤1.
657 pub fn with_bfgs_step_cap_psi(mut self, cap: Option<f64>) -> Self {
658 self.bfgs_step_cap_psi = cap.filter(|v| v.is_finite() && *v > 0.0);
659 self
660 }
661
662 pub fn with_cache_session(mut self, session: Arc<CacheSession>) -> Self {
663 self.cache_session = Some(session);
664 self
665 }
666
667 /// Attach mirror cache sessions that receive a broadcast copy of
668 /// the final-result finalize write. See
669 /// `OuterConfig::cache_mirror_sessions`.
670 pub fn with_cache_mirror_sessions(mut self, sessions: Vec<Arc<CacheSession>>) -> Self {
671 self.cache_mirror_sessions = sessions;
672 self
673 }
674
675 pub fn with_problem_size(mut self, n_obs: usize, p_coefficients: usize) -> Self {
676 self.rho_uncertainty_problem_size = crate::rho_uncertainty::RhoUncertaintyProblemSize {
677 n_obs: Some(n_obs),
678 p_coefficients: Some(p_coefficients),
679 };
680 self
681 }
682
683 /// Override the fallback policy. Default is [`FallbackPolicy::Automatic`].
684 ///
685 /// Set [`FallbackPolicy::Disabled`] when the caller requires the primary
686 /// plan to stand on its own. Exact-Hessian objectives use this to ensure
687 /// failures surface on the analytic geometry instead of being reinterpreted
688 /// by a different optimizer class.
689 pub fn with_fallback_policy(mut self, policy: FallbackPolicy) -> Self {
690 self.fallback_policy = policy;
691 self
692 }
693
694 /// Demand a measured PSD analytic Hessian at the terminal mint.
695 ///
696 /// Use this when a downstream coefficient-mode selection is only defined
697 /// for a certified local minimum, rather than for a merely stationary point
698 /// whose tiny negative curvature was cleared by the gradient-residue floor.
699 pub fn with_require_measured_psd(mut self, required: bool) -> Self {
700 self.require_measured_psd = required;
701 self
702 }
703
704 /// Derive the capability flags from the builder state.
705 /// `fixed_point_available` is set to `false` here; `build_objective`
706 /// overrides it based on whether an EFS closure is actually provided.
707 fn capability(&self) -> OuterCapability {
708 OuterCapability {
709 gradient: self.gradient,
710 hessian: self.hessian,
711 prefer_gradient_only: self.prefer_gradient_only,
712 disable_fixed_point: self.disable_fixed_point,
713 n_params: self.n_params,
714 psi_dim: self.psi_dim,
715 fixed_point_available: false,
716 barrier_config: self.barrier_config.clone(),
717 }
718 }
719
720 /// Derive the runner configuration from the builder state.
721 pub(crate) fn config(&self) -> OuterConfig {
722 OuterConfig {
723 tolerance: self.tolerance,
724 rel_cost_tolerance: self.rel_cost_tolerance,
725 required_projected_gradient_norm: self.required_projected_gradient_norm,
726 require_measured_psd: self.require_measured_psd,
727 max_iter: self.max_iter,
728 model_domain_bounds: self.bounds.clone(),
729 search_bounds_override: None,
730 seed_config: self.seed_config,
731 rho_bound: self.rho_bound,
732 heuristic_lambdas: self.heuristic_lambdas.clone(),
733 initial_rho: self.initial_rho.clone(),
734 initial_rho_candidates: self.initial_rho_candidates.clone(),
735 previously_refused_seed_points: Vec::new(),
736 initial_inner_seed: None,
737 fallback_policy: self.fallback_policy,
738 screening_cap: self.screening_cap.clone(),
739 screen_initial_rho: self.screen_initial_rho,
740 // Only the cache's final-hit path can establish this, and it says
741 // so where it sets `initial_rho`.
742 initial_rho_is_prior_terminal_certificate: false,
743 outer_inner_cap: self.outer_inner_cap.clone(),
744 operator_initial_trust_radius: self.operator_initial_trust_radius,
745 arc_initial_regularization: self.arc_initial_regularization,
746 objective_scale: self.objective_scale,
747 bfgs_step_cap: self.bfgs_step_cap,
748 bfgs_step_cap_psi: self.bfgs_step_cap_psi,
749 cache_session: self.cache_session.clone(),
750 cache_mirror_sessions: self.cache_mirror_sessions.clone(),
751 rho_uncertainty_problem_size: self.rho_uncertainty_problem_size,
752 // Populated only by the persistent-cache resume path in `run` after
753 // a warm-start hit decodes a converged outer Hessian.
754 warm_start_outer_hessian: None,
755 rho_canonical_keys: self.rho_canonical_keys.clone(),
756 }
757 }
758
759 /// Construct a [`ClosureObjective`] with capability flags derived from the
760 /// builder state **and** the closures actually provided.
761 ///
762 /// `fixed_point_available` is set to `true` when `efs_fn` is `Some`,
763 /// regardless of whether `.with_efs()` was called. This is the canonical
764 /// way to create production objectives — it eliminates the drift risk of
765 /// manually entering capability flags.
766 pub fn build_objective<S, Fc, Fe, Fr, Fefs>(
767 &self,
768 state: S,
769 cost_fn: Fc,
770 eval_fn: Fe,
771 reset_fn: Option<Fr>,
772 efs_fn: Option<Fefs>,
773 ) -> ClosureObjective<S, Fc, Fe, Fr, Fefs>
774 where
775 Fc: FnMut(&mut S, &Array1<f64>) -> Result<f64, EstimationError>,
776 Fe: FnMut(&mut S, &Array1<f64>) -> Result<OuterEval, EstimationError>,
777 Fr: FnMut(&mut S),
778 Fefs: FnMut(&mut S, &Array1<f64>) -> Result<EfsEval, EstimationError>,
779 {
780 let mut cap = self.capability();
781 // Derive fixed_point_available from whether the caller actually
782 // provided an EFS hook, rather than relying on manual flags.
783 cap.fixed_point_available = efs_fn.is_some();
784 ClosureObjective {
785 state,
786 cap,
787 cost_fn,
788 eval_fn,
789 eval_order_fn: None,
790 reset_fn,
791 efs_fn,
792 fixed_point_certificate_fn: None,
793 exact_polish_fn: None,
794 rail_face_limit_fn: None,
795 soft_rho_guard_gradient_fn: None,
796 criterion_invariance_fn: None,
797 screening_proxy_fn: None::<fn(&mut S, &Array1<f64>) -> Result<f64, EstimationError>>,
798 seed_fn: None::<fn(&mut S, &Array1<f64>) -> Result<SeedOutcome, EstimationError>>,
799 terminal_eval_order: None,
800 }
801 }
802
803 /// Construct a [`ClosureObjective`] with an order-aware evaluation hook.
804 ///
805 /// This lets the runner request first-order vs second-order work based on
806 /// the active outer plan while preserving the legacy eager `eval_fn`.
807 pub fn build_objective_with_eval_order<S, Fc, Fe, Feo, Fr, Fefs>(
808 &self,
809 state: S,
810 cost_fn: Fc,
811 eval_fn: Fe,
812 eval_order_fn: Feo,
813 reset_fn: Option<Fr>,
814 efs_fn: Option<Fefs>,
815 ) -> ClosureObjective<S, Fc, Fe, Fr, Fefs, Feo>
816 where
817 Fc: FnMut(&mut S, &Array1<f64>) -> Result<f64, EstimationError>,
818 Fe: FnMut(&mut S, &Array1<f64>) -> Result<OuterEval, EstimationError>,
819 Feo: FnMut(&mut S, &Array1<f64>, OuterEvalOrder) -> Result<OuterEval, EstimationError>,
820 Fr: FnMut(&mut S),
821 Fefs: FnMut(&mut S, &Array1<f64>) -> Result<EfsEval, EstimationError>,
822 {
823 let mut cap = self.capability();
824 cap.fixed_point_available = efs_fn.is_some();
825 ClosureObjective {
826 state,
827 cap,
828 cost_fn,
829 eval_fn,
830 eval_order_fn: Some(eval_order_fn),
831 reset_fn,
832 efs_fn,
833 fixed_point_certificate_fn: None,
834 exact_polish_fn: None,
835 rail_face_limit_fn: None,
836 soft_rho_guard_gradient_fn: None,
837 criterion_invariance_fn: None,
838 screening_proxy_fn: None::<fn(&mut S, &Array1<f64>) -> Result<f64, EstimationError>>,
839 seed_fn: None::<fn(&mut S, &Array1<f64>) -> Result<SeedOutcome, EstimationError>>,
840 terminal_eval_order: None,
841 }
842 }
843
844 /// Construct a [`ClosureObjective`] with both an order-aware evaluation
845 /// hook and a custom seed-screening ranking proxy. The proxy fires only
846 /// when the cascade in `rank_seeds_with_screening` calls it; outside
847 /// screening the regular cost path is unaffected.
848 pub fn build_objective_with_screening_proxy<S, Fc, Fe, Feo, Fr, Fefs, Fsp>(
849 &self,
850 state: S,
851 cost_fn: Fc,
852 eval_fn: Fe,
853 eval_order_fn: Feo,
854 reset_fn: Option<Fr>,
855 efs_fn: Option<Fefs>,
856 screening_proxy_fn: Fsp,
857 ) -> ClosureObjective<S, Fc, Fe, Fr, Fefs, Feo, Fsp>
858 where
859 Fc: FnMut(&mut S, &Array1<f64>) -> Result<f64, EstimationError>,
860 Fe: FnMut(&mut S, &Array1<f64>) -> Result<OuterEval, EstimationError>,
861 Feo: FnMut(&mut S, &Array1<f64>, OuterEvalOrder) -> Result<OuterEval, EstimationError>,
862 Fr: FnMut(&mut S),
863 Fefs: FnMut(&mut S, &Array1<f64>) -> Result<EfsEval, EstimationError>,
864 Fsp: FnMut(&mut S, &Array1<f64>) -> Result<f64, EstimationError>,
865 {
866 let mut cap = self.capability();
867 cap.fixed_point_available = efs_fn.is_some();
868 ClosureObjective {
869 state,
870 cap,
871 cost_fn,
872 eval_fn,
873 eval_order_fn: Some(eval_order_fn),
874 reset_fn,
875 efs_fn,
876 fixed_point_certificate_fn: None,
877 exact_polish_fn: None,
878 rail_face_limit_fn: None,
879 soft_rho_guard_gradient_fn: None,
880 criterion_invariance_fn: None,
881 screening_proxy_fn: Some(screening_proxy_fn),
882 seed_fn: None::<fn(&mut S, &Array1<f64>) -> Result<SeedOutcome, EstimationError>>,
883 terminal_eval_order: None,
884 }
885 }
886
887 /// Run the outer optimization with a given objective.
888 pub fn run(
889 &self,
890 obj: &mut dyn OuterObjective,
891 context: &str,
892 ) -> Result<OuterResult, EstimationError> {
893 let mut config = self.config();
894 let objective_lower = obj.outer_domain_lower_bound()?;
895 let objective_upper = obj.outer_domain_upper_bound()?;
896 if objective_lower.is_some() || objective_upper.is_some() {
897 install_objective_domain(&mut config, self.n_params, objective_lower, objective_upper)?;
898 }
899 let Some(session) = config.cache_session.clone() else {
900 return run_outer(obj, &config, context);
901 };
902 let key_hex = session.key().to_hex();
903 let short_key = &key_hex[..8.min(key_hex.len())];
904 let mut had_hit = false;
905 let mut cached_inner_seed: Option<BoundInnerSeed> = None;
906 if let Some(loaded) = session.try_load_with_source() {
907 match classify_cache_entry_for_outer(&loaded, self.n_params) {
908 CacheSeedDecision::ExactFinal {
909 rho,
910 beta,
911 iterations,
912 prior_obj_display,
913 } => {
914 log::info!(
915 "[CACHE] final-hit key={}.. context={} rho_dim={} prior_obj={:.6e} iter={} action=resume-and-recertify",
916 short_key,
917 context,
918 rho.len(),
919 prior_obj_display,
920 iterations,
921 );
922 config.initial_rho = Some(rho.clone());
923 config.screen_initial_rho = false;
924 config.initial_rho_is_prior_terminal_certificate = true;
925 if !beta.is_empty() {
926 cached_inner_seed = Some(BoundInnerSeed {
927 theta: rho,
928 beta: Array1::from_vec(beta),
929 });
930 }
931 had_hit = true;
932 }
933 CacheSeedDecision::Seed {
934 rho,
935 beta,
936 hessian,
937 prior_obj_display,
938 iteration,
939 } => {
940 let beta_len = beta.len();
941 let beta_arr = if beta.is_empty() {
942 None
943 } else {
944 Some(Array1::from_vec(beta))
945 };
946 // Adopt the transferred converged outer Hessian only when it
947 // matches this fit's full-θ dimension; a dimension drift
948 // (structural change the cache key did not capture) falls
949 // back to the scalar warm metric in run_plan.
950 config.warm_start_outer_hessian = if self.hessian.is_analytic() {
951 hessian.and_then(|(dim, flat)| {
952 if dim == self.n_params && flat.len() == dim * dim {
953 Array2::from_shape_vec((dim, dim), flat).ok()
954 } else {
955 None
956 }
957 })
958 } else {
959 None
960 };
961 if config
962 .initial_rho
963 .as_ref()
964 .is_none_or(|initial| initial != rho)
965 {
966 log::info!(
967 "[CACHE] hit key={}.. context={} rho_dim={} beta_dim={} prior_obj={:.6e} iter={}",
968 short_key,
969 context,
970 rho.len(),
971 beta_len,
972 prior_obj_display,
973 iteration,
974 );
975 config.initial_rho = Some(rho.clone());
976 config.screen_initial_rho = false;
977 had_hit = true;
978 } else {
979 log::info!(
980 "[CACHE] hit key={}.. context={} rho_dim={} beta_dim={} already-aligned prior_obj={:.6e}",
981 short_key,
982 context,
983 rho.len(),
984 beta_len,
985 prior_obj_display,
986 );
987 had_hit = true;
988 }
989 if let Some(beta) = beta_arr {
990 cached_inner_seed = Some(BoundInnerSeed { theta: rho, beta });
991 }
992 }
993 CacheSeedDecision::Discard {
994 reason: "payload-shape-mismatch",
995 ..
996 } => {
997 log::info!(
998 "[CACHE] skip key={}.. context={} reason=payload-shape-mismatch n_params={}",
999 short_key,
1000 context,
1001 self.n_params,
1002 );
1003 }
1004 CacheSeedDecision::Discard {
1005 reason,
1006 prior_obj_display,
1007 all_rho_finite,
1008 } => {
1009 log::info!(
1010 "[CACHE] skip key={}.. context={} reason={} prior_obj={:.6e} all_rho_finite={}",
1011 short_key,
1012 context,
1013 reason,
1014 prior_obj_display,
1015 all_rho_finite.unwrap_or(false),
1016 );
1017 }
1018 }
1019 } else {
1020 log::info!(
1021 "[CACHE] miss key={}.. context={} reason=fresh-fingerprint n_params={}",
1022 short_key,
1023 context,
1024 self.n_params,
1025 );
1026 }
1027 // Preserve the ownership relation between a cached coefficient vector
1028 // and the exact outer coordinate that produced it. The runner installs
1029 // this seed only after resetting for that bitwise-matching candidate;
1030 // it is never replayed at another generated seed.
1031 config.initial_inner_seed = cached_inner_seed;
1032 let mut checkpointing = CheckpointingObjective::new(
1033 obj,
1034 Arc::clone(&session),
1035 config.cache_mirror_sessions.clone(),
1036 );
1037 let result = run_outer(&mut checkpointing, &config, context);
1038 // Attach β only when a beta-bearing evaluation surfaced it at this
1039 // exact final ρ. Scalar terminal audits carry no β and may follow an
1040 // evaluation elsewhere; a bare "last β" would manufacture a false
1041 // (ρ, β) pair and let machine history select the next fit's basin
1042 // (#2486). A rho-only payload is slower to resume but remains honest.
1043 let final_beta = result
1044 .as_ref()
1045 .ok()
1046 .and_then(|result| checkpointing.inner_beta_for(&result.rho));
1047 if let Ok(result) = result.as_ref()
1048 && result.final_value.is_finite()
1049 && result.converged()
1050 && result
1051 .criterion_certificate
1052 .as_ref()
1053 .is_some_and(OuterCriterionCertificate::certifies)
1054 && let Some(bytes) = encode_iterate(
1055 &result.rho,
1056 final_beta.as_ref(),
1057 result.final_hessian.as_ref(),
1058 result.final_value,
1059 result.iterations as u64,
1060 )
1061 {
1062 let saved = session.finalize(
1063 &bytes,
1064 Some(result.final_value),
1065 Some(result.iterations as u64),
1066 );
1067 if saved {
1068 log::info!(
1069 "[CACHE] save key={}.. context={} final_obj={:.6e} iter={} resumed={}",
1070 short_key,
1071 context,
1072 result.final_value,
1073 result.iterations,
1074 had_hit,
1075 );
1076 }
1077 // Broadcast finalize to mirror keys. The seed-prefix mirror
1078 // exists so future fits with related-but-not-identical
1079 // structure can warm-start from this run via the dispatcher's
1080 // prefix lookup.
1081 for mirror in &config.cache_mirror_sessions {
1082 let mirror_saved = mirror.finalize(
1083 &bytes,
1084 Some(result.final_value),
1085 Some(result.iterations as u64),
1086 );
1087 if mirror_saved {
1088 let mirror_hex = mirror.key().to_hex();
1089 log::info!(
1090 "[CACHE] save key={}.. context={} mirror final_obj={:.6e} iter={}",
1091 &mirror_hex[..8.min(mirror_hex.len())],
1092 context,
1093 result.final_value,
1094 result.iterations,
1095 );
1096 }
1097 }
1098 }
1099 result
1100 }
1101
1102 /// Run the outer optimization and return an unforgeable certified-result
1103 /// carrier. Callers that only need checkpoints or diagnostics should use
1104 /// [`Self::run`]; fit assembly after an optimized outer coordinate must use
1105 /// this boundary so a caller-constructed [`OuterResult`] cannot mint
1106 /// convergence provenance.
1107 pub fn run_certified(
1108 &self,
1109 obj: &mut dyn OuterObjective,
1110 context: &str,
1111 ) -> Result<CertifiedOuterResult, EstimationError> {
1112 let result = self.run(obj, context)?;
1113 CertifiedOuterResult::from_optimizer_result(result).map_err(|reason| {
1114 EstimationError::RemlOptimizationFailed(format!(
1115 "{context}: outer result failed certified-fit validation: {reason}"
1116 ))
1117 })
1118 }
1119}
1120
1121/// Internal outcome of one planned solver/multistart attempt.
1122///
1123/// Exhausted checkpoints carry resumable work only. They never pass through
1124/// finalization, cache promotion, uncertainty diagnostics, or fitted-model
1125/// construction.
1126pub(crate) enum PlanRunOutcome {
1127 Converged(OuterResult),
1128 Exhausted(OuterResult),
1129 FirstOrderFallbackRequested(FirstOrderFallbackRequest),
1130 FixedPointContinuationRequested(FixedPointContinuationRequest),
1131}
1132
1133/// Which certificate concluded a CONVERGED outer run (#2235/#2241).
1134///
1135/// `OuterResult.converged == true` bundles genuinely different endings, each
1136/// with its own certificate. Distinguishing them is pure evidence for the
1137/// caller's termination report — every variant is a converged fit. There is
1138/// deliberately no "budget/freeze" variant: exhaustion is a typed error
1139/// carrying the resume checkpoint, never a minted fit (SPEC 20; the #2235
1140/// forcing-function redesign deleted the freeze lanes).
1141#[derive(Clone, Copy, Debug, PartialEq)]
1142pub enum OuterConvergedVia {
1143 /// The bound-projected analytic gradient at the returned point cleared the
1144 /// solver's absolute/score-scaled stationarity tolerance.
1145 GradientStationary,
1146 /// Criterion-flat certificate (#2241/#2253): the criterion stalled over the
1147 /// cost-stall window and the residual projected gradient sits inside the
1148 /// flat certificate band — the score-relative stationarity bound
1149 /// (`flat_valley_converged_grad_bound`), the probe-noise-floor bound
1150 /// measured from the stall window's own value scatter, and/or the
1151 /// curvature-scaled Newton-decrement bound (`newton_predicted_decrease`),
1152 /// under which a residual above the gradient-magnitude bands is still
1153 /// stationary when the second-order-predicted improvement `½·gᵀH⁻¹g` is below
1154 /// the outer objective tolerance. `certificate_bound` is the operative
1155 /// (widened) bound the residual actually cleared.
1156 CriterionFlat {
1157 residual_grad_norm: f64,
1158 certificate_bound: f64,
1159 },
1160 /// Every optimized coordinate carried an explicit analytic fixed-point
1161 /// equation and the KKT-projected residual cleared the solver tolerance.
1162 FixedPointStationary {
1163 projected_residual_inf_norm: f64,
1164 certificate_bound: f64,
1165 },
1166 /// Fellner–Schall model-state fixed point (#2235 verdict 2): two
1167 /// consecutive outer evaluations restored the same banked incumbent, so a
1168 /// further outer update provably does not change the fitted state. The
1169 /// analytic first-order certificate is still taken at the incumbent.
1170 RecurrentIncumbent { consecutive_restores: usize },
1171 /// Stationary-at-asymptote (#2348 Inc 1 / #2299 layer 3): the interior
1172 /// (non-railed) coordinates are gradient-stationary, and every coordinate
1173 /// railed at the infinite-/zero-smoothing box bound is certified on a
1174 /// confirmed exponential tail (Thm 2.1) whose fitted model has reached the
1175 /// rail limit to within the estimand tolerance. The typed rail supersedes
1176 /// the generic gradient/criterion-flat verdict for a railed optimum.
1177 AsymptoteStationary { rails: usize },
1178}
1179
1180impl OuterConvergedVia {
1181 /// Stable wire name for termination reports; the enum owns the vocabulary
1182 /// so bindings marshal instead of mapping.
1183 pub fn as_str(&self) -> &'static str {
1184 match self {
1185 Self::GradientStationary => "converged_stationary",
1186 Self::CriterionFlat { .. } => "converged_criterion_flat",
1187 Self::FixedPointStationary { .. } => "converged_fixed_point",
1188 Self::RecurrentIncumbent { .. } => "incumbent_stationary",
1189 Self::AsymptoteStationary { .. } => "converged_asymptote_rail",
1190 }
1191 }
1192}
1193
1194/// Typed lifecycle of an outer optimization result.
1195///
1196/// A solver claim and an analytic certificate are different stages, but they
1197/// belong to one state machine. Encoding them in one enum makes it impossible
1198/// to retain a certified success verdict after a later certificate refusal.
1199#[derive(Clone, Copy, Debug, PartialEq)]
1200enum OuterTermination {
1201 /// The solver exhausted or refused without claiming convergence.
1202 Exhausted,
1203 /// The solver claimed convergence, but no terminal analytic certificate
1204 /// currently authorizes the point. `proposed_via` is reserved for the
1205 /// recurrent-incumbent fixed-point signal that certification must corroborate.
1206 SolverClaimed {
1207 proposed_via: Option<OuterConvergedVia>,
1208 },
1209 /// A terminal analytic certificate authorizes the point and records why.
1210 Certified(OuterConvergedVia),
1211}
1212
1213impl OuterTermination {
1214 fn from_solver_claim(claimed: bool) -> Self {
1215 if claimed {
1216 Self::SolverClaimed { proposed_via: None }
1217 } else {
1218 Self::Exhausted
1219 }
1220 }
1221
1222 fn solver_claimed_convergence(self) -> bool {
1223 !matches!(self, Self::Exhausted)
1224 }
1225
1226 fn is_certified(self) -> bool {
1227 matches!(self, Self::Certified(_))
1228 }
1229
1230 fn certified_via(self) -> Option<OuterConvergedVia> {
1231 match self {
1232 Self::Certified(via) => Some(via),
1233 Self::Exhausted | Self::SolverClaimed { .. } => None,
1234 }
1235 }
1236
1237 fn proposed_via(self) -> Option<OuterConvergedVia> {
1238 match self {
1239 Self::SolverClaimed { proposed_via } => proposed_via,
1240 Self::Exhausted | Self::Certified(_) => None,
1241 }
1242 }
1243
1244 /// Revoke any earlier screening certificate before measuring a new
1245 /// screening/mint verdict. Only the model-state recurrent-incumbent signal
1246 /// survives as a proposal; ordinary gradient/rail verdicts must be re-earned.
1247 fn begin_certification(&mut self) {
1248 let proposed_via = match *self {
1249 Self::SolverClaimed {
1250 proposed_via: Some(via @ OuterConvergedVia::RecurrentIncumbent { .. }),
1251 }
1252 | Self::Certified(via @ OuterConvergedVia::RecurrentIncumbent { .. }) => Some(via),
1253 Self::Exhausted
1254 | Self::SolverClaimed { .. }
1255 | Self::Certified(_) => None,
1256 };
1257 if !matches!(*self, Self::Exhausted) {
1258 *self = Self::SolverClaimed { proposed_via };
1259 }
1260 }
1261
1262 fn certify(&mut self, via: OuterConvergedVia) {
1263 *self = Self::Certified(via);
1264 }
1265
1266 /// A refused analytic pass may retain the factual solver claim for resume
1267 /// policy, but never a proposed or certified success verdict.
1268 fn refuse_certificate(&mut self) {
1269 if !matches!(*self, Self::Exhausted) {
1270 *self = Self::SolverClaimed { proposed_via: None };
1271 }
1272 }
1273}
1274
1275/// Which lane actually produced an [`OuterResult`].
1276///
1277/// `OuterResult::solver_termination` is `None` whenever no `opt` solver
1278/// produced the result, and its own doc names three ways that happens — a
1279/// cache short-circuit, a synthesized checkpoint, the per-atom Fellner–Schall
1280/// lane — without recording WHICH. A refusal then reads
1281/// `termination=<no opt solver produced this result>` and the reader is left to
1282/// infer the lane from iteration counts, which is the #2465 shape: the decision
1283/// is made, and the basis for it is dropped by the emitter that holds it. On
1284/// the #1575 binomial fixture that absence is the whole question — a result at
1285/// `|Pg| = 5.991e-1` after 13 of 300 permitted outer iterations, PSD Hessian,
1286/// nothing railed, is a search that stopped with descent still available, and
1287/// "which lane stopped it" is the first thing anyone needs.
1288///
1289/// [`OuterResult::new`] defaults to [`Self::Solver`]; the gam-side lanes that
1290/// synthesize a result overwrite it at their construction site.
1291#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1292pub enum OuterResultOrigin {
1293 /// An `opt` solver ran and its solution was translated into this result.
1294 Solver,
1295 /// A seed was accepted as its own optimum with zero outer iterations.
1296 SeedAcceptedWithoutIteration,
1297 /// ARC exhausted its budget on a last iterate WORSE than the best feasible
1298 /// iterate it had seen, so the best iterate was substituted (#1371/#1476).
1299 ArcBestIterateSubstitution,
1300 /// ARC hit a run of infeasible probes with no synchronized Hessian, so a
1301 /// checkpoint was rebuilt from the stored best iterate.
1302 ArcInfeasibleStallCheckpoint,
1303 /// ARC was stopped at a point its own terminal certificate accepts: the
1304 /// Newton decrement ½gᵀH⁻¹g of the rail-projected gradient sat at or below
1305 /// the criterion's resolution under a PSD reduced Hessian (#2817).
1306 ArcCurvatureStationaryStop,
1307 /// The BFGS cost-stall guard halted the search and published its best
1308 /// iterate, which was rebuilt into this result.
1309 BfgsCostStallExit,
1310 /// The per-atom Fellner–Schall frontier lane.
1311 PerAtomFellnerSchall,
1312 /// The parameter space is empty; there was nothing to optimize.
1313 EmptyParameterSpace,
1314 /// A caller-supplied point audited without running any optimizer.
1315 StationaryPointAudit,
1316}
1317
1318/// Result of a completed outer optimization.
1319#[derive(Clone, Debug)]
1320pub struct OuterResult {
1321 /// Optimized log-smoothing parameters.
1322 pub rho: Array1<f64>,
1323 /// Final objective value.
1324 pub final_value: f64,
1325 /// Total outer iterations across all solver restarts.
1326 pub iterations: usize,
1327 /// Final gradient norm, when the solver computed an actual gradient.
1328 pub final_grad_norm: Option<f64>,
1329 /// Final gradient when the solver is gradient-based.
1330 pub final_gradient: Option<Array1<f64>>,
1331 /// Final Hessian when the solver tracks one.
1332 pub final_hessian: Option<Array2<f64>>,
1333 /// Single authoritative termination lifecycle. Private so downstream
1334 /// callers cannot manufacture convergence without the optimizer transition.
1335 termination: OuterTermination,
1336 /// Which plan was actually used (may differ from initial if fallback fired).
1337 pub plan_used: OuterPlan,
1338 /// Final trust radius for the internal operator trust-region solver.
1339 ///
1340 /// A non-converged operator-ARC attempt may be restarted by the budget
1341 /// ladder. Restarting only from the last θ but resetting the trust radius
1342 /// is not a warm start: it replays the same rejected large trial steps.
1343 /// Carry this globalization state so retries resume from the scale the
1344 /// previous attempt already learned.
1345 pub operator_trust_radius: Option<f64>,
1346 /// Why the internal operator trust-region solver stopped.
1347 ///
1348 /// Derived from `Self::termination` in
1349 /// `bridges::solution_into_outer_result`; do not set it independently
1350 /// or the two can disagree.
1351 pub operator_stop_reason: Option<OperatorTrustRegionStopReason>,
1352 /// Which test the underlying `opt` solver stopped on, and the
1353 /// quantity it was decided against.
1354 ///
1355 /// Distinct from `OuterTermination`, which is gam's *certification*
1356 /// state machine (did a terminal analytic certificate authorize this
1357 /// point). This is the solver's own account of why it stopped
1358 /// searching, and the two answer different questions: a run can stop
1359 /// on a satisfied gradient test and still fail certification, or
1360 /// exhaust its budget and be certified by a later re-measurement.
1361 ///
1362 /// Carried from the solver rather than reconstructed here. Before
1363 /// `opt` reported this, `operator_stop_reason` was hand-populated on
1364 /// the matrix-free branch alone by matching the coarse
1365 /// `OptimizationStatus`, so every other route left it `None` — and
1366 /// `None` rendered identically to "there was nothing to say", which
1367 /// made #2547's "WHY it stopped is unrecorded" unanswerable without a
1368 /// new probe.
1369 ///
1370 /// `None` here means no `opt` solver produced this result at all (a
1371 /// cache short-circuit, a synthesized checkpoint, the per-atom
1372 /// Fellner–Schall lane) — an honest absence, not a dropped verdict.
1373 pub solver_termination: Option<TerminationReason>,
1374 /// First-order optimality self-audit at the returned point (#934).
1375 ///
1376 /// `None` when no analytic gradient was measured at termination
1377 /// (gradient-free solvers, cache-hit short-circuits, per-atom EFS) or
1378 /// when an audit probe failed to evaluate. Populated once by
1379 /// `run_outer` after the solver ladder returns, outside all hot loops.
1380 pub criterion_certificate: Option<OuterCriterionCertificate>,
1381 /// Probe-noise-floor gradient bound measured by the cost-stall guard at a
1382 /// halted stall (#2241): σ̂/Δ, the criterion's evaluation-noise floor over
1383 /// the stall window divided by the radius the accepted steps actually
1384 /// probed. Present only on results rebuilt from a cost-stall exit;
1385 /// `certify_outer_optimality` folds it into the stationarity bound so the
1386 /// final re-measured gradient is judged against the same flat certificate
1387 /// the guard granted.
1388 /// Why the solver's line search gave up, when it did (#2465).
1389 ///
1390 /// `opt` reports `LineSearchFailureReason` plus the attempt count on
1391 /// `BfgsError::LineSearchFailed`, and the two variants have opposite
1392 /// causes: `StepSizeTooSmall` means the direction WAS a descent direction
1393 /// and no usable step decreased the objective — an objective/gradient
1394 /// inconsistency or evaluation noise — while `MaxAttempts` means the
1395 /// bracketing never closed, a pathological landscape. The bridge used to
1396 /// drop both on the floor: a line-search failure whose last iterate is
1397 /// finite is returned as `Ok(non-converged)`, so the caller never sees the
1398 /// `Err` that carries them, and the certificate could say only
1399 /// `termination=line_search_failed(|g|=…)` — the verdict without the
1400 /// quantity it was decided against.
1401 ///
1402 /// `None` means no line search failed (or no `opt` solver produced this
1403 /// result at all).
1404 pub line_search_failure: Option<(LineSearchFailureReason, usize)>,
1405 pub flat_noise_grad_bound: Option<f64>,
1406 /// Post-fit PSIS diagnostic for whether sampled smoothing-parameter weights
1407 /// show evidence that plug-in REML/LAML intervals are unreliable. Populated
1408 /// once by `run_outer` when the exact rho Hessian is cheap enough to use.
1409 pub rho_uncertainty_diagnostic: Option<crate::rho_uncertainty::RhoUncertaintyDiagnostic>,
1410 /// Reseed point minted by a refused certification whose tail snap CONFIRMED
1411 /// an exponential tail (#2348 Inc 2b). A snap is a waypoint, never a
1412 /// candidate optimum: even an interior coordinate stationary before the
1413 /// snap can move when it is coupled to the tail coordinate (#2358). The
1414 /// plan runner retries ONCE from this point, allowing every coordinate to
1415 /// re-descend or remain on the rail before the natural certificate judges
1416 /// the result.
1417 pub tail_snap_reseed: Option<Array1<f64>>,
1418 /// Saddle-escape reseed point minted by a refused certification whose
1419 /// interior reduced Hessian is a certified strict saddle — small projected
1420 /// gradient, `hessian_psd = Some(false)`, no railed coordinate (#2357). A
1421 /// gradient-only convergence gate (ARC's, or the cost-stall guard's) can
1422 /// ARRIVE at such a saddle with its gradient already below tolerance and
1423 /// stop, even though the certified negative-curvature eigendirection is a
1424 /// strict descent direction the optimizer never took. This point is
1425 /// `ρ + α·v` for the most-negative-curvature eigenvector `v`, stepped off
1426 /// the saddle ridge to a strictly-lower objective; the plan runner reseeds
1427 /// the outer search ONCE from it (reseed gate closed so it cannot recurse),
1428 /// which lets the optimizer descend to the true PSD minimum exactly as an
1429 /// identical warm-started resume does by hand.
1430 pub saddle_escape_reseed: Option<Array1<f64>>,
1431 /// What the criterion measured about this result's own analytic Hessian,
1432 /// when the certificate adjudicated a disputed negative curvature (#2748).
1433 ///
1434 /// Set exactly where `SaddleAdjudication::Contradicted` is reached with a
1435 /// determined ladder fit. It is a certified lower bound on `‖δH‖₂` for the
1436 /// assembly — the measurement `gam_linalg::curvature_resolution`'s Law 2
1437 /// requires and supplies no value for — and it exists on the result so the
1438 /// downstream ρ-curvature gates judge by the same evidence this certificate
1439 /// did instead of re-deciding from the matrix alone (#2428).
1440 pub criterion_hessian_error: Option<CriterionCurvatureDisagreement>,
1441 /// Wrong-rail pull-back reseed point minted by a refused certification whose
1442 /// coordinate sits AT the ρ box bound but whose clean-band probes prove the
1443 /// objective DECREASES as the coordinate moves INWARD (#2392). The outer
1444 /// search drove the coordinate to the wrong bound — its terminal gradient is
1445 /// deep-λ instrument noise, so the trust region never proposed the large
1446 /// inward move — while a drift-band-clean, above-noise-floor run of probes a
1447 /// few e-folds inside carries a pencil constant of the sign OPPOSITE the rail
1448 /// (descent points away from the bound, `∂V/∂ρ > 0` at an upper rail). This
1449 /// point moves that coordinate to its clean-band interior scale, where the
1450 /// gradient is informative again; the plan runner reseeds ONCE (gate closed)
1451 /// and the optimizer descends to the true interior optimum. Gated strictly on
1452 /// the opposite-sign clean-tail proof, so a GENUINE rail (descent toward the
1453 /// bound) never mints it and no real λ→∞ optimum is pulled off its rail.
1454 pub wrong_rail_reseed: Option<Array1<f64>>,
1455 /// Active-set reduction reseed minted by a refused certification whose
1456 /// INTERIOR is not stationary while a coordinate is railed at the ρ box with
1457 /// a deep-λ noise-floor gradient (#2392). The railed coordinate's
1458 /// ill-conditioned Hessian row poisons the joint Newton/ARC steps, so the
1459 /// interior cannot polish; freezing that coordinate at its bound and
1460 /// re-running lets the optimizer converge the interior in the well-conditioned
1461 /// REDUCED space. The reseed carries the frozen box (`bounds`, with
1462 /// `lower[k]==upper[k]==rail` for each frozen coordinate); the plan runner's
1463 /// re-certification under the ORIGINAL bounds then judges every pinned
1464 /// coordinate's KKT sign at the reduced optimum (an inward-feasible-descent
1465 /// gradient unfreezes it — no silent clamping of a coordinate that stops
1466 /// wanting the rail).
1467 pub active_set_reseed: Option<ActiveSetReseed>,
1468 /// `(noise_floor σ̂, probe_radius Δ)` the cost-stall guard measured over its
1469 /// stall window, when a stall produced this result. Present whether or not
1470 /// their ratio licensed a `flat_noise_grad_bound`: σ̂ is the per-step
1471 /// objective change the no-improvement window judged and Δ is the radius the
1472 /// accepted steps moved, and a window that filled because the search took
1473 /// microscopic steps is told from one that filled because the surface is
1474 /// flat by Δ, not by the verdict.
1475 pub cost_stall_probe_scale: Option<(f64, f64)>,
1476 /// Which lane produced this result. See [`OuterResultOrigin`].
1477 pub origin: OuterResultOrigin,
1478 /// Seed start points this plan run STARTED and whose mandatory analytic
1479 /// certificate then REFUSED (#2569).
1480 ///
1481 /// The certify-resume loop in `run_outer` re-runs the outer search seeded
1482 /// at the refused checkpoint, and the seed cascade it re-enters is allowed
1483 /// to fall through its `seed_budget` while nothing has certified
1484 /// (`should_start_next_seed`). The fall-through lands on the SAME generated
1485 /// lattice seed every round — a point that does not depend on the
1486 /// checkpoint, reached from a state `obj.reset()` has restored — so the
1487 /// cascade re-derives a verdict it already recorded. Measured on the #2569
1488 /// grouped-binomial design: one cold seed re-run 17 times per fit, each
1489 /// repetition terminating at the identical `|g|` after the identical outer
1490 /// iteration count, for 18-48% of the fit's wall clock.
1491 ///
1492 /// Carrying the points forward lets the next resume skip exactly those
1493 /// seeds, on the same grounds #2080's `cold_entry_leg_refusal` replays a
1494 /// recorded cold-entry verdict: re-running them would reproduce the
1495 /// recorded refusal digit for digit. A seed that has NOT been started and
1496 /// refused is never suppressed, so no rescue path is closed.
1497 pub refused_seed_points: Vec<Array1<f64>>,
1498}
1499
1500/// An active-set reduction reseed (#2392): re-run the outer search with a set of
1501/// railed coordinates FROZEN at their box bounds so the optimizer polishes the
1502/// interior in the reduced space.
1503#[derive(Clone, Debug)]
1504pub struct ActiveSetReseed {
1505 /// The reseed point: the refused checkpoint with the frozen coordinates
1506 /// pinned at their bounds (`rho[k] == bounds.0[k] == bounds.1[k]`).
1507 pub rho: Array1<f64>,
1508 /// The reduced-space box: `lower[k] == upper[k] == rail` for every frozen
1509 /// coordinate `k`, the original bounds elsewhere.
1510 pub bounds: (Array1<f64>, Array1<f64>),
1511}
1512
1513impl OuterResult {
1514 pub fn new(
1515 rho: Array1<f64>,
1516 final_value: f64,
1517 iterations: usize,
1518 solver_claimed_convergence: bool,
1519 plan_used: OuterPlan,
1520 ) -> Self {
1521 Self {
1522 rho,
1523 final_value,
1524 iterations,
1525 final_grad_norm: None,
1526 final_gradient: None,
1527 final_hessian: None,
1528 termination: OuterTermination::from_solver_claim(solver_claimed_convergence),
1529 plan_used,
1530 operator_trust_radius: None,
1531 operator_stop_reason: None,
1532 solver_termination: None,
1533 criterion_certificate: None,
1534 line_search_failure: None,
1535 flat_noise_grad_bound: None,
1536 rho_uncertainty_diagnostic: None,
1537 tail_snap_reseed: None,
1538 saddle_escape_reseed: None,
1539 criterion_hessian_error: None,
1540 wrong_rail_reseed: None,
1541 active_set_reseed: None,
1542 cost_stall_probe_scale: None,
1543 origin: OuterResultOrigin::Solver,
1544 refused_seed_points: Vec::new(),
1545 }
1546 }
1547
1548 /// Whether this result owns a terminal analytic convergence certificate.
1549 pub fn converged(&self) -> bool {
1550 self.termination.is_certified()
1551 }
1552
1553 /// Which analytic certificate concluded this run.
1554 pub fn converged_via(&self) -> Option<OuterConvergedVia> {
1555 self.termination.certified_via()
1556 }
1557
1558 /// Whether the underlying solver claimed convergence before analytic
1559 /// certification. Certified results necessarily originated from a claim or
1560 /// an explicit stationary-point audit.
1561 pub(crate) fn solver_claimed_convergence(&self) -> bool {
1562 self.termination.solver_claimed_convergence()
1563 }
1564
1565 /// Human-readable rendering of `final_grad_norm` for diagnostics. Returns
1566 /// `"n/a"` when no gradient was measured (gradient-free / cache-hit paths).
1567 pub fn final_grad_norm_report(&self) -> String {
1568 match self.final_grad_norm {
1569 Some(g) => format!("{g:.3e}"),
1570 None => "n/a".to_string(),
1571 }
1572 }
1573}
1574
1575/// Validated evidence that an outer optimization terminated at a finite,
1576/// analytically certified optimum.
1577///
1578/// The inner [`OuterResult`] is private so downstream fit assembly cannot turn
1579/// a status boolean into convergence provenance. Construction consumes the
1580/// optimizer result and revalidates the certificate at the ownership boundary.
1581#[derive(Clone, Debug)]
1582pub struct CertifiedOuterResult {
1583 result: OuterResult,
1584}
1585
1586impl CertifiedOuterResult {
1587 /// The sole constructor is reached from [`OuterProblem::run_certified`]
1588 /// after the optimizer has produced the result. Keeping this private is
1589 /// load-bearing: `OuterResult` is also a public diagnostic/checkpoint
1590 /// payload, so a public conversion would let downstream code fabricate a
1591 /// certificate-shaped result without ever running an objective.
1592 fn from_optimizer_result(result: OuterResult) -> Result<Self, String> {
1593 if !result.converged() {
1594 return Err(format!(
1595 "outer optimization did not converge after {} iterations",
1596 result.iterations
1597 ));
1598 }
1599 if !result.final_value.is_finite() {
1600 return Err(format!(
1601 "outer optimization returned a non-finite objective: {}",
1602 result.final_value
1603 ));
1604 }
1605 if result.rho.iter().any(|value| !value.is_finite()) {
1606 return Err("outer optimization returned non-finite hyperparameters".to_string());
1607 }
1608 if result
1609 .final_grad_norm
1610 .is_some_and(|value| !value.is_finite() || value < 0.0)
1611 {
1612 return Err(format!(
1613 "outer optimization returned an invalid gradient norm: {:?}",
1614 result.final_grad_norm
1615 ));
1616 }
1617 let certificate = result
1618 .criterion_certificate
1619 .as_ref()
1620 .ok_or_else(|| "outer optimization returned no analytic certificate".to_string())?;
1621 if !certificate.certifies() {
1622 return Err(format!(
1623 "outer optimization certificate does not certify: {}",
1624 certificate.summary()
1625 ));
1626 }
1627 Ok(Self { result })
1628 }
1629
1630 /// Exact optimizer-owned hyperparameter vector covered by the certificate.
1631 pub fn rho(&self) -> &Array1<f64> {
1632 &self.result.rho
1633 }
1634
1635 pub fn iterations(&self) -> usize {
1636 self.result.iterations
1637 }
1638
1639 pub fn final_value(&self) -> f64 {
1640 self.result.final_value
1641 }
1642
1643 pub fn final_grad_norm(&self) -> Option<f64> {
1644 self.result.final_grad_norm
1645 }
1646
1647 /// Exact analytic gradient re-measured by the optimizer-owned terminal
1648 /// certificate. Downstream selected-profile finalizers use this to prove
1649 /// that a retained objective payload is the one certified at `rho()`.
1650 pub fn final_gradient(&self) -> Option<&Array1<f64>> {
1651 self.result.final_gradient.as_ref()
1652 }
1653
1654 pub fn criterion_certificate(&self) -> &OuterCriterionCertificate {
1655 self.result
1656 .criterion_certificate
1657 .as_ref()
1658 .expect("CertifiedOuterResult always owns a validated certificate")
1659 }
1660
1661 /// The analytic outer ρ-Hessian measured at the certified point, when the
1662 /// certification retained one. This is the curvature evidence behind the
1663 /// certificate's PSD verdict — and the `V_ρ = H_ρ⁻¹` input to first-order
1664 /// smoothing-correction inflation (#2346).
1665 pub fn final_hessian(&self) -> Option<&Array2<f64>> {
1666 self.result.final_hessian.as_ref()
1667 }
1668}
1669
1670#[cfg(test)]
1671#[path = "certified_outer_result_tests.rs"]
1672mod certified_outer_result_tests;
1673
1674/// Typed refusal from [`audit_stationary_point`]. The rejected point and every
1675/// analytic certificate field measured before refusal remain available to the
1676/// caller; `source` records why those measurements did not certify.
1677#[derive(Debug)]
1678pub struct OuterStationaryPointRejection {
1679 pub result: OuterResult,
1680 pub source: EstimationError,
1681}
1682
1683impl std::fmt::Display for OuterStationaryPointRejection {
1684 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1685 std::fmt::Display::fmt(&self.source, f)
1686 }
1687}
1688
1689impl std::error::Error for OuterStationaryPointRejection {
1690 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1691 Some(&self.source)
1692 }
1693}
1694
1695/// Apply the shared analytic outer-optimality authority to one caller-supplied
1696/// point without running an optimizer or taking a step.
1697///
1698/// The objective controls whether evaluating that point mutates its profiled
1699/// state. Callers auditing an already-installed inner state must put their
1700/// objective in a frozen evaluation mode before calling this function.
1701/// `iterations == 0` in the returned result is structural: no optimization loop
1702/// exists on this path.
1703pub fn audit_stationary_point(
1704 obj: &mut dyn OuterObjective,
1705 rho: Array1<f64>,
1706 context: &str,
1707) -> Result<OuterResult, OuterStationaryPointRejection> {
1708 let config = OuterConfig::default();
1709 let selected_plan = plan(&obj.capability());
1710 // There is intentionally no independent value-only probe. The analytic
1711 // sample is the authority being audited, and infinity records that no
1712 // optimizer-produced terminal value exists to compare against it.
1713 let mut result = OuterResult::new(rho, f64::INFINITY, 0, false, selected_plan);
1714 result.origin = OuterResultOrigin::StationaryPointAudit;
1715 match certify_outer_optimality(obj, &config, context, &mut result) {
1716 Ok(certificate) => {
1717 result.criterion_certificate = Some(certificate);
1718 Ok(result)
1719 }
1720 Err(source) => Err(OuterStationaryPointRejection { result, source }),
1721 }
1722}
1723
1724// ─── First-order optimality certificate (#934) ────────────────────────
1725//
1726// The objective↔gradient desync bug genus (#748, #752, #808, #901, …) has a
1727// universal signature: at the returned "optimum" the optimizer claims
1728// convergence while the criterion is not actually stationary there (or the
1729// optimizer stalls and rails λ). The certificate makes the engine check
1730// itself, once, at θ̂, on every generic outer fit — purely from the ANALYTIC
1731// objective, per SPEC rule 2 (finite differences never run outside tests;
1732// the FD gradient oracle now lives in the test-only `fd_audit` module): the
1733// KKT-projected analytic gradient norm against the same score-relative
1734// stationarity bound the outer loop already uses to accept flat-valley
1735// stalls (#1690), a scaled PSD probe of the tracked outer Hessian, and the
1736// λ-rail facts every desync postmortem asks for. It is the runtime
1737// enforcement layer for the criterion-atom architecture (#931).
1738//
1739// A failed certificate REJECTS the fit as typed non-convergence — never a
1740// warn-and-continue diagnostic — so a nonstationary point can never be
1741// minted into a fit (SPEC rule 20).
1742
1743/// Cholesky positive-SEMIdefiniteness probe for the (small, outer-dim) final
1744/// Hessian, with a roundoff-scale diagonal shift. Returns `None` when the
1745/// matrix is empty, non-square, or non-finite; `Some(false)` when the shifted
1746/// matrix has a non-positive pivot — i.e. the curvature is genuinely
1747/// indefinite, not merely semidefinite-within-noise.
1748///
1749/// The shift is `√ε · max(1, max|H_ii|)`: eigenvalues assembled through
1750/// O(‖H‖)-scaled arithmetic carry O(ε·‖H‖) roundoff, so a `√ε`-relative
1751/// margin cleanly separates a true negative direction from accumulated
1752/// floating-point noise on a flat (near-semidefinite) valley.
1753pub(crate) fn certificate_hessian_is_psd(hessian: &Array2<f64>) -> Option<bool> {
1754 certificate_hessian_is_psd_at_resolution(hessian, 0.0)
1755}
1756
1757/// [`certificate_hessian_is_psd`] with an explicit **measured** curvature
1758/// resolution, which the shift is raised to when it is the larger (#2748).
1759///
1760/// The `√ε·max(1, max|H_ii|)` shift above is a statement about the *arithmetic*
1761/// that assembled `H` — accumulated round-off at the matrix's own scale. It is
1762/// not, and does not claim to be, a statement about the *assembly*: an outer
1763/// criterion Hessian is a derivative of a quantity computed through an inner
1764/// solve, a log-determinant and a trace contraction, and its error is not
1765/// bounded by `√ε·‖H‖`. `gam_linalg::curvature_resolution` calls the second
1766/// quantity `‖δH‖₂` and requires it to be MEASURED, per site, from an identity
1767/// that is exactly zero in exact arithmetic.
1768///
1769/// `measured_resolution` is that measurement. `0.0` reproduces the historical
1770/// shift bit for bit, and since the two are combined by `max` this can only
1771/// ever admit a point the previous rule refused — never refuse one it admitted.
1772/// The shift the certificate's definiteness verdict is ACTUALLY taken at, and
1773/// the single owner of that number (#2748).
1774///
1775/// It is the larger of the measured `‖δH‖₂` and the arithmetic shift
1776/// `√ε·max(max|H_ii|, 1)`. The second is what decides whenever no identity
1777/// could be measured, and it is not small: on a ρ-Hessian whose largest
1778/// diagonal is under 1 it is a flat `√ε = 1.49e-8`.
1779///
1780/// It is `pub(crate)` and separate because the verdict travels. The
1781/// smoothing-correction re-judges the same direction at the same point against
1782/// a resolution built from its own eigensolver's backward error — `2.19e-16` on
1783/// the measured #2748 `geo_disease` k=12 cell — and refused a direction the
1784/// certificate had cleared at `1.49e-8`, eight orders wider. A verdict and the
1785/// standard it was taken at have to travel together, or the second layer
1786/// applies a strictly stronger test than the first and the fit dies between
1787/// them.
1788pub(crate) fn certificate_curvature_shift(hessian: &Array2<f64>, measured_resolution: f64) -> f64 {
1789 let n = hessian.nrows();
1790 let max_diag = (0..n).fold(0.0_f64, |acc, j| acc.max(hessian[[j, j]].abs()));
1791 let arithmetic_shift = f64::EPSILON.sqrt() * max_diag.max(1.0);
1792 if measured_resolution.is_finite() && measured_resolution > arithmetic_shift {
1793 measured_resolution
1794 } else {
1795 arithmetic_shift
1796 }
1797}
1798
1799pub(crate) fn certificate_hessian_is_psd_at_resolution(
1800 hessian: &Array2<f64>,
1801 measured_resolution: f64,
1802) -> Option<bool> {
1803 let n = hessian.nrows();
1804 if n == 0 || hessian.ncols() != n || hessian.iter().any(|v| !v.is_finite()) {
1805 return None;
1806 }
1807 let shift = certificate_curvature_shift(hessian, measured_resolution);
1808 let mut chol = hessian.clone();
1809 for j in 0..n {
1810 chol[[j, j]] += shift;
1811 }
1812 for j in 0..n {
1813 for k in 0..j {
1814 let l_jk = chol[[j, k]];
1815 for i in j..n {
1816 chol[[i, j]] -= chol[[i, k]] * l_jk;
1817 }
1818 }
1819 let pivot = chol[[j, j]];
1820 if !(pivot > 0.0) || !pivot.is_finite() {
1821 return Some(false);
1822 }
1823 let inv_sqrt = 1.0 / pivot.sqrt();
1824 for i in j..n {
1825 chol[[i, j]] *= inv_sqrt;
1826 }
1827 }
1828 Some(true)
1829}
1830
1831/// PSD verdict of the outer Hessian restricted to its UN-RAILED coordinates
1832/// (#2299 box-KKT reduced-Hessian / critical-cone gate).
1833///
1834/// A coordinate railed at ±`rho_bound` with an outward gradient is at the box-KKT
1835/// constrained optimum: its curvature direction is the flat/indefinite
1836/// infinite-smoothing plateau of a fully-saturated penalty (λ ~ 1e13), carrying
1837/// no feasible descent. Including it makes the FULL Hessian indefinite and used to
1838/// disable the very flatness certificate that exists to handle rails, so an
1839/// honest railed optimum ground to `max_iter` and refused. Judging PSD on the
1840/// INTERIOR (un-railed) sub-block is the standard reduced-Hessian condition: a
1841/// genuinely indefinite *interior* direction still keeps the sub-block non-PSD,
1842/// so this can never over-certify a real saddle. When every coordinate is railed
1843/// the interior is empty — there is no feasible curvature to certify and the rail
1844/// KKT signs are the whole certificate — so the empty sub-block is trivially PSD.
1845/// With no railed coordinate and no declared invariance it is exactly
1846/// [`certificate_hessian_is_psd`].
1847///
1848/// # The invariance argument (#2676)
1849///
1850/// `invariance` carries the directions along which the criterion is EXACTLY
1851/// constant by construction of its penalty map (see
1852/// [`crate::penalty_invariance`]). They are removed from the judged subspace
1853/// alongside the railed coordinates, for the same reason: a coordinate railed
1854/// at its bound and a direction the criterion does not vary along are both
1855/// places where "is the curvature positive?" has no answer that is about the
1856/// fit. On such a direction `t' H_rho t = sum_k g_k t_k^2` identically, so its
1857/// sign is the sign of the disagreement between the gradient code and the
1858/// Hessian code — measured at `|sigma|/floor = 0.99925` on `geo_disease_matern`.
1859///
1860/// `None` reproduces the pre-#2676 sub-block extraction bit for bit; that is
1861/// what every objective declaring no invariance gets, which is nearly all of
1862/// them.
1863pub(crate) fn certificate_hessian_is_psd_off_railed(
1864 hessian: &Array2<f64>,
1865 railed: &[usize],
1866 invariance: Option<&Array2<f64>>,
1867) -> Option<bool> {
1868 certificate_hessian_is_psd_off_railed_at_resolution(hessian, railed, invariance, 0.0)
1869}
1870
1871/// [`certificate_hessian_is_psd_off_railed`] carrying a **measured** curvature
1872/// resolution down to the definiteness test (#2748).
1873pub(crate) fn certificate_hessian_is_psd_off_railed_at_resolution(
1874 hessian: &Array2<f64>,
1875 railed: &[usize],
1876 invariance: Option<&Array2<f64>>,
1877 measured_resolution: f64,
1878) -> Option<bool> {
1879 let n = hessian.nrows();
1880 let deflate = invariance.filter(|basis| basis.nrows() == n && basis.ncols() > 0);
1881 if railed.is_empty() && deflate.is_none() {
1882 return certificate_hessian_is_psd_at_resolution(hessian, measured_resolution);
1883 }
1884 let judged = crate::penalty_invariance::judged_subspace_basis(n, railed, deflate);
1885 let Some(judged) = judged else {
1886 // Nothing left to judge: every coordinate is railed, or the criterion is
1887 // flat in every direction. There is no feasible curvature to certify and
1888 // the rail KKT signs are the whole certificate.
1889 return Some(true);
1890 };
1891 if deflate.is_none() {
1892 // Exactly the historical sub-block extraction: `judged` is the interior
1893 // indicator basis, so build the sub-block directly rather than through a
1894 // matrix product, keeping this path bit-identical.
1895 let railed_set: std::collections::BTreeSet<usize> = railed.iter().copied().collect();
1896 let interior: Vec<usize> = (0..n).filter(|k| !railed_set.contains(k)).collect();
1897 let mut sub = Array2::<f64>::zeros((interior.len(), interior.len()));
1898 for (i, &ri) in interior.iter().enumerate() {
1899 for (j, &rj) in interior.iter().enumerate() {
1900 sub[[i, j]] = hessian[[ri, rj]];
1901 }
1902 }
1903 return certificate_hessian_is_psd_at_resolution(&sub, measured_resolution);
1904 }
1905 let compressed = crate::penalty_invariance::compress_to_judged_subspace(hessian, &judged);
1906 certificate_hessian_is_psd_at_resolution(&compressed, measured_resolution)
1907}
1908
1909/// The curvature resolution this site is entitled to: `‖δH‖₂` MEASURED from the
1910/// identities that are exactly zero in exact arithmetic (#2748), never assumed.
1911///
1912/// Two are available here and both are free:
1913///
1914/// * the **symmetrization defect** `‖(H − Hᵀ)/2‖₂`. A Hessian is symmetric for
1915/// any twice-continuously-differentiable criterion, and `H[i,j]` and `H[j,i]`
1916/// are separate accumulations of the same mixed partial, so whatever survives
1917/// is the assembly's error;
1918/// * the **penalty-map invariance residual** `‖T'(H − diag(g))T‖₂` on the
1919/// directions this certificate deflates. Along them the criterion is exactly
1920/// constant in λ, so `t'H t = Σ_k g_k t_k²` identically and the residual is
1921/// error and only error — in exactly the currency a `H + diag(|g|)` gate
1922/// spends.
1923///
1924/// Both are certified LOWER bounds on `‖δH‖₂`, so their maximum is the strongest
1925/// available fact. Returns `0.0` when neither can be taken, which leaves the
1926/// historical `√ε` shift in force unchanged.
1927pub(crate) fn measured_outer_curvature_resolution(
1928 hessian: &Array2<f64>,
1929 railed: &[usize],
1930 gradient: &Array1<f64>,
1931 invariance: Option<&Array2<f64>>,
1932) -> f64 {
1933 let n = hessian.nrows();
1934 if n == 0 || hessian.ncols() != n {
1935 return 0.0;
1936 }
1937 let symmetrization = gam_linalg::matrix::symmetrization_defect_2norm(hessian);
1938 let deflate = invariance.filter(|basis| basis.nrows() == n && basis.ncols() > 0);
1939 let invariance_residual = deflate
1940 .and_then(|basis| crate::penalty_invariance::judged_subspace_basis(n, railed, Some(basis)))
1941 .and_then(|judged| crate::penalty_invariance::deflated_directions(n, &judged))
1942 .and_then(|removed| {
1943 crate::penalty_invariance::invariance_residual_2norm(hessian, gradient, &removed)
1944 })
1945 .unwrap_or(0.0);
1946 symmetrization.max(invariance_residual).max(0.0)
1947}
1948
1949/// Interior-PSD verdict judged ABOVE the per-coordinate gradient-residue noise
1950/// floor (#2349): PSD of `H + diag(|g|)` restricted to the un-excluded
1951/// coordinates.
1952///
1953/// The assembled ρ-Hessian's tail entries carry the #2298 trace-pair
1954/// cancellation residue: when the `λ²V_λλ` pair cancels to roundoff, the
1955/// surviving diagonal entry is `λV_λ = g_k` — gradient magnitude, corrupted
1956/// sign (the same tie signature the tail-snap candidate band keys on).
1957/// Measured on the #2349 multinomial checkpoint: the sole interior coordinate
1958/// had `g₁ = −1.0228e-3`, `H₁₁ = −1.0216e-3` (ratio 0.999), and that single
1959/// sub-resolution entry was the entire `interior Hessian sub-block not PSD`
1960/// refusal — the full 6×6 spectrum was `[−1.02e-3, 0.135, …, 0.904]`.
1961///
1962/// # Why the floor is safe — instrument resolution, not step economics
1963///
1964/// The residue is `O(|g_k|)`, and the sub-block is extracted AFTER flooring, so
1965/// only the *judged* (un-excluded) coordinates' gradients ever enter: a railed
1966/// coordinate's large `|g|` (measured 1.40 on the #2349 checkpoint) cannot
1967/// inflate the floor. Weyl bounds what remains exactly:
1968///
1969/// ```text
1970/// λ_min(H) + min|g| ≤ λ_min(H + diag|g|) ≤ λ_min(H) + max|g|
1971/// ```
1972///
1973/// so the floor absorbs **at most `max_k |g_k|` over the judged coordinates**.
1974/// Where this verdict can mint, those coordinates have passed gradient
1975/// stationarity, so that is at most the stationarity bound. A negative
1976/// eigenvalue smaller than that is not distinguishable from zero *by the
1977/// instrument that produced it* — the assembled tail entry IS `λV_λ = g`
1978/// (ratio 0.999 as measured) — while a genuine saddle survives untouched: the
1979/// #2357 trace's `λ_min ≈ −0.5` against `|g| ≈ 1e-3` floors to `−0.4973`, and a
1980/// `−1.50e-2` direction still refuses at a `1e-2` bound.
1981///
1982/// ## Do not widen this floor beyond `diag(|g|)`
1983///
1984/// Earlier revisions of this argument (and #2349 rounds 6/7) justified the
1985/// floor by an "exploitable improvement `≲ g²/2|H| ≈ 5e-7`". Both halves are
1986/// wrong and the pair is misleading in the permissive direction:
1987///
1988/// * the quoted number is `g²/2 = 5.23e-7`; the quoted formula evaluates to
1989/// `g²/(2|H|) = 5.12e-4` on the same measured `g = 1.0228e-3`,
1990/// `|H| = 1.0216e-3` — a 979× discrepancy. Since `|H| ≈ g` at exactly the
1991/// point this floor serves, that formula collapses to `≈ g/2`;
1992/// * and `g²/2|H|` is the Newton decrement, which bounds the improvement along
1993/// a direction of POSITIVE curvature. Along curvature `−ε` the model
1994/// `−g·t − ½εt²` is unbounded below, so the quantity it purports to bound
1995/// does not exist in the regime this floor exists for; the step is limited by
1996/// the trust radius, not the curvature.
1997///
1998/// The Weyl bound above is the correct and checkable statement. Anything that
1999/// widens the floor must re-derive against it, not against `g²/2|H|`.
2000pub(crate) fn certificate_meets_curvature_requirement(
2001 certificate: &OuterCriterionCertificate,
2002 require_measured_psd: bool,
2003 fidelity: CertificationFidelity,
2004) -> bool {
2005 matches!(fidelity, CertificationFidelity::Screening)
2006 || !require_measured_psd
2007 || certificate.hessian_psd() == Some(true)
2008 // #2612: a caller asking for a certified local minimum is asking for the
2009 // strongest statement the curvature evidence can support. When that
2010 // evidence has been CONTRADICTED by the criterion — every feasible step
2011 // along its reported negative eigenvector, over the whole range in which
2012 // the claim predicts a decrease the criterion can represent, failed to
2013 // lower the objective — the strongest supportable statement is that no
2014 // descent along it exists. Refusing here instead would refuse for the
2015 // ABSENCE of a measurement the route has just shown it cannot make,
2016 // which is the failure mode this flag's own doc block at
2017 // `with_require_measured_psd` warns about one case earlier.
2018 || matches!(
2019 certificate.curvature,
2020 CurvatureEvidence::CriterionContradicted
2021 )
2022}
2023
2024pub(crate) fn certificate_hessian_is_psd_off_railed_above_gradient_floor(
2025 hessian: &Array2<f64>,
2026 excluded: &[usize],
2027 gradient: &Array1<f64>,
2028 invariance: Option<&Array2<f64>>,
2029) -> Option<bool> {
2030 let n = hessian.nrows();
2031 if gradient.len() != n {
2032 return certificate_hessian_is_psd_off_railed(hessian, excluded, invariance);
2033 }
2034 let mut floored = hessian.clone();
2035 for k in 0..n {
2036 floored[[k, k]] += gradient[k].abs();
2037 }
2038 // The resolution is measured on the UNFLOORED `H` against the UNFLOORED
2039 // gradient: `diag(|g|)` is a deliberate softening of the test, not part of
2040 // the assembly, and the identities that measure `‖δH‖₂` are identities
2041 // about `(H, g)` as evaluated (#2748).
2042 let measured_resolution =
2043 measured_outer_curvature_resolution(hessian, excluded, gradient, invariance);
2044 certificate_hessian_is_psd_off_railed_at_resolution(
2045 &floored,
2046 excluded,
2047 invariance,
2048 measured_resolution,
2049 )
2050}
2051
2052/// Measure the gradient-residue floor's clearance on the interior sub-block:
2053/// the sub-block's smallest eigenvalue as assembled, the floor it is judged
2054/// against (`max_k |g_k|` over exactly those coordinates), and whether
2055/// `H + diag(|g|)` is PSD there.
2056///
2057/// This RECORDS the verdict; it does not replace the raw measurement. See
2058/// [`certificate_hessian_is_psd_off_railed_above_gradient_floor`] for why the
2059/// Weyl bound makes the floor safe where it can mint, and
2060/// [`crate::model_types::CurvatureFloorClearance`] for why the two facts are
2061/// kept apart.
2062pub(crate) fn interior_curvature_floor_clearance(
2063 hessian: &Array2<f64>,
2064 excluded: &[usize],
2065 gradient: &Array1<f64>,
2066 invariance: Option<&Array2<f64>>,
2067) -> Option<CurvatureFloorClearance> {
2068 use faer::Side;
2069 use gam_linalg::faer_ndarray::FaerEigh;
2070
2071 let n = hessian.nrows();
2072 if n == 0 || hessian.ncols() != n || gradient.len() != n {
2073 return None;
2074 }
2075 let excluded_set: std::collections::BTreeSet<usize> = excluded.iter().copied().collect();
2076 let interior: Vec<usize> = (0..n).filter(|k| !excluded_set.contains(k)).collect();
2077 if interior.is_empty() {
2078 return None;
2079 }
2080 let m = interior.len();
2081 let mut sub = Array2::<f64>::zeros((m, m));
2082 for (i, &ri) in interior.iter().enumerate() {
2083 for (j, &rj) in interior.iter().enumerate() {
2084 sub[[i, j]] = 0.5 * (hessian[[ri, rj]] + hessian[[rj, ri]]);
2085 }
2086 }
2087 if sub.iter().any(|v| !v.is_finite()) {
2088 return None;
2089 }
2090 // #2676: report the minimum of the block the verdict was actually reached
2091 // on. Reporting the raw interior minimum beside a verdict taken on the
2092 // deflated complement is what made every historical `[INDEF-HESS]` line
2093 // ambiguous: the number named a direction the decision no longer involved.
2094 let deflate = invariance.filter(|basis| basis.nrows() == n && basis.ncols() > 0);
2095 let sub = match deflate
2096 .and_then(|basis| crate::penalty_invariance::judged_subspace_basis(n, excluded, Some(basis)))
2097 {
2098 Some(judged) => crate::penalty_invariance::compress_to_judged_subspace(hessian, &judged),
2099 None => sub,
2100 };
2101 if sub.nrows() == 0 {
2102 return None;
2103 }
2104 let interior_min_eigenvalue = sub
2105 .eigh(Side::Lower)
2106 .ok()?
2107 .0
2108 .iter()
2109 .fold(f64::INFINITY, |acc, v| acc.min(*v));
2110 if !interior_min_eigenvalue.is_finite() {
2111 return None;
2112 }
2113 // The floor is the largest gradient among EXACTLY the judged coordinates —
2114 // the excluded ones never enter, so a railed coordinate's large |g| cannot
2115 // inflate it.
2116 let gradient_floor = interior
2117 .iter()
2118 .fold(0.0_f64, |acc, &k| acc.max(gradient[k].abs()));
2119 let cleared = certificate_hessian_is_psd_off_railed_above_gradient_floor(
2120 hessian, excluded, gradient, invariance,
2121 ) == Some(true);
2122 // The eigenvalue the verdict was ACTUALLY taken on: the same
2123 // `H + diag(|g|)`, on the same judged subspace, that
2124 // `certificate_hessian_is_psd_off_railed_above_gradient_floor` tests
2125 // (#2748). The two fields above are the ends of the Weyl sandwich
2126 // `λ_min(H) + min|g| ≤ λ_min(H + diag|g|) ≤ λ_min(H) + max|g|`, and a
2127 // reader given only the ends cannot tell why a curvature well inside
2128 // `max_k|g_k|` refused.
2129 let mut floored = hessian.clone();
2130 for k in 0..n {
2131 floored[[k, k]] += gradient[k].abs();
2132 }
2133 let floored_block = match invariance
2134 .filter(|basis| basis.nrows() == n && basis.ncols() > 0)
2135 .and_then(|basis| {
2136 crate::penalty_invariance::judged_subspace_basis(n, excluded, Some(basis))
2137 }) {
2138 Some(judged) => crate::penalty_invariance::compress_to_judged_subspace(&floored, &judged),
2139 None => {
2140 let mut block = Array2::<f64>::zeros((m, m));
2141 for (i, &ri) in interior.iter().enumerate() {
2142 for (j, &rj) in interior.iter().enumerate() {
2143 block[[i, j]] = 0.5 * (floored[[ri, rj]] + floored[[rj, ri]]);
2144 }
2145 }
2146 block
2147 }
2148 };
2149 let floored_min_eigenvalue = if floored_block.nrows() == 0 {
2150 0.0
2151 } else {
2152 floored_block
2153 .eigh(Side::Lower)
2154 .ok()?
2155 .0
2156 .iter()
2157 .fold(f64::INFINITY, |acc, v| acc.min(*v))
2158 };
2159 let measured_resolution =
2160 measured_outer_curvature_resolution(hessian, excluded, gradient, invariance);
2161 Some(CurvatureFloorClearance {
2162 interior_min_eigenvalue,
2163 gradient_floor,
2164 floored_min_eigenvalue,
2165 measured_resolution,
2166 // The shift the verdict was decided at, from the SAME owner the PSD
2167 // test calls and on the SAME block it tested, so the number recorded
2168 // and the number applied cannot drift apart (#2748).
2169 decided_at_resolution: certificate_curvature_shift(&floored_block, measured_resolution),
2170 cleared,
2171 })
2172}
2173
2174/// The **measured** disagreement between an analytic outer Hessian and the
2175/// criterion it claims to be the curvature of, along one direction (#2748).
2176///
2177/// # Why this is a measurement of the MATRIX
2178///
2179/// For a unit direction `v`, `vᵀHv` and `d²/dα² V(θ̂ + αv)|₀` are the same
2180/// number written two ways. Their difference is exactly zero in exact
2181/// arithmetic, which is what makes it admissible as a
2182/// [`gam_linalg::curvature_resolution::MeasuredHessianError`], and by Weyl
2183/// `|vᵀ(δH)v| ≤ ‖δH‖₂`, so the difference is a certified LOWER BOUND on the
2184/// assembly's own error. That is the *"how wrong is this matrix?"* quantity
2185/// `curvature_resolution`'s Law 2 supplies no value for, and which every
2186/// downstream ρ-curvature gate has been substituting an eigensolver's
2187/// *"given this matrix, how wrong is σ?"* for.
2188///
2189/// # Why it is carried rather than consumed here
2190///
2191/// The outer certificate is not the only subsystem that judges this matrix's
2192/// definiteness. `estimate::smoothing_correction::invert_identified_rho_hessian`
2193/// judges it again, later, from the matrix alone — and #2428 is precisely the
2194/// two reaching opposite verdicts on one matrix at one point. Handing the
2195/// measurement forward is what stops the second site from re-deciding a
2196/// question the first one measured.
2197#[derive(Clone, Debug)]
2198pub struct CriterionCurvatureDisagreement {
2199 /// The unit direction probed, in the FULL outer coordinate vector.
2200 ///
2201 /// Carried so a consumer whose Hessian is a coordinate sub-block can check
2202 /// that the direction lives inside its own block before reading the bound:
2203 /// `‖δH‖₂` measured on a wider matrix does not bound a sub-block's error in
2204 /// general, but a direction supported entirely on the sub-block does
2205 /// measure exactly that sub-block.
2206 pub direction: Array1<f64>,
2207 /// `vᵀHv` as the analytic outer Hessian reported it — the number in dispute.
2208 pub analytic_curvature: f64,
2209 /// What the criterion's own symmetric probe ladder measured there.
2210 pub ladder: gam_linalg::curvature_resolution::LadderCurvature,
2211}
2212
2213impl CriterionCurvatureDisagreement {
2214 /// The certified lower bound on `‖δH‖₂`, i.e. the disagreement net of the
2215 /// ladder's own uncertainty. Exactly `0.0` when the two agree.
2216 pub fn hessian_error_2norm(&self) -> f64 {
2217 self.ladder.hessian_error_against(self.analytic_curvature)
2218 }
2219
2220 /// The probed direction restricted to the leading `dimension` coordinates,
2221 /// but only when every coordinate beyond them is exactly zero and the
2222 /// restriction is still a unit vector to orthonormality round-off.
2223 ///
2224 /// Both conditions are required for the bound to transfer: the identity
2225 /// `|vᵀ(δH)v| ≤ ‖δH‖₂` is about the sub-block's own `δH` only if `v` has no
2226 /// component outside it, and the Rayleigh quotient is a curvature only if
2227 /// `v` is normalised.
2228 pub fn restricted_to_leading(&self, dimension: usize) -> Option<Array1<f64>> {
2229 if dimension == 0 || self.direction.len() < dimension {
2230 return None;
2231 }
2232 if self.direction.iter().skip(dimension).any(|value| *value != 0.0) {
2233 return None;
2234 }
2235 let head = self.direction.slice(ndarray::s![..dimension]).to_owned();
2236 let norm = head.dot(&head).sqrt();
2237 let round_off = 64.0 * (self.direction.len() as f64) * f64::EPSILON;
2238 ((norm - 1.0).abs() <= round_off).then_some(head)
2239 }
2240}
2241
2242/// What the CRITERION said about a Hessian's reported negative direction
2243/// (#2357/#2155/#2612).
2244///
2245/// The escape has always been able to distinguish "the saddle is real, here is
2246/// the descending reseed" from "no descending trial was found", but only the
2247/// first was reported to the caller: the second reached the refusal as `None`,
2248/// where it was indistinguishable from "the escape was never runnable", and the
2249/// curvature refusal proceeded on the matrix's word alone.
2250///
2251/// Those are three different states and one of them is a MEASUREMENT of the
2252/// criterion, so they are three variants.
2253#[derive(Debug)]
2254pub(crate) enum SaddleAdjudication {
2255 /// A strictly-descending feasible point exists along the reported direction:
2256 /// the point is not a minimum, and this is the one-shot reseed.
2257 Descended(Array1<f64>),
2258 /// Every feasible step along the reported direction — both signs, from one
2259 /// e-fold down to the step at which the quadratic model's own predicted
2260 /// decrease reaches the criterion's resolution — failed to lower the
2261 /// objective. The claim has been falsified over its whole falsifiable
2262 /// range.
2263 Contradicted {
2264 /// Trials actually evaluated (finite cost, not clamped back onto ρ).
2265 probed: usize,
2266 /// Smallest step the ladder reached.
2267 smallest_step: f64,
2268 /// `½|λ_min|·α_min²` — what the claim predicted at that step, against
2269 /// which the criterion's resolution was the stopping standard.
2270 predicted_at_smallest: f64,
2271 /// The criterion's own resolution, i.e. the standard that bounded the
2272 /// ladder.
2273 objective_resolution: f64,
2274 /// Best objective seen, against the baseline it had to beat.
2275 best_seen_cost: f64,
2276 /// What the ladder MEASURED about the analytic Hessian, when it could
2277 /// be determined (#2748). `None` is an absent measurement — the fit was
2278 /// undetermined, or the baseline could not be restored — and must never
2279 /// be read as a zero error.
2280 criterion_curvature: Option<CriterionCurvatureDisagreement>,
2281 },
2282 /// The adjudication could not be run: no eigen-resolvable negative
2283 /// direction, nothing left to search after rails and invariance, an
2284 /// eigensolver failure, or trials that could not be evaluated at all.
2285 /// Nothing has been established about the point either way.
2286 Declined(String),
2287}
2288
2289/// Escape point off a certified strict saddle in the free (un-railed) subspace
2290/// (#2357, generalised to the box-constrained case in #2155), and — when no
2291/// escape exists — the verdict that the criterion has CONTRADICTED the matrix
2292/// (#2612).
2293///
2294/// A gradient-only outer convergence gate — ARC's own, or the cost-stall guard's
2295/// — can ARRIVE at a point that is first-order stationary (`‖Pg‖ ≤ bound`) yet
2296/// sits on genuinely indefinite curvature in its INTERIOR (un-railed) directions,
2297/// and stop there because its gradient already cleared tolerance. The mandatory
2298/// analytic certificate then refuses the point as `INDEFINITE CURVATURE AT
2299/// INTERIOR OPTIMUM` — a verdict `certificate_hessian_is_psd_off_railed` reaches
2300/// on the reduced Hessian restricted to the un-railed coordinates, so it fires
2301/// whether or not some other coordinate happens to be railed. Such a point is a
2302/// saddle, not a minimum: the most-negative-curvature eigenvector `v` of that
2303/// reduced Hessian is a strict, box-feasible descent direction the optimizer
2304/// never took. An
2305/// identical warm-started resume escapes it trivially (its fresh cubic step moves
2306/// off the ridge, which is why the resume converges where the cold run refuses);
2307/// this reproduces that escape deterministically by stepping `ρ ± α·v` to a
2308/// strictly-lower objective and handing the point back as a one-shot reseed.
2309///
2310/// Termination is guaranteed: along a direction of negative curvature
2311/// `vᵀHv = λ_min < 0` at a near-stationary gradient,
2312/// `f(ρ ± αv) = f(ρ) ± α(g·v) + ½α²λ_min + o(α²)` strictly decreases for small
2313/// enough `α` once the sign is chosen so the first-order term is non-positive, so
2314/// the finite backtracking below always finds a descending feasible point when
2315/// one exists inside the box.
2316///
2317/// Returns `None` (no reseed; the ordinary refusal proceeds) when the Hessian
2318/// carries no eigen-resolvable negative direction, or no bounded step along it
2319/// clears the box projection with a strict objective decrease. Restores the
2320/// objective's profiled inner state to `rho` before returning either way, so the
2321/// refusal path that follows measures the checkpoint rather than the last probe.
2322fn adjudicate_negative_curvature(
2323 obj: &mut dyn OuterObjective,
2324 rho: &Array1<f64>,
2325 gradient: &Array1<f64>,
2326 hessian: &Array2<f64>,
2327 railed: &[usize],
2328 invariance: Option<&Array2<f64>>,
2329 baseline_cost: f64,
2330 objective_resolution: f64,
2331 bounds: &(Array1<f64>, Array1<f64>),
2332 context: &str,
2333) -> SaddleAdjudication {
2334 use faer::Side;
2335 use gam_linalg::faer_ndarray::FaerEigh;
2336
2337 let n = hessian.nrows();
2338 if n == 0 || hessian.ncols() != n || hessian.iter().any(|v| !v.is_finite()) {
2339 // #2665: every `None` in this function is a DIFFERENT reason the escape
2340 // did not fire, and the caller records none of them -- the refusal that
2341 // follows says only that the curvature floor did not clear. On the
2342 // SAS/mixture cluster the escape is silently absent (measured: no mint
2343 // line at all, and four resumes at a bitwise-identical rho), and the
2344 // exits cannot be told apart from the run record. The sibling
2345 // NOT ATTEMPTED warning above covers the case where this function is
2346 // never called; these cover the case where it is called and declines.
2347 return SaddleAdjudication::Declined(format!(
2348 "the analytic Hessian is not a usable square finite matrix (rows={}, cols={}, \
2349 all_finite={})",
2350 n,
2351 hessian.ncols(),
2352 hessian.iter().all(|v| v.is_finite()),
2353 ));
2354 }
2355 // The escape direction lives in the INTERIOR (un-railed) subspace — the exact
2356 // reduced Hessian / critical cone that `certificate_hessian_is_psd_off_railed`
2357 // judges for the PSD verdict. A coordinate railed at a box bound with an
2358 // outward KKT gradient is already at its constrained optimum; its curvature is
2359 // the flat/indefinite infinite-smoothing plateau (λ ~ 1e13) and carries no
2360 // feasible descent. Including it would let the step chase that spurious
2361 // direction and simply re-rail. Restricting to the un-railed block yields a
2362 // feasible descent that holds every rail fixed, so the escape generalises from
2363 // the fully-interior saddle to a box-constrained one whose free-direction
2364 // reduced Hessian is indefinite (#2357 → #2155). With no rail this is exactly
2365 // the full-Hessian eigenproblem as before.
2366 let railed_set: std::collections::BTreeSet<usize> = railed.iter().copied().collect();
2367 let interior: Vec<usize> = (0..n).filter(|k| !railed_set.contains(k)).collect();
2368 if interior.is_empty() {
2369 // Every coordinate is railed: there is no feasible interior direction and
2370 // the rail KKT signs are the whole certificate.
2371 return SaddleAdjudication::Declined(format!(
2372 "every one of the {n} outer coordinates is railed, so there is no feasible interior \
2373 direction to descend"
2374 ));
2375 }
2376 // #2676: the escape must search the SAME subspace the certificate judged.
2377 // `judged_subspace_basis` returns the interior indicator basis when there is
2378 // no invariance, so `sub` and the lift below are bit-identical on that path;
2379 // with one, the escape stops being able to pick the criterion-invariant
2380 // direction — where the only "negative curvature" available is the
2381 // chain-rule term `sum_k g_k t_k^2`, i.e. the residual gradient wearing a
2382 // curvature's clothes — instead of the genuine saddle direction that
2383 // refused.
2384 let deflate = invariance.filter(|basis| basis.nrows() == n && basis.ncols() > 0);
2385 let Some(judged) = crate::penalty_invariance::judged_subspace_basis(n, railed, deflate) else {
2386 return SaddleAdjudication::Declined(
2387 "after removing the railed coordinates and the criterion's own invariance there is \
2388 no direction left to search"
2389 .to_string(),
2390 );
2391 };
2392 let m = judged.ncols();
2393 let sub = match deflate {
2394 Some(_) => crate::penalty_invariance::compress_to_judged_subspace(hessian, &judged),
2395 None => {
2396 let mut sub = Array2::<f64>::zeros((m, m));
2397 for (i, &ri) in interior.iter().enumerate() {
2398 for (j, &rj) in interior.iter().enumerate() {
2399 sub[[i, j]] = hessian[[ri, rj]];
2400 }
2401 }
2402 sub
2403 }
2404 };
2405 let (eigenvalues, eigenvectors) = match sub.eigh(Side::Lower) {
2406 Ok(pair) => pair,
2407 Err(err) => {
2408 return SaddleAdjudication::Declined(format!(
2409 "the interior sub-block's eigendecomposition failed ({err})"
2410 ));
2411 }
2412 };
2413 // The SAME √ε·‖H‖ margin `certificate_hessian_is_psd` uses to separate a
2414 // genuine negative eigenvalue from O(ε·‖H‖) assembly roundoff: only a truly
2415 // negative direction — not a flat / near-semidefinite one — carries a descent
2416 // the reseed can exploit. Measured on the interior sub-block's diagonal so the
2417 // threshold matches the reduced PSD verdict exactly.
2418 let max_diag = interior
2419 .iter()
2420 .fold(0.0_f64, |acc, &j| acc.max(hessian[[j, j]].abs()));
2421 let neg_margin = f64::EPSILON.sqrt() * max_diag.max(1.0);
2422 let mut min_idx = 0usize;
2423 for k in 1..eigenvalues.len() {
2424 if eigenvalues[k] < eigenvalues[min_idx] {
2425 min_idx = k;
2426 }
2427 }
2428 if !(eigenvalues[min_idx] < -neg_margin) {
2429 // The certificate refuses on the FLOOR (`H + diag(|g|)` not PSD); this
2430 // gate admits on a sqrt(EPSILON)*||H|| ROUNDOFF margin. They are
2431 // different numbers, so a point can be refused for curvature AND
2432 // declined for escape, with no record of either bound. Print both so
2433 // the gap is measurable rather than inferred (#2665).
2434 return SaddleAdjudication::Declined(format!(
2435 "the interior sub-block's most negative eigenvalue does not clear the roundoff \
2436 margin: lambda_min={:.6e}, neg_margin={:.6e} (= sqrt(EPSILON) * max(1, max_k \
2437 |H_kk|) with max_diag={:.6e}), interior_dim={}",
2438 eigenvalues[min_idx], neg_margin, max_diag, m,
2439 ));
2440 }
2441 let v_sub = eigenvectors.column(min_idx);
2442 let dir_norm = v_sub.dot(&v_sub).sqrt();
2443 if !(dir_norm > 0.0) || !dir_norm.is_finite() {
2444 return SaddleAdjudication::Declined(format!(
2445 "the lambda_min={:.6e} eigenvector has an unusable norm {dir_norm:.6e}",
2446 eigenvalues[min_idx],
2447 ));
2448 }
2449 // Lift the judged eigenvector into the full ρ space through the same basis
2450 // the sub-block was taken in. Its rows are exactly zero on every railed
2451 // coordinate, so the backtracking step below still holds all rails fixed;
2452 // with no invariance the basis is the interior indicator matrix and this is
2453 // the historical scatter, multiplication by exact zeros and ones.
2454 let direction = judged.dot(&v_sub.mapv(|value| value / dir_norm));
2455 // First-order-consistent sign: move against the (tiny) gradient's projection
2456 // onto `v` so the linear term never opposes the curvature descent. With a
2457 // stationary gradient the tie is arbitrary; the opposite sign is tried below
2458 // regardless, which also covers a `v` that projects straight out of the box.
2459 let primary_sign = if gradient.dot(&direction) > 0.0 {
2460 -1.0
2461 } else {
2462 1.0
2463 };
2464 // The step ladder is DERIVED from what the claim predicts, not chosen
2465 // (#2612).
2466 //
2467 // One e-fold in log-λ is a macroscopic step across the saddle ridge, and
2468 // ARC refines from wherever this lands, so the largest step stays `1`. What
2469 // the old fixed ladder could not say is where to STOP: it halted at
2470 // `0.0625` because five entries had been written down, so a claim whose
2471 // descent only appears below that step was reported the same way as a claim
2472 // with no descent at all — and the refusal then proceeded on the matrix's
2473 // word either way.
2474 //
2475 // At a stationary point the quadratic model of the claim itself is
2476 //
2477 // ```text
2478 // V(ρ ± αv) − V(ρ) ≈ ½ λ_min α², λ_min < 0
2479 // ```
2480 //
2481 // so the claim predicts a decrease of `½|λ_min|α²`. Once that falls to the
2482 // criterion's own resolution the claim predicts nothing the criterion can
2483 // represent, and no smaller step can falsify it. That step,
2484 //
2485 // ```text
2486 // α_min = sqrt(2 · objective_resolution / |λ_min|),
2487 // ```
2488 //
2489 // is therefore the exact end of the claim's FALSIFIABLE RANGE — derived
2490 // from the eigenvalue in dispute and the same `rel_cost_tolerance`-anchored
2491 // resolution the rail and cost-stall machinery already use, with no
2492 // constant chosen here. Probing from `1` down to it and finding no descent
2493 // in either sign is a measurement of the criterion that contradicts the
2494 // matrix; stopping earlier would only have been a statement about the
2495 // ladder.
2496 let lambda_min = eigenvalues[min_idx];
2497 let alpha_min = if objective_resolution.is_finite() && objective_resolution > 0.0 {
2498 (2.0 * objective_resolution / lambda_min.abs()).sqrt().min(1.0)
2499 } else {
2500 // No usable resolution: keep the historical five-rung ladder's reach.
2501 0.0625
2502 };
2503 let mut escape_step_scales: Vec<f64> = Vec::new();
2504 let mut alpha = 1.0_f64;
2505 loop {
2506 escape_step_scales.push(alpha);
2507 // `f64::EPSILON` is where halving stops changing `ρ + αv` at all — a
2508 // property of the arithmetic, not a budget.
2509 if alpha <= alpha_min || alpha <= f64::EPSILON {
2510 break;
2511 }
2512 alpha *= 0.5;
2513 }
2514 // The strict-decrease floor is the CRITERION's resolution, not the
2515 // arithmetic's (#2612).
2516 //
2517 // The ladder above stops at `α_min = sqrt(2·objective_resolution/|λ_min|)`
2518 // on the stated ground that below it "the claim predicts nothing the
2519 // criterion can represent". A trial's MEASURED decrease is the same kind of
2520 // quantity as the claim's predicted one, so it has to be judged against the
2521 // same resolution: a step that lowers the objective by less than the
2522 // criterion can resolve has not descended, it has reproduced the noise the
2523 // ladder's own stopping rule was derived from. Accepting it as an escape
2524 // spends the one-shot reseed on a number the criterion cannot distinguish
2525 // from zero, and the retry — which cannot adjudicate again — then refuses on
2526 // the matrix's word, which is the state this whole block exists to prevent.
2527 //
2528 // Measured before this changed, with the floor at `16ε|V|` (roundoff) while
2529 // the ladder's limit used `objective_resolution`, i.e. the same function
2530 // holding two notions of "a decrease the criterion can represent" ten orders
2531 // apart:
2532 //
2533 // ```text
2534 // penguins stride-3, unbiased probe: λ_min = −6.35e−7 … −1.99e−6,
2535 // four reseeds minted on decreases 2e−6 … 4e−6 of an objective ≈ 2.158,
2536 // against objective_resolution = 1.228e−3 — three orders BELOW it;
2537 // banded quasi-separated, armed refit: λ_min = −9.19e−3 … −1.12e−2,
2538 // three reseeds on decreases 3.4e−4, 1.4e−4, 5.0e−5 of ≈ 53.66,
2539 // against a measured cost-stall noise floor of 1.91e−4.
2540 // ```
2541 //
2542 // Both fits then refused for lack of a certified optimum, and the fit that
2543 // shipped was the Firth/Jeffreys-armed one.
2544 //
2545 // Roundoff remains the hard lower limit — where `objective_resolution` is
2546 // absent or non-positive there is nothing derived to use, and a decrease
2547 // under `16ε|V|` is not a decrease under any reading.
2548 let roundoff_floor = baseline_cost.abs().max(1.0) * (16.0 * f64::EPSILON);
2549 let strict_floor = if objective_resolution.is_finite() && objective_resolution > 0.0 {
2550 objective_resolution.max(roundoff_floor)
2551 } else {
2552 roundoff_floor
2553 };
2554 // `(cost, point, sign, alpha)`. The step that produced the point is carried
2555 // because the ladder ANSWERS a different question from the one the reseed
2556 // asks (#2612): see [`expand_confirmed_descent`].
2557 let mut best: Option<(f64, Array1<f64>, f64, f64)> = None;
2558 // #2665 bookkeeping: "no descending trial", "every trial clamped back onto
2559 // rho" and "every trial evaluated non-finite" are three different failures
2560 // that all leave `best == None`. Count them so the declined exit below says
2561 // which one happened, and carry the best cost actually SEEN so the
2562 // shortfall against `baseline_cost - strict_floor` is a number.
2563 let mut probed = 0usize;
2564 let mut clamped_onto_rho = 0usize;
2565 let mut eval_failed = 0usize;
2566 let mut nonfinite = 0usize;
2567 let mut best_seen_cost = f64::INFINITY;
2568 // Every evaluation the descent search makes, kept so the ladder below can
2569 // pair the two signs at a common step instead of paying for them twice
2570 // (#2748). Recording costs nothing and changes no verdict: the loop's
2571 // order, its break and its counters are untouched.
2572 let mut evaluations: Vec<(f64, f64, f64)> = Vec::new();
2573 for sign in [primary_sign, -primary_sign] {
2574 for &alpha in escape_step_scales.iter() {
2575 let mut exact = rho.clone();
2576 for i in 0..n {
2577 exact[i] += sign * alpha * direction[i];
2578 }
2579 let trial = project_to_bounds(&exact, Some(bounds));
2580 // A fully box-clamped trial that lands back on ρ probes nothing.
2581 if outer_theta_bitwise_eq(&trial, rho) {
2582 clamped_onto_rho += 1;
2583 continue;
2584 }
2585 // A PARTIALLY clamped trial is not `ρ ± αv` either, so it cannot
2586 // enter the symmetric average — the second difference would be
2587 // taken between two different directions. It still probes descent,
2588 // which is what this loop is for, so only the ladder skips it.
2589 let unclamped = outer_theta_bitwise_eq(&trial, &exact);
2590 probed += 1;
2591 match obj.eval_cost(&trial) {
2592 Ok(cost) if cost.is_finite() => {
2593 if unclamped {
2594 evaluations.push((sign, alpha, cost));
2595 }
2596 best_seen_cost = best_seen_cost.min(cost);
2597 if cost < baseline_cost - strict_floor {
2598 best = Some((cost, trial, sign, alpha));
2599 // The ladder descends in α and the reseed only has to
2600 // LEAVE the ridge — ARC refines from wherever it lands
2601 // — so the first (largest) descending step is the
2602 // escape. Continuing would spend the rest of the
2603 // falsifiability ladder confirming a claim already
2604 // confirmed, and that ladder is now derived rather than
2605 // five rungs long.
2606 break;
2607 }
2608 }
2609 Ok(_) => nonfinite += 1,
2610 Err(_) => eval_failed += 1,
2611 }
2612 }
2613 if best.is_some() {
2614 break;
2615 }
2616 }
2617 // ── The ladder, extended until it can MEASURE rather than only falsify ──
2618 //
2619 // The loop above has just evaluated the criterion on both sides of `ρ̂`
2620 // along `v`, which is a symmetric probe ladder — the exact instrument
2621 // `gam_linalg::curvature_resolution`'s header says `ε_f` and `M₄` "come
2622 // free from". It was being spent on one boolean and discarded.
2623 //
2624 // It is spent on the boolean because the falsifiability ladder stops at
2625 // `α_min = sqrt(2·objective_resolution/|λ_min|)`, and when the claim is
2626 // small that is `≥ 1` and the ladder is ONE rung. One rung cannot fit two
2627 // parameters, so the extension below is not an optional refinement: without
2628 // it there is no measurement at all. It runs only where the descent search
2629 // has already failed, i.e. on the path that is about to hand a curvature
2630 // verdict to a gate that would otherwise judge it against an eigensolver's
2631 // backward error (#2748).
2632 if best.is_none() {
2633 LadderExtension {
2634 rho,
2635 direction: &direction,
2636 lambda_min,
2637 roundoff_floor,
2638 smallest_escape_step: escape_step_scales.last().copied().unwrap_or(1.0),
2639 bounds,
2640 context,
2641 }
2642 .run(obj, &mut evaluations);
2643 }
2644 // Restore the profiled inner state to the checkpoint ρ so the refusal path
2645 // that follows measures the checkpoint, not the last probe.
2646 //
2647 // Its RETURNED value is the ladder's baseline: `f(x)` has to come from the
2648 // same instrument, in the same state, as `f(x ± αv)`, and any drift between
2649 // this evaluation and the `baseline_cost` the caller passed in is part of
2650 // the `ε_f` the ladder is measuring rather than something to hide from it.
2651 let restored_baseline = match obj.eval_cost(rho) {
2652 Ok(cost) => Some(cost),
2653 Err(err) => {
2654 log::warn!(
2655 "[CERTIFICATE] {context}: failed to restore the objective to the checkpoint \
2656 after saddle-escape probing: {err}"
2657 );
2658 None
2659 }
2660 };
2661 if let Some((cost, _, sign, alpha)) = best {
2662 let descent = expand_confirmed_descent(
2663 obj,
2664 rho,
2665 &direction,
2666 LadderConfirmedStep {
2667 sign,
2668 alpha,
2669 cost,
2670 strict_floor,
2671 },
2672 bounds,
2673 context,
2674 );
2675 log::info!(
2676 "[CERTIFICATE] {context}: interior strict saddle (λ_min={lambda_min:.3e} < 0, |Pg| \
2677 within band); minting a negative-curvature escape reseed (objective {:.6e} → \
2678 {:.6e}) for one retry (#2357)",
2679 baseline_cost,
2680 descent.cost,
2681 );
2682 return SaddleAdjudication::Descended(descent.point);
2683 }
2684 let smallest_step = escape_step_scales
2685 .last()
2686 .copied()
2687 .unwrap_or(f64::INFINITY);
2688 let predicted_at_smallest = 0.5 * lambda_min.abs() * smallest_step * smallest_step;
2689 // `probed == 0` is not a contradiction: nothing was evaluated, so nothing
2690 // was falsified. The three ways that happens are counted separately for
2691 // exactly this reason (#2665).
2692 if probed == 0 {
2693 return SaddleAdjudication::Declined(format!(
2694 "a certified strict saddle (lambda_min={lambda_min:.6e}, neg_margin={neg_margin:.6e}) \
2695 produced no EVALUABLE trial at all: clamped_back_onto_rho={clamped_onto_rho}, \
2696 eval_failed={eval_failed}, non_finite={nonfinite} over {} step(s)",
2697 escape_step_scales.len(),
2698 ));
2699 }
2700 // The ladder's verdict on the very number in dispute. `v` has unit norm in
2701 // the outer coordinates, so `v'Hv` and `d²/dα² V(ρ̂+αv)|₀` are the same
2702 // quantity computed two ways -- one analytically, one from the criterion's
2703 // own values -- and their difference is exactly zero in exact arithmetic.
2704 let criterion_curvature = restored_baseline.and_then(|baseline| {
2705 let mut by_step: std::collections::BTreeMap<u64, (Option<f64>, Option<f64>)> =
2706 std::collections::BTreeMap::new();
2707 for &(sign, alpha, cost) in &evaluations {
2708 let slot = by_step.entry(alpha.to_bits()).or_insert((None, None));
2709 if sign > 0.0 {
2710 slot.0 = Some(cost);
2711 } else {
2712 slot.1 = Some(cost);
2713 }
2714 }
2715 let probes: Vec<gam_linalg::curvature_resolution::SymmetricProbe> = by_step
2716 .into_iter()
2717 .filter_map(|(bits, (forward, backward))| {
2718 Some(gam_linalg::curvature_resolution::SymmetricProbe::new(
2719 f64::from_bits(bits),
2720 forward?,
2721 backward?,
2722 ))
2723 })
2724 .collect();
2725 gam_linalg::curvature_resolution::measure_symmetric_ladder(baseline, &probes).map(
2726 |ladder| CriterionCurvatureDisagreement {
2727 direction: direction.clone(),
2728 analytic_curvature: lambda_min,
2729 ladder,
2730 },
2731 )
2732 });
2733 // ── The escape declined on a BORROWED tolerance; the ladder measured the
2734 // ── criterion's own, and it re-tests the descent against that ─────────
2735 //
2736 // The strict-decrease floor above is `objective_resolution` — the
2737 // optimizer's DECLARED tolerance, `rel_cost_tolerance * |V|`. #2690's
2738 // standing rule is that `eps_f` is a property of a fixture, measured on the
2739 // fixture in hand, and never borrowed; a declared tolerance is the most
2740 // borrowed quantity available. Measured on `papuan_oce4_matern_k24` the two
2741 // are `2.178e-4` (declared) and `2.549e-11` (the ladder's) — SEVEN orders
2742 // apart — and the descent the escape declined there was `~2.3e-7`, four
2743 // orders above the criterion's actual noise. The claim was falsifiable all
2744 // along; the floor could not see it.
2745 //
2746 // Three conditions, all measured on this fixture at this point, and the
2747 // conjunction is what keeps this from being a looser escape:
2748 //
2749 // 1. the criterion's own curvature along `v` is NEGATIVE — the ladder
2750 // agrees with the matrix about the sign, rather than the matrix being
2751 // believed;
2752 // 2. that curvature is RESOLVED by the ladder's own Law 1 floor
2753 // `(2/sqrt(3))*sqrt(eps_f*|M4|)` — the finest curvature ANY central
2754 // second difference of this criterion could reach — so the descent is
2755 // a property of the criterion and not of the ladder's noise;
2756 // 3. some already-evaluated trial is lower than the baseline by more than
2757 // the ARITHMETIC's own `roundoff_floor`, which is all that is left to
2758 // check once (1) and (2) hold: a stationary point with a resolved
2759 // negative curvature descends, and the reseed only has to leave the
2760 // ridge for ARC to refine from where it lands.
2761 //
2762 // ⚠ (3) is deliberately NOT "some trial beat the measured `2*eps_f`".
2763 // A criterion with evaluation error of amplitude `A` produces individual
2764 // trials that dip to `-A` by luck, while the ladder's `eps_f` estimate is
2765 // that amplitude's RMS over the rungs — so a single-trial test against
2766 // `2*eps_f` accepts noise about as often as signal. The
2767 // `unresolvable-well #2612` fixture demonstrates it: planted with an
2768 // evaluation error of `5e-8` and a well only `3.1e-8` deep, one rung dips
2769 // to `-7.6e-8` and beats `2*eps_f ~ 3.5e-8` while carrying no descent at
2770 // all. Condition (2) is the one that decides it correctly there —
2771 // `|c| = 1e-4` against a Law 1 floor of `1.07e-4`, unresolved — because a
2772 // curvature is a property of the WHOLE ladder and averages the noise the
2773 // way a single trial cannot.
2774 //
2775 // Where any of the three fails, nothing changes: on `geo_disease_matern`
2776 // the measured curvature is POSITIVE (`+8.15e-5` against a claimed
2777 // `-6.4e-6`), so (1) fails and the measurement flows on to the smoothing
2778 // correction as a `||dH||_2` instead.
2779 let measured_escape = criterion_curvature.as_ref().and_then(|measured| {
2780 let curvature = measured.ladder.curvature;
2781 if !(curvature < 0.0) {
2782 return None;
2783 }
2784 if !measured
2785 .ladder
2786 .finite_difference_resolution()
2787 .is_ok_and(|resolution| resolution.resolves(curvature))
2788 {
2789 return None;
2790 }
2791 let measured_floor = roundoff_floor;
2792 if !(measured_floor.is_finite() && measured_floor > 0.0) {
2793 return None;
2794 }
2795 let baseline = restored_baseline?;
2796 let mut descent: Option<(f64, f64, f64)> = None;
2797 for &(sign, alpha, cost) in &evaluations {
2798 if cost < baseline - measured_floor
2799 && descent.is_none_or(|(_, _, best): (f64, f64, f64)| cost < best)
2800 {
2801 descent = Some((sign, alpha, cost));
2802 }
2803 }
2804 descent.map(|(sign, alpha, cost)| (sign, alpha, cost, measured_floor, baseline))
2805 });
2806 if let Some((sign, alpha, cost, measured_floor, baseline)) = measured_escape {
2807 let descent = expand_confirmed_descent(
2808 obj,
2809 rho,
2810 &direction,
2811 LadderConfirmedStep {
2812 sign,
2813 alpha,
2814 cost,
2815 strict_floor: measured_floor,
2816 },
2817 bounds,
2818 context,
2819 );
2820 let measured = criterion_curvature
2821 .as_ref()
2822 .expect("a measured escape implies a determined ladder");
2823 log::info!(
2824 "[CERTIFICATE] {context}: the criterion CONFIRMS the reported negative curvature once its OWN evaluation error is measured rather than declared. c_criterion={:.6e} (resolved against this fixture's Law 1 floor from the measured eps_f={:.6e} and M4={:.6e}) against the analytic lambda_min={lambda_min:.6e}; a trial at step {alpha:.3e} lowered the objective {baseline:.9e} -> {cost:.9e}, a decrease of {:.6e} against the MEASURED floor {measured_floor:.6e} where the DECLARED one was {objective_resolution:.6e}. Minting the negative-curvature escape reseed the declared tolerance was hiding (#2748, #2690, #2357).",
2825 measured.ladder.curvature,
2826 measured.ladder.evaluation_error,
2827 measured.ladder.fourth_derivative,
2828 baseline - cost,
2829 );
2830 return SaddleAdjudication::Descended(descent.point);
2831 }
2832 match criterion_curvature.as_ref() {
2833 Some(measured) => log::warn!(
2834 "[CERTIFICATE] {context}: the criterion's OWN curvature along the disputed \
2835 eigenvector, from the symmetric ladder this adjudication already runs: \
2836 c_criterion={:.6e} +/- {:.6e} against the analytic lambda_min={lambda_min:.6e}, \
2837 over {} rung(s); measured eps_f={:.6e} and M4={:.6e} give this fixture's Law 1 \
2838 floor {} -- the finest curvature ANY central second difference of this criterion \
2839 could resolve. Measured ||dH||_2 from the disagreement = {:.6e} (#2748, #2690).",
2840 measured.ladder.curvature,
2841 measured.ladder.curvature_uncertainty,
2842 measured.ladder.rungs,
2843 measured.ladder.evaluation_error,
2844 measured.ladder.fourth_derivative,
2845 measured
2846 .ladder
2847 .finite_difference_resolution()
2848 .map(|resolution| format!("{resolution}"))
2849 .unwrap_or_else(|error| format!("unavailable ({error})")),
2850 measured.hessian_error_2norm(),
2851 ),
2852 None => log::info!(
2853 "[CERTIFICATE] {context}: the symmetric ladder did not determine a fit (baseline \
2854 restored: {}), so no criterion curvature and no measured ||dH||_2 are available \
2855 here. An absent measurement stays absent (#2748).",
2856 restored_baseline.is_some(),
2857 ),
2858 }
2859 log::warn!(
2860 "[CERTIFICATE] {context}: the criterion CONTRADICTS the reported negative curvature. \
2861 lambda_min={lambda_min:.6e} on the judged sub-block, and {probed} feasible trial(s) \
2862 along its eigenvector — both signs, steps {:.3e} down to {smallest_step:.3e} — lowered \
2863 the objective nowhere. The ladder ends where the claim's own predicted decrease \
2864 (½|λ_min|α² = {predicted_at_smallest:.3e}) reaches the criterion's resolution \
2865 ({objective_resolution:.3e}), so that is the WHOLE range in which the claim could have \
2866 been falsified. best cost seen={best_seen_cost:.9e} against baseline={:.9e} (needed \
2867 < {:.9e}); clamped_back_onto_rho={clamped_onto_rho}, eval_failed={eval_failed}, \
2868 non_finite={nonfinite}. The negative direction is a property of this matrix, not of \
2869 this point (#2612).",
2870 escape_step_scales.first().copied().unwrap_or(1.0),
2871 baseline_cost,
2872 baseline_cost - strict_floor,
2873 );
2874 SaddleAdjudication::Contradicted {
2875 probed,
2876 smallest_step,
2877 predicted_at_smallest,
2878 objective_resolution,
2879 best_seen_cost,
2880 criterion_curvature,
2881 }
2882}
2883
2884/// The falsifiability ladder's own confirmed step, as handed to the expansion.
2885///
2886/// These four travel together — they are one measurement (a signed step along
2887/// the negative-curvature direction, the objective there, and the floor that
2888/// decision was strict against) — so they are one argument. Splitting them into
2889/// four positional `f64`s is what pushed `expand_confirmed_descent` over the
2890/// argument count and produced an `#[allow(clippy::too_many_arguments)]`, which
2891/// this repo bans outright: the lint is naming a real thing, and four adjacent
2892/// same-typed scalars at a call site are a transposition waiting to happen.
2893#[derive(Clone, Copy, Debug)]
2894struct LadderConfirmedStep {
2895 /// Which way along `direction` the ladder confirmed the descent.
2896 sign: f64,
2897 /// The step the ladder confirmed it at.
2898 alpha: f64,
2899 /// The objective there, in the ladder's instrument state.
2900 cost: f64,
2901 /// The decrease the acceptance was strict against — the ladder's measured
2902 /// evaluation floor at the confirming site, never a declared tolerance.
2903 strict_floor: f64,
2904}
2905
2906/// The escape point a CONFIRMED negative-curvature descent actually supports
2907/// (#2612), after the step has been extended past the falsifiability ladder.
2908#[derive(Clone, Debug)]
2909struct ConfirmedDescent {
2910 /// Reseed point, already projected into the box.
2911 point: Array1<f64>,
2912 /// Step along `sign · direction` the point sits at.
2913 alpha: f64,
2914 /// Objective there, as measured in the expansion's own instrument state.
2915 cost: f64,
2916 /// Doublings evaluated. `0` means the ladder's own step stood — either
2917 /// nothing beyond it improved, or it was already the box intersection.
2918 expansions: usize,
2919 /// Whether the accepted step IS the box intersection along the ray, i.e.
2920 /// the descent ran to the constraint face rather than stopping inside it.
2921 on_box_face: bool,
2922}
2923
2924/// The largest `α ≥ 0` for which `ρ + α·d` stays inside the box, exactly.
2925///
2926/// `f64::INFINITY` when no coordinate the ray moves is bounded in the direction
2927/// it moves. Coordinates with `d_i == 0` never bind — the ray does not move
2928/// them — which is what lets the railed block (where `direction` is exactly
2929/// zero by construction) sit at its bounds without capping the step at `0`.
2930fn max_feasible_step_along(
2931 rho: &Array1<f64>,
2932 ray: &Array1<f64>,
2933 bounds: &(Array1<f64>, Array1<f64>),
2934) -> f64 {
2935 let (lower, upper) = bounds;
2936 let mut alpha = f64::INFINITY;
2937 for i in 0..rho.len() {
2938 let step = ray[i];
2939 if step > 0.0 {
2940 if let Some(&limit) = upper.get(i) {
2941 alpha = alpha.min((limit - rho[i]) / step);
2942 }
2943 } else if step < 0.0 {
2944 if let Some(&limit) = lower.get(i) {
2945 alpha = alpha.min((limit - rho[i]) / step);
2946 }
2947 }
2948 }
2949 alpha.max(0.0)
2950}
2951
2952/// Extend a confirmed negative-curvature descent to the step the criterion
2953/// actually supports, instead of the step the falsifiability ladder happened to
2954/// stop at (#2612).
2955///
2956/// # The two questions one ladder was answering
2957///
2958/// [`adjudicate_negative_curvature`] builds a single step ladder `α = 1, ½, ¼,
2959/// …` down to `α_min = sqrt(2·objective_resolution/|λ_min|)` and uses it twice.
2960/// As a falsifier it is exactly right: the smallest step at which the claim
2961/// `½|λ_min|α²` still predicts something the criterion can represent is the end
2962/// of the range in which the claim could be refuted, so probing DOWN from one
2963/// e-fold in log-λ is the whole falsifiable range and finding no descent in it
2964/// contradicts the matrix.
2965///
2966/// As a step rule it is wrong, and wrong in a direction the mathematics names.
2967/// Along a direction of negative curvature the quadratic model
2968///
2969/// ```text
2970/// V(ρ + αv) − V(ρ) ≈ α(g·v) + ½λ_min α², λ_min < 0
2971/// ```
2972///
2973/// decreases WITHOUT BOUND in `α` once the sign is chosen so the linear term is
2974/// non-positive. A model with no interior minimiser cannot supply a step length;
2975/// the step has to come from the objective itself and from the feasible box —
2976/// which is the standard treatment of a negative-curvature direction and is
2977/// exactly what a trust region does when its solution lands on the boundary.
2978/// Capping the reseed at the falsifier's largest rung silently asserts the
2979/// opposite: that one e-fold is as far as any such descent ever runs.
2980///
2981/// # What it cost, measured
2982///
2983/// On the `#2612` banded quasi-separated fixture the escape direction is `−e₁`
2984/// to six digits and the criterion falls monotonically along it all the way to
2985/// the box wall:
2986///
2987/// ```text
2988/// baseline 1.786314898942e1
2989/// ladder α=1 1.786314894043e1
2990/// ladder α=½ 1.786314883184e1 <- the ladder's pick, decrease 1.6e-7
2991/// α=1 1.786314862766e1
2992/// α=2 1.786314814710e1
2993/// α=4 1.786314708132e1
2994/// α=8 1.786314488769e1 <- box intersection, decrease 4.1e-6
2995/// ```
2996///
2997/// so the wall step is worth **26×** the ladder's, and the BFGS resume seeded at
2998/// the ladder's point makes no progress at all (reseed and next refused point
2999/// bit-identical), leaving the escape as the only thing moving ρ — one e-fold
3000/// per escape, against `OUTER_SADDLE_ESCAPE_BUDGET = 3`, on a ridge six e-folds
3001/// long. The fit refused.
3002///
3003/// # The rule, and why it needs no constant
3004///
3005/// Double the confirmed step while the criterion strictly improves, clamped to
3006/// the exact box intersection `max_feasible_step_along`, and keep the best point
3007/// seen. Termination is structural: the box intersection is finite whenever the
3008/// ray moves any bounded coordinate, doubling reaches it in `⌈log₂(α_box/α)⌉`
3009/// steps, and any non-improving trial stops the sweep immediately. The accepted
3010/// point is always the lowest measured, so it is never worse than the ladder's.
3011///
3012/// # One evaluation is spent making the comparison honest
3013///
3014/// The incumbent's cost came from the falsifiability ladder, which ran before
3015/// the symmetric extension and before the checkpoint restore, so it was measured
3016/// in a different profiled-inner state. Measured on the same fixture, the SAME
3017/// point (`sign = −1, α = 1`) evaluated in the ladder and again afterwards
3018/// differs by `3.1e-7` — larger than the descent being adjudicated — because the
3019/// profiled criterion carries warm-start hysteresis well above the `ε_f` the
3020/// symmetric ladder measures on itself. Re-evaluating the incumbent here puts
3021/// the whole comparison chain in one instrument state, for the same reason
3022/// `restored_baseline` is re-measured rather than reused.
3023fn expand_confirmed_descent(
3024 obj: &mut dyn OuterObjective,
3025 rho: &Array1<f64>,
3026 direction: &Array1<f64>,
3027 seed: LadderConfirmedStep,
3028 bounds: &(Array1<f64>, Array1<f64>),
3029 context: &str,
3030) -> ConfirmedDescent {
3031 let LadderConfirmedStep {
3032 sign,
3033 alpha,
3034 cost,
3035 strict_floor,
3036 } = seed;
3037 /// Runaway bound on the doubling sweep. Not a modelling choice — the sweep's
3038 /// END is the box intersection — but a bound on what a pathologically small
3039 /// confirmed step could ask for. Binding it is logged rather than silently
3040 /// truncating the range the escape claims to have searched.
3041 const MAX_EXPANSIONS: usize = 64;
3042
3043 let n = rho.len();
3044 let ray = direction.mapv(|value| sign * value);
3045 let point_at = |alpha: f64| -> Array1<f64> {
3046 let mut point = rho.clone();
3047 for i in 0..n {
3048 point[i] += alpha * ray[i];
3049 }
3050 project_to_bounds(&point, Some(bounds))
3051 };
3052 let alpha_box = max_feasible_step_along(rho, &ray, bounds);
3053 let mut best = ConfirmedDescent {
3054 point: point_at(alpha),
3055 alpha,
3056 cost,
3057 expansions: 0,
3058 on_box_face: alpha_box.is_finite() && alpha >= alpha_box,
3059 };
3060 if !(alpha.is_finite() && alpha > 0.0) || !strict_floor.is_finite() || strict_floor < 0.0 {
3061 return best;
3062 }
3063 // Nothing to extend into: the confirmed step already reaches (or was clamped
3064 // at) the box intersection, so the ray has no room left. Returning before the
3065 // re-measure below keeps this case exactly as cheap as it was.
3066 if !(alpha < alpha_box) {
3067 return best;
3068 }
3069 // The incumbent, re-measured in THIS instrument state so every comparison
3070 // below is between values the same profiled inner solve produced.
3071 if let Ok(reference) = obj.eval_cost(&best.point)
3072 && reference.is_finite()
3073 {
3074 best.cost = reference;
3075 }
3076 let mut expansions = 0usize;
3077 let mut truncated = false;
3078 let mut current = alpha;
3079 while current < alpha_box {
3080 if expansions >= MAX_EXPANSIONS {
3081 truncated = true;
3082 break;
3083 }
3084 let next = (2.0 * current).min(alpha_box);
3085 if !(next > current) || !next.is_finite() {
3086 break;
3087 }
3088 expansions += 1;
3089 let trial = point_at(next);
3090 // A doubling that lands back on ρ (the whole ray clamped away) probes
3091 // nothing and cannot be a reseed.
3092 if outer_theta_bitwise_eq(&trial, rho) {
3093 break;
3094 }
3095 match obj.eval_cost(&trial) {
3096 Ok(trial_cost) if trial_cost.is_finite() && trial_cost < best.cost - strict_floor => {
3097 best = ConfirmedDescent {
3098 point: trial,
3099 alpha: next,
3100 cost: trial_cost,
3101 expansions,
3102 on_box_face: next >= alpha_box,
3103 };
3104 current = next;
3105 }
3106 _ => break,
3107 }
3108 }
3109 if best.expansions > 0 || truncated {
3110 log::info!(
3111 "[CERTIFICATE] {context}: the confirmed negative-curvature descent was extended past \
3112 the falsifiability ladder's step alpha={alpha:.6e} to alpha={:.6e} over {} \
3113 doubling(s) ({} evaluated), objective {:.9e} -> {:.9e}; box intersection along the \
3114 ray is alpha_box={alpha_box:.6e} and the accepted step {} it (#2612).{}",
3115 best.alpha,
3116 best.expansions,
3117 expansions,
3118 cost,
3119 best.cost,
3120 if best.on_box_face { "IS" } else { "is inside" },
3121 if truncated {
3122 format!(" -- TRUNCATED at the {MAX_EXPANSIONS}-doubling budget, so the ray was not searched to the box")
3123 } else {
3124 String::new()
3125 },
3126 );
3127 }
3128 // Same contract as the adjudication's own checkpoint restore: leave the
3129 // profiled inner state at ρ, not at the last probe.
3130 if let Err(err) = obj.eval_cost(rho) {
3131 log::warn!(
3132 "[CERTIFICATE] {context}: failed to restore the objective to the checkpoint after \
3133 extending the negative-curvature descent: {err}"
3134 );
3135 }
3136 best
3137}
3138
3139/// Extend an adjudication's symmetric probe ladder until it can determine a
3140/// curvature, not merely fail to falsify one (#2748).
3141///
3142/// # Why an extension is needed at all
3143///
3144/// The falsifiability ladder stops at
3145/// `α_min = sqrt(2·objective_resolution/|λ_min|)`, which is the right place to
3146/// stop *asking whether the claim descends*: below it the claim predicts less
3147/// than the criterion can represent. But for a small claim that bound is `≥ 1`,
3148/// so the ladder is a SINGLE rung — and one rung cannot fit the two parameters
3149/// of `N(α) = c·α² + (M₄/12)·α⁴`. The falsification and the measurement need
3150/// different ranges, and only one of them was being run.
3151///
3152/// # Where the extension ends, and why that is derived
3153///
3154/// The claim's own predicted numerator at step `α` is `|λ_min|·α²`. It stops
3155/// being distinguishable from the objective's own arithmetic when it reaches
3156/// `roundoff_floor = 16ε·max(1,|V|)` — the same floor the escape's strict
3157/// decrease test uses — so
3158///
3159/// ```text
3160/// α_end = sqrt(roundoff_floor / |λ_min|)
3161/// ```
3162///
3163/// is where the ladder passes out of the signal and into the plateau. The
3164/// plateau is not waste: it is exactly where `ε_f` is read off, per this
3165/// module's `curvature_resolution` header, so the ladder runs TWO halvings past
3166/// `α_end` — the smallest number of plateau rungs that makes the residual a
3167/// scatter rather than a single point.
3168///
3169/// # Cost, and the refusal to hide it
3170///
3171/// Two objective evaluations per rung, on a path that has already failed to
3172/// find a descent and is about to hand a curvature verdict to a gate that can
3173/// abort the whole fit. `MAX_LADDER_RUNGS` bounds it for a pathological
3174/// `λ_min`; when it binds, that is logged rather than silently truncating the
3175/// range the measurement claims to cover.
3176struct LadderExtension<'a> {
3177 /// The certified point the ladder is centred on.
3178 rho: &'a Array1<f64>,
3179 /// Unit direction whose curvature is in dispute.
3180 direction: &'a Array1<f64>,
3181 /// The analytic claim, `v'Hv`, whose predicted numerator sets where the
3182 /// ladder passes out of signal and into plateau.
3183 lambda_min: f64,
3184 /// `16 eps * max(1, |V|)`, the objective's own arithmetic floor.
3185 roundoff_floor: f64,
3186 /// Smallest step the falsifiability ladder already reached; the extension
3187 /// starts one halving below it.
3188 smallest_escape_step: f64,
3189 /// The rho box; a clamped trial is not `rho +- alpha v` and cannot enter a
3190 /// symmetric average.
3191 bounds: &'a (Array1<f64>, Array1<f64>),
3192 /// Diagnostic label of the calling certificate.
3193 context: &'a str,
3194}
3195
3196impl LadderExtension<'_> {
3197 fn run(
3198 &self,
3199 obj: &mut dyn OuterObjective,
3200 evaluations: &mut Vec<(f64, f64, f64)>,
3201 ) {
3202 let Self {
3203 rho,
3204 direction,
3205 lambda_min,
3206 roundoff_floor,
3207 smallest_escape_step,
3208 bounds,
3209 context,
3210 } = *self;
3211 /// Rung budget. Not a statistical choice — the ladder's END is derived
3212 /// above — but a bound on the evaluations a pathological `λ_min` could
3213 /// ask for. Binding it is reported rather than silently truncating.
3214 const MAX_LADDER_RUNGS: usize = 32;
3215
3216 let n = rho.len();
3217 if !lambda_min.is_finite() || lambda_min == 0.0 || !roundoff_floor.is_finite() {
3218 return;
3219 }
3220 let alpha_end = (roundoff_floor / lambda_min.abs()).sqrt();
3221 let mut alpha = smallest_escape_step;
3222 if !alpha.is_finite() || alpha <= 0.0 {
3223 return;
3224 }
3225 let mut plateau_rungs = 0usize;
3226 let mut added = 0usize;
3227 let mut truncated = false;
3228 loop {
3229 alpha *= 0.5;
3230 // `f64::EPSILON` is where halving stops changing `ρ + αv` at all — the
3231 // same arithmetic limit the escape ladder uses.
3232 if !(alpha > f64::EPSILON) {
3233 break;
3234 }
3235 if alpha <= alpha_end {
3236 plateau_rungs += 1;
3237 }
3238 if plateau_rungs > 2 {
3239 break;
3240 }
3241 if added >= MAX_LADDER_RUNGS {
3242 truncated = true;
3243 break;
3244 }
3245 added += 1;
3246 for sign in [1.0_f64, -1.0_f64] {
3247 let mut trial = rho.clone();
3248 for i in 0..n {
3249 trial[i] += sign * alpha * direction[i];
3250 }
3251 let projected = project_to_bounds(&trial, Some(bounds));
3252 // Only an UNCLAMPED pair is `ρ ± αv`; a clamped one is a different
3253 // direction and would corrupt the symmetric average rather than
3254 // add to it.
3255 if !outer_theta_bitwise_eq(&projected, &trial) {
3256 continue;
3257 }
3258 if let Ok(cost) = obj.eval_cost(&projected)
3259 && cost.is_finite()
3260 {
3261 evaluations.push((sign, alpha, cost));
3262 }
3263 }
3264 }
3265 log::debug!(
3266 "[CERTIFICATE] {context}: extended the symmetric ladder by {added} rung(s) \
3267 ({} evaluations) down to alpha={alpha:.6e}, past the derived plateau entry \
3268 alpha_end=sqrt(roundoff_floor/|lambda_min|)={alpha_end:.6e}{}",
3269 2 * added,
3270 if truncated {
3271 format!(" -- TRUNCATED at the {MAX_LADDER_RUNGS}-rung budget, so the plateau may not have been reached")
3272 } else {
3273 String::new()
3274 },
3275 );
3276 }
3277}
3278
3279/// Second-order predicted objective decrease of a safeguarded Newton step at a
3280/// flat-valley cost-stall exit (#2253/#2249/#2015).
3281///
3282/// On a flat-valley cost-stall exit the outer criterion has provably stopped
3283/// improving (the cost-stall window fired), yet the re-measured projected
3284/// gradient can sit modestly above the score-relative flat band on a
3285/// weakly-identified small-n fit (measured: |Pg| ≈ 0.072 vs a score-relative
3286/// band ≈ 0.053 on an n=84/p=64 K=1 circle). Whether that residual is genuine
3287/// available descent is a SECOND-ORDER question. The improvement a safeguarded
3288/// Newton step buys is the Newton decrement over two:
3289///
3290/// ```text
3291/// Δpred = ½ · gᵀ H⁻¹ g,
3292/// ```
3293///
3294/// the textbook Newton stopping quantity (Boyd–Vandenberghe §9.5). When `Δpred`
3295/// is below the outer objective tolerance, no step can reduce the criterion by
3296/// more than that tolerance and the point is stationary at the resolution the
3297/// criterion can be optimized — the mathematically correct "no further descent
3298/// possible" criterion.
3299///
3300/// This is curvature-scaled, not a constant: because `H⁻¹` weights each gradient
3301/// component by the inverse eigenvalue, a residual aligned with a NEAR-FLAT
3302/// Hessian eigenvector (a linear ramp that DOES carry real descent) inflates
3303/// `gᵀ H⁻¹ g` toward the roundoff-regularized `|g_flat|² / shift` and is
3304/// REJECTED; only a residual that is small along the well-curved directions and
3305/// nearly orthogonal to the flat ones certifies. An indefinite Hessian never
3306/// reaches here — the certificate's curvature gate (`certificate_hessian_is_psd`)
3307/// rejects a genuinely indefinite point independently, and this factorization
3308/// returns `None` on a non-PSD shifted factor so the caller falls back to the
3309/// gradient-only bound.
3310///
3311/// `hessian` and `grad` are the analytic outer Hessian and the KKT-PROJECTED
3312/// gradient at the certified point. The shift `√ε · max|H_jj|` matches
3313/// [`certificate_hessian_is_psd`] so the definiteness verdict and this decrement
3314/// agree on the same regularized operator. Returns `None` when the shapes are
3315/// malformed, an entry is non-finite, the shifted factor is not PD, or the
3316/// resulting quadratic form is negative (which a PD factor rules out; retained
3317/// as a roundoff guard).
3318pub(crate) fn newton_predicted_decrease(hessian: &Array2<f64>, grad: &Array1<f64>) -> Option<f64> {
3319 let n = hessian.nrows();
3320 if n == 0 || hessian.ncols() != n || grad.len() != n {
3321 return None;
3322 }
3323 if hessian.iter().any(|v| !v.is_finite()) || grad.iter().any(|v| !v.is_finite()) {
3324 return None;
3325 }
3326 let max_diag = (0..n).fold(0.0_f64, |acc, j| acc.max(hessian[[j, j]].abs()));
3327 let shift = f64::EPSILON.sqrt() * max_diag.max(1.0);
3328 // Lower Cholesky factor L of H + shift·I (same regularization the PSD probe
3329 // uses), computed in place.
3330 let mut l = hessian.clone();
3331 for j in 0..n {
3332 l[[j, j]] += shift;
3333 }
3334 for j in 0..n {
3335 for k in 0..j {
3336 let l_jk = l[[j, k]];
3337 for i in j..n {
3338 l[[i, j]] -= l[[i, k]] * l_jk;
3339 }
3340 }
3341 let pivot = l[[j, j]];
3342 if !(pivot > 0.0) || !pivot.is_finite() {
3343 return None;
3344 }
3345 let inv_sqrt = 1.0 / pivot.sqrt();
3346 for i in j..n {
3347 l[[i, j]] *= inv_sqrt;
3348 }
3349 }
3350 // Solve (L Lᵀ) d = g for d = H_s⁻¹ g: forward-substitute L y = g, then
3351 // back-substitute Lᵀ d = y.
3352 let mut y = grad.clone();
3353 for j in 0..n {
3354 let mut s = y[j];
3355 for k in 0..j {
3356 s -= l[[j, k]] * y[k];
3357 }
3358 y[j] = s / l[[j, j]];
3359 }
3360 let mut d = y;
3361 for j in (0..n).rev() {
3362 let mut s = d[j];
3363 for k in (j + 1)..n {
3364 s -= l[[k, j]] * d[k];
3365 }
3366 d[j] = s / l[[j, j]];
3367 }
3368 let quad = grad.dot(&d); // gᵀ H_s⁻¹ g ≥ 0 for a PD factor.
3369 if !quad.is_finite() || quad < 0.0 {
3370 return None;
3371 }
3372 Some(0.5 * quad)
3373}
3374
3375/// Is outer coordinate `k` pinned within [`coordinate_rail_margin`] of either
3376/// of its own box bounds?
3377///
3378/// Factored out so the λ-block REPORT and the θ-wide certificate FACE below
3379/// cannot drift apart about what "railed" means for the same coordinate: they
3380/// differ only in which coordinates they scan, never in the test.
3381///
3382/// The margin is the shared width-capped one, so this is the exact-bound test on
3383/// [`rail_relaxed_bounds`]' relaxed endpoints. It used to be a flat
3384/// [`CERTIFICATE_RAIL_MARGIN`] while the residual projector capped the same
3385/// constant at a quarter-width, and a box narrower than `2 ×
3386/// CERTIFICATE_RAIL_MARGIN` — the raw-κ chart window on any standardised
3387/// feature set — was then covered end to end by its own two margin bands: every
3388/// κ read railed, flat κ = 0 included, and the certificate's reduced Hessian
3389/// lost every row it was supposed to judge (#2462).
3390///
3391/// Comparing against the relaxed endpoints rather than `|θ_k − bound|` also
3392/// keeps an infeasible coordinate railed. Under the absolute-value form a point
3393/// *outside* the box by more than the margin reported interior, which is the one
3394/// reading that can never be right.
3395fn outer_coordinate_is_railed(theta: &Array1<f64>, k: usize, config: &OuterConfig) -> bool {
3396 RailTest::evaluate(theta, k, config).is_railed()
3397}
3398
3399/// One coordinate's rail test: the verdict together with the interval and the
3400/// margin it was decided against (#2465).
3401///
3402/// The predicate above computed `(lo, hi)`, derived a margin from them, compared
3403/// against the relaxed endpoints, and returned a bare `bool` — so `railed=[3]`
3404/// reached the reader with everything that produced it already destroyed, and
3405/// recovering the interval on #2462 took a thirteen-point seeding sweep.
3406///
3407/// #2462 made carrying it necessary rather than merely useful: the margin is now
3408/// [`coordinate_rail_margin`], **width-capped per coordinate**, so two
3409/// coordinates in the same fit can be judged railed against different margins.
3410/// `railed=[1, 3]` is no longer even one statement, and the flag alone cannot
3411/// say which band either coordinate met.
3412#[derive(Debug, Clone, Copy)]
3413pub(crate) struct RailTest {
3414 /// Index into the θ vector.
3415 pub(crate) index: usize,
3416 /// The coordinate's value at the judged point.
3417 pub(crate) theta: f64,
3418 /// The interval it was tested against. `None` means the configured box does
3419 /// not cover this coordinate at all — which is itself the reason the verdict
3420 /// is `false`, a distinction the bare bool erased.
3421 pub(crate) box_bounds: Option<(f64, f64)>,
3422 /// The width-capped margin in force for THIS coordinate, from
3423 /// [`coordinate_rail_margin`]. Zero when the box does not cover it.
3424 pub(crate) margin: f64,
3425}
3426
3427impl RailTest {
3428 fn evaluate(theta: &Array1<f64>, k: usize, config: &OuterConfig) -> Self {
3429 let box_bounds = match config.model_domain_bounds.as_ref() {
3430 Some((lo, hi)) if k < lo.len() && k < hi.len() => Some((lo[k], hi[k])),
3431 Some(_) => None,
3432 None => Some((-config.rho_bound, config.rho_bound)),
3433 };
3434 Self {
3435 // Indexed, not `get`-ed: every caller scans an index range derived
3436 // from `theta.len()`, so an out-of-range k is a caller bug and the
3437 // predicate this replaced panicked on it. Softening that to a silent
3438 // `false` would turn a bug into a coordinate quietly reported
3439 // un-railed.
3440 theta: theta[k],
3441 index: k,
3442 margin: box_bounds.map_or(0.0, |(lo, hi)| coordinate_rail_margin(lo, hi)),
3443 box_bounds,
3444 }
3445 }
3446
3447 /// Pinned at or past either relaxed endpoint. This is the ONLY definition of
3448 /// railed in this file; both the λ-block report and the θ-wide certificate
3449 /// face route through it, and the relaxed-endpoint form (rather than
3450 /// `|θ_k − bound|`) is what keeps an infeasible coordinate railed (#2462).
3451 pub(crate) fn is_railed(self) -> bool {
3452 match self.box_bounds {
3453 Some((lo, hi)) => self.theta <= lo + self.margin || self.theta >= hi - self.margin,
3454 None => false,
3455 }
3456 }
3457}
3458
3459impl std::fmt::Display for RailTest {
3460 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3461 match self.box_bounds {
3462 Some((lo, hi)) => write!(
3463 f,
3464 "#{} theta={:.6e} box=[{:.6e}, {:.6e}] margin={:.3e} railed_at=(<={:.6e} or >={:.6e})",
3465 self.index,
3466 self.theta,
3467 lo,
3468 hi,
3469 self.margin,
3470 lo + self.margin,
3471 hi - self.margin,
3472 ),
3473 None => write!(
3474 f,
3475 "#{} theta={:.6e} box=NOT-COVERED-BY-CONFIGURED-BOUNDS",
3476 self.index, self.theta,
3477 ),
3478 }
3479 }
3480}
3481
3482/// The certificate-facing facts for `indices`: the interval and margin each
3483/// railed coordinate was judged against (#2530).
3484///
3485/// Built from [`RailTest`], which is the single definition of railed, so the
3486/// certificate reports what the predicate actually decided rather than a second
3487/// derivation of it. A coordinate the configured box does not cover contributes
3488/// nothing: it was not judged against an interval, so there is no interval to
3489/// report.
3490pub(crate) fn railed_coordinate_facts(
3491 theta: &Array1<f64>,
3492 indices: &[usize],
3493 config: &OuterConfig,
3494) -> Vec<RailedCoordinateFact> {
3495 indices
3496 .iter()
3497 .filter_map(|&k| {
3498 let test = RailTest::evaluate(theta, k, config);
3499 test.box_bounds.map(|(lower, upper)| RailedCoordinateFact {
3500 index: test.index,
3501 theta: test.theta,
3502 lower,
3503 upper,
3504 margin: test.margin,
3505 })
3506 })
3507 .collect()
3508}
3509
3510/// Render the rail tests for `indices`, so a refusal naming railed coordinates
3511/// also states the interval and margin each was judged against (#2465).
3512pub(crate) fn rail_test_summary(
3513 theta: &Array1<f64>,
3514 indices: &[usize],
3515 config: &OuterConfig,
3516) -> String {
3517 indices
3518 .iter()
3519 .map(|&k| RailTest::evaluate(theta, k, config).to_string())
3520 .collect::<Vec<_>>()
3521 .join(", ")
3522}
3523
3524/// Smoothing coordinates (leading ρ block) railed against the outer box.
3525///
3526/// This is the **report**. `OuterCriterionCertificate::lambdas_railed` indexes
3527/// *smoothing parameters*, and every consumer reads it that way — gam-report's
3528/// "λ railed" warning, the pyffi certificate surface, `is_clean`. It is
3529/// deliberately NOT the set the certificate reasons with; see
3530/// [`certificate_railed_coordinates`].
3531pub(crate) fn certificate_railed_lambdas(
3532 rho: &Array1<f64>,
3533 rho_dim: usize,
3534 config: &OuterConfig,
3535) -> Vec<usize> {
3536 (0..rho_dim.min(rho.len()))
3537 .filter(|&k| outer_coordinate_is_railed(rho, k, config))
3538 .collect()
3539}
3540
3541/// **Every** outer coordinate railed against its own box bound — the ρ block
3542/// *and* the trailing non-ρ blocks that a joint search carries in the same θ
3543/// vector under the same box: the spatial log-κ ψ coordinates and the
3544/// auxiliary coordinates.
3545///
3546/// This is the set every *decision* in [`certify_outer_optimality`] must use,
3547/// and using the λ-only report there instead was a real defect. The active-set
3548/// reasoning at a box-constrained optimum is a statement about coordinates, not
3549/// about what a coordinate happens to parameterize:
3550///
3551/// * the second-order condition only has to hold on the **feasible tangent
3552/// subspace**, so `certificate_hessian_is_psd_off_railed` deletes the railed
3553/// rows and columns. `layout.rho_dim()` is `n_params − psi_dim`, so a ψ
3554/// coordinate pinned on its data-derived κ window stayed *inside* that
3555/// sub-block and its saturated (and, near a window edge, routinely negative)
3556/// curvature row decided `hessian_psd = false` for the whole fit. That is
3557/// precisely the failure the block's own comment says must not happen — "a
3558/// rail-caused indefiniteness would disable the very certificate that exists
3559/// to certify a railed optimum (#2299)" — it was simply never extended past
3560/// the ρ block;
3561/// * the same omission hid the coordinate from the asymptote-rail mint
3562/// (#2348 Inc 1), from tail-snap, from `interior_curvature_floor_clearance`,
3563/// from the #2155 saddle escape, and from the #2392 wrong-rail pull-back and
3564/// active-set reduction. A κ search whose rail is a ψ coordinate could
3565/// therefore never be certified *by construction*, no matter how correct its
3566/// gradient was;
3567/// * and it made the refusal message actively misleading, because
3568/// `project_gradient_vector` DOES project every coordinate against its own
3569/// bound. So `|g|` could collapse to a small `|Pg|` while `railed=[]`
3570/// reported nothing responsible for the collapse — the exact disagreement
3571/// #979 measured from the other side and could not explain.
3572///
3573/// Relaxing nothing: a coordinate near a bound whose gradient still points
3574/// *into* the box keeps its feasible-descent component in `|Pg|`
3575/// ([`project_gradient_vector`] zeros only the outward half), so a genuinely
3576/// unconverged interior direction is still refused.
3577pub(crate) fn certificate_railed_coordinates(
3578 theta: &Array1<f64>,
3579 config: &OuterConfig,
3580) -> Vec<usize> {
3581 (0..theta.len())
3582 .filter(|&k| outer_coordinate_is_railed(theta, k, config))
3583 .collect()
3584}
3585
3586/// Which term of the stationarity bound's `max` chain actually set it.
3587///
3588/// `certify_outer_optimality` does not compute *a* bound; it takes the maximum of
3589/// up to five independently-derived quantities, and until #2458 nothing recorded
3590/// which one won. That is why a `bound=1.000e0` and a `bound=5.68e-6` came out of
3591/// the same message shape and telling them apart required reading this file --
3592/// the emitted number is a DERIVATIVE of a construction-site value, computed
3593/// later, so grepping the construction sites cannot match an observed bound.
3594///
3595/// The rungs are not interchangeable calibrations of one quantity. Only
3596/// [`Self::CurvatureResolvability`] is derived from what a gradient of that size
3597/// does to the criterion: `|Pg|·√(τ/Δpred)` simplifies to `√(2·h·τ)` -- the `|Pg|`
3598/// cancels -- i.e. the gradient magnitude below which the criterion cannot resolve
3599/// descent at all. The others are gradient-magnitude tests, blind to how curvature
3600/// maps a gradient to an objective change, which is exactly what this file's own
3601/// comment at the curvature block already says. Recording the rung is the
3602/// prerequisite for holding every route to the derived one: it makes "which
3603/// machinery did this route happen to have" an observable rather than an
3604/// inference.
3605#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3606pub(crate) enum StationarityBoundSource {
3607 /// The band the ENGINE declared before it saw the judged point:
3608 /// `outer_engine_gradient_band(config)` -- the arithmetic floor
3609 /// `max(tolerance, scale·√ε)`, widened by the DECLARED-scale rung
3610 /// `τ·(1 + |scale|)`. A function of the declared problem and nothing else.
3611 SolverBand,
3612 /// The CERTIFICATE's point-anchored widening `τ·(1 + |cost_at_point|)`,
3613 /// reported when it strictly exceeds [`Self::SolverBand`] (#2688). A
3614 /// different anchor and therefore a different quantity: the engine band is
3615 /// sealed at run start, this one moves with wherever the search stopped
3616 /// (#2613). Both were `solver-band` until #2688, so a `N× over bound` ratio
3617 /// could not be read as a ratio against a resolution standard.
3618 CertificateScoreRelative,
3619 /// The cost-stall guard's measured probe-noise floor `σ̂/Δ` (#2241). Diverges
3620 /// as the step collapses, which is the regime it fires in.
3621 ProbeNoiseFloor,
3622 /// `|Pg|·√(τ/Δpred)` = `√(2·h·τ)` (#2253/#2249/#2015/#2091) -- the only rung
3623 /// with a derivation from the criterion's own resolution.
3624 CurvatureResolvability,
3625 /// Twice the same-ρ spread between the run-recorded and certificate-time
3626 /// gradients (#2299): the measuring instrument's demonstrated noise.
3627 GradientReproducibility,
3628 /// `config.tolerance` judged against the EFS/fixed-point route's
3629 /// normalized residual `‖(θ⁺−θ)/scale‖_∞` -- not against a gradient norm
3630 /// at all. The route has no ladder: it exposes no analytic gradient, so
3631 /// none of the gradient-magnitude rungs above is even computable on it,
3632 /// and the certificate it mints (`OuterStationarityCertificate::FixedPoint`)
3633 /// already declares `bound: config.tolerance`. Recording it names the one
3634 /// thing the shared `bound=` field could not previously say -- that the
3635 /// number is a residual tolerance and the quantity beside it is a residual.
3636 ///
3637 /// Only the ONE refusal on that route which has actually formed the
3638 /// residual carries this. Its eight early exits formed none, and now say so.
3639 FixedPointResidual,
3640 /// The CALLER's `|Pg|` requirement (#2568), capping every rung above.
3641 ///
3642 /// The only member of this enum that is not the engine's own judgement, and
3643 /// the only one that ever TIGHTENS the bound. Reported when the caller's
3644 /// requirement is stricter than the ladder the engine would have applied, so
3645 /// a reader can tell "the engine refused this" from "the engine would have
3646 /// certified this and the caller would not" -- a distinction that matters
3647 /// because the second is not a defect in the fit.
3648 CallerRequirement,
3649}
3650
3651impl StationarityBoundSource {
3652 pub(crate) fn label(self) -> &'static str {
3653 match self {
3654 Self::SolverBand => "solver-band",
3655 Self::CertificateScoreRelative => "certificate-score-relative",
3656 Self::ProbeNoiseFloor => "probe-noise-floor",
3657 Self::CurvatureResolvability => "curvature-resolvability",
3658 Self::GradientReproducibility => "gradient-reproducibility",
3659 Self::FixedPointResidual => "fixed-point-residual",
3660 Self::CallerRequirement => "caller-requirement",
3661 }
3662 }
3663
3664 /// Whether this rung is the derived resolvability standard rather than a
3665 /// gradient-magnitude substitute adopted where that standard was unavailable.
3666 pub(crate) fn is_derived_standard(self) -> bool {
3667 // The standard is the decrement test, and #2458's whole point is that
3668 // WHICH derivative machinery produced the curvature must stop deciding
3669 // which standard a fit is held to. The answer is that every route
3670 // supplies exact curvature or none: there is no second, approximate
3671 // curvature rung, because a bound estimated by the code doing the
3672 // judging is not the same evidence and pretending otherwise is what
3673 // made the tiering invisible in the first place.
3674 matches!(self, Self::CurvatureResolvability)
3675 }
3676
3677 /// The neutral projection carried on the refusal, so a red states its own
3678 /// rung instead of leaving it to be inferred from the numbers.
3679 pub(crate) fn provenance(self) -> StationarityRung {
3680 StationarityRung {
3681 label: self.label(),
3682 derived_standard: self.is_derived_standard(),
3683 }
3684 }
3685}
3686
3687/// A stationarity bound bundled with the rung that produced it (#2458).
3688///
3689/// The bound and its provenance travelled separately before this: the bound was
3690/// a bare `f64` argument and the rung stayed a local in the certificate block,
3691/// so every refusal outside that block reported a number with no way to say
3692/// which standard it came from. Bundling them makes it impossible to pass one
3693/// without deciding the other.
3694///
3695/// The source is NOT optional, and there is no constructor for "a number with
3696/// no standard". An earlier revision carried an `unrecorded` escape that 22 of
3697/// the 30 refusal paths took; naming those 22 was the first repair, and it was
3698/// half right. Twenty of them do not have an unclassified bound — they have NO
3699/// bound. They refuse before any stationarity residual exists (a failed
3700/// terminal evaluation, a malformed gradient, a non-converged inner state), and
3701/// the configured constant they used to report was never weighed against the
3702/// point. Those now carry [`StationarityStandard::NoComparison`] and print no
3703/// bound at all; only a route that formed a residual constructs one of these.
3704#[derive(Debug, Clone, Copy)]
3705pub(crate) struct StationarityBound {
3706 value: f64,
3707 source: StationarityBoundSource,
3708}
3709
3710impl StationarityBound {
3711 /// A bound the rung ladder produced, carrying the rung it came from.
3712 pub(crate) fn from_ladder(value: f64, source: StationarityBoundSource) -> Self {
3713 Self { value, source }
3714 }
3715
3716 /// The EFS/fixed-point route's standard: `config.tolerance` against a
3717 /// normalized fixed-point residual
3718 /// ([`StationarityBoundSource::FixedPointResidual`]).
3719 ///
3720 /// This is the bound `certify_fixed_point_optimality` mints its
3721 /// `OuterStationarityCertificate::FixedPoint` against, so the refusal that
3722 /// weighs a formed residual reports the route's own declared standard
3723 /// rather than a gradient tolerance it never used.
3724 pub(crate) fn fixed_point_residual(config: &OuterConfig) -> Self {
3725 Self::from_ladder(
3726 config.tolerance,
3727 StationarityBoundSource::FixedPointResidual,
3728 )
3729 }
3730
3731 pub(crate) fn value(self) -> f64 {
3732 self.value
3733 }
3734
3735 /// Whether this value was derived by mapping the caller's residual through
3736 /// the caller's Hessian. Such a value cannot cross into a reduced face:
3737 /// the face must mint its own bound from its own Hessian and residual.
3738 pub(crate) fn requires_face_local_derivation(self) -> bool {
3739 self.source.is_derived_standard()
3740 }
3741
3742 pub(crate) fn rung(self) -> StationarityRung {
3743 self.source.provenance()
3744 }
3745}
3746
3747impl From<StationarityBound> for StationarityStandard {
3748 fn from(bound: StationarityBound) -> Self {
3749 Self::Measured {
3750 bound: bound.value(),
3751 rung: bound.rung(),
3752 }
3753 }
3754}
3755
3756fn outer_nonconvergence_error(
3757 context: &str,
3758 reason: &str,
3759 result: &OuterResult,
3760 projected_grad_norm: Option<f64>,
3761 stationarity_standard: impl Into<StationarityStandard>,
3762) -> EstimationError {
3763 // Solver provenance, appended to every outer non-convergence.
3764 //
3765 // The certificate answers "is this point stationary" — and when it is not,
3766 // the next question is always "then why did the search STOP here", which
3767 // the message did not answer. A binomial/logit P-spline (#1575/#1561)
3768 // refuses with `|Pg|=7.5e-1` against a `1.0e-2` bound, PSD Hessian, nothing
3769 // railed, all coordinates 18 e-folds inside the box, "after 7 outer
3770 // iteration(s)" — and the 7 is the whole mystery. `converged` distinguishes
3771 // a solver that CLAIMED convergence (a tolerance desync, and the only case
3772 // the #2273/#2374 resume will retry) from one that ran out of budget;
3773 // `operator_stop_reason` separates a trust-region reject floor from a
3774 // flat-valley cost stall from an iteration budget. All three are already on
3775 // the result and cost nothing to print.
3776 let reason = format!(
3777 "{reason}; solver provenance: origin={:?}, plan={}, claimed_converged={}{}{}{}",
3778 // #2465 at a fifth site. `termination=<no opt solver produced this
3779 // result>` says an `opt` solver did not decide this, and stops there;
3780 // `origin` says WHICH lane did. Both are already on the result.
3781 result.origin,
3782 result.plan_used,
3783 result.solver_claimed_convergence(),
3784 // #2547: `stop_reason` is the coarse projection. `termination` is
3785 // the test the solver actually applied plus the quantity it was
3786 // judged against, so a reader gets "the L-infinity window fired at
3787 // a threshold of 8.9e-1" rather than "it stopped". A stop that
3788 // made no stationarity claim at all (an iteration budget, a
3789 // collapsed trust region) says so, instead of leaving that to be
3790 // inferred from a bare gradient norm nothing compared it to.
3791 result
3792 .solver_termination
3793 .map(|t| {
3794 let evidence = match t.stationarity_evidence() {
3795 Some(e) => format!(
3796 " [measured={:.6e} vs threshold={:.6e}, {:?}, {:?}]",
3797 e.measured, e.threshold, e.norm, e.scaling
3798 ),
3799 None => " [made no stationarity claim]".to_string(),
3800 };
3801 format!(", termination={t}{evidence}")
3802 })
3803 .unwrap_or_else(|| ", termination=<no opt solver produced this result>".to_string()),
3804 result
3805 .operator_stop_reason
3806 .map(|stop| format!(", stop_reason={stop:?}"))
3807 .unwrap_or_default(),
3808 result
3809 .converged_via()
3810 .map(|via| format!(", converged_via={via:?}"))
3811 .unwrap_or_default(),
3812 );
3813 // #2465: the line search's own verdict, when one failed. `StepSizeTooSmall`
3814 // and `MaxAttempts` are different defects with different repairs, and
3815 // "line_search_failed" alone distinguishes neither.
3816 // The cost-stall guard's measured probe-noise floor, when it published one.
3817 // A stall halted against a noise bound and a stall halted against the
3818 // score-relative flat band are different verdicts, and only the first
3819 // carries this.
3820 let reason = match result.cost_stall_probe_scale {
3821 Some((noise_floor, probe_radius)) => format!(
3822 "{reason}, cost_stall_window=[noise_floor={noise_floor:.6e}, \
3823 probe_radius={probe_radius:.6e}, ratio={:.6e}]",
3824 noise_floor / probe_radius
3825 ),
3826 None => reason,
3827 };
3828 let reason = match result.flat_noise_grad_bound {
3829 Some(bound) => format!("{reason}, flat_noise_grad_bound={bound:.6e}"),
3830 None => reason,
3831 };
3832 let reason = match result.line_search_failure {
3833 Some((failure_reason, max_attempts)) => format!(
3834 "{reason}, line_search={failure_reason:?} after {max_attempts} attempt(s) \
3835 [StepSizeTooSmall = the direction descended but no step improved the \
3836 objective; MaxAttempts = the bracket never closed]"
3837 ),
3838 None => reason,
3839 };
3840 EstimationError::RemlDidNotConverge {
3841 context: context.to_string(),
3842 reason,
3843 iterations: result.iterations,
3844 final_value: result.final_value,
3845 projected_grad_norm,
3846 stationarity_standard: stationarity_standard.into(),
3847 rho_checkpoint: result.rho.to_vec(),
3848 }
3849}
3850
3851/// Roundoff envelope for two independently assembled values of the same outer
3852/// criterion at the same point.
3853///
3854/// Value-only and derivative-bearing evaluation lanes may use different
3855/// kernels and reduction trees, so bitwise identity is not a valid contract.
3856/// Their relative disagreement must nevertheless stay below the square root of
3857/// machine epsilon: beyond that scale the derivative sample is not evidence
3858/// about the scalar objective the optimizer's value lane ranks.
3859pub fn outer_value_agreement_bound(value_only: f64, derivative_sample: f64) -> f64 {
3860 f64::EPSILON.sqrt() * value_only.abs().max(derivative_sample.abs()).max(1.0)
3861}
3862
3863/// `lane_inner_convergence` is `(value_lane, derivative_lane)`, each captured
3864/// IMMEDIATELY after its own evaluation — `inner_solve_converged` reads one
3865/// shared snapshot, so a flag sampled after both lanes describes only the
3866/// second (#2228). `None` means the caller did not sample that lane.
3867fn audit_outer_value_agreement(
3868 context: &str,
3869 value_only: f64,
3870 derivative_sample: f64,
3871 result: &mut OuterResult,
3872 projected_grad_norm: Option<f64>,
3873 stationarity_standard: impl Into<StationarityStandard>,
3874 lane_inner_convergence: (Option<bool>, Option<bool>),
3875) -> Result<(), EstimationError> {
3876 let bound = outer_value_agreement_bound(value_only, derivative_sample);
3877 let disagreement = (value_only - derivative_sample).abs();
3878 if disagreement <= bound {
3879 return Ok(());
3880 }
3881 // Which lane's inner solve converged decides whether this is roundoff
3882 // between two reduction trees (the bound's premise) or a warm-start basin
3883 // gap the bound was never derived for.
3884 let lane_note = |flag: Option<bool>| match flag {
3885 Some(true) => "converged",
3886 Some(false) => "NOT-CONVERGED",
3887 None => "unsampled",
3888 };
3889 let inner_evidence = format!(
3890 ", inner solve: value-lane={}, derivative-lane={}",
3891 lane_note(lane_inner_convergence.0),
3892 lane_note(lane_inner_convergence.1),
3893 );
3894
3895 // The value-only lane is the scalar criterion authority. Preserve it on
3896 // the resumable checkpoint rather than the derivative lane's inconsistent
3897 // scalar; no certificate may be attached to this mixed evidence.
3898 result.final_value = value_only;
3899 Err(outer_nonconvergence_error(
3900 context,
3901 &format!(
3902 "cost-only value disagrees with analytic-sample value at the same outer point: \
3903 value-only={value_only:.16e}, analytic-sample={derivative_sample:.16e}, \
3904 disagreement={disagreement:.3e}, roundoff bound={bound:.3e}{inner_evidence}"
3905 ),
3906 result,
3907 projected_grad_norm,
3908 stationarity_standard,
3909 ))
3910}
3911
3912fn certify_fixed_point_optimality(
3913 obj: &mut dyn OuterObjective,
3914 config: &OuterConfig,
3915 context: &str,
3916 result: &mut OuterResult,
3917 fidelity: CertificationFidelity,
3918) -> Result<OuterCriterionCertificate, EstimationError> {
3919 let layout = obj.capability().theta_layout();
3920 // A fixed-point residual is only a certificate for the scalar objective
3921 // when both lanes price that same objective at the same rho. Sample the
3922 // authoritative value lane first; the analytic fixed-point evaluator then
3923 // remains the installed terminal-state owner.
3924 //
3925 // MINT ONLY (#2359), for the same reason as on the analytic path: whether
3926 // the two lanes agree is terminal proof work, not a ranking signal, and on
3927 // this route it is the ONLY thing that distinguishes screening from mint —
3928 // there is no order-four ladder to reserve here.
3929 let value_only = match fidelity {
3930 CertificationFidelity::Screening => None,
3931 CertificationFidelity::Mint => {
3932 let sample = obj
3933 .eval_with_order(&result.rho, OuterEvalOrder::Value)
3934 .map_err(|err| {
3935 outer_nonconvergence_error(
3936 context,
3937 &format!("terminal value-only certificate evaluation failed: {err}"),
3938 result,
3939 None,
3940 StationarityStandard::NoComparison,
3941 )
3942 })?
3943 .cost;
3944 if !sample.is_finite() {
3945 return Err(outer_nonconvergence_error(
3946 context,
3947 "terminal value-only certificate evaluation returned a non-finite objective value",
3948 result,
3949 None,
3950 StationarityStandard::NoComparison,
3951 ));
3952 }
3953 Some(sample)
3954 }
3955 };
3956 // Sampled HERE, not after the certificate lane below: `inner_solve_converged`
3957 // returns one shared snapshot describing the most recent inner solve, so a
3958 // read taken after both lanes cannot speak for this one (#2228). Mirrors the
3959 // capture-immediately discipline already used for `terminal_inner_converged`.
3960 let value_lane_inner_converged =
3961 value_only.map(|_| inner_solve_converged(config.outer_inner_cap.as_ref()));
3962 // #2228: these two lanes run back-to-back on ONE `&mut obj` with no reset
3963 // between them. Criteria that fit `(t, β)` in place therefore warm-start
3964 // this certificate lane from wherever the value-only lane above stopped —
3965 // the lanes are not two evaluations of one function, the second is a
3966 // continuation of the first.
3967 //
3968 // That is the hazard `OuterObjective::owns_terminal_coefficient_mode`
3969 // already documents and measures ("disagree by a whole basin (measured:
3970 // 9.1931e2 vs 9.1671e2 on the cause-specific survival gate)"), but its
3971 // `obj.reset()` remedy fires once BEFORE `finalize_outer_result`, not
3972 // between the pair here.
3973 //
3974 // It matters because `outer_value_agreement_bound` — applied to exactly
3975 // these two scalars below — is a sqrt(EPSILON) ROUNDOFF envelope, derived
3976 // for "different kernels and reduction trees". A warm-start basin gap is
3977 // not roundoff.
3978 //
3979 // MEASURED CORRECTION (2026-07-31). I originally wrote here that the
3980 // audit's 1.5x..1.1e7x spread "has the shape of a divergence that usually
3981 // lands in the same basin and occasionally does not". A direct test of the
3982 // mild end refutes that for at least that end. On
3983 // `pure_duchon_aniso_fit_optimizes_without_introducing_hybrid_scale`:
3984 //
3985 // value-only=-3.6708914555093685e1 analytic-sample=-3.6708920839323071e1
3986 // disagreement=6.284e-6 bound=5.470e-7
3987 // inner solve: value-lane=converged, derivative-lane=converged
3988 //
3989 // BOTH lanes converged. If the certificate lane warm-starts from the value
3990 // lane's converged point and itself converges, it should stay there and the
3991 // scalars should agree — so chaining does not explain this one, and the
3992 // spread is probably NOT a single mechanism. At |value| ~ 36.7 the bound is
3993 // sqrt(EPSILON)*36.7 = 5.47e-7 and the gap is ~11.5x it, i.e. the two cost
3994 // routes differ by ~1.7e-7 RELATIVE: an order above roundoff, many orders
3995 // below a basin gap.
3996 //
3997 // The live question for this end is therefore not state chaining but
3998 // whether sqrt(EPSILON) can cover two lanes that evaluate at different
3999 // ORDERS (`Value` vs `ValueAndGradient`/`VGH`) and hence through different
4000 // kernels — the same "machine constant standing in for a measured quantity"
4001 // shape as #2614's requested resolution. To test the OTHER end, capture the
4002 // same flags on a high-end (1.1e7x) firing; the instrumentation is on both
4003 // audit sites now, so it costs one run.
4004 //
4005 // Adding a reset here is NOT a safe drive-by: the same trait doc states
4006 // that a `false` (default) objective "retains the very state its evaluation
4007 // at result.rho depends on and must not be reset". Any fix has to reset
4008 // before BOTH lanes, under the `owns_terminal_coefficient_mode` guard, so
4009 // the pair shares one baseline instead of chaining.
4010 let evaluation = obj
4011 .eval_fixed_point_certificate(&result.rho)
4012 .map_err(|err| {
4013 outer_nonconvergence_error(
4014 context,
4015 &format!("analytic fixed-point certificate evaluation failed: {err}"),
4016 result,
4017 None,
4018 StationarityStandard::NoComparison,
4019 )
4020 })?;
4021 let certificate_lane_inner_converged = inner_solve_converged(config.outer_inner_cap.as_ref());
4022 if !certificate_lane_inner_converged {
4023 return Err(outer_nonconvergence_error(
4024 context,
4025 "terminal fixed-point evidence was evaluated at a non-converged inner state",
4026 result,
4027 None,
4028 StationarityStandard::NoComparison,
4029 ));
4030 }
4031 if evaluation.coordinates.len() != layout.n_params {
4032 return Err(outer_nonconvergence_error(
4033 context,
4034 &format!(
4035 "fixed-point certificate returned {} coordinates for an outer problem of dimension {}",
4036 evaluation.coordinates.len(),
4037 layout.n_params
4038 ),
4039 result,
4040 None,
4041 StationarityStandard::NoComparison,
4042 ));
4043 }
4044 if !evaluation.cost.is_finite() {
4045 return Err(outer_nonconvergence_error(
4046 context,
4047 "fixed-point certificate returned a non-finite objective value",
4048 result,
4049 None,
4050 StationarityStandard::NoComparison,
4051 ));
4052 }
4053 if let Some(value_only) = value_only {
4054 audit_outer_value_agreement(
4055 context,
4056 value_only,
4057 evaluation.cost,
4058 result,
4059 None,
4060 StationarityStandard::NoComparison,
4061 (
4062 value_lane_inner_converged,
4063 Some(certificate_lane_inner_converged),
4064 ),
4065 )?;
4066 }
4067
4068 let mut normalized_updates = Vec::with_capacity(layout.n_params);
4069 let mut uncovered = Vec::new();
4070 for (index, coordinate) in evaluation.coordinates.iter().enumerate() {
4071 match coordinate {
4072 FixedPointCoordinateCertificate::Covered { update, scale }
4073 if update.is_finite() && scale.is_finite() && *scale > 0.0 =>
4074 {
4075 normalized_updates.push(*update / *scale);
4076 }
4077 FixedPointCoordinateCertificate::Covered { update, scale } => {
4078 uncovered.push(format!(
4079 "coordinate {index} has invalid covered residual update={update} scale={scale}"
4080 ));
4081 normalized_updates.push(f64::NAN);
4082 }
4083 FixedPointCoordinateCertificate::Uncovered { reason } => {
4084 uncovered.push(format!("coordinate {index}: {reason}"));
4085 normalized_updates.push(f64::NAN);
4086 }
4087 }
4088 }
4089 if !uncovered.is_empty() {
4090 return Err(outer_nonconvergence_error(
4091 context,
4092 &format!(
4093 "fixed-point certificate lacks root-equivalent analytic coverage: {}",
4094 uncovered.join("; ")
4095 ),
4096 result,
4097 None,
4098 StationarityStandard::NoComparison,
4099 ));
4100 }
4101
4102 let (lower, upper) = outer_model_domain_bounds_template(config, layout.n_params);
4103 let mut raw_inf = 0.0_f64;
4104 let mut projected_inf = 0.0_f64;
4105 for index in 0..layout.n_params {
4106 let update = normalized_updates[index];
4107 raw_inf = raw_inf.max(update.abs());
4108 // `update` is a signed descent/update direction, the negative of the
4109 // gradient convention used by `projected_gradient_norm`: at a lower
4110 // bound a negative update points out of the box, and at an upper bound
4111 // a positive update does. Only those infeasible multiplier components
4112 // are removed.
4113 let projected = if result.rho[index] <= lower[index] {
4114 update.max(0.0)
4115 } else {
4116 update
4117 };
4118 let projected = if result.rho[index] >= upper[index] {
4119 projected.min(0.0)
4120 } else {
4121 projected
4122 };
4123 projected_inf = projected_inf.max(projected.abs());
4124 }
4125
4126 result.final_value = evaluation.cost;
4127 result.final_grad_norm = None;
4128 result.final_gradient = None;
4129 result.final_hessian = None;
4130
4131 let certificate = OuterCriterionCertificate {
4132 stationarity: OuterStationarityCertificate::FixedPoint {
4133 residual_inf_norm: raw_inf,
4134 projected_residual_inf_norm: projected_inf,
4135 bound: config.tolerance,
4136 rung: StationarityBoundSource::FixedPointResidual.provenance().into(),
4137 covered_coordinates: layout.n_params,
4138 },
4139 // The EFS/fixed-point route exposes no analytic Hessian, so there was
4140 // a curvature question and nothing could answer it (#2561).
4141 curvature: CurvatureEvidence::NotAvailable,
4142 lambdas_railed: certificate_railed_lambdas(&result.rho, layout.rho_dim(), config),
4143 railed_facts: railed_coordinate_facts(
4144 &result.rho,
4145 // #2624: the EVIDENCE field, unlike `lambdas_railed` above, is not
4146 // lambda-scoped -- it states the interval and margin each
4147 // coordinate was judged against, and the judgement is taken on the
4148 // theta-wide face. A psi/log-kappa coordinate pinned on its own
4149 // window was structurally absent from it.
4150 &certificate_railed_coordinates(&result.rho, config),
4151 config,
4152 ),
4153 curvature_floor: None,
4154 };
4155 result.criterion_certificate = Some(certificate.clone());
4156 if !certificate.certifies() {
4157 return Err(outer_nonconvergence_error(
4158 context,
4159 &certificate.summary(),
4160 result,
4161 Some(projected_inf),
4162 StationarityBound::fixed_point_residual(config),
4163 ));
4164 }
4165
4166 let via = match result.termination.proposed_via() {
4167 Some(via @ OuterConvergedVia::RecurrentIncumbent { .. }) => via,
4168 _ => OuterConvergedVia::FixedPointStationary {
4169 projected_residual_inf_norm: projected_inf,
4170 certificate_bound: config.tolerance,
4171 },
4172 };
4173 result.termination.certify(via);
4174 log::info!("[CERTIFICATE] {context}: {}", certificate.summary());
4175 Ok(certificate)
4176}
4177
4178/// Build the mandatory analytic optimality certificate at the returned point.
4179///
4180/// The objective is evaluated at the selected point through both its
4181/// authoritative value-only lane and its analytic derivative lane. The two
4182/// values must agree within their roundoff envelope before the derivative
4183/// sample can certify the scalar objective. Missing, malformed, non-finite, or
4184/// split-objective evidence is non-convergence: an optimizer status bit cannot
4185/// substitute for a stationarity certificate. Exact analytic curvature is
4186/// checked when the objective declares it and can materialize it; BFGS/EFS
4187/// solver geometry is never mistaken for objective curvature.
4188/// Which derivative fidelity a certification pass is allowed to spend.
4189///
4190/// #2359: the generic REML/LAML Hessian consumes the row-family derivative
4191/// ladder through order FOUR while its analytic gradient stops at order three,
4192/// so order four is a mint-time cost, not a per-candidate one. A multi-start
4193/// screens every seed it starts — that screening is a first-order gate
4194/// (stationarity, KKT projection, rail facts), and `curvature_admissible()`
4195/// reads `hessian_psd != Some(false)`, so a `None` curvature verdict certifies
4196/// on stationarity alone. The one order-four evaluation belongs to the winner,
4197/// once, and its verdict is the one that mints.
4198/// Remove the soft rho-guard BARRIER's gradient from the coordinates the KKT
4199/// projection is about to treat as sitting AT a box bound (#2545).
4200///
4201/// # The defect this closes
4202///
4203/// The REML criterion carries an unconditional `log cosh` barrier whose only job
4204/// is to keep the outer SEARCH off the `ρ → ±RHO_BOUND` walls. A `log cosh`
4205/// gradient SATURATES — `w·a·tanh(a·ρ̃) → w·a = 1.3333e-7` at `RHO_BOUND = 30`
4206/// — instead of decaying. At an upper rail [`project_gradient_vector`] keeps
4207/// exactly the positive part (`gi.max(0.0)`), and the barrier's contribution
4208/// there IS positive, so `|Pg| ≥ 1.3333e-7` at every upper rail no matter how
4209/// clean the fit: a λ=∞ face could never register as a constrained stationary
4210/// point. Measured on the #2450 fixture at ρ=30, the barrier was `1.332439e-7`
4211/// of a total `1.332521e-7` — 99.999% of the residual — with the criterion's own
4212/// `87.51·e^{−ρ}` face tail underneath it.
4213///
4214/// # Why removing it is not "judging a different function"
4215///
4216/// Only at an ACTIVE bound, and that is the whole argument. The barrier is a
4217/// numerical device, structurally separate from `configured_rho_prior_atom` (the
4218/// prior the user DECLARED), added unconditionally at `w = 1e-6`. At a
4219/// coordinate pinned to its bound the bound is enforced EXACTLY by the box
4220/// projection — the barrier's job is already done by something else — so its
4221/// surviving gradient is a KKT multiplier against a constraint the projection
4222/// already accounts for, counted twice. INTERIOR coordinates keep the full
4223/// gradient: there the optimizer descends criterion-plus-barrier and halts where
4224/// their SUM vanishes, so subtracting the barrier from an interior stationarity
4225/// test would judge a function nothing optimized and manufacture
4226/// "solver converged, certificate refused".
4227///
4228/// `guard` is the barrier gradient the objective published
4229/// ([`OuterObjective::soft_rho_guard_gradient`]) — the same atom's emission the
4230/// criterion ADDED, so the subtraction cannot drift from the addition, and it
4231/// carries the weight anchor `ρ̃ = ρ − log g(w)` that a re-derived raw-ρ closed
4232/// form would drop on every weighted fit (#877). `None`, or any shape the
4233/// coordinates do not line up with, returns the gradient untouched.
4234///
4235/// `bounds` MUST be the same box the subsequent [`project_gradient_vector`] call
4236/// uses (the rail-relaxed one at certificate time), and the active test here is
4237/// the same `>= upper` / `<= lower` test, so "railed" means one thing to the
4238/// subtraction and to the projection.
4239pub(crate) fn gradient_with_rail_barrier_removed(
4240 rho: &Array1<f64>,
4241 gradient: &Array1<f64>,
4242 bounds: &(Array1<f64>, Array1<f64>),
4243 guard: Option<&Array1<f64>>,
4244) -> Array1<f64> {
4245 let (lower, upper) = bounds;
4246 let Some(guard) = guard else {
4247 return gradient.clone();
4248 };
4249 if guard.len() != gradient.len()
4250 || rho.len() != gradient.len()
4251 || lower.len() != gradient.len()
4252 || upper.len() != gradient.len()
4253 {
4254 return gradient.clone();
4255 }
4256 Array1::from_iter((0..gradient.len()).map(|i| {
4257 if (rho[i] >= upper[i] || rho[i] <= lower[i]) && guard[i].is_finite() {
4258 gradient[i] - guard[i]
4259 } else {
4260 gradient[i]
4261 }
4262 }))
4263}
4264
4265#[derive(Clone, Copy, Debug, PartialEq, Eq)]
4266pub(crate) enum CertificationFidelity {
4267 /// Per-candidate multi-start gate. Never spends order four.
4268 Screening,
4269 /// The single terminal mint audit. Spends order four when the objective
4270 /// declares an analytic Hessian.
4271 Mint,
4272}
4273
4274pub(crate) fn certify_outer_optimality(
4275 obj: &mut dyn OuterObjective,
4276 config: &OuterConfig,
4277 context: &str,
4278 result: &mut OuterResult,
4279) -> Result<OuterCriterionCertificate, EstimationError> {
4280 certify_outer_optimality_with_fidelity(obj, config, context, result, CertificationFidelity::Mint)
4281}
4282
4283pub(crate) fn certify_outer_optimality_with_fidelity(
4284 obj: &mut dyn OuterObjective,
4285 config: &OuterConfig,
4286 context: &str,
4287 result: &mut OuterResult,
4288 fidelity: CertificationFidelity,
4289) -> Result<OuterCriterionCertificate, EstimationError> {
4290 result.termination.begin_certification();
4291 let terminal_cap_guard = config
4292 .outer_inner_cap
4293 .as_ref()
4294 .map(FullFidelityInnerCapGuard::lift);
4295 if terminal_cap_guard.is_some() || obj.owns_terminal_coefficient_mode() {
4296 // `reset` is deliberately conditional on the presence of the cap
4297 // contract. Those are the REML/mixture objectives whose search cache
4298 // can contain a coarse inner state; uncapped objectives retain their
4299 // ordinary stateful certification semantics.
4300 //
4301 // The `owns_terminal_coefficient_mode()` disjunct (#2334) closes the
4302 // gap for cap-less objectives that install an owned coefficient mode:
4303 // the certifying re-eval below must start from the same clean baseline
4304 // that `finalize_outer_result` used, so the mode's objective bitwise
4305 // matches the certified `final_value` even when the inner solve is
4306 // bimodal at `rho_star`.
4307 obj.reset();
4308 }
4309 let outcome =
4310 certify_outer_optimality_at_terminal_fidelity(obj, config, context, result, true, fidelity);
4311 drop(terminal_cap_guard);
4312 if outcome.is_err() {
4313 result.termination.refuse_certificate();
4314 }
4315 outcome
4316}
4317
4318fn certify_outer_optimality_at_terminal_fidelity(
4319 obj: &mut dyn OuterObjective,
4320 config: &OuterConfig,
4321 context: &str,
4322 result: &mut OuterResult,
4323 allow_tail_snap: bool,
4324 fidelity: CertificationFidelity,
4325) -> Result<OuterCriterionCertificate, EstimationError> {
4326 let capability = obj.capability();
4327 let layout = capability.theta_layout();
4328 layout
4329 .validate_point_len(&result.rho, "outer certificate point")
4330 .map_err(|err| {
4331 EstimationError::RemlOptimizationFailed(format!(
4332 "{context}: invalid outer certificate point: {err}"
4333 ))
4334 })?;
4335 // Certification is an ownership boundary: reinstall the model-domain
4336 // derivative face before either screening or mint evaluates the selected
4337 // point. This makes the terminal audit independent of whichever
4338 // canonicalized plan or prior fit last touched this thread's IFT state, and
4339 // prevents an active-set search override from surviving as derivative
4340 // geometry. Screening and mint can differ in derivative order, never in the
4341 // model whose feasible directions they differentiate (#2514).
4342 let model_domain_bounds_for_derivatives =
4343 outer_model_domain_bounds_template(config, layout.n_params);
4344 crate::estimate::reml::outer_eval::record_current_outer_rho_model_upper_bounds_for_ift(
4345 &model_domain_bounds_for_derivatives.1,
4346 );
4347 if result.rho.iter().any(|value| !value.is_finite()) {
4348 return Err(outer_nonconvergence_error(
4349 context,
4350 "the selected checkpoint contains non-finite coordinates",
4351 result,
4352 None,
4353 StationarityStandard::NoComparison,
4354 ));
4355 }
4356 if layout.n_params == 0 {
4357 let value = obj.eval_cost(&result.rho).map_err(|err| {
4358 outer_nonconvergence_error(
4359 context,
4360 &format!("zero-dimensional final objective evaluation failed: {err}"),
4361 result,
4362 Some(0.0),
4363 StationarityStandard::NoComparison,
4364 )
4365 })?;
4366 if !value.is_finite() {
4367 return Err(outer_nonconvergence_error(
4368 context,
4369 "the zero-dimensional final objective is non-finite",
4370 result,
4371 Some(0.0),
4372 StationarityStandard::NoComparison,
4373 ));
4374 }
4375 let certificate = OuterCriterionCertificate {
4376 stationarity: OuterStationarityCertificate::AnalyticGradient {
4377 grad_norm: 0.0,
4378 projected_grad_norm: 0.0,
4379 bound: outer_gradient_tolerance(config).abs,
4380 // No smoothing estimand: the empty score is stationary by
4381 // construction, not by clearing this band (#2530).
4382 rung: StationarityRung::EMPTY_ESTIMAND.into(),
4383 },
4384 // No estimand, so no curvature exists to be admissible — the
4385 // second-order twin of the EMPTY_ESTIMAND rung above (#2561).
4386 curvature: CurvatureEvidence::NoEstimand,
4387 lambdas_railed: Vec::new(),
4388 railed_facts: Vec::new(),
4389 curvature_floor: None,
4390 };
4391 result.final_value = value;
4392 result.final_grad_norm = Some(0.0);
4393 result.final_gradient = Some(Array1::zeros(0));
4394 result.final_hessian = None;
4395 result
4396 .termination
4397 .certify(OuterConvergedVia::GradientStationary);
4398 result.criterion_certificate = Some(certificate.clone());
4399 return Ok(certificate);
4400 }
4401 if matches!(result.plan_used.solver, Solver::Efs | Solver::HybridEfs)
4402 && capability.gradient != Derivative::Analytic
4403 {
4404 return certify_fixed_point_optimality(obj, config, context, result, fidelity);
4405 }
4406 if capability.gradient != Derivative::Analytic {
4407 return Err(outer_nonconvergence_error(
4408 context,
4409 "the objective exposes no analytic gradient for final certification",
4410 result,
4411 None,
4412 StationarityStandard::NoComparison,
4413 ));
4414 }
4415
4416 // Sample the scalar authority FIRST and leave the derivative-bearing
4417 // evaluator as the terminal state owner. This is one same-rho audit, not a
4418 // finite-difference derivative: it catches a split value/gradient
4419 // implementation without violating the production no-FD contract.
4420 //
4421 // The order is load-bearing in two independent ways, and #2583 briefly
4422 // inverted it:
4423 //
4424 // * Every real outer objective's derivative lane is a warm-started inner
4425 // SOLVE, so evaluating it ADVANCES the inner state. A value sample
4426 // taken afterwards reads the state that solve left behind, not the
4427 // state it priced. The value lane, by contrast, reads the installed
4428 // state without advancing it, so value-then-derivative is the only
4429 // order in which both samples price ONE inner state.
4430 // * The mint's derivative-bearing evaluation must be the LAST evaluation
4431 // of the certification, because it is the terminal coefficient-mode
4432 // owner every downstream consumer reads (#2359). A trailing value-only
4433 // request re-installs a value-only pass as that owner.
4434 //
4435 // Inverting it also made the audit VACUOUS on the one route it was aimed
4436 // at: the REML objective's outer-eval cache serves any `Value` request from
4437 // whatever entry the derivative call just wrote, so the audit compared a
4438 // number with itself and could not fire. Agreement by cache lookup is not
4439 // agreement between two assemblies of the criterion.
4440 //
4441 // The genuine #2583 hazard — two INNER SOLVES at one ρ, disagreeing at
4442 // inner-tolerance scale rather than at the √ε scale this bound is
4443 // calibrated for — is addressed where it lives: the value lane's bundle is
4444 // stored under the shared ρ key, so the derivative lane that follows reuses
4445 // that inner solution instead of re-solving from it.
4446 //
4447 // BOTH fidelities pay this, deliberately. It is tempting to reserve it for
4448 // the mint the way order four is reserved (#2359), but the two are not
4449 // alike: order four is evidence the filter does not consume, while the
4450 // value-agreement audit is a REFUSAL GATE on the very number the
4451 // multistart ranks by. `retain_best_outer_checkpoint` orders candidates on
4452 // `final_value`; admitting a candidate whose value lane disagrees with its
4453 // derivative lane lets a desynced candidate win the ranking on a number
4454 // nothing has validated, and mint then refuses the whole fit instead of a
4455 // runner-up carrying it. Pinned by
4456 // `analytic_hessian_candidate_screening_requires_only_first_order_evidence_2414`,
4457 // which asserts screening's orders are exactly [Value, ValueAndGradient].
4458 //
4459 // The EFS/fixed-point route is different and IS gated on fidelity — see
4460 // `certify_fixed_point_optimality`. It returns above this block, and there
4461 // screening and mint are otherwise identical work (no order-four ladder to
4462 // reserve), so running the audit twice buys nothing at all.
4463 let value_only = obj
4464 .eval_with_order(&result.rho, OuterEvalOrder::Value)
4465 .map_err(|err| {
4466 outer_nonconvergence_error(
4467 context,
4468 &format!("terminal value-only certificate evaluation failed: {err}"),
4469 result,
4470 result.final_grad_norm,
4471 StationarityStandard::NoComparison,
4472 )
4473 })?
4474 .cost;
4475 // Sampled HERE, while the shared inner-progress snapshot still describes
4476 // THIS lane. The guard further down reads it after the analytic lane has
4477 // also run, so it can only speak for that one (#2228). Measured: this is the
4478 // audit site that actually fires in practice, and it was reporting
4479 // "value-lane=unsampled" because only the fixed-point route was wired.
4480 let value_lane_inner_converged = inner_solve_converged(config.outer_inner_cap.as_ref());
4481 if !value_only.is_finite() {
4482 return Err(outer_nonconvergence_error(
4483 context,
4484 "terminal value-only certificate evaluation returned a non-finite objective value",
4485 result,
4486 result.final_grad_norm,
4487 StationarityStandard::NoComparison,
4488 ));
4489 }
4490
4491 // Order four is reserved for the mint audit (#2359). A screening pass takes
4492 // the same first-order evidence the no-analytic-Hessian path already
4493 // certifies on, so the multi-start keeps its filter without building the
4494 // order-four family tower once per seed.
4495 // One boolean drives BOTH the request below and the requirement at the
4496 // analytic-Hessian block: a pass that does not ask for curvature must not
4497 // then refuse the candidate for not supplying it. Splitting them made every
4498 // screened candidate of an analytic-Hessian objective fail certification
4499 // with "declared analytic curvature but returned none at the final point" —
4500 // a statement about this pass's own eval order, not about the candidate.
4501 //
4502 // #2596 addendum, and the reason this line is no longer the whole story:
4503 // the reservation is sound for the CURVATURE VERDICT (a missing verdict is
4504 // permissive) but NOT for the stationarity BOUND, one of whose rungs the
4505 // same Hessian owns and where a missing rung is strictly tightening. The
4506 // screening pass therefore re-acquires the Hessian, for the bound alone,
4507 // when the un-widened band would refuse — see `screening_bound_curvature`
4508 // below. That escalation is the reason this boolean can stay
4509 // fidelity-gated: it still governs the ORDINARY path, and order four still
4510 // costs nothing on any candidate that clears its first-order band.
4511 let wants_analytic_hessian =
4512 capability.hessian.is_analytic() && matches!(fidelity, CertificationFidelity::Mint);
4513 let order = if wants_analytic_hessian {
4514 OuterEvalOrder::ValueGradientHessian
4515 } else {
4516 OuterEvalOrder::ValueAndGradient
4517 };
4518 let evaluation = obj.eval_with_order(&result.rho, order).map_err(|err| {
4519 outer_nonconvergence_error(
4520 context,
4521 &format!("analytic final-point evaluation failed: {err}"),
4522 result,
4523 result.final_grad_norm,
4524 StationarityStandard::NoComparison,
4525 )
4526 })?;
4527
4528 let analytic_lane_inner_converged = inner_solve_converged(config.outer_inner_cap.as_ref());
4529 if !analytic_lane_inner_converged {
4530 return Err(outer_nonconvergence_error(
4531 context,
4532 "terminal analytic evidence was evaluated at a non-converged inner state",
4533 result,
4534 None,
4535 StationarityStandard::NoComparison,
4536 ));
4537 }
4538 layout
4539 .validate_gradient_len(&evaluation.gradient, "outer certificate gradient")
4540 .map_err(|err| {
4541 outer_nonconvergence_error(
4542 context,
4543 &format!("malformed analytic final gradient: {err}"),
4544 result,
4545 None,
4546 StationarityStandard::NoComparison,
4547 )
4548 })?;
4549 if !evaluation.cost.is_finite() || evaluation.gradient.iter().any(|value| !value.is_finite()) {
4550 return Err(outer_nonconvergence_error(
4551 context,
4552 "the analytic final-point value or gradient is non-finite",
4553 result,
4554 None,
4555 StationarityStandard::NoComparison,
4556 ));
4557 }
4558
4559 let bounds = outer_model_domain_bounds_template(config, layout.n_params);
4560 // A penalty creeping toward the ±rho_bound infinite-smoothing ceiling never reaches
4561 // it EXACTLY — each outer step only shrinks the gap, so it lands strictly inside the
4562 // box (the #2299 checkpoint sits at ρ=29.9938, not 30). `certificate_railed_lambdas`
4563 // then flags it railed via `CERTIFICATE_RAIL_MARGIN`, but the exact `x >= upper` /
4564 // `x <= lower` box-KKT projection treats it as INTERIOR and its outward pull inflates
4565 // |Pg| above the (tiny) stationarity bound — the fit refuses a genuine railed optimum.
4566 // Project the stationarity residual with the box endpoints relaxed inward by that SAME
4567 // rail margin, so "railed" means ONE thing to the detector AND the projector: a
4568 // within-tolerance coordinate whose gradient points OUT of the box has its KKT-multiplier
4569 // component removed rather than counted as a stationarity residual (#2299). The
4570 // projection only zeros the OUTWARD half (`.max(0.0)`/`.min(0.0)`), so a coordinate near
4571 // the bound that still has feasible-descent gradient keeps it and is never falsely
4572 // certified.
4573 let rail_projection_bounds = rail_relaxed_bounds(&bounds);
4574 let grad_norm = evaluation.gradient.dot(&evaluation.gradient).sqrt();
4575 // The terminal inner coefficients β(ρ̂), published by the REML bridge on
4576 // every eval (`inner_beta_hint`). Used to scale the estimand tolerance for
4577 // the asymptote-rail certificate (#2348 Inc 1).
4578 let terminal_beta = evaluation.inner_beta_hint.clone();
4579 // KKT-projected gradient VECTOR (not just its norm): the norm feeds the
4580 // stationarity certificate below, and the vector feeds the curvature-scaled
4581 // flat-valley Newton decrement (#2253/#2249/#2015) once the analytic Hessian
4582 // is in hand.
4583 //
4584 // #2545: at a coordinate the projection is about to treat as AT its bound,
4585 // the criterion's unconditional `log cosh` barrier contributes a saturated
4586 // `+w·a = 1.3333e-7` that `gi.max(0.0)` retains — a standing residual no fit
4587 // can clear, so a λ=∞ face could never certify. Remove exactly that term,
4588 // exactly on those coordinates, from the certificate's VIEW of the gradient
4589 // (`gradient_with_rail_barrier_removed` documents why an interior coordinate
4590 // must keep it). `result.final_gradient` and `grad_norm` below stay the raw
4591 // criterion gradient — this is what the certificate JUDGES, not what the
4592 // criterion IS.
4593 let rail_barrier_gradient = obj.soft_rho_guard_gradient(&result.rho);
4594 let certificate_gradient = gradient_with_rail_barrier_removed(
4595 &result.rho,
4596 &evaluation.gradient,
4597 &rail_projection_bounds,
4598 rail_barrier_gradient.as_ref(),
4599 );
4600 if log::log_enabled!(log::Level::Info) && certificate_gradient != evaluation.gradient {
4601 let removed = (&evaluation.gradient - &certificate_gradient)
4602 .iter()
4603 .map(|v| v * v)
4604 .sum::<f64>()
4605 .sqrt();
4606 log::info!(
4607 "[CERTIFICATE-BARRIER] {context}: removed the soft rho-guard barrier from the \
4608 railed coordinates of the certificate's gradient view (#2545), \
4609 ||removed||={removed:.6e}"
4610 );
4611 }
4612 let projected_gradient = project_gradient_vector(
4613 &result.rho,
4614 &certificate_gradient,
4615 Some(&rail_projection_bounds),
4616 );
4617 let projected_grad_norm = projected_gradient.iter().map(|v| v * v).sum::<f64>().sqrt();
4618 // #2514: preserve the literal first-order geometry used by this pass without
4619 // dumping an O(p) vector for large outer problems. A temporary active-set
4620 // search box can differ from the model's feasible box; when that happens,
4621 // screening and mint may otherwise print identical rho/raw gradients while
4622 // silently projecting against different faces. Record every detector-active
4623 // or actually projected coordinate, capped only in the log payload.
4624 if log::log_enabled!(log::Level::Info) {
4625 const PROJECTION_RECORD_LIMIT: usize = 32;
4626 let mut active_or_projected = 0usize;
4627 let mut projection_records = Vec::new();
4628 for k in 0..result.rho.len() {
4629 let detector_railed = outer_coordinate_is_railed(&result.rho, k, config);
4630 let raw = evaluation.gradient[k];
4631 let projected = projected_gradient[k];
4632 if detector_railed || raw.to_bits() != projected.to_bits() {
4633 active_or_projected += 1;
4634 if projection_records.len() < PROJECTION_RECORD_LIMIT {
4635 projection_records.push((
4636 k,
4637 result.rho[k],
4638 bounds.0[k],
4639 bounds.1[k],
4640 rail_projection_bounds.0[k],
4641 rail_projection_bounds.1[k],
4642 raw,
4643 projected,
4644 detector_railed,
4645 ));
4646 }
4647 }
4648 }
4649 if active_or_projected > 0 {
4650 log::info!(
4651 "[CERTIFICATE-PROJECTION] {context}: fidelity={fidelity:?}, active_or_projected={active_or_projected}, records(index,rho,model_lo,model_hi,projection_lo,projection_hi,raw_g,projected_g,detector_railed)={projection_records:?}{}",
4652 if active_or_projected > projection_records.len() {
4653 " (truncated)"
4654 } else {
4655 ""
4656 },
4657 );
4658 }
4659 }
4660 // Anchored at the criterion value of the point being JUDGED, which is what
4661 // the mgcv `magic` rule means and what the solver's own band deliberately
4662 // no longer does (#2613): see `outer_stationarity_band_at`.
4663 //
4664 // #2688: the band and its rung arrive TOGETHER. This used to be followed by
4665 // `let mut bound_source = SolverBand;`, so the engine's declared band, the
4666 // point-anchored widening and the caller's cap -- three quantities, one of
4667 // which is not a defect in the fit -- all reported one label.
4668 let band_at_point = outer_stationarity_band_and_rung_at(config, evaluation.cost);
4669 let solver_bound = band_at_point.bound;
4670 let mut bound_source = band_at_point.source;
4671 if bound_source == StationarityBoundSource::CallerRequirement {
4672 // #2568's audit line, which until #2688 could not print on this path:
4673 // the cap it announces had already been applied silently in the band
4674 // helper, so the guard below that emits it was false by construction.
4675 log::info!(
4676 "[2568-REQUIREMENT] {context}: caller requires |Pg| <= {:.6e}; \
4677 engine bound was {:.6e} (rung {}); measured |Pg| = \
4678 {projected_grad_norm:.6e}",
4679 solver_bound,
4680 band_at_point.engine_bound,
4681 band_at_point.engine_source.label(),
4682 );
4683 }
4684 // #2458: this used to open with a rung gated on
4685 // `operator_stop_reason == CostStallFlatValley` that installed
4686 // `flat_valley_converged_grad_bound(cost)` — `1e-3·(1 + |score|)` capped at
4687 // `1.0`. Two things were wrong with it, and they compound.
4688 //
4689 // First, the gate is an EXIT REASON and the bound is a pure function of the
4690 // criterion value. The same point, with the same criterion, the same
4691 // gradient and the same curvature, was judged against two different
4692 // standards depending on which stop reason the operator happened to record.
4693 // That is this issue's thesis in its purest form: a predicate answering
4694 // "how did the loop stop?" consumed where a property of the objective is
4695 // needed.
4696 //
4697 // Second, and worse, the constant was redundant with a MEASUREMENT taken in
4698 // the same regime, and won exactly where the measurement had declined. A
4699 // cost-stall exit carries the guard's probe-noise floor `σ̂/Δ`
4700 // (`flat_noise_grad_bound`, applied just below) — the criterion's own
4701 // demonstrated gradient resolution at the step scale the search actually
4702 // probed. When `σ̂/Δ` exceeds `FLAT_VALLEY_CONVERGED_ABS_GRAD_CAP` the guard
4703 // reports `ProbeNoiseVerdict::Unresolvable` and licenses NO bound, because
4704 // "the criterion resolves no gradient at this step scale" (`bridges.rs`).
4705 // The deleted rung then supplied `min(1e-3·(1+|score|), 1.0)` anyway. So at
4706 // precisely the points where the instrument measured itself too noisy to
4707 // certify anything, the certificate substituted a constant and certified.
4708 // A measurement that declines must make the certificate decline, not be
4709 // replaced by a number that was never measured.
4710 //
4711 // What remains in the ladder below is, in every rung: the configured band,
4712 // a measurement (`flat_noise_grad_bound`, gradient reproducibility), a
4713 // derivation (`|Pg|·√(τ/Δpred)`), or the caller's own requirement. No rung
4714 // is a magic relative constant, and none is selected by how the search
4715 // exited. `FLAT_VALLEY_CONVERGED_REL_GRAD` / `_ABS_GRAD_CAP` are retained
4716 // because the cost-stall guard itself still uses the cap as its resolution
4717 // ceiling — that use is a measured ratio compared against a declared
4718 // ceiling, which is a different thing from a bound built out of one.
4719 let mut stationarity_bound = solver_bound;
4720 // #2241 — a cost-stall exit carries the guard's measured probe-noise-floor
4721 // gradient bound σ̂/Δ. The certificate must judge the re-measured final
4722 // gradient against the same flat band the guard certified, or the guard's
4723 // noise-scale convergence would be granted in the loop and revoked here.
4724 if let Some(noise_bound) = result.flat_noise_grad_bound
4725 && noise_bound.is_finite()
4726 {
4727 if noise_bound > stationarity_bound {
4728 bound_source = StationarityBoundSource::ProbeNoiseFloor;
4729 stationarity_bound = noise_bound;
4730 }
4731 }
4732 // #2568 -- the caller's requirement caps the ladder's TOP. Every rung above
4733 // widens, so capping here is the only placement that cannot be defeated by a
4734 // rung that fires later; in particular the score-relative widening is what
4735 // produced the saturated `bound = 1.000e0` this issue was filed against.
4736 //
4737 // #2688 -- this is now the SECOND cap, not the only one, and it says so.
4738 // `outer_stationarity_band_and_rung_at` already capped the engine's own
4739 // band and labelled the result; what reaches here is a bound that a
4740 // widening ABOVE (today: the probe-noise floor) pushed back past the
4741 // requirement. Before #2688 `required < stationarity_bound` was false by
4742 // construction on every exit where nothing widened the already-capped
4743 // value, which is why this audit line had never been observed to print.
4744 //
4745 // No new refusal path is needed and none is added: the acceptance test below
4746 // already compares `projected_grad_norm` against `stationarity_bound`, so
4747 // tightening the bound refuses the fit through the machinery that was always
4748 // there, with the rung naming who decided.
4749 //
4750 // Both numbers are reported. The engine's own bound is what the fit would
4751 // have been held to and is the quantity a reader needs to judge whether the
4752 // requirement was reasonable; the requirement is what actually decided. A
4753 // refusal that named only one of them would be unauditable in exactly the
4754 // way #2465 is about.
4755 if let Some(required) = config.required_projected_gradient_norm
4756 && required < stationarity_bound
4757 {
4758 log::info!(
4759 "[2568-REQUIREMENT] {context}: caller requires |Pg| <= {required:.6e}; \
4760 engine bound was {stationarity_bound:.6e} (rung {}); measured |Pg| = \
4761 {projected_grad_norm:.6e}",
4762 bound_source.label(),
4763 );
4764 bound_source = StationarityBoundSource::CallerRequirement;
4765 stationarity_bound = required;
4766 }
4767 audit_outer_value_agreement(
4768 context,
4769 value_only,
4770 evaluation.cost,
4771 result,
4772 Some(projected_grad_norm),
4773 StationarityBound::from_ladder(stationarity_bound, bound_source),
4774 (
4775 Some(value_lane_inner_converged),
4776 Some(analytic_lane_inner_converged),
4777 ),
4778 )?;
4779
4780 // The optimizer's own recorded best-iterate evidence, captured before the
4781 // fresh certificate-time measurement overwrites it below. Together with
4782 // `evaluation` this is a SECOND independent measurement of the objective
4783 // at the same ρ — the raw material for the gradient-reproducibility floor
4784 // further down, at zero additional objective evaluations.
4785 let run_recorded_gradient = result.final_gradient.take();
4786 let run_recorded_value = result.final_value;
4787
4788 // Install measured first-order evidence before any fallible curvature
4789 // processing. If curvature is malformed, the retained resume checkpoint
4790 // still carries the exact value/gradient that caused certification to stop.
4791 result.final_value = evaluation.cost;
4792 result.final_grad_norm = Some(projected_grad_norm);
4793 result.final_gradient = Some(evaluation.gradient);
4794
4795 // #2596 — a pass that spends LESS evidence must not produce a STRONGER
4796 // refusal than the pass that mints.
4797 //
4798 // The stationarity bound is a LADDER, and one of its rungs — the
4799 // curvature-resolvability widening below — is owned by the analytic outer
4800 // Hessian. `Screening` deliberately does not spend the order-four ladder
4801 // (#2359), and that reservation was argued on the CURVATURE CONJUNCT:
4802 // `curvature_admissible()` reads `hessian_psd != Some(false)`, so a `None`
4803 // curvature verdict certifies on stationarity alone. True — but the Hessian
4804 // ALSO owns a rung of the stationarity BOUND, and there `None` is not
4805 // permissive: it silently reverts screening to the un-widened solver band.
4806 // Screening therefore applied a strictly tighter standard than the mint, and
4807 // a candidate it refused was discarded rather than deferred.
4808 //
4809 // Measured (#2596, lognormal location-scale AFT with a double-penalty
4810 // `s(z, bs="tp", k=10)`): the BFGS converged to the correct interior optimum
4811 // ρ = (0.378, −4.975) at cost 4.1926 with |Pg| = 7.29e-5 against a solver
4812 // band of 5.19e-5 — refused by a factor of 1.4. Both interior seeds were
4813 // refused, the multi-start fell through to the seed lattice's
4814 // over-smoothing boundary candidate, and THAT certified vacuously (at a
4815 // railed corner the box-KKT projection makes |Pg| identically zero) and was
4816 // minted at cost 110.94. The published smoothing parameter's own LAML was
4817 // 26× worse than the one the optimizer had already measured, and the fitted
4818 // smooth carried none of its signal. Every sibling arm of the same suite
4819 // reached the mint and got the rung that would have saved this one
4820 // (`curvature-scaled flat-valley bound 1.537e-3 … widened from
4821 // gradient-band 2.359e-4`); which side of the band a fit lands on is which
4822 // side the last BFGS step stopped on, not a statistical distinction.
4823 //
4824 // So spend the ladder at screening too — but ONLY when the un-widened bound
4825 // would refuse, and ONLY for the bound. The escalated curvature never
4826 // reaches the curvature verdict, the rail certificate, or the tail-snap, so
4827 // this can only ever turn a screening refusal into a screening
4828 // certification and never the reverse. A fit that clears its first-order
4829 // band is byte-identical and pays nothing, so #2359's "order four exactly
4830 // once, at the mint" continues to hold for every healthy fit.
4831 let screening_bound_curvature = if !wants_analytic_hessian
4832 && capability.hessian.is_analytic()
4833 && projected_grad_norm > stationarity_bound
4834 {
4835 log::info!(
4836 "[CERTIFICATE] {context}: screening's first-order band would refuse \
4837 (|Pg|={projected_grad_norm:.3e} > bound={stationarity_bound:.3e}, rung={}); \
4838 spending the order-four ladder so this refusal is judged by the SAME \
4839 stationarity bound the mint applies (#2596)",
4840 bound_source.label(),
4841 );
4842 // A failed or malformed escalation is NOT a refusal of the candidate: it
4843 // only means this rung is unavailable, which is exactly the state the
4844 // pass was already in. Fall through to the un-widened comparison.
4845 match obj.eval_with_order(&result.rho, OuterEvalOrder::ValueGradientHessian) {
4846 Ok(escalated) => match escalated.hessian.materialize_dense() {
4847 Ok(Some(hessian))
4848 if layout
4849 .validate_hessian_shape(&hessian, "outer certificate Hessian")
4850 .is_ok()
4851 && hessian.iter().all(|value| value.is_finite()) =>
4852 {
4853 Some(hessian)
4854 }
4855 _ => {
4856 log::info!(
4857 "[CERTIFICATE] {context}: the escalated order-four evaluation \
4858 supplied no usable curvature; keeping the first-order bound"
4859 );
4860 None
4861 }
4862 },
4863 Err(error) => {
4864 log::info!(
4865 "[CERTIFICATE] {context}: the escalated order-four evaluation failed \
4866 ({error}); keeping the first-order bound"
4867 );
4868 None
4869 }
4870 }
4871 } else {
4872 None
4873 };
4874
4875 let analytic_hessian = if wants_analytic_hessian {
4876 match evaluation.hessian.materialize_dense() {
4877 Ok(Some(hessian)) => {
4878 layout
4879 .validate_hessian_shape(&hessian, "outer certificate Hessian")
4880 .map_err(|err| {
4881 outer_nonconvergence_error(
4882 context,
4883 &format!("malformed analytic final Hessian: {err}"),
4884 result,
4885 Some(projected_grad_norm),
4886 StationarityBound::from_ladder(stationarity_bound, bound_source),
4887 )
4888 })?;
4889 if hessian.iter().any(|value| !value.is_finite()) {
4890 return Err(outer_nonconvergence_error(
4891 context,
4892 "the analytic final Hessian contains non-finite entries",
4893 result,
4894 Some(projected_grad_norm),
4895 StationarityBound::from_ladder(stationarity_bound, bound_source),
4896 ));
4897 }
4898 Some(hessian)
4899 }
4900 Ok(None) => {
4901 return Err(outer_nonconvergence_error(
4902 context,
4903 "the objective declared analytic curvature but returned none at the final point",
4904 result,
4905 Some(projected_grad_norm),
4906 StationarityBound::from_ladder(stationarity_bound, bound_source),
4907 ));
4908 }
4909 Err(err) => {
4910 return Err(outer_nonconvergence_error(
4911 context,
4912 &format!("analytic final Hessian could not be certified: {err}"),
4913 result,
4914 Some(projected_grad_norm),
4915 StationarityBound::from_ladder(stationarity_bound, bound_source),
4916 ));
4917 }
4918 }
4919 } else {
4920 None
4921 };
4922
4923 // #2458 — the same escalation, for the routes that cannot take the one
4924 // above.
4925 //
4926 // The block above closes a gap between two FIDELITIES of one route. The
4927 // remaining gap is between two ROUTES: a route declaring
4928 // `DeclaredHessianForm::Unavailable` has no order-four ladder to spend at
4929 // either fidelity, so it can never reach the curvature-resolvability rung
4930 // and is held to the raw reproducibility band -- the strictest tier in the
4931 // subsystem, awarded to the route that knows the least. Measured across six
4932 // tests of one subsystem in one binary: bounds from 5.675e-6 to 4.771e-2, a
4933 // factor of 8,406, with one |Pg| = 4.637052e-7 graded against four of them.
4934 //
4935 // That gap is #2458 and it is real. It is NOT closed here, and the attempt
4936 // to close it here (`f2cae93ee`, reverted by `finite_difference_outer_hessian`'s
4937 // removal) is the reason this comment exists rather than a code block.
4938 // Forward-differencing the route's analytic gradient produces a number that
4939 // decides which fits are certified, and SPEC line 2 permits finite
4940 // differences only outside production. The workspace's one other production
4941 // finite difference -- the psi audit in `run_plan.rs`, behind
4942 // `outer_gradient_fd_capture_enabled` -- is read by nothing outside tests:
4943 // it RECORDS what happened. A rung that overwrites `stationarity_bound`
4944 // DECIDES what is true, and the precedent does not reach it.
4945 //
4946 // The correct fix is upstream and is being applied there: a route with no
4947 // analytic Hessian should acquire one, not have one estimated on its behalf
4948 // by the code judging it. The named producer -- the constant-curvature outer
4949 // problem, a ONE-parameter profiled Gaussian REML -- now derives its single
4950 // second derivative in closed form and declares `DeclaredHessianForm::Dense`
4951 // (`gam-models/src/fit_orchestration/drivers/spatial_optimization.rs`), so it
4952 // reaches `CurvatureResolvability` by the ordinary path with the family's own
4953 // exact curvature. A route that still declares `Unavailable` is held to the
4954 // raw band and the run record says so (`derived_standard=false`), which is a
4955 // typed inability to certify rather than a silently different standard.
4956
4957 // Curvature-scaled stationarity (#2253/#2249/#2015/#2091). The re-measured
4958 // projected gradient can sit modestly ABOVE the score-relative / probe-noise
4959 // bands even though NO step reduces the objective by more than the outer
4960 // tolerance — a weakly-identified small-n fit reaches this by a flat-valley
4961 // cost-stall, and an *already-stationary* fit reaches it at iteration 0 when
4962 // the plan search exhausts without stepping (a 2-parameter Gaussian-linear
4963 // REML lands λ→0 at a genuine interior optimum whose |Pg|≈1e-7 sits just above
4964 // an absolute score·1e-9 gradient floor tighter than the REML gradient's
4965 // matrix-factorization round-off). Whether that residual is genuine descent is
4966 // a second-order question the flat bands above cannot answer: they are
4967 // gradient-magnitude tests, blind to how the local curvature maps a gradient
4968 // to an objective change. The Newton decrement `½·gᵀH⁻¹g` (see
4969 // `newton_predicted_decrease`) IS that map — the exact predicted improvement of
4970 // a safeguarded second-order step. When it is below the outer objective
4971 // tolerance, the point is stationary at the resolution the criterion can be
4972 // optimized ("no further descent possible"), independent of HOW the solver
4973 // stopped.
4974 //
4975 // Applied whenever a PSD-along-gradient analytic Hessian is in hand (NOT gated
4976 // to a specific exit reason: the decrement test is the certificate, the exit
4977 // reason is not). It can NEVER wrongly certify a fit with real available
4978 // descent: it only widens when `curvature_grad_bound > stationarity_bound`, so
4979 // a well-identified fit that already clears `solver_bound` is untouched; a
4980 // gradient aligned with a near-flat Hessian direction inflates the decrement
4981 // and is rejected; a globally indefinite Hessian is rejected independently by
4982 // the `hessian_psd` gate inside `certifies()`. The derived widening is a
4983 // genuine, direction-aware curvature-scaled GRADIENT bound — the largest ‖Pg‖
4984 // that, in this gradient's direction under this curvature, still predicts a
4985 // decrease of exactly `objective_tol` — not a constant bump: because the
4986 // decrement scales quadratically with ‖g‖ at fixed direction, that bound is
4987 // `‖Pg‖·√(objective_tol/Δpred)`, which clears the actual ‖Pg‖ iff
4988 // `Δpred ≤ objective_tol`.
4989 // `screening_bound_curvature` is the #2596 escalation: at `Mint` it is
4990 // always `None` and this reads `analytic_hessian` exactly as before; at
4991 // `Screening` it is `Some` only on the would-refuse path, and it feeds THIS
4992 // rung and nothing else.
4993 if let Some(hessian) = analytic_hessian
4994 .as_ref()
4995 .or(screening_bound_curvature.as_ref())
4996 && let Some(predicted_decrease) = newton_predicted_decrease(hessian, &projected_gradient)
4997 && predicted_decrease.is_finite()
4998 && predicted_decrease > 0.0
4999 {
5000 // The SAME relative cost floor the cost-stall guard used to declare the
5001 // criterion stalled (run_plan.rs), so certification asserts nothing
5002 // tighter than the loop already proved about this surface.
5003 let objective_tol = outer_rel_cost_floor(config) * (1.0 + evaluation.cost.abs());
5004 let curvature_grad_bound =
5005 projected_grad_norm * (objective_tol / predicted_decrease).sqrt();
5006 if curvature_grad_bound.is_finite() && curvature_grad_bound > stationarity_bound {
5007 log::info!(
5008 "[CERTIFICATE] {context}: curvature-scaled flat-valley bound {curvature_grad_bound:.3e} \
5009 (|Pg|={projected_grad_norm:.3e}, Newton ½gᵀH⁻¹g={predicted_decrease:.3e} ≤ tol {objective_tol:.3e}) \
5010 widened from gradient-band {stationarity_bound:.3e}"
5011 );
5012 stationarity_bound = curvature_grad_bound;
5013 bound_source = StationarityBoundSource::CurvatureResolvability;
5014 }
5015 }
5016
5017 // Gradient-reproducibility floor (#2299 fully-saturated smooth). A
5018 // stationarity certificate cannot resolve below the reproducibility of its
5019 // own measuring instrument: at a rail-adjacent optimum (λ ~ 1e12, the term
5020 // collapsed onto its penalty null space, edf saturated) the analytic
5021 // gradient is a difference of enormous canceling log-det terms whose
5022 // evaluation drifts run to run, so |Pg| measures round-off, not slope —
5023 // observed as the SAME ρ returning |g| ∈ {2.5e-3 … 4.5e-2} across
5024 // consecutive evaluations while the objective stays flat to 1e-7.
5025 //
5026 // The certifier already holds TWO independent measurements at this ρ: the
5027 // optimizer's recorded best-iterate gradient (`run_recorded_gradient`) and
5028 // the fresh certificate-time `evaluation` — so the instrument's
5029 // demonstrated noise costs ZERO additional objective evaluations (scripted
5030 // test objectives keep their exact call counts). A REAL residual gradient
5031 // reproduces (spread ≈ 0, no widening — genuine descent can never be
5032 // masked, and a deterministic objective yields bit-identical pairs), while
5033 // cancellation noise decorrelates (spread ~ |Pg|). The widening is gated
5034 // on the two measurements' objective VALUES agreeing to the same relative
5035 // floor the cost-stall guard uses, and the PSD gate below is unchanged.
5036 if projected_grad_norm > stationarity_bound
5037 && let Some(prior_gradient) = run_recorded_gradient.as_ref()
5038 && layout
5039 .validate_gradient_len(prior_gradient, "outer run-recorded gradient")
5040 .is_ok()
5041 && prior_gradient.iter().all(|value| value.is_finite())
5042 && run_recorded_value.is_finite()
5043 {
5044 const GRADIENT_REPRODUCIBILITY_WIDENING: f64 = 2.0;
5045 let objective_tol = config
5046 .rel_cost_tolerance
5047 .unwrap_or(config.tolerance * 1.0e-2)
5048 .max(COST_STALL_REL_TOL_FLOOR)
5049 * (1.0 + evaluation.cost.abs());
5050 let cost_drift = (run_recorded_value - evaluation.cost).abs();
5051 // The spread must compare two measurements of the SAME quantity, so the
5052 // run-recorded gradient gets the identical #2545 barrier removal the
5053 // certificate-time one got. Comparing a barrier-removed view against a
5054 // barrier-bearing one would inject a deterministic `w·a` into a number
5055 // whose entire meaning is "how much of |Pg| is instrument noise".
5056 let prior_projected = project_gradient_vector(
5057 &result.rho,
5058 &gradient_with_rail_barrier_removed(
5059 &result.rho,
5060 prior_gradient,
5061 &rail_projection_bounds,
5062 rail_barrier_gradient.as_ref(),
5063 ),
5064 Some(&rail_projection_bounds),
5065 );
5066 let spread = (&prior_projected - &projected_gradient)
5067 .iter()
5068 .map(|v| v * v)
5069 .sum::<f64>()
5070 .sqrt();
5071 let repro_bound = GRADIENT_REPRODUCIBILITY_WIDENING * spread;
5072 if cost_drift <= objective_tol
5073 && repro_bound.is_finite()
5074 && repro_bound > stationarity_bound
5075 && projected_grad_norm <= repro_bound
5076 {
5077 log::info!(
5078 "[CERTIFICATE] {context}: gradient-reproducibility floor widened the \
5079 stationarity bound to {repro_bound:.3e} (|Pg|={projected_grad_norm:.3e}, \
5080 same-ρ spread between the run-recorded and certificate-time gradients \
5081 {spread:.3e}, cost drift {cost_drift:.3e} ≤ tol {objective_tol:.3e})"
5082 );
5083 stationarity_bound = repro_bound;
5084 bound_source = StationarityBoundSource::GradientReproducibility;
5085 }
5086 }
5087
5088 // #2458/#2479 -- the bound's own provenance, emitted UNCONDITIONALLY rather
5089 // than only when a rung happens to widen. A certificate that does not carry
5090 // which of its five terms decided it can only be re-derived from source,
5091 // never audited; the same complaint this file's refusal messages make about
5092 // the fits they refuse. Fidelity and the exact rho identify whether a
5093 // screening verdict and the terminal mint measured the same candidate.
5094 // `derived_standard=false` is the actionable bit: it says this verdict rests
5095 // on a gradient-magnitude substitute because the resolvability form was
5096 // unavailable on this route, not because the problem called for it.
5097 log::info!(
5098 "[CERTIFICATE-BOUND] {context}: fidelity={fidelity:?}, rho={:?}, \
5099 bound {stationarity_bound:.6e} set by {} (derived_standard={}, \
5100 |Pg|={projected_grad_norm:.6e}, |g|={grad_norm:.6e}, \
5101 solver_band={solver_bound:.6e}, cost={:.6e})",
5102 result.rho.to_vec(),
5103 bound_source.label(),
5104 bound_source.is_derived_standard(),
5105 evaluation.cost,
5106 );
5107
5108 // Large-step flatness certificate (#2299 fully-saturated smooth). After the
5109 // reproducibility floor a coordinate that has collapsed EXACTLY onto its
5110 // penalty null space (λ ~ 1e12, edf saturated) can still carry a projected
5111 // gradient component that is DETERMINISTIC cancellation bias from the
5112 // 1e12-conditioned logdet derivative (≈ ε·κ·scale). Being deterministic it
5113 // reproduces run to run, so the spread-keyed reproducibility floor above
5114 // cannot see it; and the Newton decrement anti-rescues, because the near-null
5115 // Hessian direction inflates gᵀH⁻¹g by design. The decisive question is
5116 // second-order-independent: does the criterion actually MOVE along that
5117 // coordinate at MACROSCOPIC scale? This block answers it directly — it probes
5118 // the objective a full e-fold in λ to either side of a near-null-curvature
5119 // coordinate and, for coordinates whose value is provably flat there, removes
5120 // their measured gradient component (numerical bias, not slope) from the
5121 // projected gradient before the bound test. A coordinate whose large-step
5122 // value MOVES is left untouched, so a genuine pseudologdet ramp still refuses.
5123 //
5124 // Gated as narrowly as possible: it runs only when the certificate would
5125 // OTHERWISE refuse on |Pg|, only with an analytic Hessian that is PSD-within-
5126 // noise in hand, and only probes coordinates whose curvature row is below the
5127 // roundoff floor — so a well-conditioned objective (every scripted mock at its
5128 // certification point) probes nothing and pays zero extra evaluations.
5129 // The coordinates railed at ±rho_bound (the infinite-smoothing ceiling). Their
5130 // saturated curvature direction makes the FULL Hessian indefinite, so the
5131 // flatness certificate below — and the final curvature gate — judge PSD on the
5132 // interior (un-railed) sub-block instead, or a rail-caused indefiniteness would
5133 // disable the very certificate that exists to certify a railed optimum (#2299).
5134 // The FACE the certificate reasons on: every θ coordinate against its own
5135 // bound, ρ and non-ρ alike. See `certificate_railed_coordinates` for why
5136 // the λ-block report cannot be used here.
5137 let certificate_railed = certificate_railed_coordinates(&result.rho, config);
5138 // #2676: the directions along which THIS criterion is exactly constant by
5139 // construction of its penalty map. Read once, at the certified point, and
5140 // threaded to every curvature test below, so the certificate and the
5141 // smoothing correction judge the same subspace instead of being able to
5142 // reach opposite verdicts on one matrix at one point. `None` for every
5143 // objective that declares no invariance, which restores the pre-#2676
5144 // behaviour bit for bit.
5145 let criterion_invariance = obj.criterion_invariant_directions(&result.rho);
5146 if let Some(basis) = criterion_invariance.as_ref() {
5147 log::info!(
5148 "[CERTIFICATE] {context}: deflating {} criterion-invariant direction(s) of {} \
5149 before the curvature verdict -- their rho-curvature is the chain-rule term \
5150 `sum_k g_k t_k^2` identically, so its sign measures the gradient code against \
5151 the Hessian code, not the fit (#2676)",
5152 basis.ncols(),
5153 basis.nrows(),
5154 );
5155 }
5156 // The λ-block REPORT that ships on the certificate. Same predicate,
5157 // narrower scan, because `lambdas_railed` indexes smoothing parameters.
5158 let railed_lambda_block = certificate_railed_lambdas(&result.rho, layout.rho_dim(), config);
5159
5160 // Typed stationary-at-asymptote rail certificate (#2348 Inc 1, #2299 layer 3,
5161 // #2337 Thm 2.1). Before falling through to the generic gradient/criterion-flat
5162 // verdict, POSITIVELY certify a railed optimum: the interior (non-railed)
5163 // coordinates are gradient-stationary, and each coordinate railed at the
5164 // infinite-/zero-smoothing box bound sits on a confirmed exponential tail whose
5165 // fitted model has already reached the rail limit to within the estimand
5166 // tolerance. This supersedes the untyped `lambdas_railed` flag with a proof that
5167 // the criterion improvement and coefficient travel still available by running to
5168 // the rail are both below tolerance.
5169 //
5170 // Computed in its own statement so the borrow of `analytic_hessian` ends before
5171 // the mint branch moves it onto the result. Gated to a genuinely railed optimum
5172 // with outward pull (`grad_norm` above the stationarity bound) and an analytic
5173 // Hessian: a well-conditioned interior fit, or a coordinate merely resting near a
5174 // bound with a vanishing gradient, probes nothing and keeps its ordinary verdict.
5175 let asymptote_objective_tol = config
5176 .rel_cost_tolerance
5177 .unwrap_or(config.tolerance * 1.0e-2)
5178 .max(COST_STALL_REL_TOL_FLOOR)
5179 * (1.0 + evaluation.cost.abs());
5180 let rail_outcome = match analytic_hessian.as_ref() {
5181 Some(hessian) if !certificate_railed.is_empty() && grad_norm > stationarity_bound => {
5182 Some(try_certify_asymptote_rail(
5183 obj,
5184 &AsymptoteRailInputs {
5185 rho: &result.rho,
5186 projected_gradient: &projected_gradient,
5187 railed: &certificate_railed,
5188 layout,
5189 hessian,
5190 bounds: &bounds,
5191 terminal_beta: terminal_beta.as_ref(),
5192 stationarity_bound: StationarityBound::from_ladder(stationarity_bound, bound_source),
5193 objective_tol: asymptote_objective_tol,
5194 context,
5195 },
5196 )?)
5197 }
5198 _ => None,
5199 };
5200 // A refused railed mint carries its typed decline reason into the final
5201 // refusal summary (mirroring the tail-snap decline note), so a railed
5202 // non-mint names the gate that refused instead of failing silently.
5203 let mut asymptote_rail_note: Option<String> = None;
5204 let mut probes_ran = rail_outcome.is_some();
5205 if let Some(outcome) = rail_outcome {
5206 match outcome {
5207 Err(reason) => asymptote_rail_note = Some(reason),
5208 Ok(minted) => {
5209 let (interior_projected_grad_norm, effective_interior_bound, rails) = minted;
5210 // The tail probes were derivative-bearing evaluations at probe
5211 // ρ's, so the EVALUATOR-side terminal-mode carrier now owns the
5212 // last probe, not the checkpoint (#2155 regression: every
5213 // custom-family at-point mint then failed the bitwise terminal
5214 // theta identity at fit assembly). Re-evaluate at the minted
5215 // point and ship ITS numbers as the terminal facts: the same
5216 // evaluation sets the evaluator carrier, so the optimizer
5217 // certificate and the owned mode are bitwise-identical by
5218 // construction. The certified stationarity facts (interior
5219 // norms, rails) remain the judged ones.
5220 let restored = obj
5221 .eval_with_order(&result.rho, OuterEvalOrder::ValueAndGradient)
5222 .map_err(|err| {
5223 EstimationError::RemlOptimizationFailed(format!(
5224 "{context}: failed to re-own the certified point after \
5225 asymptote-rail probing: {err}"
5226 ))
5227 })?;
5228 result.final_value = restored.cost;
5229 let restored_projected = project_gradient_vector(
5230 &result.rho,
5231 &gradient_with_rail_barrier_removed(
5232 &result.rho,
5233 &restored.gradient,
5234 &rail_projection_bounds,
5235 rail_barrier_gradient.as_ref(),
5236 ),
5237 Some(&rail_projection_bounds),
5238 );
5239 result.final_grad_norm = Some(
5240 restored_projected
5241 .iter()
5242 .map(|v| v * v)
5243 .sum::<f64>()
5244 .sqrt(),
5245 );
5246 result.final_gradient = Some(restored.gradient);
5247 let certificate = OuterCriterionCertificate {
5248 stationarity: OuterStationarityCertificate::AsymptoteRail {
5249 interior_projected_grad_norm,
5250 // The bound that admitted the interior: the raw stationarity
5251 // bound, or the curvature-scaled flat-valley widening when the
5252 // interior sub-block's Newton decrement is below the loop's
5253 // cost resolution (shared judgment with the Inc 2c mint).
5254 bound: effective_interior_bound.value(),
5255 rung: effective_interior_bound.rung().into(),
5256 rails,
5257 },
5258 curvature: CurvatureEvidence::Measured { psd: true },
5259 lambdas_railed: railed_lambda_block.clone(),
5260 railed_facts: railed_coordinate_facts(
5261 &result.rho,
5262 // #2624: theta-wide, see `certificate_railed_coordinates`.
5263 &certificate_railed,
5264 config,
5265 ),
5266 curvature_floor: None,
5267 };
5268 // Move the certified curvature onto the result; the mint path returns
5269 // immediately, so the fall-through below never observes the move.
5270 result.final_hessian = analytic_hessian;
5271 result.criterion_certificate = Some(certificate.clone());
5272 if !certificate.certifies() {
5273 return Err(outer_nonconvergence_error(
5274 context,
5275 &certificate.summary(),
5276 result,
5277 Some(interior_projected_grad_norm),
5278 StationarityBound::from_ladder(stationarity_bound, bound_source),
5279 ));
5280 }
5281 result
5282 .termination
5283 .certify(OuterConvergedVia::AsymptoteStationary {
5284 rails: certificate.stationarity.rails().len(),
5285 });
5286 log::info!("[CERTIFICATE] {context}: {}", certificate.summary());
5287 return Ok(certificate);
5288 }
5289 }
5290 }
5291
5292 let mut certified_projected_grad_norm = projected_grad_norm;
5293 if projected_grad_norm > stationarity_bound
5294 && let Some(hessian) = analytic_hessian.as_ref()
5295 && certificate_hessian_is_psd_off_railed(
5296 hessian,
5297 &certificate_railed,
5298 criterion_invariance.as_ref(),
5299 ) == Some(true)
5300 {
5301 let n = layout.n_params;
5302 // Curvature scale of the analytic outer Hessian: its dominant diagonal,
5303 // the same ‖H‖ scale `certificate_hessian_is_psd` and
5304 // `newton_predicted_decrease` regularize against. A coordinate's curvature
5305 // ROW is indistinguishable from the assembly's roundoff — it has no
5306 // curvature the arithmetic can resolve and has collapsed onto the penalty
5307 // null space — when its largest entry falls below the SAME √ε·‖H‖ margin
5308 // those two probes use to separate a real curvature direction from
5309 // O(ε·‖H‖) accumulation noise. This is the derivation of the threshold:
5310 // NULL_CURVATURE_REL = √ε (machine epsilon's square root, the assembled
5311 // Hessian's relative resolution), scaled by the Hessian's own max-diagonal
5312 // magnitude, floored at 1 exactly as the PSD/Newton shift is.
5313 let max_diag = (0..n).fold(0.0_f64, |acc, j| acc.max(hessian[[j, j]].abs()));
5314 let null_curvature_threshold = f64::EPSILON.sqrt() * max_diag.max(1.0);
5315 // The SAME relative cost floor the cost-stall guard and both widenings
5316 // above use: certification asserts nothing tighter about this surface's
5317 // macroscopic flatness than the loop already proved.
5318 let objective_tol = config
5319 .rel_cost_tolerance
5320 .unwrap_or(config.tolerance * 1.0e-2)
5321 .max(COST_STALL_REL_TOL_FLOOR)
5322 * (1.0 + evaluation.cost.abs());
5323 // One e-fold in log-λ per coordinate (ρ IS log-λ): the +δ/−δ pair spans e²
5324 // in λ, a macroscopic move across which no genuine descent slope can hide.
5325 const LARGE_STEP_DELTA: f64 = 1.0;
5326 let mut saturated_flat: Vec<usize> = Vec::new();
5327 let mut probe_reports: Vec<String> = Vec::new();
5328 let mut probed_any = false;
5329 let mut probe_failed = false;
5330 for k in 0..n {
5331 let row_inf = (0..n).fold(0.0_f64, |acc, j| acc.max(hessian[[k, j]].abs()));
5332 // Only near-null-curvature coordinates the measured gradient actually
5333 // loads on can be responsible for |Pg| exceeding the band; skip every
5334 // other coordinate, so no probe fires on a well-conditioned surface.
5335 if row_inf > null_curvature_threshold || projected_gradient[k] == 0.0 {
5336 continue;
5337 }
5338 let mut plus = result.rho.clone();
5339 plus[k] += LARGE_STEP_DELTA;
5340 let mut minus = result.rho.clone();
5341 minus[k] -= LARGE_STEP_DELTA;
5342 probed_any = true;
5343 let (Ok(cost_plus), Ok(cost_minus)) = (obj.eval_cost(&plus), obj.eval_cost(&minus))
5344 else {
5345 // A failed probe is not evidence of flatness — refuse to classify
5346 // (conservative) and leave |Pg| intact for the bound test.
5347 probe_failed = true;
5348 break;
5349 };
5350 if !cost_plus.is_finite() || !cost_minus.is_finite() {
5351 probe_failed = true;
5352 break;
5353 }
5354 let up = (cost_plus - evaluation.cost).abs();
5355 let down = (cost_minus - evaluation.cost).abs();
5356 if up <= objective_tol && down <= objective_tol {
5357 saturated_flat.push(k);
5358 probe_reports.push(format!("k={k} |ΔV|+={up:.3e} |ΔV|-={down:.3e}"));
5359 }
5360 }
5361 if !probe_failed && !saturated_flat.is_empty() {
5362 // Recompute |Pg| with the provably macroscopically-flat coordinates
5363 // removed: their measured gradient is deterministic cancellation bias,
5364 // not slope. Coordinates that moved keep their component and still count
5365 // against the bound.
5366 let reduced_sq = (0..n)
5367 .filter(|k| !saturated_flat.contains(k))
5368 .map(|k| projected_gradient[k] * projected_gradient[k])
5369 .sum::<f64>();
5370 certified_projected_grad_norm = reduced_sq.sqrt();
5371 let flat_list = saturated_flat
5372 .iter()
5373 .map(usize::to_string)
5374 .collect::<Vec<_>>()
5375 .join(", ");
5376 let probe_summary = probe_reports.join("; ");
5377 log::info!(
5378 "[CERTIFICATE] {context}: large-step flatness certificate classified \
5379 coordinate(s) [{flat_list}] saturated-flat (curvature row ≤ \
5380 {null_curvature_threshold:.3e}, probed Δ=±{LARGE_STEP_DELTA} with \
5381 {probe_summary}, cost-flat to tol {objective_tol:.3e}); projected \
5382 gradient reduced from {projected_grad_norm:.3e} to \
5383 {certified_projected_grad_norm:.3e}"
5384 );
5385 }
5386 // `eval_cost` warm-starts the inner solve, so the probes moved the objective
5387 // off the certified point. Restore it to ρ̂ once iff we actually probed, so
5388 // the downstream state (and the rho-uncertainty diagnostic) sees the fitted
5389 // point. A failure to re-evaluate the same ρ that certified moments ago is a
5390 // genuinely broken objective and refuses conservatively.
5391 if probed_any {
5392 obj.eval_cost(&result.rho).map_err(|err| {
5393 outer_nonconvergence_error(
5394 context,
5395 &format!(
5396 "failed to restore the objective to the certified point after \
5397 flatness probing: {err}"
5398 ),
5399 result,
5400 Some(certified_projected_grad_norm),
5401 StationarityBound::from_ladder(stationarity_bound, bound_source),
5402 )
5403 })?;
5404 }
5405 }
5406
5407 let mut certificate = OuterCriterionCertificate {
5408 stationarity: OuterStationarityCertificate::AnalyticGradient {
5409 grad_norm,
5410 projected_grad_norm: certified_projected_grad_norm,
5411 bound: stationarity_bound,
5412 rung: bound_source.provenance().into(),
5413 },
5414 // The RAW measurement — unchanged, and what every consumer that asks
5415 // for a genuine PSD certificate keeps receiving.
5416 curvature: match analytic_hessian.as_ref() {
5417 Some(hessian) => CurvatureEvidence::from_measurement(
5418 certificate_hessian_is_psd_off_railed(
5419 hessian,
5420 &certificate_railed,
5421 criterion_invariance.as_ref(),
5422 ),
5423 ),
5424 // A screening pass deliberately declines the order-four ladder
5425 // (the documented design at `CertificationFidelity`); a Mint pass
5426 // reaching here simply has no analytic Hessian to test. Those were
5427 // the same `None` before #2561, which is why the design's own
5428 // promise — the winner's verdict is the one that mints — could not
5429 // be checked by anyone.
5430 None if matches!(fidelity, CertificationFidelity::Screening) => {
5431 CurvatureEvidence::NotSpent
5432 }
5433 None => CurvatureEvidence::NotAvailable,
5434 },
5435 lambdas_railed: railed_lambda_block.clone(),
5436 // #2624: `lambdas_railed` above is the lambda-scoped REPORT and stays
5437 // that way; `railed_facts` is the EVIDENCE for a decision taken on the
5438 // theta-wide face (`certificate_railed`, used by the off-railed PSD
5439 // test and the curvature floor immediately below), so it must carry the
5440 // same coordinates that decision was taken on. On an exact-joint
5441 // spatial route the psi coordinate is the only one carrying gradient,
5442 // and it was the one coordinate the refusal could not print.
5443 railed_facts: railed_coordinate_facts(&result.rho, &certificate_railed, config),
5444 // The floor's verdict on that same curvature, recorded beside it.
5445 curvature_floor: analytic_hessian.as_ref().and_then(|hessian| {
5446 interior_curvature_floor_clearance(
5447 hessian,
5448 &certificate_railed,
5449 &projected_gradient,
5450 criterion_invariance.as_ref(),
5451 )
5452 }),
5453 };
5454 // Certify-time tail snap (#2348 Inc 2). About to refuse a point whose
5455 // residual gradient is carried by un-railed coordinates crawling a
5456 // CONFIRMED exponential tail toward the ρ-box (the one-e-fold-per-step
5457 // grind: the loop budget can exhaust strictly inside the box, where the
5458 // Inc 1 railed mint can never fire), snap those coordinates to their box
5459 // bound and publish that point as a one-shot optimization reseed. The
5460 // resumed optimizer lets coupled interior coordinates move before the
5461 // FULL Inc 1 rail discipline judges the result — this path grants nothing
5462 // by itself.
5463 let mut tail_snap_note: Option<String> = None;
5464 if allow_tail_snap
5465 && !certificate.certifies()
5466 && grad_norm > stationarity_bound
5467 && let Some(hessian) = analytic_hessian.as_ref()
5468 {
5469 probes_ran = true;
5470 match try_tail_snap_to_rail(
5471 obj,
5472 &AsymptoteRailInputs {
5473 rho: &result.rho,
5474 projected_gradient: &projected_gradient,
5475 railed: &certificate_railed,
5476 layout,
5477 hessian,
5478 bounds: &bounds,
5479 terminal_beta: terminal_beta.as_ref(),
5480 stationarity_bound: StationarityBound::from_ladder(stationarity_bound, bound_source),
5481 objective_tol: asymptote_objective_tol,
5482 context,
5483 },
5484 )? {
5485 TailSnapOutcome::TailStationaryAtPoint {
5486 rails,
5487 interior_projected_grad_norm,
5488 effective_interior_bound,
5489 } => {
5490 // #2348 Inc 2c: the confirmed tails extrapolate below the bound
5491 // AT the checkpoint — mint the typed asymptote certificate for
5492 // the point as it stands. `hessian_psd` is the interior
5493 // sub-block verdict established before probing (the full
5494 // matrix is expected non-PD from the noise-corrupted tail
5495 // entry); the stored bound is the one that actually certified
5496 // the interior (raw, or the sub-block curvature-scaled
5497 // flat-valley bound).
5498 //
5499 // Re-own the minted point first: the tail probes were
5500 // derivative-bearing evaluations at probe ρ's, so the
5501 // evaluator-side terminal-mode carrier owns the last probe —
5502 // shipping the pre-probe terminal numbers then fails the
5503 // bitwise terminal theta identity at custom-family fit
5504 // assembly (the #2155 all-links regression). One fresh
5505 // evaluation at the checkpoint sets the carrier AND supplies
5506 // the terminal facts, so both sides are bitwise-identical by
5507 // construction.
5508 let restored = obj
5509 .eval_with_order(&result.rho, OuterEvalOrder::ValueAndGradient)
5510 .map_err(|err| {
5511 EstimationError::RemlOptimizationFailed(format!(
5512 "{context}: failed to re-own the certified point after \
5513 tail-snap probing: {err}"
5514 ))
5515 })?;
5516 result.final_value = restored.cost;
5517 let restored_projected = project_gradient_vector(
5518 &result.rho,
5519 &gradient_with_rail_barrier_removed(
5520 &result.rho,
5521 &restored.gradient,
5522 &rail_projection_bounds,
5523 rail_barrier_gradient.as_ref(),
5524 ),
5525 Some(&rail_projection_bounds),
5526 );
5527 result.final_grad_norm = Some(
5528 restored_projected
5529 .iter()
5530 .map(|v| v * v)
5531 .sum::<f64>()
5532 .sqrt(),
5533 );
5534 result.final_gradient = Some(restored.gradient);
5535 let certificate = OuterCriterionCertificate {
5536 stationarity: OuterStationarityCertificate::AsymptoteRail {
5537 interior_projected_grad_norm,
5538 bound: effective_interior_bound.value(),
5539 rung: effective_interior_bound.rung().into(),
5540 rails,
5541 },
5542 curvature: CurvatureEvidence::Measured { psd: true },
5543 lambdas_railed: railed_lambda_block.clone(),
5544 railed_facts: railed_coordinate_facts(
5545 &result.rho,
5546 // #2624: theta-wide, see `certificate_railed_coordinates`.
5547 &certificate_railed,
5548 config,
5549 ),
5550 curvature_floor: None,
5551 };
5552 result.final_hessian = analytic_hessian;
5553 result.criterion_certificate = Some(certificate.clone());
5554 if !certificate.certifies() {
5555 return Err(outer_nonconvergence_error(
5556 context,
5557 &certificate.summary(),
5558 result,
5559 Some(interior_projected_grad_norm),
5560 effective_interior_bound,
5561 ));
5562 }
5563 result
5564 .termination
5565 .certify(OuterConvergedVia::AsymptoteStationary {
5566 rails: certificate.stationarity.rails().len(),
5567 });
5568 log::info!(
5569 "[CERTIFICATE] {context}: tail-stationary at the checkpoint \
5570 (#2348 Inc 2c): {}",
5571 certificate.summary()
5572 );
5573 return Ok(certificate);
5574 }
5575 TailSnapOutcome::ConfirmedNeedsReseed(snapped) => {
5576 log::info!(
5577 "[CERTIFICATE] {context}: confirmed exponential tail on un-railed \
5578 coordinate(s); publishing the snapped point {snapped} as a waypoint \
5579 for one re-optimization retry (#2348 Inc 2b / #2358)"
5580 );
5581 tail_snap_note = Some(
5582 "tail confirmed; retry seeded at the snapped rail waypoint".to_string(),
5583 );
5584 result.tail_snap_reseed = Some(snapped);
5585 }
5586 TailSnapOutcome::Declined(reason) => {
5587 tail_snap_note = Some(reason);
5588 }
5589 }
5590 }
5591 // Install the measured evidence before deciding its verdict. A rejected
5592 // candidate is retained only as a resumable checkpoint, and that
5593 // checkpoint must carry the actual analytic residual/curvature evidence
5594 // that caused the rejection rather than the optimizer's stale terminal
5595 // status.
5596 result.final_hessian = analytic_hessian;
5597 result.criterion_certificate = Some(certificate.clone());
5598 // Screening deliberately spends no curvature. The caller's strict
5599 // second-order requirement belongs exclusively to the terminal mint; applying
5600 // it here would reject every first-order candidate as `NotSpent` and turn a
5601 // two-basin comparison into seed-budget exhaustion.
5602 let mut curvature_requirement_met =
5603 certificate_meets_curvature_requirement(&certificate, config.require_measured_psd, fidelity);
5604 // #2612 — ADJUDICATE a curvature refusal against the criterion, BEFORE
5605 // deciding it.
5606 //
5607 // This block used to live inside the refusal below, which meant its verdict
5608 // could only ever mint a reseed: a run that found no descending trial
5609 // returned `None`, indistinguishable from "the escape was never runnable",
5610 // and the refusal then proceeded on the matrix's word. But "no feasible step
5611 // along the reported negative eigenvector lowers the objective, anywhere in
5612 // the range where the claim predicts a decrease the criterion can represent"
5613 // is a MEASUREMENT of the criterion, and it contradicts the matrix. Spending
5614 // a refusal on evidence the criterion has just falsified is the failure mode
5615 // #2665 documented from the other side (an analytic `λ_min = −1721.5` whose
5616 // objective curvature along the same eigenvector is `+121.6`), and no
5617 // resolution bound can catch it — the matrix is not imprecise there, it is
5618 // wrong.
5619 //
5620 // Moving it here changes nothing about a real saddle: a descending trial
5621 // still mints the same one-shot reseed, and the refusal that follows is
5622 // still the refusal a genuinely indefinite point earns.
5623 // The ADJUDICATION is a measurement and is NOT gated by `allow_tail_snap`
5624 // (#2612). That flag is the one-shot budget for the RESEED — it exists so
5625 // the retry pass cannot mint a second escape and recurse. Adjudicating
5626 // cannot recurse: it evaluates a bounded, derived ladder of trial points and
5627 // its only two outcomes here are "a descent exists" (which still needs the
5628 // budget to be spent, and is refused below when there is none) and "the
5629 // criterion contradicts the matrix" (which publishes no reseed at all).
5630 // Gating the measurement on the reseed budget made the retry pass refuse on
5631 // the matrix's word for want of a measurement it could have made for free —
5632 // the same "nobody looked" / "it was measured and it is a saddle" collapse
5633 // `CurvatureEvidence` was introduced to prevent.
5634 let strict_curvature_refused =
5635 config.require_measured_psd && certificate.hessian_psd() == Some(false);
5636 result.saddle_escape_reseed = None;
5637 if certificate.is_stationary()
5638 && (!certificate.curvature_not_refused() || strict_curvature_refused)
5639 && let Some(hessian) = result.final_hessian.clone()
5640 && let Some(gradient) = result.final_gradient.clone()
5641 {
5642 probes_ran = true;
5643 let saddle_rho = result.rho.clone();
5644 let baseline_cost = result.final_value;
5645 // `curvature_not_refused()` is `false` exactly when the REDUCED
5646 // (off-railed) Hessian is indefinite, so a railed coordinate does not
5647 // waive the adjudication: rails are passed through and held fixed while
5648 // the step searches the free-direction saddle (#2155). They must be held
5649 // fixed on the SAME face the reduction was taken on — the certificate's
5650 // λ-block report would leave a railed ψ free to be stepped out of its
5651 // box.
5652 match adjudicate_negative_curvature(
5653 obj,
5654 &saddle_rho,
5655 &gradient,
5656 &hessian,
5657 &certificate_railed,
5658 criterion_invariance.as_ref(),
5659 baseline_cost,
5660 asymptote_objective_tol,
5661 &bounds,
5662 context,
5663 ) {
5664 SaddleAdjudication::Descended(point) => {
5665 // The reseed — and only the reseed — is one-shot. On the retry
5666 // pass the descent is still a real finding: the criterion agrees
5667 // with the matrix, so the refusal that follows is the refusal a
5668 // genuine saddle earns, and it is recorded as such rather than
5669 // as an escape that was never run.
5670 if allow_tail_snap {
5671 result.saddle_escape_reseed = Some(point);
5672 } else {
5673 log::info!(
5674 "[CERTIFICATE] {context}: the criterion CONFIRMS the reported negative \
5675 curvature (a feasible trial along its eigenvector lowers the objective \
5676 by more than the criterion's resolution), and the one-shot escape \
5677 reseed has already been spent on this fit. The refusal that follows is \
5678 a measured saddle, not an unmeasured one (#2612)."
5679 );
5680 }
5681 }
5682 SaddleAdjudication::Contradicted {
5683 probed,
5684 smallest_step,
5685 predicted_at_smallest,
5686 objective_resolution,
5687 best_seen_cost,
5688 criterion_curvature,
5689 } => {
5690 // #2748: the ladder's verdict on the analytic Hessian outlives
5691 // this function. `invert_identified_rho_hessian` judges the SAME
5692 // matrix at the SAME point later, and without this it does so
5693 // against an eigensolver's backward error -- a bound on the
5694 // decomposition, not on the assembly -- and refuses fits this
5695 // certificate accepted. That is #2428, and carrying the
5696 // measurement is what removes the asymmetry rather than
5697 // widening either side's bar.
5698 result.criterion_hessian_error = criterion_curvature;
5699 log::info!(
5700 "[CERTIFICATE] {context}: WITHDRAWING the curvature verdict — {probed} \
5701 evaluated trial(s) down to step {smallest_step:.3e}, where the claim's own \
5702 predicted decrease {predicted_at_smallest:.3e} reaches the criterion's \
5703 resolution {objective_resolution:.3e}; best cost seen {best_seen_cost:.9e} \
5704 never fell below the checkpoint. The certificate records \
5705 `criterion-contradicted`, NOT a PSD claim (#2612)."
5706 );
5707 // The verdict is withdrawn, not inverted: nothing here showed
5708 // the point IS a minimum. `curvature_floor` goes with it —
5709 // every field in it (`interior_min_eigenvalue`, the floor, the
5710 // floored eigenvalue) is a statement about the matrix whose
5711 // negative direction has just been falsified, and reporting
5712 // them beside a withdrawn verdict is exactly the #2550
5713 // misdirection.
5714 certificate.curvature = CurvatureEvidence::CriterionContradicted;
5715 certificate.curvature_floor = None;
5716 result.criterion_certificate = Some(certificate.clone());
5717 curvature_requirement_met = certificate_meets_curvature_requirement(
5718 &certificate,
5719 config.require_measured_psd,
5720 fidelity,
5721 );
5722 }
5723 SaddleAdjudication::Declined(reason) => {
5724 log::info!("[CERTIFICATE] {context}: saddle escape declined -- {reason}");
5725 }
5726 }
5727 }
5728 if !certificate.certifies() || !curvature_requirement_met {
5729 // Mint the #2392 reseeds fresh for THIS refused point: clear any value a
5730 // prior (multistart / pre-polish) certification of a different ρ left on
5731 // the result so the resume loop never consumes a stale pull-back/freeze.
5732 result.wrong_rail_reseed = None;
5733 result.active_set_reseed = None;
5734 // Reaching here means the #2612 adjudication above did NOT withdraw the
5735 // curvature verdict: either it minted a descending reseed (a real
5736 // saddle, and this refusal carries the retry), or it declined, or the
5737 // refusal is not about curvature at all.
5738 //
5739 // #2665: when a MEASURED analytic Hessian is what refuses the fit, say
5740 // whether that Hessian agrees with the objective it claims to be the
5741 // curvature of. On the SAS/mixture cluster it does not: the rho/rho
5742 // block agreed to 2.8e-5 relative while the psi/psi block was
5743 // SIGN-FLIPPED in every entry, and that block's eigenvalue was the
5744 // whole of `interior lambda_min = -1721.5`. Without this, a wrong
5745 // Hessian and a genuine saddle print identically, and the refusal
5746 // reads as a verdict about the point when it is a verdict about the
5747 // matrix.
5748 //
5749 // Gated on `hessian_psd() == Some(false)` and NOT on
5750 // `strict_curvature_refused`: `require_measured_psd` is FALSE on the
5751 // flexible-link path that motivated this, so the stricter gate never
5752 // fires there. Cost is 2n gradient evaluations on a path that is about
5753 // to refuse the fit outright, capped at n <= 8 so a wide theta cannot
5754 // turn a refusal into a long run.
5755 if certificate.hessian_psd() == Some(false)
5756 && let Some(h_an) = result.final_hessian.clone()
5757 && result.rho.len() == h_an.nrows()
5758 && h_an.nrows() == h_an.ncols()
5759 && (1..=8).contains(&result.rho.len())
5760 {
5761 let n = result.rho.len();
5762 let mut h_fd = Array2::<f64>::zeros((n, n));
5763 let mut complete = true;
5764 for k in 0..n {
5765 // Central differences of the ANALYTIC gradient: this compares
5766 // the Hessian against the derivative the search itself
5767 // consumes, so a disagreement cannot be blamed on a different
5768 // objective. Step is relative where the coordinate is large
5769 // (rho reaches ~21 here) and absolute near zero.
5770 let step = (result.rho[k].abs() * 1e-6).max(1e-5);
5771 let mut plus = result.rho.clone();
5772 plus[k] += step;
5773 let mut minus = result.rho.clone();
5774 minus[k] -= step;
5775 match (
5776 obj.eval_with_order(&plus, OuterEvalOrder::ValueAndGradient),
5777 obj.eval_with_order(&minus, OuterEvalOrder::ValueAndGradient),
5778 ) {
5779 (Ok(forward), Ok(backward)) => {
5780 for i in 0..n {
5781 h_fd[[i, k]] =
5782 (forward.gradient[i] - backward.gradient[i]) / (2.0 * step);
5783 }
5784 }
5785 // A refusal here is EVIDENCE, not an absence of it: an
5786 // objective that cannot be evaluated a step away from the
5787 // point it just refused explains a failed line search on its
5788 // own, with no gradient defect anywhere. Swallowing the
5789 // reason left the reader with "could not evaluate" and
5790 // nothing to act on, so name the coordinate, the side, the
5791 // step, and what the evaluator actually said.
5792 (forward, backward) => {
5793 complete = false;
5794 for (side, outcome) in [("+", forward), ("-", backward)] {
5795 if let Err(error) = outcome {
5796 log::info!(
5797 "[CERTIFICATE] {context}: the FD-vs-analytic Hessian probe \
5798 could not evaluate the objective at coordinate {k} \
5799 (rho[{k}]={:.6e}, side {side}, step {step:.3e}): {error}",
5800 result.rho[k],
5801 );
5802 }
5803 }
5804 }
5805 }
5806 }
5807 // Re-own the checkpoint: the probes above were derivative-bearing
5808 // evaluations, so the evaluator-side terminal carrier holds the
5809 // last one until this runs.
5810 if let Err(err) = obj.eval_cost(&result.rho) {
5811 log::warn!(
5812 "[CERTIFICATE] {context}: could not restore the checkpoint after the \
5813 FD-vs-analytic Hessian probe: {err}"
5814 );
5815 }
5816 if complete {
5817 let rho_dim = layout.rho_dim().min(n);
5818 log::info!(
5819 "[CERTIFICATE] {context}: FD-vs-analytic outer Hessian at the refused \
5820 point (rho_dim={rho_dim} of {n}): analytic={h_an:?} finite_difference={h_fd:?}"
5821 );
5822 // Report BOTH norms per block. `|H_fd - H_an|` says they
5823 // disagree; `|H_fd + H_an|` says HOW: near zero means the block
5824 // is negated, and ~2*|H_an| is what a block that AGREES looks
5825 // like. The two call for different repairs.
5826 for (label, rows, cols) in [
5827 ("rho_rho", 0..rho_dim, 0..rho_dim),
5828 ("rho_psi", 0..rho_dim, rho_dim..n),
5829 ("psi_rho", rho_dim..n, 0..rho_dim),
5830 ("psi_psi", rho_dim..n, rho_dim..n),
5831 ] {
5832 let (mut difference, mut sum, mut analytic) = (0.0f64, 0.0f64, 0.0f64);
5833 for i in rows.clone() {
5834 for j in cols.clone() {
5835 difference += (h_fd[[i, j]] - h_an[[i, j]]).powi(2);
5836 sum += (h_fd[[i, j]] + h_an[[i, j]]).powi(2);
5837 analytic += h_an[[i, j]].powi(2);
5838 }
5839 }
5840 if analytic > 0.0 {
5841 log::info!(
5842 "[CERTIFICATE] {context}: FD-vs-analytic block {label}: \
5843 |H_fd-H_an|={:.6e} |H_fd+H_an|={:.6e} |H_an|={:.6e} \
5844 relative={:.6e}",
5845 difference.sqrt(),
5846 sum.sqrt(),
5847 analytic.sqrt(),
5848 difference.sqrt() / analytic.sqrt(),
5849 );
5850 }
5851 }
5852 } else {
5853 log::info!(
5854 "[CERTIFICATE] {context}: the FD-vs-analytic Hessian probe could not \
5855 evaluate the objective at every displaced point; no comparison reported"
5856 );
5857 }
5858 }
5859 // #2665: the escape below is the remedy for a point that is first-order
5860 // stationary yet refused for curvature — exactly the SAS/mixture-link
5861 // failures, where `|Pg|` lands 1-10x BELOW its bound and only the
5862 // second-order conjunct refuses, on a MEASURED analytic Hessian with
5863 // `lambda_min ~ -1.6e3` against a `~1e-5` gradient floor.
5864 //
5865 // But the guard is a FIVE-way conjunction, and three of its conjuncts
5866 // can be false because something was never populated rather than
5867 // because a decision was taken. When that happens the refusal message
5868 // says only "indefinite curvature", and the fact that the remedy was
5869 // never even attempted leaves no trace at all. Name the blocking
5870 // conjunct at the moment of refusal so a saddle refusal can be
5871 // attributed instead of guessed.
5872 if certificate.is_stationary()
5873 && (!certificate.curvature_not_refused() || strict_curvature_refused)
5874 {
5875 // `allow_tail_snap` is no longer a blocker here: the adjudication
5876 // runs on every refusal (#2612) and only the RESEED is one-shot, so
5877 // the two remaining conjuncts are the only ways the measurement can
5878 // fail to happen at all.
5879 let escape_blocker = if result.final_hessian.is_none() {
5880 Some("final_hessian=None (no terminal Hessian retained on this route)")
5881 } else if result.final_gradient.is_none() {
5882 Some("final_gradient=None (no terminal gradient retained on this route)")
5883 } else {
5884 None
5885 };
5886 if let Some(reason) = escape_blocker {
5887 log::warn!(
5888 "[CERTIFICATE] {context}: negative-curvature saddle escape NOT ATTEMPTED at a \
5889 first-order-stationary point that is being refused for curvature \
5890 (hessian_psd={:?}, require_measured_psd={}); blocked by {reason}. The \
5891 refusal that follows is therefore not evidence that no escape exists — \
5892 the remedy was never run (#2665).",
5893 certificate.hessian_psd(),
5894 config.require_measured_psd,
5895 );
5896 }
5897 }
5898 // #2392 — wrong-rail pull-back and active-set reduction. A coordinate at
5899 // the ρ box whose deep-λ terminal gradient is instrument noise leaves the
5900 // outer search unable to move it: the trust region's local model is flat
5901 // there. Two evidence-gated one-shot reseeds recover the fit (both gated
5902 // by `allow_tail_snap` so the retry pass cannot recurse):
5903 // (1) WRONG-RAIL PULL-BACK: the coordinate's clean-band probes (a few
5904 // e-folds inside, above the noise floor) prove the objective
5905 // DECREASES inward — it was driven to the wrong bound. Reseed it at
5906 // its clean-band interior scale so the optimizer descends to the
5907 // true interior optimum. Fires ONLY on the opposite-sign clean-tail
5908 // proof, so a genuine λ→∞/λ→0 rail is never pulled off its bound.
5909 // (2) ACTIVE-SET REDUCTION: no wrong rail, but the INTERIOR is not
5910 // stationary while a rail is present — the railed coordinate's
5911 // ill-conditioned Hessian row poisons the joint step. Freeze the
5912 // KKT-ACTIVE rail(s) at their bound and re-run so the interior
5913 // converges in the reduced space; the plan runner re-certifies the
5914 // polished point under the ORIGINAL box, so the reduction can
5915 // never redefine the feasible set.
5916 //
5917 // "Active" is decided by the PROJECTOR, at freeze time, not by
5918 // distance to a bound (#2454): a coordinate on its bound whose
5919 // projected gradient survives still has feasible descent and is
5920 // left free. This is where the un-freeze has to happen. Doing it
5921 // after the reduced solve is not available — the retry is one-shot
5922 // (`allow_tail_snap` is cleared on it, so no second reseed can be
5923 // published) and the only path back off a frozen bound is the
5924 // wrong-rail pull-back, which demands a clean opposite-sign
5925 // exponential tail and declines on any coordinate that has none.
5926 // (1) takes precedence: a wrong rail must be pulled back, never frozen.
5927 if allow_tail_snap && !certificate_railed.is_empty() {
5928 let beta_norm = terminal_beta
5929 .as_ref()
5930 .map(|b| b.dot(b).sqrt())
5931 .filter(|v| v.is_finite())
5932 .unwrap_or(0.0);
5933 let mut rail_tol =
5934 AsymptoteTolerances::exp4_rail_bands(ASYMPTOTE_ESTIMAND_REL_TOL * (1.0 + beta_norm));
5935 rail_tol.tail_drift_rel = TAIL_SNAP_DRIFT_REL;
5936 let (lower, upper) = &bounds;
5937 let mut wrong_rail_point: Option<Array1<f64>> = None;
5938 for &k in certificate_railed.iter() {
5939 if k >= result.rho.len() || k >= lower.len() || k >= upper.len() {
5940 continue;
5941 }
5942 let side = if (upper[k] - result.rho[k]).abs() <= (result.rho[k] - lower[k]).abs() {
5943 AsymptoteSide::Upper
5944 } else {
5945 AsymptoteSide::Lower
5946 };
5947 if let Some(target) = detect_wrong_rail_pullback(
5948 obj,
5949 &result.rho,
5950 k,
5951 side,
5952 &rail_tol,
5953 (lower[k], upper[k]),
5954 )? {
5955 let mut reseed = result.rho.clone();
5956 reseed[k] = target;
5957 wrong_rail_point = Some(reseed);
5958 break;
5959 }
5960 }
5961 if let Some(reseed) = wrong_rail_point {
5962 result.wrong_rail_reseed = Some(reseed);
5963 } else if let Some(hessian) = result.final_hessian.as_ref() {
5964 // Active-set reduction needs curvature to prove that the free
5965 // subspace is genuinely unpolished. Wrong-rail pull-back above
5966 // is a first-order tail-sign proof and deliberately does NOT:
5967 // gradient-only BFGS objectives can rail incorrectly too, and
5968 // withholding a valid pull-back merely because they do not
5969 // materialize H would make recovery depend on solver class.
5970 let interior_indices =
5971 interior_face_indices(&projected_gradient, &certificate_railed);
5972 let interior_not_stationary = !interior_indices.is_empty()
5973 && certify_interior_stationarity(
5974 &projected_gradient,
5975 &hessian,
5976 &interior_indices,
5977 StationarityBound::from_ladder(stationarity_bound, bound_source),
5978 asymptote_objective_tol,
5979 )
5980 .is_err();
5981 if interior_not_stationary {
5982 let mut froz_lower = lower.clone();
5983 let mut froz_upper = upper.clone();
5984 let mut reseed = result.rho.clone();
5985 let mut froze_any = false;
5986 for &k in certificate_railed.iter() {
5987 if k >= reseed.len() {
5988 continue;
5989 }
5990 // Freeze the KKT-ACTIVE set, not the proximity set
5991 // (#2454). `certificate_railed` is a DISTANCE test —
5992 // "within `CERTIFICATE_RAIL_MARGIN` of a bound" — and
5993 // being near a bound says nothing about whether the
5994 // constraint is active. The active set is the one the
5995 // projector already decided: at a bound it keeps only
5996 // the feasible-descent half, so a coordinate whose
5997 // projected component survives is one the search can
5998 // still move INWARD, and `interior_face_indices` (three
5999 // statements up) has already classified it as interior
6000 // for exactly that reason.
6001 //
6002 // Freezing it anyway pins the coordinate carrying the
6003 // descent that made `interior_not_stationary` true in
6004 // the first place, and the reduced solve then converges
6005 // everything ELSE around it. Measured on #2454's Matérn
6006 // monotone fixture, whose ψ seed sits on its box edge:
6007 // ψ is frozen at −2.4849 with `∂V/∂ψ = −4.3164`
6008 // (FD-confirmed), the three ρ converge to
6009 // `‖g_ρ‖ = 1.03e-4`, the solver reports
6010 // `claimed_converged=true` off that reduced norm, and
6011 // the re-certification under the original box then
6012 // refuses at `|Pg| = 4.316e0` against a `7.06e-4`
6013 // bound — with ψ bit-identical to its seed after 26
6014 // outer iterations, because it was never free to move.
6015 //
6016 // The un-freeze the reduction's contract promises ("a
6017 // frozen coordinate whose gradient turns inward
6018 // re-certifies under the ORIGINAL box") cannot rescue
6019 // this: the gradient had ALREADY turned inward when the
6020 // freeze was taken, so the reduced solve is asked to
6021 // polish a face that was never active.
6022 if projected_gradient
6023 .get(k)
6024 .is_some_and(|component| *component != 0.0)
6025 {
6026 log::info!(
6027 "[ACTIVE-SET] {context}: coordinate {k} is within the rail \
6028 margin of its bound but its projected gradient is \
6029 {:.6e} (feasible descent remains), so it is INTERIOR and \
6030 stays free rather than being frozen (#2454)",
6031 projected_gradient[k],
6032 );
6033 continue;
6034 }
6035 let rail = if (upper[k] - reseed[k]).abs() <= (reseed[k] - lower[k]).abs() {
6036 upper[k]
6037 } else {
6038 lower[k]
6039 };
6040 reseed[k] = rail;
6041 froz_lower[k] = rail;
6042 froz_upper[k] = rail;
6043 froze_any = true;
6044 }
6045 if froze_any {
6046 result.active_set_reseed = Some(ActiveSetReseed {
6047 rho: reseed,
6048 bounds: (froz_lower, froz_upper),
6049 });
6050 }
6051 }
6052 }
6053 }
6054 // Carry the railed-mint and tail-snap decline evidence into the
6055 // refusal so a railed or budget-exhausted crawl explains which
6056 // certificate gate refused instead of failing silently.
6057 let mut summary = certificate.summary();
6058 if !curvature_requirement_met {
6059 // This gate refuses on two OPPOSITE grounds and used to report both
6060 // with one sentence, which reads as an optimizer failure in either
6061 // case (#2641):
6062 //
6063 // * `hessian_psd=no` — curvature WAS measured and is indefinite.
6064 // A verdict about the point. Refusing is correct; #2665 is such
6065 // a case (λ_min = -1.6e3 against a 1e-5 floor).
6066 // * `hessian_psd=n/a` — curvature was never measured at all, so
6067 // there is no eigenvalue and no verdict. Nothing about the point
6068 // has been established either way.
6069 //
6070 // The second splits again, and one branch is a CONFIGURATION
6071 // CONTRADICTION rather than anything the optimizer did: a caller can
6072 // declare `DeclaredHessianForm::Unavailable` (e.g. a lane that
6073 // deliberately avoids realizing an O(n) second-order slab) while the
6074 // same outer problem sets `require_measured_psd`. That combination
6075 // can never be satisfied by any amount of optimizer work, so say so
6076 // at the refusal instead of sending the reader to the solver.
6077 let detail = match certificate.hessian_psd() {
6078 Some(false) => "this objective requires a positive-semidefinite analytic Hessian \
6079 at the selected local minimum, and the Hessian measured HERE is indefinite \
6080 (`hessian_psd=no`) — a curvature verdict about this point, not a missing \
6081 measurement"
6082 .to_string(),
6083 _ if !capability.hessian.is_analytic() => format!(
6084 "this objective requires a MEASURED positive-semidefinite analytic Hessian at \
6085 the selected local minimum, but the problem declared \
6086 `{:?}`, so no Hessian was ever evaluated and `hessian_psd` is \
6087 unavailable. CONFIGURATION CONTRADICTION: the same outer problem both \
6088 suppressed the analytic Hessian and required a measured one. No optimizer \
6089 result can satisfy this — fix the construction (declare the Hessian, at \
6090 least for the terminal certification evaluation) rather than the search",
6091 capability.hessian
6092 ),
6093 _ => "this objective requires a MEASURED positive-semidefinite analytic Hessian \
6094 at the selected local minimum. The Hessian is declared available, yet none \
6095 was materialized at certification, so `hessian_psd` is unavailable and NO \
6096 curvature verdict has been reached about this point"
6097 .to_string(),
6098 };
6099 summary = format!("{summary}; {detail}");
6100 }
6101 // `summary()` prints `lambdas_railed`, which is the λ-block report. When
6102 // the face the certificate actually reasoned on is wider — a joint
6103 // [ρ, ψ] search with a κ coordinate on its data-derived window — say so,
6104 // or the refusal reads as `railed=[]` while the projector has already
6105 // discarded that coordinate's outward pull and nothing explains where
6106 // `|g|` went (#979).
6107 if certificate_railed.len() != railed_lambda_block.len() {
6108 summary = format!(
6109 "{summary}; outer coordinates railed (theta-wide, incl. non-rho blocks): \
6110 {certificate_railed:?}"
6111 );
6112 }
6113 // #2465: `railed=[…]` without its box is unfalsifiable from the run
6114 // record. Every value here is live at the emission site.
6115 if !certificate_railed.is_empty() {
6116 summary = format!(
6117 "{summary}; rail tests: [{}]",
6118 rail_test_summary(&result.rho, &certificate_railed, config)
6119 );
6120 }
6121 // #2465 again, one level up: the `solver provenance` this refusal is
6122 // about to append reports the terminating `|g|` of the run that produced
6123 // `result`. When that run executed under an active-set reduction, its
6124 // box PINNED some coordinates (`lower == upper`) and its `|g|` therefore
6125 // ranges over the FREE ones only, while `|Pg|` above ranges over all of
6126 // θ under the model domain. The two are then not the same quantity, and
6127 // nothing in the string said so: on #2454's Matérn fixture the refusal
6128 // read `claimed_converged=true, gradient_tolerance(|g|=1.029235e-4 <
6129 // 5.487011e-4)` beside `|Pg|=4.316e0`, a four-order gap that is entirely
6130 // the pinned ψ coordinate and looks like a contradiction until the two
6131 // coordinate sets are named. Name them.
6132 if let Some((search_lower, search_upper)) = config.search_bounds_override.as_ref() {
6133 let pinned: Vec<usize> = (0..search_lower.len().min(search_upper.len()))
6134 .filter(|&k| search_lower[k] == search_upper[k])
6135 .collect();
6136 if !pinned.is_empty() {
6137 summary = format!(
6138 "{summary}; NOTE the run that produced this point searched a REDUCED box \
6139 with coordinate(s) {pinned:?} pinned (active-set reduction), so the \
6140 solver-provenance |g| below ranges over the FREE coordinates only and is \
6141 NOT comparable with the |Pg| above, which ranges over all of theta under \
6142 the model domain"
6143 );
6144 }
6145 }
6146 if let Some(note) = asymptote_rail_note {
6147 summary = format!("{summary}; asymptote-rail declined: {note}");
6148 }
6149 let summary = match tail_snap_note {
6150 Some(note) => format!("{summary}; tail-snap declined: {note}"),
6151 None => summary,
6152 };
6153 return Err(outer_nonconvergence_error(
6154 context,
6155 &summary,
6156 result,
6157 Some(certified_projected_grad_norm),
6158 StationarityBound::from_ladder(stationarity_bound, bound_source),
6159 ));
6160 }
6161
6162 // #2155 regression, the LAST carrier-stealing path: the rail-mint and
6163 // tail-snap attempts probe with derivative-bearing evaluations, and the
6164 // ORDINARY certificate can still certify after a declined attempt (e.g. a
6165 // KKT-railed projection whose raw gradient norm sits above the bound), so
6166 // this success would ship pre-probe terminal numbers while the evaluator's
6167 // terminal-mode carrier owns the last probe — refusing the bitwise theta
6168 // identity at custom-family fit assembly. Re-own the certified point with
6169 // one fresh evaluation and ship ITS numbers; the mint branches re-own for
6170 // themselves before their early returns, and the judged stationarity facts
6171 // above remain the measured pre-probe ones.
6172 if probes_ran {
6173 let restored = obj
6174 .eval_with_order(&result.rho, OuterEvalOrder::ValueAndGradient)
6175 .map_err(|err| {
6176 EstimationError::RemlOptimizationFailed(format!(
6177 "{context}: failed to re-own the certified point after rail/tail probing: {err}"
6178 ))
6179 })?;
6180 result.final_value = restored.cost;
6181 let restored_projected = project_gradient_vector(
6182 &result.rho,
6183 &gradient_with_rail_barrier_removed(
6184 &result.rho,
6185 &restored.gradient,
6186 &rail_projection_bounds,
6187 rail_barrier_gradient.as_ref(),
6188 ),
6189 Some(&rail_projection_bounds),
6190 );
6191 result.final_grad_norm = Some(
6192 restored_projected
6193 .iter()
6194 .map(|v| v * v)
6195 .sum::<f64>()
6196 .sqrt(),
6197 );
6198 result.final_gradient = Some(restored.gradient);
6199 }
6200 // #2235/#2241 — record WHICH certificate concluded this run. A
6201 // Fellner–Schall model-state fixed point was pre-stamped by the runner and
6202 // is preserved (this analytic certificate is its corroborating evidence);
6203 // otherwise the verdict is decided by which stationarity band the measured
6204 // projected gradient actually cleared: the solver's own tolerance
6205 // (gradient-stationary) or only the widened flat certificate band
6206 // (criterion-flat, #2241).
6207 let via = match result.termination.proposed_via() {
6208 Some(via @ OuterConvergedVia::RecurrentIncumbent { .. }) => via,
6209 _ if certified_projected_grad_norm <= solver_bound => {
6210 OuterConvergedVia::GradientStationary
6211 }
6212 _ => OuterConvergedVia::CriterionFlat {
6213 residual_grad_norm: certified_projected_grad_norm,
6214 certificate_bound: stationarity_bound,
6215 },
6216 };
6217 result.termination.certify(via);
6218 log::info!("[CERTIFICATE] {context}: {}", certificate.summary());
6219 Ok(certificate)
6220}
6221
6222/// Estimand tolerance relative to the fitted coefficient scale for the
6223/// asymptote-rail certificate (#2348 Inc 1): the remaining coefficient travel
6224/// to the rail limit must fall below `ASYMPTOTE_ESTIMAND_REL_TOL·(1 + ‖β‖)` for
6225/// the fitted model to be certified equal to the rail-limit fit.
6226const ASYMPTOTE_ESTIMAND_REL_TOL: f64 = 1.0e-4;
6227
6228/// Number of one-e-fold-in-`ρ` probes stepped back from a railed coordinate
6229/// toward the interior when reconstructing its exponential tail (#2348 Inc 1).
6230/// Enough to span both the finite-difference floor next to the rail (rejected)
6231/// and a confirmable-tail run further in.
6232// 18 e-folds: the window must REACH the finite-difference-clean constant-ĉ
6233// band from a coordinate railed AT the box ceiling. The fused-Hessian
6234// trajectory (#2348) rails fits at ρ=30 that previously stalled mid-box, and
6235// the #2299 fixture's clean band sits 13–16 e-folds inside — the old 12-probe
6236// window (sized for mid-box crawls) stopped one row short of it, so a fully
6237// confirmed tail declined with "no finite-difference-clean tail window". Six
6238// extra value+gradient evals, paid only at certification of railed fits.
6239const ASYMPTOTE_PROBE_COUNT: usize = 18;
6240
6241/// Local confirmation resolution used only when the one-e-fold ladder cannot
6242/// find a clean run. A finite smoothing box can intersect a perfectly regular
6243/// asymptote before three whole e-folds of the leading-order tail are visible;
6244/// half-e-fold probes resolve that band without relaxing any certificate gate.
6245///
6246/// The probes remain equally spaced, which is load-bearing: the estimand
6247/// certificate interprets consecutive coefficient moves as one geometric
6248/// sequence. Six samples cover three e-folds, so the fallback still observes
6249/// curvature over a material interval instead of manufacturing constancy from
6250/// an arbitrarily small neighborhood.
6251const ASYMPTOTE_LOCAL_PROBE_DELTA: f64 = 0.5;
6252const ASYMPTOTE_LOCAL_PROBE_COUNT: usize = 6;
6253
6254/// Read-only inputs to [`try_certify_asymptote_rail`], bundled so the certify
6255/// path passes one borrow rather than a long positional argument list.
6256struct AsymptoteRailInputs<'a> {
6257 rho: &'a Array1<f64>,
6258 projected_gradient: &'a Array1<f64>,
6259 railed: &'a [usize],
6260 /// Which slots of `rho` carry `log λ`. The active-set half of the
6261 /// certificate reads `railed` as-is; the tail law reads it through
6262 /// [`tail_law_coordinates`], which is the whole difference.
6263 layout: OuterThetaLayout,
6264 hessian: &'a Array2<f64>,
6265 bounds: &'a (Array1<f64>, Array1<f64>),
6266 terminal_beta: Option<&'a Array1<f64>>,
6267 /// The ladder bound the full problem was judged against, carrying the rung
6268 /// that set it. A gradient-magnitude rung is already in the exact projected
6269 /// residual currency used on the face. A curvature-derived rung is not:
6270 /// the interior judgment must REPLACE it with its own sub-block curvature
6271 /// bound, and the replacement's rung is what the resulting certificate —
6272 /// or refusal — must report (#2458/#2559).
6273 stationarity_bound: StationarityBound,
6274 /// The run's relative objective tolerance resolved at the certified cost —
6275 /// the same flat-valley floor the cost-stall guard and the curvature-scaled
6276 /// widening use. The tail-snap interior judgment applies the identical
6277 /// Newton-decrement criterion on the interior SUB-BLOCK (the full-Hessian
6278 /// widening is disabled exactly when a noise-corrupted tail entry makes the
6279 /// full matrix non-PD).
6280 objective_tol: f64,
6281 context: &'a str,
6282}
6283
6284impl AsymptoteRailInputs<'_> {
6285 /// Split the railed set into the coordinates the exponential tail law
6286 /// speaks for and the ones it does not (#2453).
6287 ///
6288 /// Both halves stay in the active set: they are equally deleted from the
6289 /// interior gradient and the interior Hessian sub-block, because that
6290 /// reasoning is about a bound and not about a quantity. Only the first
6291 /// half may be *certified* by — or *required* to produce — a tail, since
6292 /// [`OuterThetaLayout::coordinate_is_log_smoothing`] is what makes
6293 /// `ĉ = ∓e^{±ρ}·∂V/∂ρ` a theorem rather than an arithmetic accident.
6294 ///
6295 /// The second half needs no tail: its bound is attainable, so the
6296 /// outward-gradient complementarity the projector already enforces is a
6297 /// complete first-order certificate there.
6298 fn tail_law_coordinates(&self) -> (Vec<usize>, Vec<usize>) {
6299 self.railed
6300 .iter()
6301 .copied()
6302 .partition(|&k| self.layout.coordinate_is_log_smoothing(k))
6303 }
6304}
6305
6306/// Attempt the typed stationary-at-asymptote rail certificate (#2348 Inc 1).
6307///
6308/// Returns `Some((interior_projected_grad_norm, rails))` when the interior
6309/// (non-railed) coordinates are gradient-stationary, the interior Hessian
6310/// sub-block is PSD, and EVERY railed coordinate is certified on a confirmed
6311/// exponential tail whose fitted model has reached the rail limit to within the
6312/// estimand tolerance. Returns `None` (fall through to the generic verdict) on
6313/// any failure — a non-stationary interior, indefinite interior curvature, or
6314/// any railed coordinate whose tail is not confirmable. Never errors on a
6315/// refusal; the only `Err` is a genuinely broken objective that cannot restore
6316/// its inner state to the certified point after probing.
6317fn try_certify_asymptote_rail(
6318 obj: &mut dyn OuterObjective,
6319 inputs: &AsymptoteRailInputs<'_>,
6320) -> Result<Result<(f64, StationarityBound, Vec<RailCoordinate>), String>, EstimationError> {
6321 let rho = inputs.rho;
6322 let projected_gradient = inputs.projected_gradient;
6323 let railed = inputs.railed;
6324 // The interior (non-railed) coordinates must be stationary in their own
6325 // right: the asymptote certificate speaks only to the railed directions,
6326 // never rescues a still-descending interior. Judged by the SAME two-stage
6327 // criterion as the Inc 2c at-point mint: a gradient-magnitude bound may
6328 // judge the exact KKT-projected residual directly; a curvature-derived
6329 // bound is reminted from the interior sub-block before it may admit
6330 // anything. A fit whose remaining interior Newton step would improve the
6331 // cost by less than the loop's own cost resolution is at its interior
6332 // optimum, and the residual gradient is the deep-λ instrument noise floor
6333 // (evaluations beside a saturated rail share the rail's logdet noise).
6334 let interior_indices = interior_face_indices(projected_gradient, railed);
6335 let (interior_projected_grad_norm, effective_interior_bound) =
6336 match certify_interior_stationarity(
6337 projected_gradient,
6338 inputs.hessian,
6339 &interior_indices,
6340 inputs.stationarity_bound,
6341 inputs.objective_tol,
6342 ) {
6343 Ok(certified) => certified,
6344 Err(reason) => return Ok(Err(reason)),
6345 };
6346 // The interior sub-block (railed coordinates removed) must be admissible
6347 // curvature for a minimum. A rail-caused indefiniteness in the saturated
6348 // direction is expected and excluded; genuine interior negative curvature is
6349 // not, and refuses the certificate.
6350 // #2676: read from the objective at THIS point, exactly as the generic
6351 // verdict does. A rail certificate that judged the invariance would decline
6352 // on the same rounding residual the generic path used to refuse on, and the
6353 // two paths would disagree about one matrix.
6354 let criterion_invariance = obj.criterion_invariant_directions(rho);
6355 if certificate_hessian_is_psd_off_railed_above_gradient_floor(
6356 inputs.hessian,
6357 railed,
6358 projected_gradient,
6359 criterion_invariance.as_ref(),
6360 ) != Some(true)
6361 {
6362 return Ok(Err("interior Hessian sub-block is not PSD".to_string()));
6363 }
6364 let beta_norm = inputs
6365 .terminal_beta
6366 .map(|b| b.dot(b).sqrt())
6367 .filter(|v| v.is_finite())
6368 .unwrap_or(0.0);
6369 let estimand_tol = ASYMPTOTE_ESTIMAND_REL_TOL * (1.0 + beta_norm);
6370 let mut tol = AsymptoteTolerances::exp4_rail_bands(estimand_tol);
6371 // Real REML tails hold ĉ to ~5e-3 relative, not the exp4 synthetic
6372 // characterization's 1e-3 (measured on the #2299 fixture during Inc 2c);
6373 // the tail-snap path already certifies against the widened band, and the
6374 // railed mint must judge the SAME physical tail by the same standard.
6375 tol.tail_drift_rel = TAIL_SNAP_DRIFT_REL;
6376 let (lower, upper) = inputs.bounds;
6377
6378 // #2453: only the log-λ coordinates go to the tail law. A ψ rail stays in
6379 // the active set above (it is excluded from the interior gradient and the
6380 // interior sub-block just like any other bound-active coordinate) but is
6381 // certified by complementarity, not by an asymptote it does not have.
6382 let (tail_railed, box_railed) = inputs.tail_law_coordinates();
6383 if tail_railed.is_empty() {
6384 return Ok(Err(format!(
6385 "no railed coordinate parameterizes log λ; {} bound-active ψ coordinate(s) {:?} carry \
6386 no exponential tail to certify",
6387 box_railed.len(),
6388 box_railed,
6389 )));
6390 }
6391 if !box_railed.is_empty() {
6392 log::info!(
6393 "[CERTIFICATE] {}: {} bound-active ψ coordinate(s) {:?} held out of the tail law \
6394 (their box endpoints are attainable, so complementarity certifies them); tail law \
6395 runs on log-λ coordinate(s) {:?}",
6396 inputs.context,
6397 box_railed.len(),
6398 box_railed,
6399 tail_railed,
6400 );
6401 }
6402
6403 // #2348 Inc 5 — the ANALYTIC face proof, first resort.
6404 //
6405 // Measuring a tail beside the ρ box asks the criterion for derivative
6406 // information exactly where its logdet pair cancels; and even when it
6407 // succeeds it speaks for ONE coordinate along ONE ray. The analytic route
6408 // forms the λ=∞ limit itself — the null-space-restricted fit, and the
6409 // exact first-order form of the logdet and trace terms there — and its
6410 // positive definiteness proves the whole face against every way of coming
6411 // off it. When the objective cannot form that limit, or the proof does not
6412 // hold, the measured-tail path below is unchanged.
6413 match try_certify_face_analytically(obj, inputs, &tail_railed, estimand_tol)? {
6414 Ok((rails, proof)) => {
6415 log::info!(
6416 "[CERTIFICATE] {}: analytic λ=∞ face proof on {} coordinate(s): λ_min(C)={:.6e} \
6417 > margin {:.3e}, joint pencil ĉ={:.6e}, remaining value gap {:.3e}, estimand \
6418 travel {:.3e}",
6419 inputs.context,
6420 rails.len(),
6421 proof.min_curvature,
6422 proof.curvature_margin,
6423 proof.joint_tail_constant,
6424 proof.value_gap,
6425 proof.estimand_travel,
6426 );
6427 return Ok(Ok((
6428 interior_projected_grad_norm,
6429 effective_interior_bound,
6430 rails,
6431 )));
6432 }
6433 Err(reason) => log::info!(
6434 "[CERTIFICATE] {}: analytic λ=∞ face proof declined ({reason}); measuring the tail \
6435 instead",
6436 inputs.context,
6437 ),
6438 }
6439
6440 let mut rails: Vec<RailCoordinate> = Vec::new();
6441 let mut decline: Option<String> = None;
6442 let mut probed_any = false;
6443 for &k in tail_railed.iter() {
6444 if k >= rho.len() || k >= lower.len() || k >= upper.len() {
6445 decline = Some(format!("railed coordinate {k} outside the box layout"));
6446 break;
6447 }
6448 // Which rail: the box endpoint the coordinate sits nearest. `Upper`
6449 // (λ → ∞) probes step ρ downward into the tail; `Lower` (λ → 0) step up.
6450 let side = if (upper[k] - rho[k]).abs() <= (rho[k] - lower[k]).abs() {
6451 AsymptoteSide::Upper
6452 } else {
6453 AsymptoteSide::Lower
6454 };
6455 probed_any = true;
6456 match build_and_assess_rail_coordinate(obj, rho, k, side, &tol, (lower[k], upper[k]))? {
6457 Ok(rail) => rails.push(rail),
6458 Err(reason) => {
6459 decline = Some(reason);
6460 break;
6461 }
6462 }
6463 }
6464
6465 // The probes warm-started the inner solve away from ρ̂; restore it so the
6466 // shipped fitted state (and the ρ-uncertainty diagnostic) sees the certified
6467 // point. A failure here is a genuinely broken objective, not a refusal.
6468 if probed_any {
6469 obj.eval_cost(rho).map_err(|err| {
6470 EstimationError::RemlOptimizationFailed(format!(
6471 "{}: failed to restore the objective to the certified point after \
6472 asymptote-rail probing: {err}",
6473 inputs.context
6474 ))
6475 })?;
6476 }
6477
6478 if let Some(reason) = decline {
6479 return Ok(Err(reason));
6480 }
6481 if rails.is_empty() {
6482 return Ok(Err(
6483 "no railed coordinate produced a certifiable tail".to_string()
6484 ));
6485 }
6486 Ok(Ok((
6487 interior_projected_grad_norm,
6488 effective_interior_bound,
6489 rails,
6490 )))
6491}
6492
6493/// The analytic face law is falsified against production criterion VALUES, so
6494/// the admissible discrepancy is exactly the error sources the comparison is
6495/// made of: the run's own cost resolution (absolute, once per evaluation), the
6496/// law's `O(e^{−ρ})` second-order remainder (relative), and the digits the
6497/// Gram/Schur assembly loses. `FACE_LAW_ERROR_SLACK` widens that budget by the
6498/// factor a two-point difference accumulates — each evaluation carries its own
6499/// resolution error and the remainder enters both the predicted and the
6500/// measured side — so a CORRECT law is never refused by its own error bars,
6501/// while a wrong one (which misses by orders of magnitude, not by slack) still
6502/// is. The falsification only runs where the budget leaves a discriminating
6503/// band; where it does not, the analytic route declines rather than minting
6504/// unfalsifiable evidence.
6505const FACE_LAW_ERROR_SLACK: f64 = 4.0;
6506
6507/// Floor on the falsification band.
6508///
6509/// The derived numerical budget can land many orders below the honest fidelity
6510/// of a closed form that deliberately does not model the criterion's
6511/// stabilization ridge or its reparameterization; holding the law to that
6512/// budget would reject a CORRECT law over a difference that changes no
6513/// decision. What the falsification exists to catch is a structurally wrong law
6514/// — a missing Schur term, the wrong dispersion convention, a sign error — and
6515/// those miss by orders of magnitude, not by slack (measured on a fixture whose
6516/// released directions genuinely earned their cost: 100%). Half the predicted
6517/// drop is the coarsest band that still separates those two worlds.
6518const FACE_LAW_ORDER_BAND: f64 = 0.5;
6519
6520/// Prove the rail face analytically (#2348 Inc 5): ask the objective for the
6521/// exact λ→∞ limit, test the first-order form, and falsify the resulting law
6522/// against the production criterion before minting anything from it.
6523///
6524/// Returns the minted rail coordinates plus the proof, or a human-readable
6525/// decline that the caller logs before falling back to the measured tail.
6526fn try_certify_face_analytically(
6527 obj: &mut dyn OuterObjective,
6528 inputs: &AsymptoteRailInputs<'_>,
6529 tail_railed: &[usize],
6530 estimand_tol: f64,
6531) -> Result<Result<(Vec<RailCoordinate>, RailFaceProof), String>, EstimationError> {
6532 let rho = inputs.rho;
6533 let (lower, upper) = inputs.bounds;
6534 // The analytic limit is the INFINITE-smoothing face. A coordinate railed at
6535 // the zero-smoothing bound is the opposite limit (the penalty leaves the
6536 // model rather than pinning it) and belongs to the measured-tail path.
6537 for &k in tail_railed.iter() {
6538 if k >= rho.len() || k >= lower.len() || k >= upper.len() {
6539 return Ok(Err("railed coordinate outside the box layout".to_string()));
6540 }
6541 if (upper[k] - rho[k]).abs() > (rho[k] - lower[k]).abs() {
6542 return Ok(Err(format!(
6543 "coordinate {k} rails at the zero-smoothing bound; the analytic face limit \
6544 covers λ→∞ only"
6545 )));
6546 }
6547 }
6548 let limit = match obj.rail_face_limit(rho, tail_railed)? {
6549 RailFaceLimitOutcome::Available(limit) => *limit,
6550 // The decline is typed, and the distinction is worth carrying into the
6551 // refusal: "outside this closed form" invites a different one, while
6552 // "the face is unavailable" is a statement about the face.
6553 RailFaceLimitOutcome::OutsideClosedForm { reason } => {
6554 return Ok(Err(format!("outside the analytic closed form: {reason}")));
6555 }
6556 RailFaceLimitOutcome::FaceUnavailable { reason } => {
6557 return Ok(Err(format!("the λ=∞ face is unavailable: {reason}")));
6558 }
6559 };
6560 let proof = match certify_rail_face(&limit) {
6561 RailFaceVerdict::Certified(proof) => proof,
6562 RailFaceVerdict::Refused { reason } => return Ok(Err(reason)),
6563 };
6564 // The face being optimal does not by itself mean the SHIPPED fit is the
6565 // limit fit; the estimand gate is the same one the measured path applies,
6566 // now answered by the exact first-order coefficient offset rather than a
6567 // geometric extrapolation of observed steps.
6568 if !(proof.estimand_travel <= estimand_tol) {
6569 return Ok(Err(format!(
6570 "λ=∞ face proven, but the shipped fit has not reached it: coefficient travel \
6571 {:.3e} > estimand tolerance {estimand_tol:.3e}",
6572 proof.estimand_travel
6573 )));
6574 }
6575 if let Err(reason) = falsify_face_law(obj, inputs, &limit, &proof)? {
6576 return Ok(Err(reason));
6577 }
6578 let rails: Vec<RailCoordinate> = limit
6579 .face
6580 .iter()
6581 .zip(limit.face_rho.iter())
6582 .zip(proof.tail_constants.iter())
6583 .map(|((&index, &rho_k), &tail_constant)| RailCoordinate {
6584 index,
6585 side: AsymptoteSide::Upper,
6586 tail_constant,
6587 // On the tail law the remaining value gap of one coordinate is
6588 // exactly `c_k·e^{−ρ_k}`; a coordinate the rest of the face has
6589 // already pinned reports `c_k = 0` because releasing it does not
6590 // move the criterion at all.
6591 value_gap: tail_constant * (-rho_k).exp(),
6592 estimand_travel_bound: proof.estimand_travel,
6593 // The face was PROVEN, so the standard it cleared is the form's own
6594 // eigen-backward-error margin — not a finite-difference floor, and
6595 // not a quantity comparable with one.
6596 evidence: RailTailEvidence::AnalyticFaceProof {
6597 min_curvature: proof.min_curvature,
6598 curvature_margin: proof.curvature_margin,
6599 },
6600 })
6601 .collect();
6602 Ok(Ok((rails, proof)))
6603}
6604
6605/// Falsify the analytic face law against the production criterion.
6606///
6607/// The law predicts `V(ρ) − V_∞ = ½tr((Σλ_kQᵀS_kQ)⁻¹C)`, so pulling every face
6608/// coordinate back by `Δ` must raise the criterion by exactly
6609/// `gap·(e^{Δ} − 1)`. That is a VALUE comparison — no derivative, no
6610/// cancellation — and it costs two evaluations instead of a probe ladder.
6611///
6612/// `Δ` is not a knob: the measurement error is `resolution/(gap·(e^Δ−1))` and
6613/// the law's own remainder is `O(e^{Δ−ρ})`, so their sum is minimized at
6614/// `Δ* = ½(ln(resolution/gap) + ρ)`, where both equal `√(resolution·e^{−ρ}/gap)`.
6615fn falsify_face_law(
6616 obj: &mut dyn OuterObjective,
6617 inputs: &AsymptoteRailInputs<'_>,
6618 limit: &RailFaceLimit,
6619 proof: &RailFaceProof,
6620) -> Result<Result<(), String>, EstimationError> {
6621 const FACE_LAW_DOMAIN_MARGIN: f64 = 1.0e-6;
6622 let rho = inputs.rho;
6623 let (lower, _) = inputs.bounds;
6624 let gap = proof.value_gap;
6625 let resolution = inputs.objective_tol;
6626 if !(gap > 0.0) || !(resolution > 0.0) {
6627 return Ok(Err(format!(
6628 "the face law carries no resolvable value gap: gap={gap:.3e}, cost resolution \
6629 {resolution:.3e}"
6630 )));
6631 }
6632 let deepest = limit
6633 .face_rho
6634 .iter()
6635 .fold(f64::INFINITY, |acc, v| acc.min(*v));
6636 let ideal = 0.5 * ((resolution / gap).ln() + deepest);
6637 let room = limit
6638 .face
6639 .iter()
6640 .zip(limit.face_rho.iter())
6641 .map(|(&k, &rho_k)| rho_k - lower[k] - FACE_LAW_DOMAIN_MARGIN)
6642 .fold(f64::INFINITY, f64::min);
6643 let delta = ideal.min(room);
6644 // One e-fold is the natural unit of the law being tested; below that the
6645 // predicted change is not a statement about a tail.
6646 if !(delta >= 1.0) || !delta.is_finite() {
6647 return Ok(Err(format!(
6648 "no room inside the box to falsify the face law: Δ={delta:.3e} e-folds"
6649 )));
6650 }
6651 let predicted = gap * (delta.exp() - 1.0);
6652 let measurement_error = resolution / predicted;
6653 let remainder_error = (delta - deepest).exp();
6654 let assembly_error = f64::EPSILON.sqrt() * limit.form_conditioning.sqrt();
6655 let budget = measurement_error + remainder_error + assembly_error;
6656 let admissible = (FACE_LAW_ERROR_SLACK * budget).max(FACE_LAW_ORDER_BAND);
6657 if !(admissible < 1.0) {
6658 return Ok(Err(format!(
6659 "the face law cannot be falsified here: error budget {budget:.3e} (measurement \
6660 {measurement_error:.3e}, remainder {remainder_error:.3e}, assembly \
6661 {assembly_error:.3e}) leaves no discriminating band"
6662 )));
6663 }
6664 let baseline = obj.eval_cost(rho)?;
6665 let mut pulled_back = rho.clone();
6666 for &k in limit.face.iter() {
6667 pulled_back[k] -= delta;
6668 }
6669 let pulled_value = obj.eval_cost(&pulled_back)?;
6670 // Every exit below ships the certified point, so restore it before judging.
6671 obj.eval_cost(rho)?;
6672 if !baseline.is_finite() || !pulled_value.is_finite() {
6673 return Ok(Err(
6674 "the criterion is not finite at the falsification points".to_string()
6675 ));
6676 }
6677 let measured = pulled_value - baseline;
6678 let discrepancy = (predicted - measured).abs() / predicted;
6679 if discrepancy > admissible {
6680 return Ok(Err(format!(
6681 "the analytic face law does not reproduce the criterion: pulling the face back \
6682 {delta:.2} e-folds should raise V by {predicted:.6e}, measured {measured:.6e} \
6683 (relative {discrepancy:.3e} > admissible {admissible:.3e})"
6684 )));
6685 }
6686 Ok(Ok(()))
6687}
6688
6689/// The coordinates a stationarity residual must still account for.
6690///
6691/// A coordinate leaves the interior only when the box has genuinely pinned it:
6692/// railed AND the projection zeroed its gradient, i.e. its entire pull was the
6693/// infeasible KKT multiplier. [`project_gradient_vector`] keeps a near-bound
6694/// coordinate's INWARD component on purpose — that component is feasible
6695/// descent — so dropping the row for *every* railed coordinate discards exactly
6696/// what the projector was written to preserve.
6697///
6698/// Deleting agrees with this only when every railed gradient points strictly
6699/// outward. `matern_nu_sweep_uniform_quality_on_sin1` is the counterexample
6700/// (#2471): coordinate 3 was reported railed while `1.2018e1` of its projected
6701/// gradient survived — 66.7x the stationarity bound, and 99.99% of the reported
6702/// `|Pg|`. With it deleted the interior norm read `1.986e-1`, i.e. 1.10x the
6703/// bound, which reads as "essentially converged" at a point still carrying that
6704/// much feasible descent. The certificate refused anyway, so the number misled
6705/// the reader rather than the verdict — but the ledger built on it classified a
6706/// genuine non-convergence as a railed-coordinate accounting artifact.
6707///
6708/// Since the projection either keeps a component unchanged or zeroes it, the
6709/// norm over these indices is exactly `‖Pg‖`. Where this differs from deleting
6710/// it includes MORE residual coordinates. Curvature evidence is deliberately
6711/// recomputed on that exact set: unlike the residual norm, a Newton decrement
6712/// is not transferable across Hessian subspaces or their regularization scales.
6713pub(crate) fn interior_face_indices(
6714 projected_gradient: &Array1<f64>,
6715 railed: &[usize],
6716) -> Vec<usize> {
6717 (0..projected_gradient.len())
6718 .filter(|k| !railed.contains(k) || projected_gradient[*k] != 0.0)
6719 .collect()
6720}
6721
6722/// Interior stationarity judgment shared by the Inc 1 railed mint and the
6723/// Inc 2c at-point mint (#2348/#2559).
6724///
6725/// The supplied indices make `interior_grad_norm` the exact KKT-projected
6726/// residual of the face being judged: normally [`interior_face_indices`]
6727/// itself, and with already-proven tail coordinates removed on the Inc 2c
6728/// route. A gradient-magnitude ladder rung can therefore judge it directly.
6729/// [`StationarityBoundSource::CurvatureResolvability`] is different: its value
6730/// contains the caller's Hessian and Newton decrement, so reusing it here would
6731/// make the ordinary rail path's early comparison reduce to the caller's own
6732/// `Δpred <= objective_tol` test. It must instead be derived from `sub_h` and
6733/// `sub_g`. Exact zero is stationary without a curvature scale.
6734///
6735/// The face-local curvature path certifies only when the active-face Newton
6736/// step would improve the objective by at most `objective_tol` — the loop's
6737/// own cost resolution. Returns the bound that actually admitted the norm;
6738/// `Err` carries both caller and face-local evidence when descent remains.
6739pub(crate) fn certify_interior_stationarity(
6740 gradient: &Array1<f64>,
6741 hessian: &Array2<f64>,
6742 interior_indices: &[usize],
6743 stationarity_bound: StationarityBound,
6744 objective_tol: f64,
6745) -> Result<(f64, StationarityBound), String> {
6746 let interior_grad_norm = interior_indices
6747 .iter()
6748 .map(|&k| gradient[k] * gradient[k])
6749 .sum::<f64>()
6750 .sqrt();
6751 if interior_grad_norm <= stationarity_bound.value()
6752 && (interior_grad_norm == 0.0
6753 || !stationarity_bound.requires_face_local_derivation())
6754 {
6755 return Ok((interior_grad_norm, stationarity_bound));
6756 }
6757 let m = interior_indices.len();
6758 let mut sub_h = Array2::<f64>::zeros((m, m));
6759 let mut sub_g = Array1::<f64>::zeros(m);
6760 for (i, &ri) in interior_indices.iter().enumerate() {
6761 sub_g[i] = gradient[ri];
6762 for (j, &rj) in interior_indices.iter().enumerate() {
6763 sub_h[[i, j]] = hessian[[ri, rj]];
6764 }
6765 }
6766 match newton_predicted_decrease(&sub_h, &sub_g) {
6767 Some(predicted_decrease) if predicted_decrease.is_finite() && predicted_decrease > 0.0 => {
6768 if predicted_decrease <= objective_tol {
6769 let curvature_grad_bound =
6770 interior_grad_norm * (objective_tol / predicted_decrease).sqrt();
6771 if curvature_grad_bound.is_finite() && curvature_grad_bound >= interior_grad_norm {
6772 // The returned bound is no longer the caller's: it is the
6773 // interior sub-block's own `|Pg_int|·√(τ/Δpred)`. Carrying
6774 // the caller's rung with it would report a widened bound
6775 // under the standard that did NOT set it (#2458).
6776 return Ok((
6777 interior_grad_norm,
6778 StationarityBound::from_ladder(
6779 curvature_grad_bound,
6780 StationarityBoundSource::CurvatureResolvability,
6781 ),
6782 ));
6783 }
6784 }
6785 Err(format!(
6786 "interior not stationary: active-face |Pg|={interior_grad_norm:.3e}, \
6787 caller bound {:.3e} from {}; active-face Newton decrement \
6788 {predicted_decrease:.3e} > cost resolution {objective_tol:.3e}",
6789 stationarity_bound.value(),
6790 stationarity_bound.rung().label,
6791 ))
6792 }
6793 _ => Err(format!(
6794 "interior not stationary: active-face |Pg|={interior_grad_norm:.3e}, \
6795 caller bound {:.3e} from {}; the active-face Hessian and residual \
6796 yield no positive finite PD Newton decrement",
6797 stationarity_bound.value(),
6798 stationarity_bound.rung().label,
6799 )),
6800 }
6801}
6802
6803/// Curvature-tie acceptance band for a certify-time tail-snap candidate
6804/// (#2348 Inc 2). On the #2337 Thm 2.1 exponential tail `V = V_∞ + c·e^{∓ρ}`
6805/// the coordinate's own curvature equals its gradient magnitude EXACTLY
6806/// (`H_kk = c·e^{∓ρ} = |g_k|`, unit decay rate in ρ = log λ), so `H_kk/|g_k| ≈ 1`
6807/// is a zero-cost analytic signature separating a live tail crawl from a
6808/// genuinely unconverged curved coordinate before any probe is spent. The band
6809/// tolerates the `O(e^{∓2ρ})` next-order term and assembly round-off; the
6810/// probing confirmation is the rigorous gate.
6811const TAIL_SNAP_CURVATURE_BAND: (f64, f64) = (0.25, 4.0);
6812
6813/// Certify-time tail snap (#2348 Inc 2): when certification is about to refuse
6814/// a point whose gradient residual is carried entirely by coordinates crawling
6815/// an exponential tail TOWARD the ρ-box (the one-e-fold-per-Newton-step grind
6816/// the asymptote certificate exists to kill — the loop can exhaust its budget
6817/// strictly inside the box, where the Inc 1 railed mint can never fire),
6818/// positively confirm each such coordinate's tail from the current point and
6819/// return the point with those coordinates snapped to their box bound as an
6820/// optimization waypoint. The caller re-runs the outer search from that point;
6821/// only the resulting point is eligible for the Inc 1 rail certificate.
6822///
6823/// Refusal semantics mirror [`try_certify_asymptote_rail`]: any gate failure
6824/// returns `Ok(None)` (fall through to the ordinary refusal); the only `Err` is
6825/// a genuinely broken objective that cannot restore its inner state after
6826/// probing. Gates, in order of cost:
6827/// 1. candidate coordinates = un-railed, `|g_k|` above the stationarity bound,
6828/// positive own-curvature within [`TAIL_SNAP_CURVATURE_BAND`] of `|g_k|`
6829/// (the tail-law tie), with the rail side read from the gradient sign;
6830/// 2. the interior Hessian sub-block (railed + candidates excluded) is PSD;
6831/// 3. every candidate's tail is confirmed by the same probing engine the rail
6832/// mint uses (`CertifiedAtAsymptote` or `OnTailNotYetEquivalent`).
6833/// Outcome of a certify-time tail-snap attempt: a point already stationary on
6834/// its confirmed tail, a confirmed-tail waypoint requiring one optimization
6835/// retry (#2348 Inc 2b / #2358), or a human-readable decline reason carried
6836/// into the refusal summary.
6837#[derive(Debug)]
6838enum TailSnapOutcome {
6839 /// Every candidate's confirmed tail EXTRAPOLATES to a gradient already
6840 /// below the stationarity bound at the CURRENT point (#2348 Inc 2c): the
6841 /// coordinate is tail-stationary where it stands, and the measured local
6842 /// gradient is instrument noise (observed on the #2299 fixture: measured
6843 /// |g|=1.04e-2 at ρ=26.56 vs the clean-band extrapolation ĉ·e^{−ρ} ≈
6844 /// 1.9e-8, 100× below the bound). Mint the AsymptoteRail at this point —
6845 /// no snap, no reseed. `interior_projected_grad_norm` and the
6846 /// `effective_interior_bound` that certified it (the raw stationarity
6847 /// bound, or the interior sub-block's curvature-scaled flat-valley bound —
6848 /// the full-Hessian widening is disabled precisely because the noisy tail
6849 /// entry makes the full matrix non-PD) ride along for the certificate.
6850 TailStationaryAtPoint {
6851 rails: Vec<RailCoordinate>,
6852 interior_projected_grad_norm: f64,
6853 effective_interior_bound: StationarityBound,
6854 },
6855 /// Tails confirmed, but the current point is not already
6856 /// tail-stationary. The snapped rail point is only a waypoint: the plan
6857 /// runner must retry ONCE from it so coupled coordinates can reoptimize
6858 /// before certification.
6859 ConfirmedNeedsReseed(Array1<f64>),
6860 Declined(String),
6861}
6862
6863/// Relative drift band for the tail-snap confirmation window, wider than the
6864/// exp4 characterization band (1e-3). The snap's evidentiary strength comes
6865/// from the EXTRAPOLATED-GAP margin, not the band tightness: a 1–2% spread in
6866/// `ĉ` across the clean run moves the extrapolated remaining gradient
6867/// `ĉ·e^{∓ρ}` by the same 1–2%, immaterial against the orders-of-magnitude
6868/// margin the at-point/stationarity decisions demand — while the true tail on
6869/// a REAL fixture still carries visible sub-percent curvature contamination at
6870/// probe depth (measured on #2299: ĉ ∈ {6544, 6565, 6574} over three e-folds,
6871/// drift 4.6e-3, against a wildly swinging noise region above).
6872const TAIL_SNAP_DRIFT_REL: f64 = 1.0e-2;
6873
6874fn try_tail_snap_to_rail(
6875 obj: &mut dyn OuterObjective,
6876 inputs: &AsymptoteRailInputs<'_>,
6877) -> Result<TailSnapOutcome, EstimationError> {
6878 let rho = inputs.rho;
6879 let gradient = inputs.projected_gradient;
6880 let hessian = inputs.hessian;
6881 let (lower, upper) = inputs.bounds;
6882 let n = gradient.len();
6883 if rho.len() != n
6884 || hessian.nrows() != n
6885 || hessian.ncols() != n
6886 || lower.len() < n
6887 || upper.len() < n
6888 {
6889 return Ok(TailSnapOutcome::Declined("shape mismatch".to_string()));
6890 }
6891
6892 let mut candidates: Vec<(usize, AsymptoteSide)> = Vec::new();
6893 let mut rejected: Vec<String> = Vec::new();
6894 for k in 0..n {
6895 if inputs.railed.contains(&k) {
6896 continue;
6897 }
6898 // #2453: snapping a coordinate to its bound and declaring the fit
6899 // finished is an act the tail law authorizes and nothing else does.
6900 // For a ψ coordinate — a curvature, a log length-scale — there is no
6901 // `λ = e^ρ` behind the box, so a gradient pointing at the endpoint is
6902 // just an unfinished search along that quantity, and pinning it there
6903 // would manufacture an optimum out of an arithmetic coincidence.
6904 if !inputs.layout.coordinate_is_log_smoothing(k) {
6905 rejected.push(format!(
6906 "k={k}: psi coordinate (rho_dim={}), no exponential tail law",
6907 inputs.layout.rho_dim()
6908 ));
6909 continue;
6910 }
6911 let g_k = gradient[k];
6912 let side = match AsymptoteSide::from_gradient(g_k, inputs.stationarity_bound.value()) {
6913 Some(side) => side,
6914 None => continue,
6915 };
6916 // A tail candidate must be DEEP toward the bound its gradient points
6917 // at — within the probe span of the box. The tail law is an asymptotic
6918 // statement; a coordinate sitting many probe-spans inside the interior
6919 // (every scripted mock optimum, every ordinary unconverged fit) has no
6920 // asymptote to confirm there, and probing it would spend a dozen
6921 // objective evaluations per would-refuse certification for nothing
6922 // (breaking eval-count-asserting harnesses along the way).
6923 let probe_span = ASYMPTOTE_PROBE_COUNT as f64;
6924 let deep_enough = match side {
6925 AsymptoteSide::Upper => upper[k] - rho[k] <= probe_span,
6926 AsymptoteSide::Lower => rho[k] - lower[k] <= probe_span,
6927 };
6928 if !deep_enough {
6929 rejected.push(format!(
6930 "k={k}: ρ={:.2} more than {probe_span:.0} e-folds inside the box",
6931 rho[k]
6932 ));
6933 continue;
6934 }
6935 let h_kk = hessian[[k, k]];
6936 // The tie is judged on |H_kk|/|g_k| — MAGNITUDE only. On the exact
6937 // tail `H_kk = |g_k|` (positive), but the assembled ρ-Hessian's tail
6938 // entry is `λV_λ + λ²V_λλ`, and when the `λ²V_λλ` trace pair cancels
6939 // to roundoff in the deep-smoothing regime (the #2298 rail-cancellation
6940 // class), what survives is `λV_λ = g_k` — magnitude right, SIGN
6941 // flipped (measured on the #2299 fixture: g=-1.040e-2, H_kk=-1.018e-2,
6942 // ratio -0.979). The sign at the tail is exactly the corrupted datum,
6943 // so it cannot gate; the probing confirmation is the rigorous test.
6944 let ratio = h_kk.abs() / g_k.abs();
6945 if !(TAIL_SNAP_CURVATURE_BAND.0..=TAIL_SNAP_CURVATURE_BAND.1).contains(&ratio) {
6946 rejected.push(format!(
6947 "k={k}: g={g_k:.3e} H_kk={h_kk:.3e} |ratio|={ratio:.3e} outside tie band"
6948 ));
6949 continue;
6950 }
6951 candidates.push((k, side));
6952 }
6953 if candidates.is_empty() {
6954 return Ok(TailSnapOutcome::Declined(if rejected.is_empty() {
6955 "no super-bound coordinate".to_string()
6956 } else {
6957 format!(
6958 "no candidate passed the curvature tie ({})",
6959 rejected.join("; ")
6960 )
6961 }));
6962 }
6963
6964 // The curvature left after excluding the railed + candidate directions
6965 // must be admissible for a minimum; a genuinely indefinite interior
6966 // refuses before any probe is spent.
6967 let excluded: Vec<usize> = inputs
6968 .railed
6969 .iter()
6970 .copied()
6971 .chain(candidates.iter().map(|(k, _)| *k))
6972 .collect();
6973 let criterion_invariance = obj.criterion_invariant_directions(rho);
6974 if certificate_hessian_is_psd_off_railed_above_gradient_floor(
6975 hessian,
6976 &excluded,
6977 gradient,
6978 criterion_invariance.as_ref(),
6979 ) != Some(true)
6980 {
6981 return Ok(TailSnapOutcome::Declined(
6982 "interior Hessian sub-block not PSD".to_string(),
6983 ));
6984 }
6985
6986 let beta_norm = inputs
6987 .terminal_beta
6988 .map(|b| b.dot(b).sqrt())
6989 .filter(|v| v.is_finite())
6990 .unwrap_or(0.0);
6991 let mut tol =
6992 AsymptoteTolerances::exp4_rail_bands(ASYMPTOTE_ESTIMAND_REL_TOL * (1.0 + beta_norm));
6993 tol.tail_drift_rel = TAIL_SNAP_DRIFT_REL;
6994 let mut decline: Option<String> = None;
6995 // Rails for candidates whose confirmed tail extrapolates to an
6996 // already-below-bound gradient at the CURRENT point; when every candidate
6997 // qualifies, the point is minted where it stands (#2348 Inc 2c).
6998 let mut at_point_rails: Vec<RailCoordinate> = Vec::new();
6999 for (k, side) in &candidates {
7000 let verdict = match probe_tail_window(obj, rho, *k, *side, &tol, (lower[*k], upper[*k]))? {
7001 (Some(window), rows) => match assess_coordinate(&window, &tol) {
7002 AsymptoteVerdict::CertifiedAtAsymptote {
7003 side: assessed_side,
7004 tail_constant,
7005 estimand_travel_bound,
7006 ..
7007 } => {
7008 // Extrapolate the confirmed tail law to the current point:
7009 // the TRUE remaining gradient there, immune to the local
7010 // instrument noise the certificate measured.
7011 let extrapolated_gap = match assessed_side {
7012 AsymptoteSide::Upper => tail_constant * (-rho[*k]).exp(),
7013 AsymptoteSide::Lower => tail_constant * rho[*k].exp(),
7014 };
7015 if extrapolated_gap.is_finite()
7016 && extrapolated_gap <= inputs.stationarity_bound.value()
7017 {
7018 at_point_rails.push(RailCoordinate {
7019 index: *k,
7020 side: assessed_side,
7021 tail_constant,
7022 value_gap: extrapolated_gap,
7023 estimand_travel_bound,
7024 evidence: RailTailEvidence::ProbedTail {
7025 noise_floor: tol.tail_noise_floor,
7026 drift_band: tol.tail_drift_rel,
7027 },
7028 });
7029 }
7030 None
7031 }
7032 AsymptoteVerdict::OnTailNotYetEquivalent { .. } => None,
7033 AsymptoteVerdict::NoAsymptote { reason } => {
7034 Some(format!("{reason}; probes: {rows}"))
7035 }
7036 },
7037 (None, rows) => Some(format!(
7038 "no finite-difference-clean tail run; probes: {rows}"
7039 )),
7040 };
7041 if let Some(reason) = verdict {
7042 decline = Some(format!("candidate k={k} tail unconfirmed: {reason}"));
7043 break;
7044 }
7045 }
7046 // #2349 round 7: a multi-coordinate rail face. When a candidate's OWN
7047 // one-dimensional tail law fails and several candidates ride out together,
7048 // the marginal law is the wrong object — overlapping penalties share range
7049 // space, so a lone coordinate's gradient saturates once the others
7050 // dominate the shared term (the measured #2349 ladder swept ĉ₀ across 8
7051 // orders of magnitude). The scalar section along the joint face direction
7052 // has the ordinary exponential tail; certify THAT with the same
7053 // discipline, and mint every face coordinate from the joint law.
7054 // A face confirmed through the JOINT fallback snaps as a WAYPOINT, never a
7055 // candidate optimum: the joint law certifies the direction of the optimum,
7056 // but individual face coordinates can hold interior optima once the others
7057 // sit railed (measured on the #2349 fixture: after snapping the 5-face,
7058 // coordinate 0's own gradient crossed zero near ρ₀ ≈ 7.5 — 4.5 e-folds
7059 // inside its snapped rail — while V dropped 3.86 from the checkpoint). The
7060 // reseed retry re-descends from the snapped point with the rails free to
7061 // hold or relax; a direct re-certification there would refuse exactly that
7062 // relaxation.
7063 if decline.is_some() && candidates.len() >= 2 {
7064 let (window, joint_rows) =
7065 probe_joint_tail_window(obj, rho, &candidates, &tol, (lower, upper))?;
7066 match window.as_ref().map(|w| assess_coordinate(w, &tol)) {
7067 Some(AsymptoteVerdict::CertifiedAtAsymptote {
7068 tail_constant,
7069 estimand_travel_bound,
7070 ..
7071 }) => {
7072 // Extrapolate the joint law back to the checkpoint: the true
7073 // remaining directional gradient there, immune to the local
7074 // instrument noise. All face gradients share one sign
7075 // structure along the face, so the joint gap bounds each
7076 // coordinate's own remaining gradient.
7077 let r0 = candidates
7078 .iter()
7079 .map(|(k, side)| match side {
7080 AsymptoteSide::Upper => rho[*k],
7081 AsymptoteSide::Lower => -rho[*k],
7082 })
7083 .sum::<f64>()
7084 / candidates.len() as f64;
7085 let joint_gap = tail_constant * (-r0).exp();
7086 if joint_gap.is_finite() && joint_gap <= inputs.stationarity_bound.value() {
7087 at_point_rails = candidates
7088 .iter()
7089 .map(|(k, side)| RailCoordinate {
7090 index: *k,
7091 side: *side,
7092 tail_constant,
7093 value_gap: joint_gap,
7094 estimand_travel_bound,
7095 evidence: RailTailEvidence::ProbedTail {
7096 noise_floor: tol.tail_noise_floor,
7097 drift_band: tol.tail_drift_rel,
7098 },
7099 })
7100 .collect();
7101 }
7102 decline = None;
7103 }
7104 Some(AsymptoteVerdict::OnTailNotYetEquivalent { .. }) => {
7105 // Confirmed on the joint tail; travel not yet settled — the
7106 // face snaps/reseeds below exactly as a confirmed single
7107 // candidate would.
7108 decline = None;
7109 }
7110 Some(AsymptoteVerdict::NoAsymptote { reason }) => {
7111 // A returned window IS the law: it exists only when a
7112 // drift-band-clean, above-noise-floor, uniformly-positive
7113 // pencil-constant run of MIN_TAIL_SAMPLES was found, so the
7114 // only `NoAsymptote` reachable from it is the estimand
7115 // contraction gate — the β-steps in the retained (deep
7116 // interior) rows still move, i.e. the checkpoint is genuinely
7117 // NOT at the face limit yet (measured on the #2349 checkpoint:
7118 // ĉ settled to 34.2 over the last four probes while the crawl
7119 // was still travelling). That is the same state as
7120 // `OnTailNotYetEquivalent`: the law says WHERE the optimum is;
7121 // the snap below reoptimizes from the face, granting nothing
7122 // by itself.
7123 log::info!(
7124 "[CERTIFICATE] joint {}-coordinate face: pencil-constant run \
7125 confirmed but estimand not settled at the checkpoint \
7126 ({reason}); snapping the face for re-optimization",
7127 candidates.len(),
7128 );
7129 decline = None;
7130 }
7131 None => {
7132 decline = Some(format!(
7133 "{}; joint {}-coordinate face: no finite-difference-clean run; joint probes: {joint_rows}",
7134 decline.take().unwrap_or_default(),
7135 candidates.len(),
7136 ));
7137 }
7138 }
7139 }
7140 // The probes warm-started the inner solve away from the checkpoint; every
7141 // exit below leaves the CURRENT point as the shipped state, so restore it
7142 // before returning. A failure here is a genuinely broken objective.
7143 obj.eval_cost(rho).map_err(|err| {
7144 EstimationError::RemlOptimizationFailed(format!(
7145 "{}: failed to restore the objective to the certified point after \
7146 tail-snap probing: {err}",
7147 inputs.context
7148 ))
7149 })?;
7150 if let Some(reason) = decline {
7151 return Ok(TailSnapOutcome::Declined(reason));
7152 }
7153
7154 let interior_indices: Vec<usize> = interior_face_indices(gradient, inputs.railed)
7155 .into_iter()
7156 .filter(|k| !candidates.iter().any(|(c, _)| c == k))
7157 .collect();
7158 // #2348 Inc 2c: every candidate's confirmed tail already extrapolates
7159 // BELOW the stationarity bound at the current point — the fit is
7160 // tail-stationary where it stands and the measured local gradient is
7161 // instrument noise. Judge the interior with the shared two-stage
7162 // criterion (`certify_interior_stationarity`): the raw bound, then the
7163 // curvature-scaled flat-valley bound on the interior SUB-BLOCK (the
7164 // full-Hessian widening is unavailable here exactly because the
7165 // noise-corrupted tail entry makes the full matrix non-PD).
7166 if at_point_rails.len() == candidates.len() {
7167 if let Ok((interior_projected_grad_norm, effective_interior_bound)) =
7168 certify_interior_stationarity(
7169 gradient,
7170 hessian,
7171 &interior_indices,
7172 inputs.stationarity_bound,
7173 inputs.objective_tol,
7174 )
7175 {
7176 return Ok(TailSnapOutcome::TailStationaryAtPoint {
7177 rails: at_point_rails,
7178 interior_projected_grad_norm,
7179 effective_interior_bound,
7180 });
7181 }
7182 // Real interior descent remains: fall through to the reseed path so
7183 // one more optimizer pass polishes it.
7184 }
7185
7186 let mut snapped = rho.clone();
7187 for (k, side) in &candidates {
7188 snapped[*k] = match side {
7189 AsymptoteSide::Upper => upper[*k],
7190 AsymptoteSide::Lower => lower[*k],
7191 };
7192 }
7193
7194 // Tails confirmed, but a finite snap can change every coupled coordinate's
7195 // optimum even when its PRE-snap gradient was stationary (#2358 measured
7196 // the location-scale interior gradient jumping to 0.5). The snap proves a
7197 // direction and supplies a waypoint; only a resumed optimization and its
7198 // subsequent certificate can prove the endpoint.
7199 Ok(TailSnapOutcome::ConfirmedNeedsReseed(snapped))
7200}
7201
7202/// Reconstruct one railed coordinate's exponential tail by probing the analytic
7203/// gradient back from the rail at coarse and, when needed, local resolution;
7204/// locate the longest finite-difference-clean run (rejecting the noise floor
7205/// adjacent to the rail); and assess it against the tail law (#2348 Inc 1 /
7206/// #2337 Thm 2.1). Returns the certified [`RailCoordinate`] or `None` if no
7207/// confirmable tail is found.
7208fn build_and_assess_rail_coordinate(
7209 obj: &mut dyn OuterObjective,
7210 rho: &Array1<f64>,
7211 coord: usize,
7212 side: AsymptoteSide,
7213 tol: &AsymptoteTolerances,
7214 domain: (f64, f64),
7215) -> Result<Result<RailCoordinate, String>, EstimationError> {
7216 let window = match probe_tail_window(obj, rho, coord, side, tol, domain)? {
7217 (Some(window), _) => window,
7218 (None, rows) => {
7219 // #2450: name the floor instead of leaving the reader to find it.
7220 // The criterion ALWAYS carries the soft rho-guard barrier
7221 // (`soft_rho_guard_prior_atom`), and a `log cosh` barrier's gradient
7222 // SATURATES to `w*a` rather than decaying, so once the REML tail
7223 // falls below it, `c_hat = -e^rho * dV/drho` diverges and no tail is
7224 // observable THROUGH the guard at any box width or probe count. That
7225 // is a different failure from a noisy or truncated window, and
7226 // reporting both as "no finite-difference-clean tail window" cost
7227 // real time: it reads as a fixture/conditioning problem when it is a
7228 // property of the objective. Measured: the floor is exactly
7229 // `w*a*tanh(a*rho)` = 1.3324e-7 at rho=30 against a predicted
7230 // 1.3324e-7 (five significant figures, every coordinate).
7231 let guard_floor = crate::estimate::RHO_SOFT_PRIOR_WEIGHT
7232 * (crate::estimate::RHO_SOFT_PRIOR_SHARPNESS / crate::estimate::RHO_BOUND);
7233 // #2545: say WHICH of the two failures this is. The probe ladder now
7234 // subtracts the barrier when the objective publishes it, so a
7235 // refusal from a publishing objective is genuinely about the window;
7236 // a refusal from a non-publishing one may still be the barrier.
7237 let barrier_note = match obj
7238 .soft_rho_guard_gradient(rho)
7239 .and_then(|guard| guard.get(coord).copied())
7240 {
7241 Some(value) => format!(
7242 "this objective PUBLISHES its soft rho-guard barrier gradient \
7243 ({value:.4e} here, saturating at w*a={guard_floor:.4e} instead of \
7244 decaying) and every probe below already has it SUBTRACTED (#2545), \
7245 so the window is what failed, not the barrier"
7246 ),
7247 None => format!(
7248 "this objective publishes NO soft rho-guard barrier gradient, so if \
7249 its criterion carries the barrier the probes below still include it; \
7250 that gradient saturates at w*a={guard_floor:.4e} rather than decaying, \
7251 and any tail whose |dV/drho| is at or below that is unobservable \
7252 THROUGH the barrier rather than merely unclean (#2450/#2545)"
7253 ),
7254 };
7255 return Ok(Err(format!(
7256 "k={coord}: no finite-difference-clean tail window; {barrier_note}; \
7257 probes {rows}"
7258 )));
7259 }
7260 };
7261 match assess_coordinate(&window, tol) {
7262 AsymptoteVerdict::CertifiedAtAsymptote {
7263 side,
7264 tail_constant,
7265 value_gap,
7266 estimand_travel_bound,
7267 } => Ok(Ok(RailCoordinate {
7268 index: coord,
7269 side,
7270 tail_constant,
7271 value_gap,
7272 estimand_travel_bound,
7273 evidence: RailTailEvidence::ProbedTail {
7274 noise_floor: tol.tail_noise_floor,
7275 drift_band: tol.tail_drift_rel,
7276 },
7277 })),
7278 other => Ok(Err(format!("k={coord}: tail verdict {other:?}"))),
7279 }
7280}
7281
7282/// Detect a WRONG-RAIL coordinate (#2392): one sitting AT its ρ box bound whose
7283/// clean-band probes prove the objective strictly DECREASES as the coordinate
7284/// moves INWARD — the outer search drove it to the wrong bound. Returns the
7285/// interior ρ to reseed the coordinate at (the deepest drift-clean probe, where
7286/// `|g|` is largest and the descent is most informative) when the proof holds,
7287/// else `None`.
7288///
7289/// # Proof condition (evidence-gated; cannot launder a genuine λ→∞ / λ→0 optimum)
7290///
7291/// Probe up to [`ASYMPTOTE_PROBE_COUNT`] e-folds inward and let the FIRST
7292/// contiguous clean, drift-stable run of at least [`MIN_TAIL_SAMPLES`] decide
7293/// the local rail:
7294/// 1. above the gradient interior floor, `|g| > interior_grad_tol` (so a probe
7295/// whose gradient has decayed into finite-difference cancellation next to the
7296/// rail is excluded rather than read as a settled tail);
7297/// 2. above the pencil-constant noise floor, `|ĉ| > tail_noise_floor`, where
7298/// `ĉ = side.tail_constant(ρ, g)` uses the coordinate's ACTUAL rail side;
7299/// 3. drift-band-clean in `ĉ` within `tail_drift_rel` (the same constant-pencil
7300/// band the genuine tail uses — `run_drift_within_band` keys on `|mean|`, so a
7301/// uniformly-negative run is judged on its magnitude).
7302///
7303/// A first clean run with `ĉ < 0` proves descent AWAY from the bound and returns
7304/// its deepest point. A first clean run with `ĉ > 0` proves descent TOWARD the
7305/// bound and refuses the pull-back immediately. Deciding on the first clean run
7306/// is load-bearing: the question is the LOCAL orientation of the objective at
7307/// this rail. Continuing another fifteen expensive objective evaluations after
7308/// that proof could discover a remote sign reversal in the interior, but must
7309/// not use it to relabel a locally genuine bound as a wrong rail.
7310fn detect_wrong_rail_pullback(
7311 obj: &mut dyn OuterObjective,
7312 rho: &Array1<f64>,
7313 coord: usize,
7314 side: AsymptoteSide,
7315 tol: &AsymptoteTolerances,
7316 domain: (f64, f64),
7317) -> Result<Option<f64>, EstimationError> {
7318 const PROBE_DELTA: f64 = 1.0;
7319 const PROBE_DOMAIN_MARGIN: f64 = 1.0e-6;
7320 // Upper rail (ρ → +∞): step ρ DOWN into the interior. Lower rail: step UP.
7321 let sign = match side {
7322 AsymptoteSide::Upper => -1.0,
7323 AsymptoteSide::Lower => 1.0,
7324 };
7325 // The closest finite-difference-clean constant-pencil run is the local rail
7326 // evidence. Noise rows reset the run; a sign change starts a new candidate.
7327 let mut run_sign = 0_i8;
7328 let mut run_constants: Vec<f64> = Vec::with_capacity(MIN_TAIL_SAMPLES);
7329 for j in 1..=ASYMPTOTE_PROBE_COUNT {
7330 let stepped = rho[coord] + sign * (j as f64) * PROBE_DELTA;
7331 if stepped <= domain.0 + PROBE_DOMAIN_MARGIN || stepped >= domain.1 - PROBE_DOMAIN_MARGIN {
7332 break;
7333 }
7334 let mut probe = rho.clone();
7335 probe[coord] = stepped;
7336 let eval = match obj.eval_with_order(&probe, OuterEvalOrder::ValueAndGradient) {
7337 Ok(eval) => eval,
7338 Err(_) => break,
7339 };
7340 if !eval.cost.is_finite()
7341 || coord >= eval.gradient.len()
7342 || !eval.gradient[coord].is_finite()
7343 {
7344 break;
7345 }
7346 let gradient = eval.gradient[coord];
7347 let constant = side.tail_constant(stepped, gradient);
7348 let clean = constant.is_finite()
7349 && constant.abs() > tol.tail_noise_floor
7350 && gradient.abs() > tol.interior_grad_tol;
7351 if !clean {
7352 run_sign = 0;
7353 run_constants.clear();
7354 continue;
7355 }
7356 let constant_sign = if constant < 0.0 { -1 } else { 1 };
7357 if constant_sign != run_sign {
7358 run_sign = constant_sign;
7359 run_constants.clear();
7360 }
7361 run_constants.push(constant);
7362 if run_constants.len() > MIN_TAIL_SAMPLES {
7363 run_constants.remove(0);
7364 }
7365 if run_constants.len() >= MIN_TAIL_SAMPLES
7366 && run_drift_within_band(&run_constants, tol.tail_drift_rel)
7367 {
7368 return if run_sign < 0 {
7369 Ok(Some(stepped))
7370 } else {
7371 Ok(None)
7372 };
7373 }
7374 }
7375 Ok(None)
7376}
7377
7378/// Probe one coordinate's tail toward the interior (the shared probing engine
7379/// of [`build_and_assess_rail_coordinate`] and the certify-time tail snap).
7380/// The one-e-fold ladder runs first; if it finds no clean run, a short
7381/// half-e-fold ladder resolves a narrower local band without mixing step sizes
7382/// in one estimand window. Returns the longest finite-difference-clean
7383/// constant-`ĉ` run (newest sample nearest `rho[coord]`), or `None` when neither
7384/// resolution contains at least [`MIN_TAIL_SAMPLES`] clean rows. The second
7385/// element dumps `(ρ, ∂V/∂ρ, ĉ)` evidence for every attempted resolution.
7386fn probe_tail_window(
7387 obj: &mut dyn OuterObjective,
7388 rho: &Array1<f64>,
7389 coord: usize,
7390 side: AsymptoteSide,
7391 tol: &AsymptoteTolerances,
7392 domain: (f64, f64),
7393) -> Result<(Option<AsymptoteWindow>, String), EstimationError> {
7394 let (coarse_window, coarse_rows) = probe_tail_window_at_resolution(
7395 obj,
7396 rho,
7397 coord,
7398 side,
7399 tol,
7400 domain,
7401 (1.0, ASYMPTOTE_PROBE_COUNT),
7402 )?;
7403 if coarse_window.is_some() {
7404 return Ok((coarse_window, coarse_rows));
7405 }
7406
7407 // The coarse ladder is deliberately retained as the first pass: railed
7408 // dense REML fits can have several e-folds of cancellation noise beside
7409 // the box followed by a clean band far inside it. The local pass addresses
7410 // the complementary geometry exposed by #2358, where a modest finite box
7411 // contains a narrow but regular tail and unit steps skip over it.
7412 let (local_window, local_rows) = probe_tail_window_at_resolution(
7413 obj,
7414 rho,
7415 coord,
7416 side,
7417 tol,
7418 domain,
7419 (
7420 ASYMPTOTE_LOCAL_PROBE_DELTA,
7421 ASYMPTOTE_LOCAL_PROBE_COUNT,
7422 ),
7423 )?;
7424 Ok((
7425 local_window,
7426 format!("coarse[{coarse_rows}] local[{local_rows}]"),
7427 ))
7428}
7429
7430/// Probe a single equally-spaced resolution of one coordinate's tail.
7431///
7432/// Keeping each returned window at one resolution is essential for
7433/// [`assess_coordinate`]: its coefficient-travel bound estimates a geometric
7434/// ratio from consecutive steps, which is only meaningful when their `Δρ`
7435/// values are identical.
7436fn probe_tail_window_at_resolution(
7437 obj: &mut dyn OuterObjective,
7438 rho: &Array1<f64>,
7439 coord: usize,
7440 side: AsymptoteSide,
7441 tol: &AsymptoteTolerances,
7442 domain: (f64, f64),
7443 resolution: (f64, usize),
7444) -> Result<(Option<AsymptoteWindow>, String), EstimationError> {
7445 let (probe_delta, probe_count) = resolution;
7446 // Strictly-inside guard for probes against the probed coordinate's own box
7447 // interval (#2388). The ρ-gradient assembly freezes any coordinate at (or
7448 // within 1e-8 of) its recorded upper bound to the #197 KKT projection — a
7449 // literal 0.0 — so a probe at or past a box bound samples the frozen-axis
7450 // convention, not the criterion's tail: a fabricated hard-zero tail that
7451 // the drift band can never confirm. Out-of-box points are outside the
7452 // λ-selection domain altogether; they are not evidence for or against a
7453 // tail, so the ladder stops at the last strictly-in-domain probe.
7454 const PROBE_DOMAIN_MARGIN: f64 = 1.0e-6;
7455 // Upper rail (ρ → +∞): step ρ DOWN into the tail. Lower rail: step UP.
7456 let sign = match side {
7457 AsymptoteSide::Upper => -1.0,
7458 AsymptoteSide::Lower => 1.0,
7459 };
7460 // rows[r] corresponds to probe j=r+1: r=0 is the point CLOSEST to the rail,
7461 // increasing r steps further into the interior (larger |grad|).
7462 let mut rows: Vec<(f64, f64, Option<Array1<f64>>)> = Vec::new();
7463 for j in 1..=probe_count {
7464 let stepped = rho[coord] + sign * (j as f64) * probe_delta;
7465 if stepped <= domain.0 + PROBE_DOMAIN_MARGIN || stepped >= domain.1 - PROBE_DOMAIN_MARGIN {
7466 break;
7467 }
7468 let mut probe = rho.clone();
7469 probe[coord] = stepped;
7470 let eval = match obj.eval_with_order(&probe, OuterEvalOrder::ValueAndGradient) {
7471 Ok(eval) => eval,
7472 // A failed probe is not evidence against a tail; stop probing and
7473 // assess whatever clean run the earlier probes established.
7474 Err(_) => break,
7475 };
7476 if !eval.cost.is_finite()
7477 || coord >= eval.gradient.len()
7478 || !eval.gradient[coord].is_finite()
7479 {
7480 break;
7481 }
7482 // #2545: the tail law `ĉ = −e^ρ·∂V/∂ρ` is a statement about the
7483 // CRITERION's λ→∞ face. The objective may also carry an unconditional
7484 // `log cosh` numerical barrier whose gradient SATURATES at `w·a` rather
7485 // than decaying (`RHO_SOFT_PRIOR_*`, 1.3333e-7 at `RHO_BOUND = 30`), so
7486 // once the face's own `c·e^{−ρ}` falls below it the measured ĉ diverges
7487 // and NO coordinate can be certified at an asymptote — measured on the
7488 // #2450 fixture, the barrier is 99.999% of the ρ-gradient at ρ=30
7489 // (1.332439e-7 of 1.332521e-7) and the tail underneath it is a clean
7490 // `87.51·e^{−ρ}`. Subtract the barrier's own gradient, published by the
7491 // objective from the SAME atom that ADDED it (so it cannot drift, and so
7492 // it carries the weight anchor `ρ̃ = ρ − log g(w)` that a re-derived
7493 // `w·a·tanh(a·ρ)` would silently drop on every weighted fit). Objectives
7494 // that publish no barrier are unaffected: the subtrahend is 0.
7495 let barrier = obj
7496 .soft_rho_guard_gradient(&probe)
7497 .and_then(|guard| guard.get(coord).copied())
7498 .filter(|value| value.is_finite())
7499 .unwrap_or(0.0);
7500 rows.push((
7501 probe[coord],
7502 eval.gradient[coord] - barrier,
7503 eval.inner_beta_hint,
7504 ));
7505 }
7506 let rows_summary = rows
7507 .iter()
7508 .map(|(r, g, _)| {
7509 format!(
7510 "(ρ={r:.2}, g={g:.3e}, ĉ={:.3e})",
7511 side.tail_constant(*r, *g)
7512 )
7513 })
7514 .collect::<Vec<_>>()
7515 .join(" ");
7516 if rows.len() < MIN_TAIL_SAMPLES {
7517 return Ok((None, rows_summary));
7518 }
7519
7520 // Per-row pencil constant ĉ and the element-clean predicate: ĉ above the
7521 // noise floor AND the gradient above the interior floor (so a row adjacent to
7522 // the rail, whose gradient has decayed into finite-difference cancellation, is
7523 // excluded rather than mistaken for a settled tail).
7524 let constants: Vec<f64> = rows
7525 .iter()
7526 .map(|(r, g, _)| side.tail_constant(*r, *g))
7527 .collect();
7528 let element_clean: Vec<bool> = rows
7529 .iter()
7530 .zip(&constants)
7531 .map(|((_, g, _), c)| {
7532 c.is_finite() && *c > tol.tail_noise_floor && g.abs() > tol.interior_grad_tol
7533 })
7534 .collect();
7535
7536 // Longest contiguous run that is element-clean AND holds ĉ within the drift
7537 // band; ties broken toward the rail (smallest start) for the most settled
7538 // estimand.
7539 let mut best: Option<(usize, usize)> = None;
7540 for a in 0..rows.len() {
7541 if !element_clean[a] {
7542 continue;
7543 }
7544 for b in a..rows.len() {
7545 if !element_clean[b] {
7546 break;
7547 }
7548 if b - a + 1 < MIN_TAIL_SAMPLES {
7549 continue;
7550 }
7551 if !run_drift_within_band(&constants[a..=b], tol.tail_drift_rel) {
7552 continue;
7553 }
7554 let len = b - a + 1;
7555 match best {
7556 Some((ba, bb)) if bb - ba + 1 >= len => {}
7557 _ => best = Some((a, b)),
7558 }
7559 }
7560 }
7561 let (a, b) = match best {
7562 Some(run) => run,
7563 None => return Ok((None, rows_summary)),
7564 };
7565
7566 // Build the window oldest → newest: newest (window `latest`) is the row
7567 // CLOSEST to the rail (r=a). A sample's coefficient move is ‖β(r) − β(r+1)‖,
7568 // the step from the next-farther retained row toward the rail.
7569 let mut window = AsymptoteWindow::with_capacity(b - a + 1);
7570 for r in (a..=b).rev() {
7571 let (rho_r, grad_r, beta_r) = &rows[r];
7572 let coef_step_norm = match (beta_r, rows.get(r + 1).map(|row| &row.2)) {
7573 (Some(cur), Some(Some(farther))) if cur.len() == farther.len() => {
7574 (cur - farther).iter().map(|v| v * v).sum::<f64>().sqrt()
7575 }
7576 _ => 0.0,
7577 };
7578 window.push(AsymptoteSample {
7579 rho: *rho_r,
7580 grad: *grad_r,
7581 coef_step_norm,
7582 });
7583 }
7584
7585 Ok((Some(window), rows_summary))
7586}
7587
7588/// Probe a JOINT multi-coordinate rail face (#2349 round 7 / #2348): step every
7589/// face coordinate one e-fold toward the interior TOGETHER and assess the
7590/// directional gradient along the outward face direction against the same
7591/// exponential tail law, noise floor, and drift band as the single-coordinate
7592/// window.
7593///
7594/// Why a joint law exists where the per-coordinate laws fail: for OVERLAPPING
7595/// penalties (e.g. the multinomial per-class family's coalesced pseudo-logdet
7596/// `½log|Σ_s λ_s M_s|₊`) several λs ride to ∞ on one face and share range
7597/// space. Moving ONE coordinate down leaves the shared term dominated by the
7598/// others, so that coordinate's own gradient saturates and its per-probe pencil
7599/// constant `ĉ_k = |g_k|e^{ρ_k}` sweeps orders of magnitude — measured on the
7600/// #2349 checkpoint: ĉ₀ spanning 4.5e2 → 1.0e-6 over the ladder, an honest
7601/// refusal of a law that genuinely does not hold marginally. Along the face
7602/// direction `u` (`u_k = +1` toward an upper rail, `−1` toward a lower rail)
7603/// the shared term moves coherently and the scalar objective section
7604/// `t ↦ V(ρ + t·u)` has the ordinary one-dimensional exponential tail; its
7605/// pencil constant is assessed with the pseudo-coordinate `r = mean_k(u_k ρ_k)`
7606/// and the directional derivative `g_u = Σ_{k∈face} u_k g_k = dV/dt`.
7607///
7608/// The window it returns speaks the [`assess_coordinate`] conventions
7609/// verbatim: on a genuine face `g_u < 0` at every interior probe (descent runs
7610/// outward), so the verdict side is `Upper` in the pseudo-coordinate
7611/// regardless of the mix of physical sides, and `ĉ = −e^{r}·g_u` recovers the
7612/// joint tail constant. Per-coordinate rails minted from it keep their own
7613/// physical [`AsymptoteSide`].
7614fn probe_joint_tail_window(
7615 obj: &mut dyn OuterObjective,
7616 rho: &Array1<f64>,
7617 face: &[(usize, AsymptoteSide)],
7618 tol: &AsymptoteTolerances,
7619 bounds: (&Array1<f64>, &Array1<f64>),
7620) -> Result<(Option<AsymptoteWindow>, String), EstimationError> {
7621 const PROBE_DELTA: f64 = 1.0;
7622 const PROBE_DOMAIN_MARGIN: f64 = 1.0e-6;
7623 let (lower, upper) = bounds;
7624 // Outward unit direction of the face; probes step INWARD (−u).
7625 let direction: Vec<(usize, f64)> = face
7626 .iter()
7627 .map(|(k, side)| {
7628 (
7629 *k,
7630 match side {
7631 AsymptoteSide::Upper => 1.0,
7632 AsymptoteSide::Lower => -1.0,
7633 },
7634 )
7635 })
7636 .collect();
7637 let r0 = direction
7638 .iter()
7639 .map(|(k, u)| u * rho[*k])
7640 .sum::<f64>()
7641 / direction.len() as f64;
7642 let mut rows: Vec<(f64, f64, Option<Array1<f64>>)> = Vec::new();
7643 for j in 1..=ASYMPTOTE_PROBE_COUNT {
7644 let step = (j as f64) * PROBE_DELTA;
7645 let mut probe = rho.clone();
7646 let mut in_domain = true;
7647 for (k, u) in &direction {
7648 let stepped = rho[*k] - u * step;
7649 if stepped <= lower[*k] + PROBE_DOMAIN_MARGIN
7650 || stepped >= upper[*k] - PROBE_DOMAIN_MARGIN
7651 {
7652 in_domain = false;
7653 break;
7654 }
7655 probe[*k] = stepped;
7656 }
7657 if !in_domain {
7658 break;
7659 }
7660 let eval = match obj.eval_with_order(&probe, OuterEvalOrder::ValueAndGradient) {
7661 Ok(eval) => eval,
7662 Err(_) => break,
7663 };
7664 if !eval.cost.is_finite() {
7665 break;
7666 }
7667 let mut g_u = 0.0;
7668 let mut finite = true;
7669 for (k, u) in &direction {
7670 match eval.gradient.get(*k) {
7671 Some(g) if g.is_finite() => g_u += u * g,
7672 _ => {
7673 finite = false;
7674 break;
7675 }
7676 }
7677 }
7678 if !finite {
7679 break;
7680 }
7681 rows.push((r0 - step, g_u, eval.inner_beta_hint));
7682 }
7683 let rows_summary = rows
7684 .iter()
7685 .map(|(r, g, _)| {
7686 format!(
7687 "(r={r:.2}, dV/dt={g:.3e}, ĉ={:.3e})",
7688 AsymptoteSide::Upper.tail_constant(*r, *g)
7689 )
7690 })
7691 .collect::<Vec<_>>()
7692 .join(" ");
7693 if rows.len() < MIN_TAIL_SAMPLES {
7694 return Ok((None, rows_summary));
7695 }
7696 let constants: Vec<f64> = rows
7697 .iter()
7698 .map(|(r, g, _)| AsymptoteSide::Upper.tail_constant(*r, *g))
7699 .collect();
7700 let element_clean: Vec<bool> = rows
7701 .iter()
7702 .zip(&constants)
7703 .map(|((_, g, _), c)| {
7704 c.is_finite() && *c > tol.tail_noise_floor && g.abs() > tol.interior_grad_tol
7705 })
7706 .collect();
7707 let mut best: Option<(usize, usize)> = None;
7708 for a in 0..rows.len() {
7709 if !element_clean[a] {
7710 continue;
7711 }
7712 for b in a..rows.len() {
7713 if !element_clean[b] {
7714 break;
7715 }
7716 if b - a + 1 < MIN_TAIL_SAMPLES {
7717 continue;
7718 }
7719 if !run_drift_within_band(&constants[a..=b], tol.tail_drift_rel) {
7720 continue;
7721 }
7722 let len = b - a + 1;
7723 match best {
7724 Some((ba, bb)) if bb - ba + 1 >= len => {}
7725 _ => best = Some((a, b)),
7726 }
7727 }
7728 }
7729 let (a, b) = match best {
7730 Some(run) => run,
7731 None => return Ok((None, rows_summary)),
7732 };
7733 let mut window = AsymptoteWindow::with_capacity(b - a + 1);
7734 for r in (a..=b).rev() {
7735 let (rho_r, grad_r, beta_r) = &rows[r];
7736 let coef_step_norm = match (beta_r, rows.get(r + 1).map(|row| &row.2)) {
7737 (Some(cur), Some(Some(farther))) if cur.len() == farther.len() => {
7738 (cur - farther).iter().map(|v| v * v).sum::<f64>().sqrt()
7739 }
7740 _ => 0.0,
7741 };
7742 window.push(AsymptoteSample {
7743 rho: *rho_r,
7744 grad: *grad_r,
7745 coef_step_norm,
7746 });
7747 }
7748 Ok((Some(window), rows_summary))
7749}
7750
7751/// Whether a run of pencil constants holds constant within the relative drift
7752/// band `(max − min)/|mean| ≤ band` (deterministic, ordered).
7753fn run_drift_within_band(constants: &[f64], band: f64) -> bool {
7754 if constants.len() < MIN_TAIL_SAMPLES {
7755 return false;
7756 }
7757 let mut sum = 0.0_f64;
7758 let mut lo = f64::INFINITY;
7759 let mut hi = f64::NEG_INFINITY;
7760 for &c in constants {
7761 if !c.is_finite() {
7762 return false;
7763 }
7764 sum += c;
7765 lo = lo.min(c);
7766 hi = hi.max(c);
7767 }
7768 let mean = sum / constants.len() as f64;
7769 if !(mean.abs() > 0.0) {
7770 return false;
7771 }
7772 (hi - lo) / mean.abs() <= band
7773}
7774
7775pub(crate) fn compute_rho_uncertainty_diagnostic(
7776 obj: &mut dyn OuterObjective,
7777 config: &OuterConfig,
7778 context: &str,
7779 result: &mut OuterResult,
7780) -> crate::rho_uncertainty::RhoUncertaintyDiagnostic {
7781 let terminal_cap_guard = config
7782 .outer_inner_cap
7783 .as_ref()
7784 .map(FullFidelityInnerCapGuard::lift);
7785 // Do not reset here. The diagnostic intentionally runs before terminal
7786 // installation and certification; the certificate must remain the final
7787 // owner of objective state. Holding cap=0 ensures proposal evaluations use
7788 // terminal fidelity, and the selected point is reinstalled immediately
7789 // afterward before the mint audit.
7790 let diagnostic =
7791 compute_rho_uncertainty_diagnostic_at_terminal_fidelity(obj, config, context, result);
7792 drop(terminal_cap_guard);
7793 diagnostic
7794}
7795
7796fn compute_rho_uncertainty_diagnostic_at_terminal_fidelity(
7797 obj: &mut dyn OuterObjective,
7798 config: &OuterConfig,
7799 context: &str,
7800 result: &mut OuterResult,
7801) -> crate::rho_uncertainty::RhoUncertaintyDiagnostic {
7802 let cap = obj.capability();
7803 let layout = cap.theta_layout();
7804 let rho_dim = layout.rho_dim();
7805 let gate = crate::rho_uncertainty::RhoUncertaintyCostGate {
7806 sample_count: 32,
7807 problem_size: config.rho_uncertainty_problem_size,
7808 };
7809 if let Err(reason) = crate::rho_uncertainty::cost_gate_allows(rho_dim, gate) {
7810 return crate::rho_uncertainty::RhoUncertaintyDiagnostic::skipped(reason, 0);
7811 }
7812 if result.rho.len() != layout.n_params {
7813 return crate::rho_uncertainty::RhoUncertaintyDiagnostic::skipped(
7814 format!(
7815 "final outer point length {} does not match objective dimension {}",
7816 result.rho.len(),
7817 layout.n_params
7818 ),
7819 0,
7820 );
7821 }
7822 // The ρ-uncertainty diagnostic needs the EXACT outer Hessian, but it runs
7823 // BEFORE terminal certification so that the certificate remains the final
7824 // owner of objective state. Under the optimize-3/certify-4 protocol (#2359)
7825 // this diagnostic may therefore consume only curvature the SEARCH already
7826 // retained. It must never trigger its own `ValueGradientHessian` evaluation:
7827 // a gradient-only search has deliberately reserved that one order-four pass
7828 // for the mint gate. Such a search skips this optional diagnostic; the
7829 // terminal certificate still computes and persists exact curvature.
7830 if !cap.hessian.is_analytic() {
7831 return crate::rho_uncertainty::RhoUncertaintyDiagnostic::skipped(
7832 "outer Hessian is not analytic; rho-uncertainty diagnostic needs exact curvature",
7833 0,
7834 );
7835 }
7836 if result.plan_used.hessian_source != HessianSource::Analytic {
7837 return crate::rho_uncertainty::RhoUncertaintyDiagnostic::skipped(
7838 "search did not use exact outer curvature; order-four work is reserved for the terminal certificate",
7839 0,
7840 );
7841 }
7842
7843 let hessian = match result.final_hessian.as_ref() {
7844 Some(hessian) => hessian.clone(),
7845 None => {
7846 return crate::rho_uncertainty::RhoUncertaintyDiagnostic::skipped(
7847 "search retained no exact outer Hessian; order-four work is reserved for the terminal certificate",
7848 0,
7849 );
7850 }
7851 };
7852 if hessian.nrows() != layout.n_params || hessian.ncols() != layout.n_params {
7853 return crate::rho_uncertainty::RhoUncertaintyDiagnostic::skipped(
7854 format!(
7855 "exact outer Hessian shape {}x{} does not match objective dimension {}",
7856 hessian.nrows(),
7857 hessian.ncols(),
7858 layout.n_params
7859 ),
7860 1,
7861 );
7862 }
7863 let mut hessian_rho = Array2::<f64>::zeros((rho_dim, rho_dim));
7864 for row in 0..rho_dim {
7865 for col in 0..rho_dim {
7866 hessian_rho[[row, col]] = hessian[[row, col]];
7867 }
7868 }
7869 let rho_hat = result.rho.slice(ndarray::s![..rho_dim]).to_owned();
7870 let theta_hat = result.rho.clone();
7871 let cost_hat = result.final_value;
7872 let diagnostic = {
7873 let mut served_hat_cost = false;
7874 let mut criterion = |rho: &Array1<f64>| -> Option<f64> {
7875 let is_hat = rho.len() == rho_hat.len()
7876 && rho
7877 .iter()
7878 .zip(rho_hat.iter())
7879 .all(|(&left, &right)| left.to_bits() == right.to_bits());
7880 if is_hat && !served_hat_cost {
7881 served_hat_cost = true;
7882 return Some(cost_hat);
7883 }
7884 let mut theta = theta_hat.clone();
7885 for idx in 0..rho_dim {
7886 theta[idx] = rho[idx];
7887 }
7888 obj.eval_cost(&theta).ok()
7889 };
7890 crate::rho_uncertainty::rho_uncertainty_diagnostic(
7891 &rho_hat,
7892 &hessian_rho,
7893 gate,
7894 &mut criterion,
7895 )
7896 };
7897 match &diagnostic.status {
7898 crate::rho_uncertainty::RhoUncertaintyStatus::NoEvidenceOfHeavyTails => {
7899 log::info!(
7900 "[RHO uncertainty] {context}: no heavy-tail evidence at sampled rho proposals k_hat={:.3} evals={}",
7901 diagnostic.k_hat.unwrap_or(f64::NAN),
7902 diagnostic.n_evaluations,
7903 );
7904 }
7905 crate::rho_uncertainty::RhoUncertaintyStatus::HeavyTailsDetected { k_hat } => {
7906 log::warn!(
7907 "[RHO uncertainty] {context}: heavy rho-importance tail detected k_hat={:.3} evals={}",
7908 k_hat,
7909 diagnostic.n_evaluations,
7910 );
7911 }
7912 crate::rho_uncertainty::RhoUncertaintyStatus::Skipped { reason } => {
7913 log::info!("[RHO uncertainty] {context}: skipped ({reason})");
7914 }
7915 }
7916 diagnostic
7917}
7918
7919/// Why the operator trust-region outer loop stopped.
7920///
7921/// The inhabitants of this enum are exactly the image of
7922/// `bridges::stop_reason_from`, which is its ONE producer: a value only ever
7923/// reaches a `RhoOptimizerResult` by mapping an `opt::TerminationReason`.
7924/// Adding a variant that map cannot emit adds a label, not a state — that is
7925/// how `RoutingMismatch` ("family returned a non-operator Hessian mid-flight")
7926/// came to sit here unreachable, and it was deleted for it (#2670).
7927#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7928pub enum OperatorTrustRegionStopReason {
7929 Converged,
7930 RejectFloor,
7931 IterationBudget,
7932 /// The objective stopped changing on a criterion-flat surface. The
7933 /// in-loop guard may already have certified the score-relative residual or
7934 /// may have returned a non-stationary floor; either way the final analytic
7935 /// certificate needs this provenance to reproduce the guard's derived
7936 /// stationarity band exactly.
7937 CostStallFlatValley,
7938 /// The solver stopped because something failed, not because a test it
7939 /// stands behind was satisfied: the line search gave up, an objective
7940 /// evaluation failed, or the arithmetic went non-finite.
7941 ///
7942 /// These used to map to [`Self::Converged`], on the stated premise that
7943 /// they are "a hard failure the caller sees through the `Err` arm". That
7944 /// premise is false for the line-search case on the path it actually
7945 /// takes: `run_plan` turns a `BfgsError::LineSearchFailed` whose last
7946 /// iterate is finite into `Ok(non-converged)`, because that iterate is a
7947 /// usable checkpoint. The caller therefore sees no `Err`, and the coarse
7948 /// reason it does see said `Converged` — which is how a binomial/logit
7949 /// REML fit that never accepted a single step came to report
7950 /// `stop_reason=Converged after 1 outer iteration(s)` (#2614).
7951 ///
7952 /// Reporting-only today: no consumer branches on `Converged`, so this
7953 /// splits a label without moving a decision. Kept separate from
7954 /// [`Self::IterationBudget`] because "ran out of budget" and "could not
7955 /// take a step" call for different repairs.
7956 SolverFailure,
7957}
7958
7959/// Run the outer smoothing-parameter optimization.
7960///
7961/// This is the single entry point that replaces the scattered optimizer wiring
7962/// across estimate.rs, joint.rs, and custom_family.rs. It:
7963///
7964/// 1. Queries and canonicalizes the objective's capability declaration.
7965/// 2. Calls `plan()` to select solver + hessian source.
7966/// 3. Logs the plan and the analytic derivative capabilities it will consume.
7967/// 4. Generates seed candidates.
7968/// 5. Runs the chosen solver on candidates in heuristic order up to budget.
7969/// 6. If the configured fallback policy allows it, re-plans with degraded
7970/// capabilities chosen centrally inside outer_strategy and retries.
7971/// 7. Returns the best result (including which plan was actually used).
7972///
7973/// Do not wrap `run_outer` calls in try/catch with ad-hoc solver recovery.
7974/// Callers should declare only the primary capability and, at most, whether
7975/// automatic fallback is enabled at all.
7976///
7977/// Bound on the certify-last checkpoint-resume loop (#2273/#2374). When a
7978/// solver CLAIMS convergence but the mandatory analytic certificate refuses,
7979/// the loop re-runs the outer search seeded AT the refused checkpoint (with a
7980/// fresh metric for gradient-only outers, since `final_hessian` is `None`)
7981/// while each resume strictly reduces the objective — real descent the claim
7982/// left unexploited. #2273 introduced this as a SINGLE retry for the
7983/// stale-tolerance desync (one reseed re-anchors the in-loop tolerance to the
7984/// terminal cost scale and certifies). #2374 generalized it to a
7985/// progress-bounded loop: a gradient-only `opt::Bfgs` outer in log-λ space can
7986/// exit its flat-valley `StallPolicy` at `‖g‖∞ ≤ tol·(1 + ‖ρ‖∞)` — a gate
7987/// inflated ~10× by a railed coordinate — reporting `Ok(converged)` at a
7988/// checkpoint whose projected gradient the un-inflated certificate correctly
7989/// rejects, and a single fresh-metric reseed rarely lands the optimum in one
7990/// hop (the transformation-survival LAML of #2373 needed two). This bound caps
7991/// how many such reseeds are attempted before the honest non-convergence is
7992/// surfaced; a fit that certifies on the first pass never enters the loop, and
7993/// a reseed that fails to reduce the objective (a genuine non-stationary floor
7994/// a fresh metric cannot escape) stops the loop immediately regardless of the
7995/// remaining budget.
7996const OUTER_CERTIFY_RESUME_BUDGET: usize = 16;
7997
7998/// Max **interior** strict-saddle escape resumes (#2357/#2155/#2612).
7999///
8000/// The pathology this guards is named in #2155/#2363: a bimodal inner solve
8001/// whose warm re-descent keeps reporting a phantom improvement the cold
8002/// certificate cannot reproduce. `certify_resume_made_progress` is the loop's
8003/// own descent gate and it can be fooled by exactly that hysteresis — the warm
8004/// value looks improved — so a small cap is the backstop, and it stays.
8005///
8006/// It applies only to an escape whose reseed lands in the **interior** of the
8007/// box. Such an escape retires nothing and can in principle repeat forever, so a
8008/// count is the only bound available for it.
8009///
8010/// It does NOT apply to an escape whose reseed lands ON the box face, and that
8011/// distinction is the whole of #2612. The escape direction is exactly zero on
8012/// every railed coordinate (`judged_subspace_basis`), so the ray's box
8013/// intersection is set by a FREE coordinate: a reseed on the face has retired a
8014/// previously-free coordinate onto a rail. There are only `n` coordinates to
8015/// retire, so that escape cannot be the repeating pathology, and it is bounded
8016/// by [`OUTER_CERTIFY_RESUME_BUDGET`] like every other reseed kind.
8017///
8018/// The old value carried the premise *"a genuine saddle is cleared in one
8019/// escape"*, and #2612 measured that false: on the multinomial banded fixture
8020/// the criterion descends monotonically for six e-folds to the wall, and on
8021/// penguins four successive escapes each ran to a face
8022/// (`α_box = 9.39, 4.77, 9.14, 6.11`) while the criterion fell
8023/// `2.158034 → 2.156725`. Capping THAT by a count refuses a point the criterion
8024/// is still descending toward, one coordinate short of the corner.
8025pub(crate) const OUTER_SADDLE_ESCAPE_BUDGET: usize = 3;
8026
8027/// Roundoff-relative scale below which a certify-last reseed's objective
8028/// reduction is numerical noise rather than exploited descent (#2374). A
8029/// fresh-metric BFGS restart seeded AT the refused checkpoint can only reduce
8030/// the objective from that checkpoint, so `retried == prior` (to roundoff)
8031/// means the restart found no descent — a genuine stationary floor — while a
8032/// false flat-valley stall yields a reduction orders of magnitude above this
8033/// scale. The progress gate MUST anchor on roundoff, not the much larger
8034/// cost-stall relative floor: a flat valley crawls out in per-reseed steps far
8035/// smaller than `rel_cost·(1 + |cost|)` (the transformation-survival LAML moves
8036/// ~4e-5 relative per reseed), and gating on that coarser floor stops the crawl
8037/// after a single hop and refuses a well-posed fit.
8038const CERTIFY_RESUME_PROGRESS_REL: f64 = 32.0 * f64::EPSILON;
8039
8040pub(crate) fn run_outer(
8041 obj: &mut dyn OuterObjective,
8042 config: &OuterConfig,
8043 context: &str,
8044) -> Result<OuterResult, EstimationError> {
8045 // Permutation-invariant outer search (#1538/#1539). When the caller has
8046 // supplied per-coordinate structural keys that induce a non-identity
8047 // canonical order, run the ENTIRE outer pipeline (seeding, multistart,
8048 // optimization, and the #934 certificate / uncertainty audits) in that
8049 // canonical layout against a permuting wrapper, then map the result back to
8050 // the native layout. Seeding/tie-breaking then see byte-identical
8051 // coordinates for every term order, so both orders select the same λ̂.
8052 if let Some(keys) = config.rho_canonical_keys.as_ref()
8053 && let Some(perm) = canonical_permutation(keys)
8054 {
8055 let canonical_config = canonicalize_outer_config(config, &perm);
8056 let mut canonical_obj = CanonicalizedObjective::new(obj, perm.clone());
8057 let result = run_outer(&mut canonical_obj, &canonical_config, context)?;
8058 return Ok(outer_result_to_native(result, &perm));
8059 }
8060 let mut result = run_outer_uncertified(obj, config, context)?;
8061 if obj.begin_exact_polish() {
8062 // A sampled outer-derivative pilot is an optimization stage, never a
8063 // certifiable objective. Continue from its best checkpoint on the
8064 // family's exact full-data measure before the mandatory analytic
8065 // certificate. This transition is unconditional whenever the family
8066 // reports that a sample actually ran, so convergence before a nominal
8067 // phase budget cannot strand the optimizer on the stochastic surface
8068 // (#979: matrix-free TR stopped after 6 evaluations while the family
8069 // waited for a 12-evaluation counter).
8070 let pilot_iterations = result.iterations;
8071 let mut exact_config = config.clone();
8072 exact_config.initial_rho = Some(result.rho.clone());
8073 exact_config.heuristic_lambdas = None;
8074 exact_config.seed_config.max_seeds = 1;
8075 exact_config.seed_config.seed_budget = 1;
8076 exact_config.screen_initial_rho = false;
8077 exact_config.operator_initial_trust_radius = result.operator_trust_radius;
8078 exact_config.warm_start_outer_hessian = result.final_hessian.clone();
8079 log::info!(
8080 "[OUTER] {context}: sampled derivative pilot completed after {} iteration(s); \
8081 continuing from its checkpoint on the exact full-data measure",
8082 pilot_iterations,
8083 );
8084 let mut polished = run_outer_uncertified(obj, &exact_config, context)?;
8085 polished.iterations = polished.iterations.saturating_add(pilot_iterations);
8086 result = polished;
8087 }
8088 // Mandatory analytic optimality certificate (#934): once at the selected
8089 // point, outside every hot loop, for every solver path and every iteration
8090 // budget. Missing or failed evidence is typed non-convergence; there is no
8091 // max-iteration or logging-level bypass.
8092 //
8093 // #2273 STALE-TOLERANCE DESYNC RETRY.
8094 //
8095 // HISTORY, because the mechanism this was written for no longer exists.
8096 // The solver's in-loop threshold used to be resolved ONCE from the SEED's
8097 // cost scale (`rel_cost·(1+|seed_cost|)`) while this certificate re-derived
8098 // the same formula at the terminal point's own, often far smaller, cost. On
8099 // a perfectly-separated binomial the score plunges between the oversmoothed
8100 // heuristic seed and the first accepted step, so the solver declared
8101 // victory against a bound orders of magnitude looser than the one that then
8102 // refused it here (measured: |g|=8.1e-1 accepted in-loop vs bound 8.3e-3 at
8103 // certification, "NOT STATIONARY after 1 outer iteration"; the pass/fail
8104 // pattern was non-monotone in n because it tracked the seed-to-terminus
8105 // cost ratio, not identifiability). The retry removed that by construction:
8106 // re-seeded AT the refused checkpoint, the retry's seed cost IS the
8107 // certificate's cost.
8108 //
8109 // #2613 removed the anchor desync itself. The solver's band
8110 // (`outer_gradient_tolerance`) is now a function of the declared problem
8111 // and of nothing else, and this certificate's band
8112 // (`outer_stationarity_band_at`) is floored at it, so
8113 // `certificate_bound >= solver_bound` holds identically — a point the
8114 // solver legitimately converged at CANNOT be refused here for being above
8115 // its band. `certificate_band_never_undercuts_the_solver_band_2613` gates
8116 // that invariant directly.
8117 //
8118 // What the retry still covers is a different desync with the same shape: a
8119 // FIDELITY one. Search-time evaluations may run under the inner-PIRLS cap,
8120 // so the gradient the solver stopped on and the gradient re-measured here
8121 // at full inner fidelity are not the same number. Re-seeding at the refused
8122 // checkpoint still collapses that difference, for the same reason. Bounded
8123 // to a single retry; only fires when the solver CLAIMED convergence (a
8124 // budget-exhausted result is not a desync — its refusal is genuine).
8125 // CERTIFICATION-LAST FIT OWNERSHIP. The uncertainty diagnostic evaluates
8126 // proposal points after theta-hat and the terminal reinstallation
8127 // re-evaluates at `result.rho`, so any certificate measured BEFORE them
8128 // describes a state the caller never receives: on a nonconvex profile the
8129 // certificate-time inner mode and the finally-installed inner mode can sit
8130 // in different coefficient basins (measured on the cause-specific survival
8131 // gate as a stable bitwise mismatch, terminal 9.1931e2 vs certified
8132 // 9.1671e2, because the two paths prime the inner solve under different
8133 // eval orders). Running the diagnostic and the terminal installation
8134 // FIRST and certifying LAST makes the certificate's own evaluation the
8135 // final objective-state installer, so the sealed terminal identity fit
8136 // assembly binds against IS the certified evidence — bitwise, by
8137 // construction, independent of basin multiplicity.
8138 let certify_diagnose_and_install = |obj: &mut dyn OuterObjective,
8139 result: &mut OuterResult|
8140 -> Result<OuterCriterionCertificate, EstimationError> {
8141 result.rho_uncertainty_diagnostic = Some(compute_rho_uncertainty_diagnostic(
8142 obj, config, context, result,
8143 ));
8144 // Reinstall the selected point under cap=0 so the certificate below
8145 // measures the full-fidelity state belonging to `result.rho`, not
8146 // the diagnostic's final proposal (seeding beta alone does not
8147 // restore weights, factors, or link state). Reset forces a real
8148 // installation instead of an LRU value hit.
8149 let terminal_cap_guard = config
8150 .outer_inner_cap
8151 .as_ref()
8152 .map(FullFidelityInnerCapGuard::lift);
8153 // Reset is conditional on the cap contract, mirroring
8154 // `certify_outer_optimality`'s own doctrine: REML/mixture
8155 // objectives with a cap can hold a coarse search cache that must
8156 // not be installed as terminal state, while uncapped stateful
8157 // objectives (reactive-domain entries among them) retain the very
8158 // state their evaluation at `result.rho` depends on — an
8159 // unconditional reset here wiped it and made the certification
8160 // evaluation non-finite on the reactive fixture.
8161 //
8162 // OR-in the terminal-coefficient-mode ownership signal (#2334):
8163 // objectives that install an owned coefficient mode here but hold
8164 // their inner cap in a different field (custom families) leave
8165 // `outer_inner_cap` `None`, so the cap gate alone never fires and
8166 // `finalize` here could land in a different inner basin than the
8167 // certifying re-eval below — a spurious bitwise bind failure on a
8168 // bimodal inner solve. Forcing the reset for mode-owning objectives
8169 // makes both installations start from the same clean baseline.
8170 if terminal_cap_guard.is_some() || obj.owns_terminal_coefficient_mode() {
8171 obj.reset();
8172 }
8173 let terminal_installation = obj.finalize_outer_result(&result.rho, &result.plan_used);
8174 let terminal_inner_converged = inner_solve_converged(config.outer_inner_cap.as_ref());
8175 drop(terminal_cap_guard);
8176 terminal_installation?;
8177 if !terminal_inner_converged {
8178 return Err(outer_nonconvergence_error(
8179 context,
8180 "final outer state installation did not converge at full inner fidelity",
8181 result,
8182 result.final_grad_norm,
8183 StationarityStandard::NoComparison,
8184 ));
8185 }
8186 certify_outer_optimality(obj, config, context, result)
8187 };
8188 // Certify-last checkpoint-resume loop (#2273 stale-tolerance desync,
8189 // generalized by #2374). A solver that CLAIMS convergence but fails the
8190 // mandatory analytic certificate is re-run once per iteration seeded AT the
8191 // refused checkpoint — re-anchoring the in-loop tolerance to the terminal
8192 // cost scale and, for gradient-only outers (`final_hessian == None`),
8193 // restarting `opt::Bfgs` with a fresh inverse-Hessian metric that breaks the
8194 // flat-valley `StallPolicy` false stop the accumulated metric crawled into.
8195 // Looping (rather than the original single retry) matters because that stall
8196 // gate is inflated by `(1 + ‖ρ‖∞)` in log-λ space, so one fresh-metric
8197 // reseed rarely lands the optimum in a single hop. The loop stops the moment
8198 // certification passes; it also stops — regardless of remaining budget —
8199 // when a reseed fails to strictly reduce the objective, because a point that
8200 // a fresh-metric restart cannot improve is a genuine non-stationary floor
8201 // (or a true flat valley), not an exploitable false stall, and further
8202 // reseeds would only re-derive the same refusal. A result that never claimed
8203 // convergence (e.g. a budget-exhausted `MaxIterationsReached`) is refused
8204 // immediately with no reseed: its non-convergence is genuine.
8205 let mut resumes_remaining = OUTER_CERTIFY_RESUME_BUDGET;
8206 // INTERIOR strict-saddle escapes are bounded separately and tightly: one that
8207 // lands inside the box retires nothing, so a count is the only bound there is
8208 // for it, and a non-convergent bimodal-inner grind (#2155/#2363) is cut off
8209 // well before it exhausts the general resume budget (#2357). An escape that
8210 // lands on the box FACE has retired a free coordinate onto a rail and is
8211 // bounded by the general budget instead — see [`OUTER_SADDLE_ESCAPE_BUDGET`]
8212 // (#2612).
8213 let mut interior_saddle_escapes_remaining: usize = OUTER_SADDLE_ESCAPE_BUDGET;
8214 // #2569 — seed points this loop has already started and already had refused.
8215 // A resume changes `initial_rho` and nothing else the cascade reads, and it
8216 // runs from a reset objective, so a NON-initial seed re-entered on a later
8217 // round terminates exactly where it terminated before. Measured on the
8218 // grouped-binomial design of #2569: 17 rounds re-ran one cold lattice seed
8219 // to the identical `|g|` after the identical 42 outer iterations, 1628 s of
8220 // a 9016 s fit. Accumulated across rounds and handed to the retry so the
8221 // cascade replays the recorded verdict instead of re-deriving it.
8222 let mut refused_seed_points: Vec<Array1<f64>> = Vec::new();
8223 let certificate = loop {
8224 let claimed_converged = result.solver_claimed_convergence();
8225 match certify_diagnose_and_install(obj, &mut result) {
8226 Ok(certificate) => break certificate,
8227 Err(refusal) => {
8228 // #2357/#2155 — interior strict-saddle escape. When the refusal
8229 // is a first-order-stationary point whose reduced (off-railed)
8230 // Hessian is indefinite, the certificate publishes a
8231 // negative-curvature reseed stepped strictly BELOW the saddle
8232 // (`adjudicate_negative_curvature`). Reseeding the resume at the
8233 // refused checkpoint itself would re-descend straight back to that
8234 // zero-gradient saddle — the #2273/#2374 stale-tolerance resume
8235 // anchors the tolerance and breaks flat-valley stalls, but it
8236 // cannot break a genuine saddle. Seed at the escape point instead,
8237 // off the ridge, and start from a FRESH outer metric so the
8238 // saddle's indefinite curvature is not transferred into the
8239 // restart. This is the run_outer-level consumer of the reseed that
8240 // the multistart-loop consumer (`run_outer_with_plan`) mints only
8241 // when a per-seed claim is already stationary; the terminal
8242 // certificate is where stationarity is reached for the binomial
8243 // link-wiggle families, so without this the reseed was minted and
8244 // then dropped.
8245 let saddle_escape_reseed = result.saddle_escape_reseed.take();
8246 let resume_from_saddle_escape = saddle_escape_reseed.is_some();
8247 // Did this escape RETIRE a free coordinate onto a rail (#2612)?
8248 //
8249 // Read off the reseed rather than plumbed down from the
8250 // adjudication, because it is a property of the two points and
8251 // the box and nothing else: a coordinate the refused checkpoint
8252 // held strictly inside the box now sits on a bound. The escape
8253 // direction is exactly zero on every already-railed coordinate,
8254 // so the ray's box intersection can only be set by a free one —
8255 // which is why "landed on the face" and "retired a free
8256 // coordinate" are the same event.
8257 let saddle_escape_retires_a_coordinate =
8258 saddle_escape_reseed.as_ref().is_some_and(|reseed| {
8259 let (lower, upper) =
8260 outer_model_domain_bounds_template(config, reseed.len());
8261 (0..reseed.len()).any(|i| {
8262 let on_bound = reseed[i] <= lower[i] || reseed[i] >= upper[i];
8263 let was_interior = result
8264 .rho
8265 .get(i)
8266 .is_some_and(|held| *held > lower[i] && *held < upper[i]);
8267 on_bound && was_interior
8268 })
8269 });
8270 // #2348 Inc 2b, completed (#2349 round 8): a confirmed-tail
8271 // snap that needs a re-descent publishes the snapped face as
8272 // `tail_snap_reseed` — previously minted and then DROPPED
8273 // (declared, set, never consumed), so every ConfirmedNeedsReseed
8274 // outcome fell through to the plain refusal. The joint tail law
8275 // is first-order evidence of WHERE the optimum is, so the retry
8276 // is warranted regardless of the solver's convergence claim,
8277 // exactly like the saddle-escape reseed (measured on the #2349
8278 // fixture: the face snap descends 3.86 with |Pg| dropping
8279 // 2.05 → 0.35; the retry lets over-snapped coordinates relax
8280 // back to their interior optima while the rest hold the rail).
8281 let tail_snap_reseed = if resume_from_saddle_escape {
8282 result.tail_snap_reseed.take();
8283 None
8284 } else {
8285 result.tail_snap_reseed.take()
8286 };
8287 let resume_from_tail_snap = tail_snap_reseed.is_some();
8288 // #2392 — wrong-rail pull-back and active-set reduction reseeds,
8289 // consumed with LOWER precedence than the saddle/tail-snap
8290 // reseeds. A higher-precedence reseed DROPS them (take-and-discard)
8291 // so no stale reseed leaks into a later iteration, exactly as the
8292 // saddle escape drops a co-minted tail snap above. Both are
8293 // first-order evidence (a proven inward descent / a poisoned-rail
8294 // interior), so — like the tail snap — they fire regardless of the
8295 // solver's convergence claim.
8296 let higher_precedence_reseed = resume_from_saddle_escape || resume_from_tail_snap;
8297 let wrong_rail_reseed = if higher_precedence_reseed {
8298 result.wrong_rail_reseed.take();
8299 None
8300 } else {
8301 result.wrong_rail_reseed.take()
8302 };
8303 let resume_from_wrong_rail = wrong_rail_reseed.is_some();
8304 let active_set_reseed = if higher_precedence_reseed || resume_from_wrong_rail {
8305 result.active_set_reseed.take();
8306 None
8307 } else {
8308 result.active_set_reseed.take()
8309 };
8310 let resume_from_active_set = active_set_reseed.is_some();
8311 let active_set_rho = active_set_reseed.as_ref().map(|a| a.rho.clone());
8312 let active_set_bounds = active_set_reseed.map(|a| a.bounds);
8313 // A published reseed means the refused point IS first-order
8314 // stationary (the escape mint gate requires `is_stationary`), so
8315 // it is a genuine saddle escapable regardless of whether the
8316 // solver "claimed" convergence: for exact-Hessian link-wiggle
8317 // families the terminal certificate — not the in-loop gate — is
8318 // where stationarity is first reached, so they arrive here with
8319 // `converged == false` yet stationary. The #2273/#2374
8320 // stale-tolerance resume, which reseeds AT the refused checkpoint,
8321 // still requires a genuine convergence claim (a budget-exhausted
8322 // non-stationary iterate has no desync to remove).
8323 if (!claimed_converged
8324 && !resume_from_saddle_escape
8325 && !resume_from_tail_snap
8326 && !resume_from_wrong_rail
8327 && !resume_from_active_set)
8328 || resumes_remaining == 0
8329 || (resume_from_saddle_escape
8330 && !saddle_escape_retires_a_coordinate
8331 && interior_saddle_escapes_remaining == 0)
8332 {
8333 return Err(refusal);
8334 }
8335 resumes_remaining -= 1;
8336 if resume_from_saddle_escape && !saddle_escape_retires_a_coordinate {
8337 // An INTERIOR escape retires nothing, so it can in principle
8338 // repeat forever — a pathological objective (a bimodal inner
8339 // solve whose warm re-descent keeps reporting a phantom
8340 // improvement the cold certificate cannot reproduce, #2155 /
8341 // #2363) would otherwise burn the whole resume budget
8342 // re-escaping a family of shallow saddles that never
8343 // certifies. A count is the only bound available for that, so
8344 // the small cap stands and past it the honest refusal is
8345 // taken.
8346 //
8347 // An escape that landed on the box FACE is a different event
8348 // (#2612): it retired a previously-free coordinate onto a
8349 // rail, there are only `n` coordinates to retire, and the
8350 // criterion strictly decreased on the way. It is bounded by
8351 // `resumes_remaining` above, like every other reseed kind, and
8352 // by the descent gate below, which stops the loop the moment a
8353 // resume fails to strictly improve.
8354 interior_saddle_escapes_remaining -= 1;
8355 }
8356 let prior_iterations = result.iterations;
8357 let prior_value = result.final_value;
8358 log::info!(
8359 "[OUTER] {context}: analytic certification refused after \
8360 {prior_iterations} iteration(s) (final_value={prior_value:.6e}); re-running \
8361 seeded {} so the in-loop tolerance anchors to the terminal cost scale \
8362 ({resumes_remaining} resume(s) left after this one; #2273/#2374/#2155)",
8363 if resume_from_saddle_escape {
8364 "off the negative-curvature saddle ridge"
8365 } else if resume_from_tail_snap {
8366 "at the confirmed-tail snapped face"
8367 } else if resume_from_wrong_rail {
8368 "at the wrong-rail coordinate's clean-band interior scale"
8369 } else if resume_from_active_set {
8370 "with the poisoned rail frozen so the interior polishes in the reduced box"
8371 } else {
8372 "at the refused checkpoint"
8373 }
8374 );
8375 let mut retry_cfg = config.clone();
8376 retry_cfg.initial_rho = Some(
8377 saddle_escape_reseed
8378 .or(tail_snap_reseed)
8379 .or(wrong_rail_reseed)
8380 .or(active_set_rho)
8381 .unwrap_or_else(|| result.rho.clone()),
8382 );
8383 // Active-set reduction (#2392): the polish runs in the REDUCED
8384 // (frozen) box so the interior converges without the railed
8385 // coordinate's ill-conditioned Hessian row poisoning the step. The
8386 // loop re-certifies the polished point under the ORIGINAL box at
8387 // the top of the next iteration (`certify_outer_optimality` reads
8388 // `model_domain_bounds`, which `search_bounds_override` cannot
8389 // redefine), so the reduction can narrow the SEARCH but never the
8390 // feasible set a certificate is judged against. `config.clone()`
8391 // reset each iteration, so the frozen box never persists past
8392 // this run.
8393 //
8394 // A coordinate is only frozen here if the projector had already
8395 // zeroed it (#2454), i.e. it was on an active constraint when the
8396 // freeze was taken. That test is at freeze time deliberately: the
8397 // retry is one-shot, so there is no second reduction to undo a
8398 // wrong freeze, and the only path back off a frozen bound is the
8399 // wrong-rail pull-back, which needs a clean opposite-sign
8400 // exponential tail and declines on any coordinate without one.
8401 if let Some(frozen_bounds) = active_set_bounds {
8402 retry_cfg.search_bounds_override = Some(frozen_bounds);
8403 }
8404 retry_cfg.heuristic_lambdas = None;
8405 retry_cfg.seed_config.max_seeds = 1;
8406 retry_cfg.seed_config.seed_budget = 1;
8407 retry_cfg.screen_initial_rho = false;
8408 // `seed_budget = 1` above is NOT binding: `should_start_next_seed`
8409 // lets the cascade continue past it while nothing has certified,
8410 // and on a resume the reseeded slot-0 candidate is exactly what
8411 // failed certification, so `best` is `None` and the fall-through
8412 // fires every round on the same regenerated lattice seed (#2569).
8413 // Suppress only seeds this loop has ALREADY started and had
8414 // refused; a seed that has never run is still reachable, so the
8415 // fall-through keeps its rescue role.
8416 for point in result.refused_seed_points.iter() {
8417 if !refused_seed_points.contains(point) {
8418 refused_seed_points.push(point.clone());
8419 }
8420 }
8421 retry_cfg.previously_refused_seed_points = refused_seed_points.clone();
8422 // Every reseed kind lands at a genuinely different point, so the
8423 // refused checkpoint's metric (trust radius, outer Hessian)
8424 // must not be transferred into the restart.
8425 let fresh_metric = resume_from_saddle_escape
8426 || resume_from_tail_snap
8427 || resume_from_wrong_rail
8428 || resume_from_active_set;
8429 retry_cfg.operator_initial_trust_radius = if fresh_metric {
8430 None
8431 } else {
8432 result.operator_trust_radius
8433 };
8434 retry_cfg.warm_start_outer_hessian = if fresh_metric {
8435 None
8436 } else {
8437 result.final_hessian.clone()
8438 };
8439 obj.reset();
8440 match run_outer_uncertified(obj, &retry_cfg, context) {
8441 Ok(mut retried) => {
8442 retried.iterations = retried.iterations.saturating_add(prior_iterations);
8443 // Progress gate. A fresh-metric reseed seeded AT the
8444 // checkpoint can only descend from it, so a reduction at
8445 // roundoff scale means it found no descent — a genuine
8446 // stationary floor — while a false flat-valley stall
8447 // yields a reduction orders of magnitude larger. Gate on
8448 // roundoff (NOT the coarser cost-stall floor) so a valley
8449 // that crawls out in tiny per-reseed steps is not cut off
8450 // after one hop; stop only when a reseed truly stalls, so
8451 // the next iteration certifies the best point once more
8452 // and takes the honest refusal.
8453 let improved = certify_resume_made_progress(
8454 prior_value,
8455 retried.final_value,
8456 CERTIFY_RESUME_PROGRESS_REL,
8457 );
8458 result = retried;
8459 if !improved {
8460 resumes_remaining = 0;
8461 }
8462 }
8463 // The reseed could not even run (e.g. the checkpoint is a
8464 // hard refusal wall for the objective): surface the
8465 // certification refusal from the point we started this
8466 // iteration at, which carries the checkpoint evidence.
8467 Err(_) => return Err(refusal),
8468 }
8469 }
8470 }
8471 };
8472 result.criterion_certificate = Some(certificate);
8473 Ok(result)
8474}
8475
8476/// Build a CANONICAL-order copy of an [`OuterConfig`] for the
8477/// permutation-invariant outer search (#1538/#1539).
8478///
8479/// `perm[c]` is the native coordinate at canonical slot `c`. Every
8480/// per-coordinate config field (initial ρ seed, heuristic-λ seed, per-axis
8481/// bounds, transferred warm Hessian) is reordered native→canonical so the
8482/// optimizer's seeding and multistart operate entirely in canonical space;
8483/// scalar fields are copied verbatim. `rho_canonical_keys` is cleared so the
8484/// recursive [`run_outer`] frame runs the normal (identity-order) pipeline on
8485/// the already-canonical objective.
8486fn canonicalize_outer_config(config: &OuterConfig, perm: &[usize]) -> OuterConfig {
8487 // Permute a per-coordinate slice native→canonical; pass through any length
8488 // that does not match the permutation (defensive — should not occur).
8489 let permute_vec = |v: &[f64]| -> Vec<f64> {
8490 if v.len() == perm.len() {
8491 perm.iter().map(|&i| v[i]).collect()
8492 } else {
8493 v.to_vec()
8494 }
8495 };
8496 let permute_arr = |a: &Array1<f64>| -> Array1<f64> {
8497 if a.len() == perm.len() {
8498 Array1::from_iter(perm.iter().map(|&i| a[i]))
8499 } else {
8500 a.clone()
8501 }
8502 };
8503 let mut canonical = config.clone();
8504 canonical.rho_canonical_keys = None;
8505 if let Some(initial) = config.initial_rho.as_ref() {
8506 canonical.initial_rho = Some(permute_arr(initial));
8507 }
8508 canonical.previously_refused_seed_points = config
8509 .previously_refused_seed_points
8510 .iter()
8511 .map(permute_arr)
8512 .collect();
8513 canonical.initial_rho_candidates = config
8514 .initial_rho_candidates
8515 .iter()
8516 .map(permute_arr)
8517 .collect();
8518 if let Some(bound) = config.initial_inner_seed.as_ref() {
8519 canonical.initial_inner_seed = Some(BoundInnerSeed {
8520 theta: permute_arr(&bound.theta),
8521 beta: bound.beta.clone(),
8522 });
8523 }
8524 if let Some(h) = config.heuristic_lambdas.as_ref() {
8525 canonical.heuristic_lambdas = Some(permute_vec(h));
8526 }
8527 if let Some((lower, upper)) = config.model_domain_bounds.as_ref() {
8528 canonical.model_domain_bounds = Some((permute_arr(lower), permute_arr(upper)));
8529 }
8530 if let Some((lower, upper)) = config.search_bounds_override.as_ref() {
8531 canonical.search_bounds_override = Some((permute_arr(lower), permute_arr(upper)));
8532 }
8533 // A transferred dense outer Hessian is in native coordinate order; permute
8534 // it into canonical order so the BFGS warm metric stays aligned. (None on
8535 // the cold-start canonicalized path, so this is usually a no-op.)
8536 if let Some(h) = config.warm_start_outer_hessian.as_ref()
8537 && h.nrows() == perm.len()
8538 && h.ncols() == perm.len()
8539 {
8540 let mut hc = Array2::<f64>::zeros((perm.len(), perm.len()));
8541 for (a, &ia) in perm.iter().enumerate() {
8542 for (b, &ib) in perm.iter().enumerate() {
8543 hc[[a, b]] = h[[ia, ib]];
8544 }
8545 }
8546 canonical.warm_start_outer_hessian = Some(hc);
8547 }
8548 canonical
8549}
8550
8551/// The solver ladder behind [`run_outer`], without the #934 self-audit.
8552pub(crate) fn run_outer_uncertified(
8553 obj: &mut dyn OuterObjective,
8554 config: &OuterConfig,
8555 context: &str,
8556) -> Result<OuterResult, EstimationError> {
8557 let cap = primary_capability_for_config(obj.capability(), config, context);
8558 cap.validate_layout(context)?;
8559 // #2370: reject a degenerate / inverted ρ-box up front, as a typed error.
8560 // Every downstream stage — the per-atom EFS path below and
8561 // `run_outer_with_plan` — projects seeds against these bounds with
8562 // `f64::clamp`, whose `min > max` (or NaN) precondition panics *inside the
8563 // Rust boundary* and surfaces as an opaque `GamError: ... panicked` across
8564 // the FFI, violating the fail-loudly contract. The configured box can invert
8565 // whenever an independently-derived upper bound drifts below the lower wall
8566 // (e.g. the custom-family effective-df ceiling vs. `rho_lower_bound`).
8567 // Validating the *effective* template here — the same one every consumer
8568 // reads — turns any such inversion into `EstimationError::InvalidInput`
8569 // regardless of how the bounds were constructed.
8570 {
8571 let (model_lo, model_hi) =
8572 outer_model_domain_bounds_template(config, cap.n_params);
8573 let (bound_lo, bound_hi) = outer_search_bounds_template(config, cap.n_params);
8574 if model_lo.len() != cap.n_params
8575 || model_hi.len() != cap.n_params
8576 || bound_lo.len() != cap.n_params
8577 || bound_hi.len() != cap.n_params
8578 {
8579 return Err(EstimationError::InvalidInput(format!(
8580 "{context}: outer bound dimension mismatch: parameters={}, model_lower={}, model_upper={}, search_lower={}, search_upper={}",
8581 cap.n_params,
8582 model_lo.len(),
8583 model_hi.len(),
8584 bound_lo.len(),
8585 bound_hi.len(),
8586 )));
8587 }
8588 for i in 0..cap.n_params {
8589 if !(model_lo[i].is_finite() && model_hi[i].is_finite())
8590 || model_lo[i] > model_hi[i]
8591 {
8592 return Err(EstimationError::InvalidInput(format!(
8593 "{context}: outer model-domain bounds are invalid at coordinate {i}: lower={}, upper={}",
8594 model_lo[i], model_hi[i]
8595 )));
8596 }
8597 if bound_lo[i] < model_lo[i] || bound_hi[i] > model_hi[i] {
8598 return Err(EstimationError::InvalidInput(format!(
8599 "{context}: outer search bounds escape the model domain at coordinate {i}: model=[{}, {}], search=[{}, {}]",
8600 model_lo[i], model_hi[i], bound_lo[i], bound_hi[i]
8601 )));
8602 }
8603 if !(bound_lo[i].is_finite() && bound_hi[i].is_finite()) {
8604 return Err(EstimationError::InvalidInput(format!(
8605 "{context}: outer rho bounds are non-finite at coordinate {i}: \
8606 lower={}, upper={}",
8607 bound_lo[i], bound_hi[i]
8608 )));
8609 }
8610
8611 // Report a collapsed interval with BOTH walls. `outer_bounds` below
8612 // is the backstop and rejects the same condition, but its message
8613 // names only the coordinate. The panic this guard replaced printed
8614 // `min = -10.0, max = -11.855421656441532`, and those two numbers
8615 // are what made #2370 diagnosable from a bug report alone: they
8616 // identify WHICH pair of independently-derived bounds drifted, and
8617 // by how much. A typed error must not be a weaker diagnostic than
8618 // the panic it replaced.
8619 //
8620 // Two tests constrain this string: `inverted_rho_box_is_a_typed_
8621 // error_not_a_clamp_panic_2370` greps for the word "bound", and
8622 // `the_inverted_box_refusal_carries_both_bound_values_2370` pins
8623 // both numeric walls. Keep both when rewording.
8624 if bound_lo[i] > bound_hi[i] {
8625 return Err(EstimationError::InvalidInput(format!(
8626 "{context}: outer rho bounds are inverted at coordinate {i}: \
8627 lower bound {} exceeds upper bound {}",
8628 bound_lo[i], bound_hi[i]
8629 )));
8630 }
8631 }
8632 outer_bounds(&bound_lo, &bound_hi)
8633 .map_err(|err| EstimationError::InvalidInput(format!("{context}: {err}")))?;
8634 }
8635 if let Some(initial_rho) = config.initial_rho.as_ref() {
8636 cap.theta_layout()
8637 .validate_point_len(initial_rho, "initial outer seed")
8638 .map_err(|err| {
8639 EstimationError::fatal_objective_evaluation(
8640 format!("{context}: initial outer seed validation"),
8641 err,
8642 )
8643 })?;
8644 }
8645 // Frontier ρ-scaling auto-switch (#986): at per-atom-EFS-eligible frontier
8646 // rho dimension the decoupled per-atom fixed point is the primary outer
8647 // iteration; everything else falls through to the dense / standard path
8648 // below. Routed here so every entry point inherits it (magic by default).
8649 if let Some(result) = run_per_atom_efs_if_frontier(obj, config, context)? {
8650 if result.solver_claimed_convergence() {
8651 return Ok(result);
8652 }
8653 return Err(outer_nonconvergence_error(
8654 context,
8655 "per-atom EFS exhausted its iteration budget before the fixed-point step converged",
8656 &result,
8657 None,
8658 StationarityStandard::NoComparison,
8659 ));
8660 }
8661
8662 if cap.n_params == 0 {
8663 let cost = obj.eval_cost(&Array1::zeros(0))?;
8664 let the_plan = plan(&cap);
8665 let mut result =
8666 outer_result_with_gradient_norm(Array1::zeros(0), cost, 0, Some(0.0), true, the_plan);
8667 result.origin = OuterResultOrigin::EmptyParameterSpace;
8668 return Ok(result);
8669 }
8670
8671 // Build the ordered list of capabilities to attempt: primary first, then
8672 // any centrally-derived degraded capabilities. Aux direct-search has no
8673 // degraded ladder — a single attempt either succeeds or the failure is
8674 // surfaced to the caller.
8675 let fallback_attempts = match config.fallback_policy {
8676 FallbackPolicy::Automatic => automatic_fallback_attempts(&cap),
8677 FallbackPolicy::Disabled => Vec::new(),
8678 };
8679 let mut attempts: Vec<OuterCapability> = Vec::with_capacity(1 + fallback_attempts.len());
8680 attempts.push(cap.clone());
8681 for degraded in fallback_attempts {
8682 attempts.push(degraded);
8683 }
8684
8685 let mut last_error: Option<EstimationError> = None;
8686 let mut best_checkpoint: Option<OuterResult> = None;
8687 // A recoverable refusal at the point proposed by EFS says nothing against
8688 // the finite incumbent that proposed it. Carry that incumbent across the
8689 // plan boundary exactly once; the analytic-gradient fallback must resume
8690 // it before any unrelated seed is considered.
8691 let mut fixed_point_continuation: Option<FixedPointContinuationCheckpoint> = None;
8692 // A fixed-point walk can stop normally while its mandatory analytic
8693 // screening certificate disproves the proposed root. `run_outer_with_plan`
8694 // returns that state as `Exhausted`, not `Converged`; retain its best finite
8695 // checkpoint so the already-declared analytic-gradient fallback starts
8696 // there instead of replaying the same refuted fixed-point walk or throwing
8697 // away useful work.
8698 let mut refuted_fixed_point_continuation: Option<OuterResult> = None;
8699
8700 'plan_attempts: for (attempt_idx, attempt_cap) in attempts.iter().enumerate() {
8701 let the_plan = plan(attempt_cap);
8702 if attempt_idx > 0 {
8703 log::debug!("[OUTER] {context}: primary plan failed; falling back to {the_plan}");
8704 }
8705 log_plan(context, attempt_cap, &the_plan);
8706
8707 obj.reset();
8708
8709 let mut attempt_config = config.clone();
8710 if let Some(checkpoint) = fixed_point_continuation.take() {
8711 if !matches!(the_plan.solver, Solver::Bfgs) {
8712 return Err(EstimationError::RemlOptimizationFailed(format!(
8713 "{context}: fixed-point continuation requires analytic-gradient BFGS, \
8714 but the next declared plan is {the_plan}"
8715 )));
8716 }
8717 attempt_config.initial_rho = Some(checkpoint.point.clone());
8718 attempt_config.initial_inner_seed = checkpoint.inner_seed.clone();
8719 attempt_config.screen_initial_rho = false;
8720 // This is a mid-run finite incumbent, not a terminal certificate
8721 // imported from a prior fit. Leaving the original config's cache
8722 // provenance set could let the zero-iteration resume path accept
8723 // the checkpoint without ever running the promised BFGS polish.
8724 attempt_config.initial_rho_is_prior_terminal_certificate = false;
8725 // A transferred Hessian is bound to the prior fit's terminal rho,
8726 // not to this mid-run EFS incumbent. BFGS must rebuild curvature
8727 // from gradients at the continued point instead of combining two
8728 // different checkpoints.
8729 attempt_config.warm_start_outer_hessian = None;
8730 // The finite incumbent owns the nominal slot: try it first, without
8731 // screening or neutral-seed promotion. Preserve the caller's
8732 // absolute lattice, though. If this continuation certifies, the
8733 // ordinary seed loop stops immediately and no other start runs; if
8734 // it is refused or remains nonstationary, `should_start_next_seed`
8735 // may advance through the remaining bounded lattice until a fit
8736 // certifies. Setting `max_seeds = 1` here used to erase that recovery
8737 // authority exactly when the incumbent lay outside the criterion's
8738 // finite observed-information domain (#2653).
8739 attempt_config.seed_config.seed_budget = 1;
8740 log::info!(
8741 "[OUTER] {context}: resuming {the_plan} first from the last finite {:?} \
8742 incumbent after {} iteration(s): cost={:.6e}, |step|={:.3e}, inner_beta={}",
8743 checkpoint.plan_used.solver,
8744 checkpoint.iterations,
8745 checkpoint.sample.value,
8746 checkpoint.sample.step.dot(&checkpoint.sample.step).sqrt(),
8747 checkpoint
8748 .inner_seed
8749 .as_ref()
8750 .map_or(0, |seed| seed.beta.len()),
8751 );
8752 }
8753 if let Some(checkpoint) = refuted_fixed_point_continuation.take() {
8754 if !matches!(the_plan.solver, Solver::Bfgs) {
8755 return Err(EstimationError::RemlOptimizationFailed(format!(
8756 "{context}: an analytically refuted fixed point requires \
8757 analytic-gradient BFGS, but the next declared plan is {the_plan}"
8758 )));
8759 }
8760 attempt_config.initial_rho = Some(checkpoint.rho.clone());
8761 attempt_config.initial_inner_seed = None;
8762 attempt_config.screen_initial_rho = false;
8763 attempt_config.initial_rho_is_prior_terminal_certificate = false;
8764 attempt_config.warm_start_outer_hessian = None;
8765 // The checkpoint owns the first nominal slot. Preserve the
8766 // caller's absolute recovery lattice: if this continuation still
8767 // refuses, `should_start_next_seed` may advance until a candidate
8768 // certifies, exactly as for a rho-local EFS trial refusal.
8769 attempt_config.seed_config.seed_budget = 1;
8770 log::info!(
8771 "[OUTER] {context}: analytic screening refuted the {:?} fixed point; \
8772 resuming {the_plan} first from its best finite checkpoint after {} \
8773 iteration(s): cost={:.6e}",
8774 checkpoint.plan_used.solver,
8775 checkpoint.iterations,
8776 checkpoint.final_value,
8777 );
8778 }
8779
8780 // ARC budget-exhaustion retry: when an Arc attempt runs out of
8781 // outer iterations, reseed a fresh Arc run from the previous
8782 // attempt's last ρ and trust radius. Inner caches (PIRLS LRU,
8783 // eval bundle, warm-start predictor, adaptive signals) are wiped
8784 // by `obj.reset()`; the operator-TR's Cauchy/Newton/CG state has
8785 // no resume API and is not preserved. The lever that changes for
8786 // the resumed run is the inner-PIRLS cap (uncapped via the
8787 // feedback handle), not `max_iter` — empirically the prior stall
8788 // was an inner-tolerance / model-fidelity issue, not an outer
8789 // budget shortfall, and doubling `max_iter` only replays the
8790 // same trajectory byte-for-byte. The retry is gated on observed
8791 // `‖g‖` progress so trajectories that made no headway fall
8792 // through to the degraded plan instead of replaying.
8793 let mut arc_retries_left: u32 = if matches!(the_plan.solver, Solver::Arc) {
8794 2
8795 } else {
8796 0
8797 };
8798 let mut retry_config: Option<OuterConfig> = None;
8799 // Tracks the previous ARC attempt's terminal `‖g‖`. The retry
8800 // gate compares attempt-over-attempt: if a retry didn't move
8801 // the gradient norm, the trajectory replayed (same seed, same
8802 // trust radius, cold caches, deterministic optimizer) and
8803 // further retries cannot help. First retry is unconditional
8804 // (no prior attempt to compare against).
8805 let mut prev_attempt_grad_norm: Option<f64> = None;
8806
8807 let outcome = loop {
8808 // Bind the active config by cloning into a local owned value so
8809 // subsequent retry-config assignment does not collide with the
8810 // borrow used inside this iteration body.
8811 let active_config_owned: OuterConfig = retry_config
8812 .clone()
8813 .unwrap_or_else(|| attempt_config.clone());
8814 let active_config: &OuterConfig = &active_config_owned;
8815 match run_outer_with_plan(obj, active_config, context, attempt_cap, &the_plan, true) {
8816 Ok(PlanRunOutcome::Converged(result)) => break Ok(result),
8817 Ok(PlanRunOutcome::FirstOrderFallbackRequested(request)) => {
8818 log::debug!(
8819 "[OUTER] {context}: attempt {} (plan={the_plan}) requested a joint \
8820 first-order fallback: {}",
8821 attempt_idx + 1,
8822 request.reason(),
8823 );
8824 last_error = Some(EstimationError::RemlOptimizationFailed(
8825 request.reason().to_string(),
8826 ));
8827 continue 'plan_attempts;
8828 }
8829 Ok(PlanRunOutcome::FixedPointContinuationRequested(request)) => {
8830 let has_bfgs_fallback = attempts
8831 .get(attempt_idx + 1)
8832 .is_some_and(|next| matches!(plan(next).solver, Solver::Bfgs));
8833 if !has_bfgs_fallback {
8834 return Err(EstimationError::RemlOptimizationFailed(format!(
8835 "{context}: {:?} refused a trial after {} finite iteration(s) \
8836 at rho={} (cost={:.6e}), but no analytic-gradient BFGS \
8837 continuation is declared: {}",
8838 request.checkpoint.plan_used.solver,
8839 request.checkpoint.iterations,
8840 request.checkpoint.point,
8841 request.checkpoint.sample.value,
8842 request.refusal,
8843 )));
8844 }
8845 last_error = Some(EstimationError::RemlOptimizationFailed(format!(
8846 "{:?} continuation requested after rho-local trial refusal: {}",
8847 request.checkpoint.plan_used.solver, request.refusal,
8848 )));
8849 fixed_point_continuation = Some(request.checkpoint);
8850 continue 'plan_attempts;
8851 }
8852 Ok(PlanRunOutcome::Exhausted(result)) => {
8853 // `Exhausted` is a proof-bearing outcome: every solver
8854 // claim in this plan failed the mandatory analytic
8855 // screening certificate. A fixed-point solver may still
8856 // leave `solver_claimed_convergence == true` on the retained
8857 // checkpoint because its heuristic update was zero. Do
8858 // not collapse that checkpoint back into success below;
8859 // continue it with the analytic-gradient fallback that the
8860 // capability ladder already declared.
8861 let has_bfgs_fallback = attempts
8862 .get(attempt_idx + 1)
8863 .is_some_and(|next| matches!(plan(next).solver, Solver::Bfgs));
8864 if result.solver_claimed_convergence()
8865 && matches!(the_plan.solver, Solver::Efs | Solver::HybridEfs)
8866 && has_bfgs_fallback
8867 {
8868 log::info!(
8869 "[OUTER] {context}: {:?} stopped at a fixed point, but no \
8870 candidate passed analytic screening; continuing the best finite \
8871 checkpoint with analytic-gradient BFGS",
8872 the_plan.solver,
8873 );
8874 last_error = Some(EstimationError::RemlOptimizationFailed(format!(
8875 "{:?} fixed point was refuted by analytic screening",
8876 the_plan.solver,
8877 )));
8878 refuted_fixed_point_continuation = Some(result);
8879 continue 'plan_attempts;
8880 }
8881 if arc_retries_left == 0
8882 || matches!(
8883 result.operator_stop_reason,
8884 Some(
8885 OperatorTrustRegionStopReason::RejectFloor
8886 // #1690: a flat-valley cost-stall is a CONVERGED
8887 // cost plateau over the whole stall window, not a
8888 // budget shortfall. The ARC retry only reseeds
8889 // from the same last ρ with a reset trust radius
8890 // and the same deterministic operator state, so it
8891 // replays the identical trajectory and re-halts at
8892 // the same valley floor with the same |g| (verified
8893 // on the #1690 Gamma repro: two retries, each
8894 // returning |g|=0.3646 byte-for-byte). Treat it
8895 // like `RejectFloor` and stop — the genuine
8896 // stationarity verdict is reconciled downstream
8897 // against the authoritative shipped-β gradient
8898 // (`optimizer.rs`), and a non-stationary floor is
8899 // still reported non-converged. This skips the
8900 // wasted full-trajectory replay that dominated the
8901 // count-family slowdown.
8902 | OperatorTrustRegionStopReason::CostStallFlatValley
8903 )
8904 )
8905 {
8906 break Ok(result);
8907 }
8908 // Gate the retry on attempt-over-attempt `‖g‖`
8909 // progress. The first retry is unconditional (no
8910 // prior attempt). Subsequent retries fall through
8911 // to the degraded plan when the gradient norm did
8912 // not materially shrink — the deterministic
8913 // optimizer with the same seed and trust radius
8914 // would replay the same trajectory.
8915 let Some(cur_grad_norm) = result.final_grad_norm else {
8916 log::info!(
8917 "[OUTER] {context}: ARC attempt exhausted budget at \
8918 iter={} cost={:.6e} without a final gradient norm; \
8919 falling through to degraded plan",
8920 result.iterations,
8921 result.final_value,
8922 );
8923 break Ok(result);
8924 };
8925 if let Some(prev_g) = prev_attempt_grad_norm {
8926 // The gate's job, in its own words above, is to catch a
8927 // trajectory that "didn't move the gradient norm" — a
8928 // REPLAY: same seed, same trust radius, cold caches,
8929 // deterministic optimizer, so the retry recomputes what
8930 // the previous attempt already computed. That is
8931 // `cur >= prev`.
8932 //
8933 // It was implemented as `cur < 0.5 * prev`, which is a
8934 // HALVING requirement, and a retry that improves the
8935 // gradient by less than a factor of two was declared a
8936 // replay and threw away the rest of a two-retry budget.
8937 // Measured on gam#2735's stress fixture: `|g|` went
8938 // 1.966390e0 → 1.605910e0, an 18 % reduction — plainly
8939 // not a replay — and the ladder fell through to the
8940 // degraded plan with a retry still unspent. The retry
8941 // count (`arc_retries_left = 2`) is what bounds slow
8942 // grinding; this gate only has to tell motion from
8943 // stillness.
8944 let progressed = cur_grad_norm.is_finite()
8945 && prev_g.is_finite()
8946 && cur_grad_norm < prev_g;
8947 if !progressed {
8948 log::info!(
8949 "[OUTER] {context}: ARC retry stalled at \
8950 iter={} cost={:.6e} |g|={:.6e} (prev |g|={:.6e}, \
8951 ratio {:.4}); the retry did not reduce the gradient \
8952 at all, so deterministic replay is suspected and \
8953 further retries cannot help; falling through to \
8954 degraded plan",
8955 result.iterations,
8956 result.final_value,
8957 cur_grad_norm,
8958 prev_g,
8959 cur_grad_norm / prev_g,
8960 );
8961 break Ok(result);
8962 }
8963 log::info!(
8964 "[OUTER] {context}: ARC retry reduced the gradient \
8965 {:.6e} -> {:.6e} (ratio {:.4}); spending another of \
8966 the {} remaining retries rather than reading slow \
8967 progress as a replay",
8968 prev_g,
8969 cur_grad_norm,
8970 cur_grad_norm / prev_g,
8971 arc_retries_left,
8972 );
8973 }
8974 let next_trust_radius =
8975 sanitized_operator_trust_restart_radius(result.operator_trust_radius);
8976 log::info!(
8977 "[OUTER] {context}: ARC attempt exhausted budget at \
8978 iter={} cost={:.6e} |g|={:.6e}; resuming from last \
8979 rho + trust_radius={:?}, inner-PIRLS uncapped \
8980 (objective caches wiped; operator-TR Cauchy/Newton \
8981 state is not resumable)",
8982 result.iterations,
8983 result.final_value,
8984 cur_grad_norm,
8985 next_trust_radius,
8986 );
8987 // Snapshot the cap-feedback handle before we
8988 // reassign `retry_config` (which currently backs
8989 // `active_config`'s borrow). `InnerProgressFeedback`
8990 // is an Arc-wrapper bundle, so the clone is cheap.
8991 let cap_feedback = active_config.outer_inner_cap.clone();
8992 let mut next = active_config.clone();
8993 prev_attempt_grad_norm = Some(cur_grad_norm);
8994 next.initial_rho = Some(result.rho.clone());
8995 next.operator_initial_trust_radius = next_trust_radius;
8996 retry_config = Some(next);
8997 arc_retries_left -= 1;
8998 obj.reset();
8999 // Lift any inner-PIRLS cap for the resumed run. The
9000 // schedule's cold-start ladder (3/5/10) would
9001 // re-coarsen exactly the inner solves whose tolerance
9002 // is suspected to have starved the prior trajectory.
9003 // The next outer iter consumes ρ near a near-stationary
9004 // point where exact β / gradient / Hessian is the
9005 // load-bearing input to the operator-TR geometry.
9006 if let Some(feedback) = cap_feedback.as_ref() {
9007 feedback.cap.store(0, Ordering::Relaxed);
9008 }
9009 }
9010 Err(e) => break Err(e),
9011 }
9012 };
9013
9014 match outcome {
9015 Ok(result) => {
9016 if result.solver_claimed_convergence() {
9017 return Ok(result);
9018 }
9019
9020 let improves_checkpoint = result.final_value.is_finite()
9021 && best_checkpoint.as_ref().is_none_or(|checkpoint| {
9022 !checkpoint.final_value.is_finite()
9023 || result.final_value < checkpoint.final_value
9024 });
9025 if improves_checkpoint {
9026 best_checkpoint = Some(result);
9027 }
9028
9029 let message = format!(
9030 "{context}: attempt {} (plan={the_plan}) exhausted without convergence",
9031 attempt_idx + 1
9032 );
9033 log::debug!("[OUTER] {message}; trying degraded fallback plan");
9034 last_error = Some(EstimationError::RemlOptimizationFailed(message));
9035 }
9036 Err(e) => {
9037 if e.is_fatal_outer_evaluation() {
9038 return Err(e);
9039 }
9040 log::debug!(
9041 "[OUTER] {context}: attempt {} (plan={the_plan}) failed: {e}",
9042 attempt_idx + 1
9043 );
9044 last_error = Some(e);
9045 }
9046 }
9047 }
9048
9049 if let Some(checkpoint) = best_checkpoint {
9050 // The solver ladder produced no result that its OWN internal
9051 // (raw-gradient) convergence test accepted — but that test cannot see a
9052 // railed or already-stationary optimum. At a smoothing parameter railed to
9053 // the ρ box floor (λ→0, e.g. an exact linear fit or a separated smooth),
9054 // the RAW gradient stays large along the railed axis — it "wants" to push
9055 // past the boundary — so the solver reports non-convergence and can take
9056 // zero steps, even though the KKT-PROJECTED gradient (which zeroes
9057 // outward-railed axes) is stationary and no feasible step reduces the
9058 // objective. Only the mandatory analytic certificate in `run_outer`
9059 // computes that projected gradient AND the curvature-scaled flat-valley
9060 // bound (½·gᵀH⁻¹g ≤ objective_tol), so IT, not this raw-gradient ladder, is
9061 // the sole authority on stationarity. Hand it the best finite checkpoint:
9062 // `certify_outer_optimality` mints iff the point is genuinely stationary
9063 // (interior, railed, or flat-valley) and returns typed non-convergence
9064 // otherwise, so a truly divergent fit is still rejected there.
9065 return Ok(checkpoint);
9066 }
9067
9068 Err(last_error.unwrap_or_else(|| {
9069 EstimationError::RemlOptimizationFailed(format!("all plan attempts exhausted ({context})"))
9070 }))
9071}
9072
9073// ─── Frontier ρ-scaling auto-switch (issue #986) ─────────────────────────
9074//
9075// ARD-per-atom assigns one smoothing coordinate per dictionary atom, so the
9076// ρ-vector reaches 10^4–10^5 coordinates. A dense outer quasi-Newton over that
9077// materializes an O(K²) Hessian and is impossible at scale. When the ρ-dimension
9078// is frontier-scale AND every coordinate is penalty-like with a working
9079// fixed-point hook, route the PRIMARY outer iteration to the per-atom decoupled
9080// EFS path (`crate::estimate::reml::per_atom_efs`) instead of the dense
9081// ARC/BFGS lane. The decision is auto-derived from the coordinate count alone —
9082// there is no flag — and it is additive: the dense path is unchanged for small K
9083// and for any objective that is not per-atom-EFS-eligible.
9084
9085/// Whether this capability is in the frontier ρ-scaling regime where the
9086/// per-atom decoupled EFS primary should take over from the dense outer.
9087///
9088/// Delegates the eligibility decision to
9089/// [`crate::estimate::reml::per_atom_efs::per_atom_efs_eligible`], which
9090/// requires all-penalty-like coordinates, a working `eval_efs` hook,
9091/// fixed-point not disabled, and a frontier-scale ρ-dimension. This is the
9092/// single auto-switch predicate; `plan` keeps selecting the
9093/// dense or standard-EFS solver for everything below the frontier threshold.
9094pub fn is_per_atom_efs_frontier(cap: &OuterCapability) -> bool {
9095 crate::estimate::reml::per_atom_efs::per_atom_efs_eligible(cap)
9096}
9097
9098/// Auto-switch entry point: when `cap` is frontier-scale per-atom-EFS-eligible,
9099/// run the per-atom decoupled EFS primary and return its [`OuterResult`];
9100/// otherwise return `Ok(None)` so the caller falls through to the existing dense
9101/// / standard-EFS path via [`OuterProblem::run`] / [`run_outer`].
9102///
9103/// Builds the same bounded seed and tolerance/budget the standard plan path
9104/// uses, picks the seed (initial-ρ if supplied, else the first generated
9105/// candidate — the per-atom fixed point is a contraction near the optimum and
9106/// does not need the multi-seed cascade the dense path runs for its non-convex
9107/// quasi-Newton surface), then drives the per-atom EFS loop. The shared-border
9108/// topology defaults to disjoint (every atom owns a private penalty block — the
9109/// common ARD-per-atom case); callers with a known arrow-border overlap can run
9110/// the module's `run_per_atom_efs` directly with a populated
9111/// `SharedBorderTopology`.
9112///
9113/// Additive: this function neither mutates nor bypasses the dense path; it is
9114/// the pre-dispatch shortcut [`run_outer`] calls before the dense ladder.
9115pub(crate) fn run_per_atom_efs_if_frontier(
9116 obj: &mut dyn OuterObjective,
9117 config: &OuterConfig,
9118 context: &str,
9119) -> Result<Option<OuterResult>, EstimationError> {
9120 let cap = primary_capability_for_config(obj.capability(), config, context);
9121 cap.validate_layout(context)?;
9122 if !is_per_atom_efs_frontier(&cap) {
9123 return Ok(None);
9124 }
9125
9126 let the_plan = plan(&cap);
9127 let rho_dim = cap.theta_layout().rho_dim();
9128
9129 let model_domain_bounds = outer_model_domain_bounds_template(config, cap.n_params);
9130 crate::estimate::reml::outer_eval::record_current_outer_rho_model_upper_bounds_for_ift(
9131 &model_domain_bounds.1,
9132 );
9133 let (lower, upper) = outer_search_bounds_template(config, cap.n_params);
9134
9135 // Seed: cache/explicit initial ρ if present, otherwise the first generated
9136 // candidate. The per-atom multiplicative fixed point is locally
9137 // contractive, so a single seed suffices; the heavy multi-seed cascade
9138 // exists for the dense quasi-Newton's non-convex surface, not for EFS.
9139 let seed = match config.initial_rho.as_ref() {
9140 Some(initial) if initial.len() == cap.n_params => initial.clone(),
9141 _ => {
9142 let generated = crate::seeding::generate_rho_candidates(
9143 cap.n_params,
9144 config.heuristic_lambdas.as_deref(),
9145 &config.seed_config,
9146 )?;
9147 match generated.into_iter().next() {
9148 Some(first) => first,
9149 None => Array1::<f64>::zeros(cap.n_params),
9150 }
9151 }
9152 };
9153
9154 log::info!(
9155 "[OUTER] {context}: frontier ρ-scaling (rho_dim={rho_dim}) → per-atom decoupled EFS primary"
9156 );
9157
9158 let pa_cfg = crate::estimate::reml::per_atom_efs::PerAtomEfsConfig::new(
9159 config.tolerance,
9160 config.max_iter,
9161 lower,
9162 upper,
9163 );
9164 let topology = crate::estimate::reml::per_atom_efs::SharedBorderTopology::disjoint(rho_dim);
9165
9166 obj.reset();
9167 install_matching_initial_inner_seed(obj, config, &seed, context)?;
9168 let result =
9169 crate::estimate::reml::per_atom_efs::run_per_atom_efs(obj, &seed, &pa_cfg, &topology)?;
9170 Ok(Some(result.into_outer_result(the_plan)))
9171}
9172
9173#[cfg(test)]
9174#[path = "inverted_rho_box_tests.rs"]
9175mod inverted_rho_box_tests;
9176
9177pub(crate) fn outer_bounds(lo: &Array1<f64>, hi: &Array1<f64>) -> Result<Bounds, EstimationError> {
9178 Bounds::new(lo.clone(), hi.clone(), 1e-6).map_err(|err| {
9179 EstimationError::InvalidInput(format!("outer rho bounds are invalid: {err}"))
9180 })
9181}
9182
9183pub(crate) fn outer_model_domain_bounds_template(
9184 config: &OuterConfig,
9185 n: usize,
9186) -> (Array1<f64>, Array1<f64>) {
9187 config.model_domain_bounds.clone().unwrap_or_else(|| {
9188 (
9189 Array1::<f64>::from_elem(n, -config.rho_bound),
9190 Array1::<f64>::from_elem(n, config.rho_bound),
9191 )
9192 })
9193}
9194
9195pub(crate) fn outer_search_bounds_template(
9196 config: &OuterConfig,
9197 n: usize,
9198) -> (Array1<f64>, Array1<f64>) {
9199 config
9200 .search_bounds_override
9201 .clone()
9202 .unwrap_or_else(|| outer_model_domain_bounds_template(config, n))
9203}
9204
9205/// Intersect typed objective-domain faces with the caller's declared model
9206/// domain. The resulting box is the immutable feasible set used by every
9207/// stationarity certificate. Algorithmic active-set reduction is represented
9208/// separately by `search_bounds_override` and cannot redefine this domain.
9209pub(super) fn install_objective_domain(
9210 config: &mut OuterConfig,
9211 n_params: usize,
9212 objective_lower: Option<Array1<f64>>,
9213 objective_upper: Option<Array1<f64>>,
9214) -> Result<(), EstimationError> {
9215 let (mut lower, mut upper) = outer_model_domain_bounds_template(config, n_params);
9216 if lower.len() != n_params || upper.len() != n_params {
9217 return Err(EstimationError::InvalidInput(format!(
9218 "outer configured bounds dimension mismatch: parameters={n_params}, lower={}, upper={}",
9219 lower.len(),
9220 upper.len(),
9221 )));
9222 }
9223 if let Some(domain) = objective_lower.as_ref()
9224 && domain.len() != n_params
9225 {
9226 return Err(EstimationError::InvalidInput(format!(
9227 "outer objective-domain lower-bound dimension mismatch: parameters={n_params}, lower={}",
9228 domain.len()
9229 )));
9230 }
9231 if let Some(domain) = objective_upper.as_ref()
9232 && domain.len() != n_params
9233 {
9234 return Err(EstimationError::InvalidInput(format!(
9235 "outer objective-domain upper-bound dimension mismatch: parameters={n_params}, upper={}",
9236 domain.len()
9237 )));
9238 }
9239 for index in 0..n_params {
9240 if let Some(domain) = objective_lower.as_ref() {
9241 let value = domain[index];
9242 if !value.is_finite() {
9243 return Err(EstimationError::InvalidInput(format!(
9244 "outer objective-domain lower bound[{index}] must be finite; got {value}"
9245 )));
9246 }
9247 lower[index] = lower[index].max(value);
9248 }
9249 if let Some(domain) = objective_upper.as_ref() {
9250 let value = domain[index];
9251 if !value.is_finite() {
9252 return Err(EstimationError::InvalidInput(format!(
9253 "outer objective-domain upper bound[{index}] must be finite; got {value}"
9254 )));
9255 }
9256 upper[index] = upper[index].min(value);
9257 }
9258 if !(lower[index].is_finite() && upper[index].is_finite() && lower[index] < upper[index]) {
9259 return Err(EstimationError::InvalidInput(format!(
9260 "outer objective-domain intersection is empty or non-finite at coordinate {index}: lower={}, upper={}",
9261 lower[index], upper[index]
9262 )));
9263 }
9264 }
9265 config.model_domain_bounds = Some((lower, upper));
9266 config.search_bounds_override = None;
9267 Ok(())
9268}
9269
9270pub(crate) fn outer_tolerance(value: f64) -> Result<Tolerance, EstimationError> {
9271 Tolerance::new(value)
9272 .map_err(|err| EstimationError::InvalidInput(format!("outer tolerance is invalid: {err}")))
9273}
9274
9275/// The relative cost floor shared by the cost-stall guard, the curvature-scaled
9276/// flat-valley certificate, and the certify-last resume progress gate: nothing
9277/// tighter than what the in-loop stall detector already proved about the
9278/// surface. `rel_cost_tolerance` when set, else a small fraction of the absolute
9279/// tolerance, never below `COST_STALL_REL_TOL_FLOOR`.
9280pub(crate) fn outer_rel_cost_floor(config: &OuterConfig) -> f64 {
9281 config
9282 .rel_cost_tolerance
9283 .unwrap_or(config.tolerance * 1.0e-2)
9284 .max(COST_STALL_REL_TOL_FLOOR)
9285}
9286
9287/// Whether a certify-last checkpoint reseed (#2273/#2374) exploited real descent.
9288///
9289/// A reseed that does not strictly reduce the outer objective past the shared
9290/// relative cost floor `rel_cost_floor·(1 + min(|prior|, |retried|))` is at a
9291/// genuine non-stationary floor (or a true flat valley) a fresh metric cannot
9292/// escape, so the resume loop must stop rather than spend its remaining budget
9293/// re-deriving the same refusal. Anchoring the floor on the SMALLER of the two
9294/// costs keeps a tiny uphill wobble from a metric restart from reading as
9295/// progress, and a non-finite retried value is never progress.
9296pub(crate) fn certify_resume_made_progress(
9297 prior_value: f64,
9298 retried_value: f64,
9299 rel_cost_floor: f64,
9300) -> bool {
9301 let floor = rel_cost_floor * (1.0 + prior_value.abs().min(retried_value.abs()));
9302 retried_value.is_finite() && retried_value < prior_value - floor
9303}
9304
9305/// The user-requested outer precision, expressed relative to the criterion's
9306/// magnitude — the mgcv `magic` rule `‖g‖ ≤ τ·(1 + |V|)`.
9307///
9308/// This is a CONVERGENCE tolerance, not a resolution floor: at the default
9309/// `τ = 1e-5` it sits ~670× above the arithmetic floor `√ε`. What it needs from
9310/// the caller is `|V|`, and the whole content of #2613 is *which* `|V|`.
9311#[inline]
9312pub(crate) fn outer_cost_relative_tolerance(config: &OuterConfig) -> f64 {
9313 config.rel_cost_tolerance.unwrap_or(config.tolerance)
9314}
9315
9316/// The arithmetic resolution of the declared objective scale.
9317///
9318/// A matrix-factorization REML/LAML score cannot resolve relative perturbations
9319/// below the forward-error scale √ε. Requiring a smaller absolute residual made
9320/// gradient-only / operator-curvature objectives impossible to certify unless
9321/// an unrelated Hessian or probe-noise rescue happened to be available (#2269).
9322#[inline]
9323fn outer_arithmetic_gradient_floor(config: &OuterConfig) -> f64 {
9324 config
9325 .objective_scale
9326 .map(|scale| config.tolerance.max(scale * f64::EPSILON.sqrt()))
9327 .unwrap_or(config.tolerance)
9328}
9329
9330/// The stationarity band handed to the SOLVER, and to the cost-stall guard's
9331/// stationarity gate.
9332///
9333/// It is a function of the DECLARED problem and of nothing else. `opt` resolves
9334/// a `GradientTolerance` exactly once, at run start, against the seed cost —
9335/// so handing it a `rel_cost` component makes the band a function of where a
9336/// seed happened to land. On #2392's exponentially stiff recovery that produced
9337/// an eighteen-order spread across the seeds of ONE fit: a generated lattice
9338/// seed at `ρ = 1.0`, where the criterion is `1.79e13`, gave
9339///
9340/// ```text
9341/// termination=gradient_tolerance(|g|=1.522998e-4 < 1.792397e8)
9342/// ```
9343///
9344/// i.e. the solver claimed convergence on the wrong rail against a threshold no
9345/// gradient can fail. A stationarity test that depends on the starting point is
9346/// not a stationarity test: two seeds converging to the same optimum must reach
9347/// the same verdict.
9348///
9349/// So the cost-relative term is anchored on `objective_scale` — a property of
9350/// the data (`n_obs` on the routes that set it), fixed for the whole fit. On
9351/// those routes this is magnitude-preserving, because a REML/LAML score is a
9352/// sum over `n` rows and `1 + |V| = O(n)` is what `1 + scale` already says.
9353///
9354/// When no scale is declared, gam does not know the criterion's magnitude, and
9355/// the honest band is the absolute tolerance the caller asked for. It does NOT
9356/// silently substitute a trajectory point for the thing it does not know. The
9357/// consequence — an outer loop that keeps stepping past the point a
9358/// score-relative band would have stopped at — is bounded by the cost-stall
9359/// guard, whose own score-relative rung
9360/// (`flat_valley_converged_grad_bound(best_value)`) is anchored at the BEST
9361/// iterate and is therefore the correctly-anchored version of the same idea.
9362///
9363/// The certificate keeps the point-anchored form, which is what mgcv means:
9364/// see [`outer_stationarity_band_and_rung_at`].
9365pub(crate) fn outer_gradient_tolerance(config: &OuterConfig) -> GradientTolerance {
9366 let mut abs = outer_engine_gradient_band(config);
9367 // #2568 -- a caller's requirement is the one input to this band that may
9368 // TIGHTEN it. Everything above widens: the arithmetic floor and the
9369 // scale-relative rung both exist to stop the optimizer chasing digits the
9370 // criterion cannot resolve. A caller asking for `|Pg| <= 1e-3` on a fit
9371 // whose sealed band is `1.0` is asking the search to keep working, so the
9372 // requirement enters HERE and not only at the certificate -- surfacing the
9373 // number without letting it drive the search moves the disappointment later
9374 // without changing the answer.
9375 //
9376 // `min`, never `max`: a requirement looser than the engine's own band is not
9377 // a request for anything, and honouring it would let a caller *weaken* a
9378 // standard the engine derived from the criterion's resolution.
9379 if let Some(required) = config.required_projected_gradient_norm {
9380 abs = abs.min(required);
9381 }
9382 GradientTolerance {
9383 abs,
9384 rel_initial_grad: None,
9385 // Never delegated: `opt`'s only anchor is the seed. See above.
9386 rel_cost: None,
9387 projected: true,
9388 }
9389}
9390
9391/// The engine's own declared band, BEFORE any caller requirement caps it.
9392///
9393/// Split out of [`outer_gradient_tolerance`] for #2688: a rung cannot tell
9394/// "the engine decided this" from "the caller decided this" out of a number in
9395/// which the two have already been `min`-ed together.
9396fn outer_engine_gradient_band(config: &OuterConfig) -> f64 {
9397 let mut abs = outer_arithmetic_gradient_floor(config);
9398 if let Some(scale) = config.objective_scale {
9399 abs = abs.max(outer_cost_relative_tolerance(config) * (1.0 + scale));
9400 }
9401 abs
9402}
9403
9404/// A certificate band together with the rung that produced it (#2688).
9405///
9406/// The band is decided three ways and every one of them used to reach the call
9407/// site as a bare `f64` that was then labelled `SolverBand` unconditionally.
9408/// Returning the pair is the invariant [`StationarityBound::from_ladder`]
9409/// already enforces one level up, pushed down to where the number is decided,
9410/// so no `SolverBand` literal is left at the call site for a fourth branch to
9411/// drift past.
9412#[derive(Debug, Clone, Copy)]
9413pub(crate) struct CertificateBandAt {
9414 /// The band the certificate applies.
9415 pub(crate) bound: f64,
9416 /// Which of the three inputs produced [`Self::bound`].
9417 pub(crate) source: StationarityBoundSource,
9418 /// What the ENGINE would have applied with no caller requirement, reported
9419 /// beside `bound` so a reader can judge whether the requirement was
9420 /// reasonable. Equal to [`Self::bound`] unless the cap bound.
9421 pub(crate) engine_bound: f64,
9422 /// The rung that produced [`Self::engine_bound`]. Never `CallerRequirement`.
9423 pub(crate) engine_source: StationarityBoundSource,
9424}
9425
9426/// The stationarity band a CERTIFICATE applies at the point it is judging.
9427///
9428/// Same formula, correct anchor: `cost_at_point` is the criterion value of the
9429/// candidate optimum, which is what the mgcv `magic` rule means by `f₀` and
9430/// what every consumer of a certificate reads the bound as. Unlike the solver's
9431/// band this one is resolved per point, so it costs nothing to anchor it right.
9432///
9433/// Floored at the SOLVER's band by construction. A certificate tighter than the
9434/// threshold the optimizer was told to reach manufactures the "solver claimed
9435/// convergence, certificate refused" family out of nothing but a disagreement
9436/// between two spellings of one tolerance: the solver stops exactly where it
9437/// was asked to and the certificate then declares the stop illegitimate. The
9438/// certificate may be LOOSER — that is what the score-relative widening is for
9439/// — but never stricter.
9440/// The value is bit-for-bit what this returned before #2688:
9441/// `min(max(engine, score_relative), required)` is the same number as the old
9442/// `min(max(min(engine, required), score_relative), required)`, the inner `min`
9443/// being dominated by the outer one in every ordering of the three. What
9444/// changed is that the caller now also learns WHICH of the three it got.
9445pub(crate) fn outer_stationarity_band_and_rung_at(
9446 config: &OuterConfig,
9447 cost_at_point: f64,
9448) -> CertificateBandAt {
9449 let engine_band = outer_engine_gradient_band(config);
9450 // A non-finite criterion value anchors nothing, so the declared band stands.
9451 let score_relative = if cost_at_point.is_finite() {
9452 outer_cost_relative_tolerance(config) * (1.0 + cost_at_point.abs())
9453 } else {
9454 f64::NEG_INFINITY
9455 };
9456 // Strict `>`: on a tie the widening added nothing and must not claim the rung.
9457 let (engine_bound, engine_source) = if score_relative > engine_band {
9458 (
9459 score_relative,
9460 StationarityBoundSource::CertificateScoreRelative,
9461 )
9462 } else {
9463 (engine_band, StationarityBoundSource::SolverBand)
9464 };
9465 // #2568 -- the score-relative widening above is what produced the saturated
9466 // `bound = 1.000e0`, so a caller requirement that did not survive it would
9467 // be defeated by exactly the case it was introduced for. Cap after widening.
9468 // This does NOT manufacture the "solver claimed convergence, certificate
9469 // refused" family warned about above: `outer_gradient_tolerance` is floored
9470 // at the same requirement, so the solver was told to reach the number the
9471 // certificate now applies. #2688 -- strict `<`, and the rung says so.
9472 match config.required_projected_gradient_norm {
9473 Some(required) if required < engine_bound => CertificateBandAt {
9474 bound: required,
9475 source: StationarityBoundSource::CallerRequirement,
9476 engine_bound,
9477 engine_source,
9478 },
9479 _ => CertificateBandAt {
9480 bound: engine_bound,
9481 source: engine_source,
9482 engine_bound,
9483 engine_source,
9484 },
9485 }
9486}
9487
9488pub(crate) fn outer_max_iterations(value: usize) -> Result<MaxIterations, EstimationError> {
9489 MaxIterations::new(value)
9490 .map_err(|err| EstimationError::InvalidInput(format!("outer max_iter is invalid: {err}")))
9491}
9492
9493pub(crate) fn sanitized_operator_trust_restart_radius(radius: Option<f64>) -> Option<f64> {
9494 radius
9495 .filter(|value| value.is_finite() && *value > 0.0)
9496 .map(|value| value.max(OPERATOR_TRUST_RESTART_RADIUS_FLOOR))
9497}
9498
9499pub(crate) fn bfgs_axis_step_caps(
9500 config: &OuterConfig,
9501 layout: OuterThetaLayout,
9502) -> Option<Array1<f64>> {
9503 if config.bfgs_step_cap.is_none() && config.bfgs_step_cap_psi.is_none() {
9504 return None;
9505 }
9506 let mut caps = Array1::from_elem(layout.n_params, f64::INFINITY);
9507 if let Some(cap) = config.bfgs_step_cap {
9508 for i in 0..layout.rho_dim() {
9509 caps[i] = cap;
9510 }
9511 }
9512 if let Some(cap) = config.bfgs_step_cap_psi {
9513 for i in layout.rho_dim()..layout.n_params {
9514 caps[i] = cap;
9515 }
9516 }
9517 Some(caps)
9518}
9519
9520pub(crate) enum FixedPointOuterRunError {
9521 SeedRejected(ObjectiveEvalError),
9522 IterationRejected(FixedPointContinuationRequest),
9523 ImmediateFallback(FirstOrderFallbackRequest),
9524 Failed(EstimationError),
9525}
9526
9527/// Last complete fixed-point incumbent preceding a refused trial point.
9528///
9529/// `opt::FixedPoint` currently returns only the refused evaluation's message;
9530/// it does not return its still-finite incumbent on `ObjectiveFailed`. This
9531/// carrier preserves the exact optimizer state needed to continue with a
9532/// different algorithm: outer point, criterion, proposed fixed-point step,
9533/// fixed-point status, completed iteration count, plan, and the matching inner
9534/// coefficient state when the EFS producer supplied one.
9535#[derive(Clone, Debug)]
9536pub(crate) struct FixedPointContinuationCheckpoint {
9537 pub(crate) point: Array1<f64>,
9538 pub(crate) sample: FixedPointSample,
9539 pub(crate) iterations: usize,
9540 pub(crate) plan_used: OuterPlan,
9541 pub(crate) inner_seed: Option<BoundInnerSeed>,
9542}
9543
9544/// Typed request to continue a fixed-point incumbent after one rho-local
9545/// refusal.
9546///
9547/// The refusal remains an [`ObjectiveEvalError`] all the way through the plan
9548/// boundary, so the fallback decision is based on the producer's recoverable
9549/// verdict rather than on message text.
9550#[derive(Clone, Debug)]
9551pub(crate) struct FixedPointContinuationRequest {
9552 pub(crate) checkpoint: FixedPointContinuationCheckpoint,
9553 pub(crate) refusal: ObjectiveEvalError,
9554}
9555
9556/// Carries a fixed-point objective's complete typed refusal across the lossy
9557/// `opt` fixed-point return boundary.
9558///
9559/// `OuterFixedPointBridge::eval_step` returns a typed `ObjectiveEvalError`
9560/// whose kind is the producer's verdict: `Recoverable` for a refusal that is
9561/// a property of THIS rho (a non-finite cost, a non-finite EFS step, a
9562/// bubbled `RemlOptimizationFailed`, or any `EstimationError` for which
9563/// `is_trial_point_infeasible()` answers true), `Fatal` only for a structural
9564/// failure. `opt::FixedPoint::run` then does `err.into_message()` and hands
9565/// back `FixedPointError::ObjectiveFailed { message }`. This adapter retains
9566/// the original error in a publication slot, so its producer verdict, typed
9567/// source, and any first-order routing request all survive.
9568///
9569/// `run_fixed_point_outer_solver` used to answer that String with an
9570/// unconditional `fatal_outer_evaluation`, and `is_fatal_outer_evaluation()`
9571/// is a hard `return Err(e)` in BOTH the seed loop (`run_plan.rs`) and the
9572/// strategy ladder (`run.rs`). So one recoverable per-rho refusal at outer
9573/// iteration k killed the remaining seeds and the entire fallback ladder --
9574/// including the `disable_fixed_point` BFGS plan `automatic_fallback_attempts`
9575/// builds for precisely this situation. The SEED evaluation in the same
9576/// function never had this bug: it still holds the typed error there and asks
9577/// `is_recoverable()`. Only the iterations lost the verdict, in transit.
9578///
9579/// This adapter is the publication slot that gets it back -- the same device
9580/// `recurrent_incumbent_exit` uses to hand a value out of a moved bridge. The
9581/// slot is written on EVERY evaluation (cleared to `None` on success), so an
9582/// error can never be read stale.
9583pub(crate) struct RetainingObjective<ObjFn> {
9584 inner: ObjFn,
9585 last_error: Arc<Mutex<Option<ObjectiveEvalError>>>,
9586}
9587
9588impl<ObjFn> RetainingObjective<ObjFn> {
9589 pub(crate) fn new(
9590 inner: ObjFn,
9591 last_error: Arc<Mutex<Option<ObjectiveEvalError>>>,
9592 ) -> Self {
9593 Self { inner, last_error }
9594 }
9595
9596 fn publish<T>(&self, outcome: &Result<T, ObjectiveEvalError>) {
9597 *self
9598 .last_error
9599 .lock()
9600 .expect("objective error publication lock poisoned") =
9601 outcome.as_ref().err().cloned();
9602 }
9603}
9604
9605impl<ObjFn> ZerothOrderObjective for RetainingObjective<ObjFn>
9606where
9607 ObjFn: ZerothOrderObjective,
9608{
9609 fn eval_cost(&mut self, x: &Array1<f64>) -> Result<f64, ObjectiveEvalError> {
9610 let outcome = self.inner.eval_cost(x);
9611 self.publish(&outcome);
9612 outcome
9613 }
9614}
9615
9616impl<ObjFn> FirstOrderObjective for RetainingObjective<ObjFn>
9617where
9618 ObjFn: FirstOrderObjective,
9619{
9620 fn eval_grad(&mut self, x: &Array1<f64>) -> Result<FirstOrderSample, ObjectiveEvalError> {
9621 let outcome = self.inner.eval_grad(x);
9622 self.publish(&outcome);
9623 outcome
9624 }
9625
9626 fn set_finite_difference_bounds(&mut self, bounds: Option<&Bounds>) {
9627 self.inner.set_finite_difference_bounds(bounds);
9628 }
9629}
9630
9631impl<ObjFn> SecondOrderObjective for RetainingObjective<ObjFn>
9632where
9633 ObjFn: SecondOrderObjective,
9634{
9635 fn eval_hessian(&mut self, x: &Array1<f64>) -> Result<SecondOrderSample, ObjectiveEvalError> {
9636 let outcome = self.inner.eval_hessian(x);
9637 self.publish(&outcome);
9638 outcome
9639 }
9640}
9641
9642impl<ObjFn> FixedPointObjective for RetainingObjective<ObjFn>
9643where
9644 ObjFn: FixedPointObjective,
9645{
9646 fn eval_step(&mut self, x: &Array1<f64>) -> Result<FixedPointSample, ObjectiveEvalError> {
9647 let outcome = self.inner.eval_step(x);
9648 self.publish(&outcome);
9649 outcome
9650 }
9651}
9652
9653/// Retains every successful fixed-point sample as well as the typed error
9654/// retained by [`RetainingObjective`].
9655///
9656/// A failed evaluation belongs to the proposed *next* point. Therefore it
9657/// must not overwrite `incumbent`: that slot remains the exact last finite
9658/// point from which another solver can continue.
9659pub(crate) struct RetainingFixedPointObjective<ObjFn> {
9660 inner: RetainingObjective<ObjFn>,
9661 incumbent: Arc<Mutex<FixedPointContinuationCheckpoint>>,
9662 evaluated_inner_seed: Arc<Mutex<Option<BoundInnerSeed>>>,
9663 successful_iterations: usize,
9664 plan_used: OuterPlan,
9665}
9666
9667impl<ObjFn> RetainingFixedPointObjective<ObjFn> {
9668 pub(crate) fn new(
9669 inner: ObjFn,
9670 last_error: Arc<Mutex<Option<ObjectiveEvalError>>>,
9671 incumbent: Arc<Mutex<FixedPointContinuationCheckpoint>>,
9672 evaluated_inner_seed: Arc<Mutex<Option<BoundInnerSeed>>>,
9673 plan_used: OuterPlan,
9674 ) -> Self {
9675 Self {
9676 inner: RetainingObjective::new(inner, last_error),
9677 incumbent,
9678 evaluated_inner_seed,
9679 successful_iterations: 0,
9680 plan_used,
9681 }
9682 }
9683}
9684
9685impl<ObjFn> FixedPointObjective for RetainingFixedPointObjective<ObjFn>
9686where
9687 ObjFn: FixedPointObjective,
9688{
9689 fn eval_step(&mut self, x: &Array1<f64>) -> Result<FixedPointSample, ObjectiveEvalError> {
9690 let outcome = self.inner.eval_step(x);
9691 if let Ok(sample) = &outcome {
9692 self.successful_iterations = self.successful_iterations.saturating_add(1);
9693 let inner_seed = self
9694 .evaluated_inner_seed
9695 .lock()
9696 .expect("fixed-point inner-state publication lock poisoned")
9697 .clone()
9698 .filter(|seed| outer_theta_bitwise_eq(&seed.theta, x));
9699 *self
9700 .incumbent
9701 .lock()
9702 .expect("fixed-point incumbent publication lock poisoned") =
9703 FixedPointContinuationCheckpoint {
9704 point: x.clone(),
9705 sample: sample.clone(),
9706 iterations: self.successful_iterations,
9707 plan_used: self.plan_used,
9708 inner_seed,
9709 };
9710 }
9711 outcome
9712 }
9713}
9714
9715pub(crate) fn run_fixed_point_outer_solver(
9716 obj: &mut dyn OuterObjective,
9717 layout: OuterThetaLayout,
9718 barrier_config: Option<BarrierConfig>,
9719 config: &OuterConfig,
9720 context: &str,
9721 seed: &Array1<f64>,
9722 the_plan: OuterPlan,
9723 label: &str,
9724 failure_prefix: &str,
9725) -> Result<OuterResult, FixedPointOuterRunError> {
9726 // Shared publication slot for the recurrent-restored-incumbent stop
9727 // (#2235 verdict 2): the bridge is moved into the driver, so the streak
9728 // count comes back through this cell and is stamped onto the returned
9729 // `OuterResult` below.
9730 let recurrent_incumbent_exit = Arc::new(Mutex::new(None));
9731 let evaluated_inner_seed = Arc::new(Mutex::new(None));
9732 let mut objective = OuterFixedPointBridge {
9733 obj,
9734 layout,
9735 barrier_config,
9736 fixed_point_tolerance: config.tolerance,
9737 evaluated_inner_seed: Arc::clone(&evaluated_inner_seed),
9738 consecutive_psi_zero_iters: 0,
9739 last_restored_incumbent_streak: None,
9740 recurrent_incumbent_exit: Arc::clone(&recurrent_incumbent_exit),
9741 };
9742 let seed_sample = match objective.eval_step(seed) {
9743 Ok(sample) => sample,
9744 Err(err) if first_order_fallback_request(&err).is_some() => {
9745 let request = first_order_fallback_request(&err)
9746 .expect("guard established a typed first-order fallback request")
9747 .clone();
9748 return Err(FixedPointOuterRunError::ImmediateFallback(request));
9749 }
9750 Err(err) if err.is_recoverable() => {
9751 return Err(FixedPointOuterRunError::SeedRejected(err));
9752 }
9753 Err(err) => {
9754 return Err(FixedPointOuterRunError::Failed(
9755 EstimationError::fatal_objective_evaluation(
9756 "outer fixed-point seed evaluation",
9757 err,
9758 ),
9759 ));
9760 }
9761 };
9762 let (lo, hi) = outer_search_bounds_template(config, layout.n_params);
9763 let bounds = outer_bounds(&lo, &hi).map_err(FixedPointOuterRunError::Failed)?;
9764 let tol = outer_tolerance(config.tolerance).map_err(FixedPointOuterRunError::Failed)?;
9765 let max_iter =
9766 outer_max_iterations(config.max_iter).map_err(FixedPointOuterRunError::Failed)?;
9767 // Publication slot for the complete producer error from the last failed
9768 // `eval_step`. `opt::FixedPoint` returns only its message, so this is the
9769 // ownership channel for the typed source and routing request.
9770 let last_step_error: Arc<Mutex<Option<ObjectiveEvalError>>> = Arc::new(Mutex::new(None));
9771 let seed_inner_state = evaluated_inner_seed
9772 .lock()
9773 .expect("fixed-point inner-state publication lock poisoned")
9774 .clone()
9775 .filter(|inner_seed| outer_theta_bitwise_eq(&inner_seed.theta, seed));
9776 let incumbent = Arc::new(Mutex::new(FixedPointContinuationCheckpoint {
9777 point: seed.clone(),
9778 sample: seed_sample.clone(),
9779 iterations: 0,
9780 plan_used: the_plan,
9781 inner_seed: seed_inner_state,
9782 }));
9783 let objective = RetainingFixedPointObjective::new(
9784 objective,
9785 Arc::clone(&last_step_error),
9786 Arc::clone(&incumbent),
9787 Arc::clone(&evaluated_inner_seed),
9788 the_plan,
9789 );
9790 let mut optimizer = FixedPoint::new(seed.clone(), objective)
9791 // Seed validation already paid the complete EFS inner solve. Reuse that
9792 // exact sample so iteration zero neither repeats the expensive solve nor
9793 // mistakes two evaluations at the identical rho for recurrent incumbent
9794 // evidence (#2241).
9795 .with_initial_sample(seed.clone(), seed_sample)
9796 .with_bounds(bounds)
9797 .with_tolerance(tol)
9798 .with_max_iterations(max_iter);
9799 match optimizer.run() {
9800 Ok(sol) => {
9801 let mut result = solution_into_outer_result(sol, true, the_plan);
9802 // Stamp the model-state fixed-point stop when the bridge published
9803 // one; `None` means the walk stopped through the ordinary
9804 // step-norm test instead.
9805 if let Some(consecutive_restores) =
9806 recurrent_incumbent_exit.lock().ok().and_then(|slot| *slot)
9807 {
9808 result.termination = OuterTermination::SolverClaimed {
9809 proposed_via: Some(OuterConvergedVia::RecurrentIncumbent {
9810 consecutive_restores,
9811 }),
9812 };
9813 }
9814 Ok(result)
9815 }
9816 Err(FixedPointError::MaxIterationsReached { last_solution }) => {
9817 let step_norm = last_solution.final_step_norm.expect(
9818 "a fixed-point max-iteration solution must carry its final accepted step norm",
9819 );
9820 log::warn!(
9821 "[OUTER warning] {context}: {label} hit max_iter={} at final_value={:.6e} step_norm={:.3e}",
9822 config.max_iter,
9823 last_solution.final_value,
9824 step_norm,
9825 );
9826 Ok(solution_into_outer_result(*last_solution, false, the_plan))
9827 }
9828 Err(FixedPointError::ObjectiveFailed { .. }) => {
9829 let error = last_step_error
9830 .lock()
9831 .expect("fixed-point objective error publication lock poisoned")
9832 .take()
9833 .expect("FixedPoint::ObjectiveFailed must follow a failed classified eval_step");
9834 if error.is_recoverable() {
9835 let checkpoint = incumbent
9836 .lock()
9837 .expect("fixed-point incumbent publication lock poisoned")
9838 .clone();
9839 return Err(FixedPointOuterRunError::IterationRejected(
9840 FixedPointContinuationRequest {
9841 checkpoint,
9842 refusal: error,
9843 },
9844 ));
9845 }
9846 Err(FixedPointOuterRunError::Failed(
9847 EstimationError::fatal_objective_evaluation(
9848 "outer fixed-point evaluation",
9849 error,
9850 ),
9851 ))
9852 }
9853 Err(e) => Err(FixedPointOuterRunError::Failed(
9854 EstimationError::RemlOptimizationFailed(format!("{failure_prefix}: {e:?}")),
9855 )),
9856 }
9857}
9858
9859#[cfg(test)]
9860#[path = "asymptote_rail_certify_tests.rs"]
9861mod asymptote_rail_certify_tests;
9862
9863#[cfg(test)]
9864#[path = "rail_barrier_removal_tests.rs"]
9865mod rail_barrier_removal_tests;
9866
9867#[cfg(test)]
9868#[path = "certify_resume_progress_tests.rs"]
9869mod certify_resume_progress_tests;
9870
9871#[cfg(test)]
9872#[path = "outer_stationarity_band_tests.rs"]
9873mod outer_stationarity_band_tests;
9874
9875#[cfg(test)]
9876#[path = "criterion_curvature_ladder_2748_tests.rs"]
9877mod criterion_curvature_ladder_2748_tests;