gam_solve/rho_optimizer/capability.rs
1use super::*;
2
3/// Declares what a specific model path can provide to the outer optimizer.
4///
5/// Each call site that optimizes smoothing parameters constructs one of these
6/// to describe its analytic derivative coverage. The [`plan`] function then
7/// selects the optimizer and Hessian strategy.
8///
9/// HISTORY: this crossover used to be 8 — a "performance choice" that routed
10/// every small-dimensional problem WITH an analytic gradient to BFGS on the
11/// theory that a dense quasi-Newton is cheaper below the cutoff. On the
12/// criteria that actually fail (the SAE manifold Laplace evidence: 2–7 ρ
13/// coordinates, piecewise-smooth basin-envelope value, inner-solve truncation
14/// noise), BFGS is not cheaper — its Strong-Wolfe line search is the consumer
15/// of the entire probe-lane / wall / escape / rescue apparatus, and every cost
16/// probe is a full inner re-convergence. EFS is the declared canonical REML
17/// method, needs only the traces `tr(H⁻¹S_k)` (no line search, no Wolfe, no
18/// value/gradient-lane agreement), and already drives every large fit. The
19/// crossover is therefore 0: a fixed-point-capable objective routes to
20/// EFS/HybridEfs at EVERY dimension, and BFGS remains the fallback for
21/// objectives with no fixed-point hook (or after `disable_fixed_point`).
22pub(crate) const SMALL_OUTER_BFGS_MAX_PARAMS: usize = 0;
23
24pub(crate) const SECOND_ORDER_GEOMETRY_PROBE_MAX_PARAMS: usize = 64;
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub struct OuterThetaLayout {
28 pub n_params: usize,
29 pub psi_dim: usize,
30}
31
32impl OuterThetaLayout {
33 pub const fn new(n_params: usize, psi_dim: usize) -> Self {
34 Self { n_params, psi_dim }
35 }
36
37 pub const fn rho_dim(&self) -> usize {
38 self.n_params.saturating_sub(self.psi_dim)
39 }
40
41 fn validate_capability(&self, context: &str) -> Result<(), EstimationError> {
42 if self.psi_dim > self.n_params {
43 return Err(EstimationError::RemlOptimizationFailed(format!(
44 "{context}: invalid outer theta layout (psi_dim={} exceeds n_params={})",
45 self.psi_dim, self.n_params
46 )));
47 }
48 Ok::<(), _>(())
49 }
50
51 pub(crate) fn validate_point_len(
52 &self,
53 theta: &Array1<f64>,
54 context: &str,
55 ) -> Result<(), ObjectiveEvalError> {
56 if theta.len() != self.n_params {
57 return Err(ObjectiveEvalError::recoverable(format!(
58 "{context}: outer theta length mismatch: got {}, expected {} (rho_dim={}, psi_dim={})",
59 theta.len(),
60 self.n_params,
61 self.rho_dim(),
62 self.psi_dim
63 )));
64 }
65 Ok::<(), _>(())
66 }
67
68 pub(crate) fn validate_gradient_len(
69 &self,
70 gradient: &Array1<f64>,
71 context: &str,
72 ) -> Result<(), ObjectiveEvalError> {
73 if gradient.len() != self.n_params {
74 return Err(ObjectiveEvalError::recoverable(format!(
75 "{context}: outer gradient length mismatch: got {}, expected {} (rho_dim={}, psi_dim={})",
76 gradient.len(),
77 self.n_params,
78 self.rho_dim(),
79 self.psi_dim
80 )));
81 }
82 Ok::<(), _>(())
83 }
84
85 pub(crate) fn validate_hessian_shape(
86 &self,
87 hessian: &Array2<f64>,
88 context: &str,
89 ) -> Result<(), ObjectiveEvalError> {
90 if hessian.nrows() != self.n_params || hessian.ncols() != self.n_params {
91 return Err(ObjectiveEvalError::recoverable(format!(
92 "{context}: outer Hessian shape mismatch: got {}x{}, expected {}x{} (rho_dim={}, psi_dim={})",
93 hessian.nrows(),
94 hessian.ncols(),
95 self.n_params,
96 self.n_params,
97 self.rho_dim(),
98 self.psi_dim
99 )));
100 }
101 Ok::<(), _>(())
102 }
103
104 pub(crate) fn validate_efs_eval(
105 &self,
106 eval: &EfsEval,
107 context: &str,
108 ) -> Result<(), ObjectiveEvalError> {
109 if eval.steps.len() != self.n_params {
110 return Err(ObjectiveEvalError::recoverable(format!(
111 "{context}: outer EFS step length mismatch: got {}, expected {} (rho_dim={}, psi_dim={})",
112 eval.steps.len(),
113 self.n_params,
114 self.rho_dim(),
115 self.psi_dim
116 )));
117 }
118 if let Some(ref psi_gradient) = eval.psi_gradient
119 && psi_gradient.len() != self.psi_dim
120 {
121 return Err(ObjectiveEvalError::recoverable(format!(
122 "{context}: outer EFS psi-gradient length mismatch: got {}, expected {}",
123 psi_gradient.len(),
124 self.psi_dim
125 )));
126 }
127 if let Some(ref psi_indices) = eval.psi_indices {
128 if psi_indices.len() != self.psi_dim {
129 return Err(ObjectiveEvalError::recoverable(format!(
130 "{context}: outer EFS psi-index count mismatch: got {}, expected {}",
131 psi_indices.len(),
132 self.psi_dim
133 )));
134 }
135 if psi_indices.iter().any(|&idx| idx >= self.n_params) {
136 return Err(ObjectiveEvalError::recoverable(format!(
137 "{context}: outer EFS psi index out of range for n_params={}",
138 self.n_params
139 )));
140 }
141 }
142 Ok(())
143 }
144}
145
146#[derive(Clone, Debug)]
147pub struct OuterCapability {
148 pub gradient: Derivative,
149 /// Declared shape of the analytic Hessian (or its absence). Replaces
150 /// the binary `Derivative` so the planner can route between dense
151 /// ARC and matrix-free trust-region *before* seed evaluation. See
152 /// [`DeclaredHessianForm`].
153 pub hessian: DeclaredHessianForm,
154 /// Number of smoothing (+ any auxiliary hyper-) parameters being optimized.
155 pub n_params: usize,
156 /// Number of ψ (design-moving) coordinates among the extended
157 /// hyperparameter coordinates. When 0, all coords are penalty-like and
158 /// pure EFS is eligible (given `fixed_point_available`). When > 0,
159 /// hybrid EFS is eligible instead: EFS for ρ + preconditioned gradient
160 /// for ψ.
161 ///
162 /// # Hybrid EFS strategy (when `psi_dim > 0`)
163 ///
164 /// Enabled when `psi_dim > 0`, `fixed_point_available`, and either the
165 /// analytic gradient is unavailable or the problem is above the small-
166 /// dimensional BFGS crossover.
167 /// Combines:
168 /// - Standard EFS multiplicative fixed-point updates for ρ coordinates
169 /// - Safeguarded preconditioned gradient steps for ψ coordinates:
170 /// `Δψ = -α G⁺ g_ψ` where G is the trace Gram matrix
171 ///
172 /// Mathematically necessary because no EFS-type fixed-point iteration
173 /// exists for indefinite B_ψ (see response.md Section 2). The structural
174 /// requirement for EFS is `H^{-1/2} B_d H^{-1/2} ≽ 0` (PSD) plus fixed
175 /// nullspace — exactly what penalty-like coords satisfy and design-moving
176 /// coords do not.
177 ///
178 /// The hybrid is O(1) H⁻¹ solves per iteration (same as pure EFS),
179 /// compared to O(dim(θ)) for BFGS.
180 pub psi_dim: usize,
181 /// Whether the objective actually implements `eval_efs()` for fixed-point
182 /// plans. Structural eligibility (`psi_dim == 0` / `psi_dim > 0`)
183 /// is not sufficient by itself: if this is false, the planner must stay on
184 /// Newton/BFGS-style plans even when EFS or Hybrid-EFS would otherwise be
185 /// mathematically admissible.
186 pub fixed_point_available: bool,
187 /// Optional log-barrier configuration for structural monotonicity constraints.
188 /// When present, EFS is still eligible at plan time, but the EFS iteration
189 /// loop performs a quantitative check each step: if
190 /// `barrier_curvature_is_significant(β, ref_diag, threshold)` fires, EFS
191 /// is abandoned and the fallback ladder routes to a first-order joint
192 /// optimizer.
193 ///
194 /// Previously this was a binary `barrier_active: bool` that unconditionally
195 /// blocked EFS. The quantitative check allows EFS when constraints exist but
196 /// the barrier curvature is negligible (coefficients far from their bounds).
197 pub barrier_config: Option<BarrierConfig>,
198 /// Policy hint for derivative-free auxiliary optimizers only. Primary REML
199 /// optimization ignores this flag when an analytic Hessian exists: exact
200 /// second-order geometry must not be hidden behind a quasi-Newton policy.
201 pub prefer_gradient_only: bool,
202 /// Policy hint: even when the objective implements `eval_efs()` and the
203 /// coordinate structure is penalty-like, the planner must NOT select
204 /// EFS/HybridEfs for this problem.
205 ///
206 /// Set by the caller for problem classes where the Wood-Fasiolo structural
207 /// property (`H^{-1/2} B_k H^{-1/2} ≽ 0` plus parameter-independent
208 /// nullspace) is known not to hold — e.g. GAMLSS/location-scale families
209 /// where the joint Hessian is β-dependent and cross-block smoothers
210 /// induce non-diagonal curvature that the EFS multiplicative fixed-point
211 /// cannot resolve. Also set by the automatic fallback cascade when an
212 /// EFS/HybridEfs attempt failed to converge, so the next attempt falls
213 /// back to analytic-gradient BFGS rather than retrying EFS.
214 pub disable_fixed_point: bool,
215}
216
217impl OuterCapability {
218 pub const fn theta_layout(&self) -> OuterThetaLayout {
219 OuterThetaLayout::new(self.n_params, self.psi_dim)
220 }
221
222 pub fn validate_layout(&self, context: &str) -> Result<(), EstimationError> {
223 self.theta_layout().validate_capability(context)
224 }
225
226 /// True when all coordinates are penalty-like (no ψ coords).
227 pub const fn all_penalty_like(&self) -> bool {
228 self.psi_dim == 0
229 }
230 /// True when ψ (design-moving) coordinates are present.
231 pub const fn has_psi_coords(&self) -> bool {
232 self.psi_dim > 0
233 }
234
235 fn efs_plan_eligible(&self) -> bool {
236 self.fixed_point_available
237 && !self.disable_fixed_point
238 && self.all_penalty_like()
239 // A fixed-point-capable objective routes to EFS at every dimension
240 // (see `SMALL_OUTER_BFGS_MAX_PARAMS`): the former ≤8-coordinate
241 // BFGS crossover sent exactly the failing small fits into the
242 // fragile Wolfe/probe lane while large fits got the robust
243 // trace-based fixed point.
244 && (self.gradient == Derivative::Unavailable
245 || self.n_params > SMALL_OUTER_BFGS_MAX_PARAMS)
246 }
247
248 fn hybrid_efs_plan_eligible(&self) -> bool {
249 self.fixed_point_available
250 && !self.disable_fixed_point
251 && self.has_psi_coords()
252 && (self.gradient == Derivative::Unavailable
253 || self.n_params > SMALL_OUTER_BFGS_MAX_PARAMS)
254 }
255
256 fn declared_hessian_for_planning(&self) -> Derivative {
257 if self.hessian.is_analytic() {
258 Derivative::Analytic
259 } else {
260 Derivative::Unavailable
261 }
262 }
263}
264
265/// Which solver algorithm to use for the outer optimization.
266#[derive(Clone, Copy, Debug, PartialEq, Eq)]
267pub enum Solver {
268 /// Adaptive Regularized Cubic; fastest convergence, requires Hessian.
269 Arc,
270 /// BFGS; gradient only, builds a dense curvature approximation.
271 Bfgs,
272 /// Extended Fellner-Schall; multiplicative fixed-point iteration.
273 /// Only valid when all hyperparameter coordinates are penalty-like.
274 /// Needs no gradient or Hessian — only traces tr(H^{-1} A_k) and
275 /// Frobenius norms from the inner solution.
276 Efs,
277 /// Hybrid EFS + preconditioned gradient.
278 ///
279 /// Used when ψ (design-moving) coordinates are present alongside ρ
280 /// (penalty-like) coordinates. Combines:
281 /// - Standard EFS multiplicative fixed-point steps for ρ coords
282 /// - Safeguarded preconditioned gradient steps for ψ coords:
283 /// `Δψ = -α G⁺ g_ψ` where `G_{de} = tr(H⁻¹ B_d H⁻¹ B_e)`
284 ///
285 /// This hybrid exists because no EFS-type fixed-point iteration can
286 /// guarantee convergence for indefinite B_ψ (proven by counterexample
287 /// in response.md Section 2). The key structural property that EFS
288 /// needs — `H^{-1/2} B_d H^{-1/2} ≽ 0` plus parameter-independent
289 /// nullspace — holds for penalty-like coords but fails for
290 /// design-moving coords where B_ψ has mixed inertia.
291 ///
292 /// The preconditioned gradient uses the same trace Gram matrix that
293 /// EFS already computes, so the cost is O(1) H⁻¹ solves per iteration
294 /// (same as pure EFS), compared to O(dim(θ)) for full BFGS.
295 HybridEfs,
296}
297
298/// How the Hessian will be obtained for the outer optimizer.
299#[derive(Clone, Copy, Debug, PartialEq, Eq)]
300pub enum HessianSource {
301 /// Exact analytic Hessian provided by the objective.
302 Analytic,
303 /// No explicit Hessian; BFGS builds a rank-2 approximation from
304 /// gradient history.
305 BfgsApprox,
306 /// No explicit Hessian or gradient needed. EFS uses traces and
307 /// Frobenius norms from the inner solution directly.
308 EfsFixedPoint,
309 /// Hybrid EFS + preconditioned gradient for ψ coordinates.
310 /// EFS traces for ρ coords, trace Gram matrix + gradient for ψ coords.
311 HybridEfsFixedPoint,
312}
313
314/// Requested derivative order for an outer objective evaluation.
315///
316/// This enum is for the shared `eval` bridge where the runner needs value-only,
317/// first-order, or second-order information depending on the active plan.
318///
319/// Single-sourced on the lower `gam-model-api` crate so the gam-models
320/// fit_orchestration drivers and the gam-solve runner share one type (#1521).
321pub use gam_model_api::OuterEvalOrder;
322
323/// The outer optimization plan. Produced by [`plan`], consumed by the runner.
324#[derive(Clone, Copy, Debug, PartialEq, Eq)]
325pub struct OuterPlan {
326 pub solver: Solver,
327 pub hessian_source: HessianSource,
328}
329
330pub(crate) const EFS_FIRST_ORDER_FALLBACK_MARKER: &str = "[outer-efs-first-order-fallback]";
331
332/// Whether outer_strategy should automatically derive a retry ladder from the
333/// primary capability, or disable retries entirely.
334#[derive(Clone, Copy, Debug, PartialEq, Eq)]
335pub enum FallbackPolicy {
336 /// Centralized retry path chosen from the declared capability.
337 Automatic,
338 /// No retries; use only the primary plan.
339 Disabled,
340}
341
342impl std::fmt::Display for OuterPlan {
343 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
344 write!(
345 f,
346 "solver={:?}, hessian_source={:?}",
347 self.solver, self.hessian_source
348 )
349 }
350}
351
352impl OuterPlan {
353 /// Stable, grep-friendly routing token for large-scale/log regression
354 /// assertions. Emits `solver=<Solver>;hessian=<Source>;matrix-free=<bool>`.
355 /// Planning alone does not prove the runtime Hessian representation;
356 /// matrix-free routing is decided after the seed evaluation returns an
357 /// operator Hessian, so the static plan token reports `false`.
358 pub fn routing_log_line(&self) -> String {
359 let matrix_free = false;
360 format!(
361 "solver={:?};hessian={:?};matrix-free={}",
362 self.solver, self.hessian_source, matrix_free
363 )
364 }
365}
366
367/// Select the outer optimization strategy from the declared capability.
368///
369/// This is a pure function with no side effects. All policy lives here.
370pub fn plan(cap: &OuterCapability) -> OuterPlan {
371 use Derivative as D;
372 use HessianSource as H;
373 use Solver as S;
374
375 match (cap.gradient, cap.declared_hessian_for_planning()) {
376 (D::Analytic, D::Analytic) => OuterPlan {
377 solver: S::Arc,
378 hessian_source: H::Analytic,
379 },
380 // EFS: all penalty-like coords and no analytic Hessian. With an
381 // analytic gradient this is the many-parameter fast path; without one
382 // it is the only declared analytic solver at any dimension.
383 // Multiplicative fixed-point needs only traces — no gradient evals.
384 // Much cheaper than BFGS for k=10-50 smoothing parameters.
385 //
386 // When a log-barrier is present (monotonicity constraints), EFS is
387 // still selected here. The EFS iteration loop in `run_outer` performs
388 // a quantitative check each step via `barrier_curvature_is_significant`
389 // and bails out early if the barrier curvature becomes non-negligible
390 // relative to the penalized Hessian diagonal.
391 (D::Analytic, D::Unavailable) if cap.efs_plan_eligible() => OuterPlan {
392 solver: S::Efs,
393 hessian_source: H::EfsFixedPoint,
394 },
395 (D::Unavailable, D::Unavailable) if cap.efs_plan_eligible() => OuterPlan {
396 solver: S::Efs,
397 hessian_source: H::EfsFixedPoint,
398 },
399
400 // Hybrid EFS: ψ (design-moving) coords present alongside ρ coords.
401 //
402 // When ψ coords are present, pure EFS is invalid because B_ψ can be
403 // indefinite (see response.md Section 2 for the counterexample). But
404 // falling back to full BFGS wastes the cheap EFS structure for ρ coords.
405 //
406 // The hybrid strategy uses EFS for ρ-coords and a safeguarded
407 // preconditioned gradient step for ψ-coords:
408 // Δψ = -α G⁺ g_ψ, G_{de} = tr(H⁻¹ B_d H⁻¹ B_e)
409 //
410 // This stays O(1) H⁻¹ solves per iteration (vs O(dim(θ)) for BFGS)
411 // and uses the same trace Gram matrix that EFS already computes.
412 (D::Analytic, D::Unavailable) if cap.hybrid_efs_plan_eligible() => OuterPlan {
413 solver: S::HybridEfs,
414 hessian_source: H::HybridEfsFixedPoint,
415 },
416 (D::Unavailable, D::Unavailable) if cap.hybrid_efs_plan_eligible() => OuterPlan {
417 solver: S::HybridEfs,
418 hessian_source: H::HybridEfsFixedPoint,
419 },
420
421 // Gradient-only problems should use a gradient-only optimizer.
422 (D::Analytic, D::Unavailable) => OuterPlan {
423 solver: S::Bfgs,
424 hessian_source: H::BfgsApprox,
425 },
426 // No analytic gradient (with or without a declared Hessian), and the
427 // EFS/HybridEFS fixed-point lane ruled out above. Every outer objective
428 // in the tree now supplies an analytic gradient, so a cost-only
429 // capability is a programming error. Emit a BFGS plan so it surfaces
430 // loudly with context: the runner rejects it because BFGS requires the
431 // analytic gradient this capability declares is absent. We deliberately
432 // do NOT invent a working primary here — a cost-only objective has no
433 // solver, by design.
434 (D::Unavailable, _) => OuterPlan {
435 solver: S::Bfgs,
436 hessian_source: H::BfgsApprox,
437 },
438 }
439}
440
441/// Log the outer optimization plan. Called once per fit at the start of
442/// outer optimization so the user can see what strategy was selected and why.
443pub fn log_plan(context: &str, cap: &OuterCapability, the_plan: &OuterPlan) {
444 let hess_warning = match the_plan.hessian_source {
445 HessianSource::BfgsApprox if cap.n_params > 0 => {
446 " [no Hessian: BFGS approximation]".to_string()
447 }
448 _ => String::new(),
449 };
450 let barrier_note = if cap.barrier_config.is_some() && cap.efs_plan_eligible() {
451 " [EFS with runtime barrier-curvature guard]"
452 } else {
453 ""
454 };
455 let hybrid_note = if the_plan.solver == Solver::HybridEfs {
456 " [hybrid EFS(ρ) + preconditioned-gradient(ψ)]"
457 } else {
458 ""
459 };
460 // Promoted to info: this fires once per outer optimization dispatch and
461 // tells the user immediately whether ARC, BFGS, EFS, etc. was selected
462 // and why. That information is otherwise inferred only from the per-iter
463 // log tag prefix once the loop has started.
464 log::info!(
465 "[OUTER] {context}: n_params={}, gradient={:?}, hessian={:?} -> {} [{}]{hess_warning}{barrier_note}{hybrid_note}",
466 cap.n_params,
467 cap.gradient,
468 cap.hessian,
469 the_plan,
470 the_plan.routing_log_line(),
471 );
472}
473
474pub(crate) fn requests_immediate_first_order_fallback(message: &str) -> bool {
475 message.contains(EFS_FIRST_ORDER_FALLBACK_MARKER)
476}
477
478/// Disable the EFS/HybridEfs planner path, forcing BFGS-class solvers on the
479/// next attempt. Returns `None` if fixed-point is already disabled.
480pub(crate) fn disable_fixed_point(cap: &OuterCapability) -> Option<OuterCapability> {
481 (!cap.disable_fixed_point && (cap.efs_plan_eligible() || cap.hybrid_efs_plan_eligible())).then(
482 || {
483 let mut degraded = cap.clone();
484 degraded.disable_fixed_point = true;
485 degraded
486 },
487 )
488}
489
490pub(crate) fn automatic_fallback_attempts(cap: &OuterCapability) -> Vec<OuterCapability> {
491 // Production fallback ladder is strictly analytic-gradient.
492 //
493 // The cascade is:
494 // 1. If the primary plan is EFS/HybridEFS AND an analytic gradient is
495 // available, retry with fixed-point disabled so the analytic
496 // derivative declaration is evaluated directly.
497 // 2. If the primary plan is Arc (declared (Analytic, Analytic)
498 // capability), do NOT add a degraded fallback. Demoting to
499 // BFGS+BfgsApprox in this case discards the analytic outer Hessian
500 // ARC was using — a strictly weaker geometry — and silently masks
501 // ARC's actual failure mode (e.g. budget exhaustion, indefinite
502 // curvature) under a BFGS Strong-Wolfe plateau on a flat surface.
503 // ARC retries are handled by the per-attempt budget-bump retry
504 // ladder in `run_outer_with_strategy`; once that is exhausted, the
505 // caller surfaces the underlying ARC failure verbatim.
506 // 3. Otherwise (e.g. (Analytic, Unavailable) without EFS eligibility,
507 // which is the BFGS primary), there is nothing to degrade further
508 // — the caller surfaces the RemlOptimizationFailed error so the
509 // non-convergence is visible.
510 let mut attempts = Vec::new();
511
512 if cap.gradient == Derivative::Analytic
513 && matches!(plan(cap).solver, Solver::Efs | Solver::HybridEfs)
514 && let Some(no_fp_cap) = disable_fixed_point(cap)
515 {
516 attempts.push(no_fp_cap.clone());
517 return attempts;
518 }
519
520 // Arc primary: no lateral demotion to BFGS. The runner's ARC-budget-bump
521 // retry covers cases where ARC needed more iterations; if even that is
522 // exhausted, the caller sees the genuine analytic-Hessian non-convergence
523 // rather than a misleading BFGS-on-flat-surface plateau.
524 if matches!(plan(cap).solver, Solver::Arc) {
525 return attempts;
526 }
527
528 attempts
529}
530
531pub(crate) fn disabled_fallback_hybrid_efs_has_standalone_bfgs_primary(
532 cap: &OuterCapability,
533 config: &OuterConfig,
534) -> bool {
535 config.fallback_policy == FallbackPolicy::Disabled
536 && cap.gradient == Derivative::Analytic
537 && matches!(plan(cap).solver, Solver::HybridEfs)
538}
539
540pub(crate) fn primary_capability_for_config(
541 mut cap: OuterCapability,
542 config: &OuterConfig,
543 context: &str,
544) -> OuterCapability {
545 if disabled_fallback_hybrid_efs_has_standalone_bfgs_primary(&cap, config) {
546 // HybridEFS is not a standalone first-order method for ψ coordinates:
547 // when ψ backtracking proves non-descent, the bridge intentionally
548 // surfaces `EFS_FIRST_ORDER_FALLBACK_MARKER` so the runner can switch
549 // to a joint gradient solver that enforces ∇ψ V = 0. With fallback
550 // disabled and an analytic gradient available, selecting HybridEFS as
551 // the only primary attempt is internally inconsistent; BFGS is the
552 // standalone first-order primary for that capability.
553 log::info!(
554 "[OUTER] {context}: HybridEFS requires the automatic first-order \
555 escape path for ψ coordinates; fallback is disabled, so routing the \
556 primary attempt to analytic-gradient BFGS"
557 );
558 cap.disable_fixed_point = true;
559 }
560 cap
561}