Source code for slsqp_jax.state

"""State / result dataclasses for the SLSQP solver and its sub-components.

This module consolidates every JAX-pytree dataclass and NamedTuple that
flows between the SLSQP outer solver, the QP layer, and the inner
projector / Krylov solvers.  Splitting these out of the algorithmic
modules makes the data flow legible at a glance and removes an
otherwise pervasive set of cross-module imports.

The dataclasses are split into three thematic groups:

* **Outer-loop state** (:class:`SLSQPState`, :class:`SLSQPDiagnostics`,
  :func:`get_diagnostics`, :func:`_init_diagnostics`) — produced and
  consumed by :class:`slsqp_jax.SLSQP` across iterations.
* **QP-layer state** (:class:`QPResult`, :class:`QPSolverResult`,
  :class:`QPState`) — produced and consumed by the QP active-set loop
  and the bound-fixing pass.
* **Inner-solver state** (:class:`InnerSolveResult`,
  :class:`ProjectionContext`) — produced by the equality-constrained
  inner solvers and consumed by the QP active-set loop.
"""

from __future__ import annotations

from collections.abc import Callable
from typing import Any, NamedTuple

import equinox as eqx
import jax.numpy as jnp
from jaxtyping import Array, Bool, Float, Int

from slsqp_jax.hessian import LBFGSHistory
from slsqp_jax.types import Scalar, Vector

# ---------------------------------------------------------------------------
# Inner-solver state
# ---------------------------------------------------------------------------


[docs] class InnerSolveResult(NamedTuple): """Result from an inner equality-constrained QP solve. Attributes: d: Search direction. multipliers: Lagrange multipliers (shape ``(m,)``; entries for inactive constraints are zero). converged: True when the inner Krylov / projection iteration satisfied its tolerance. proj_residual: Post-solve constraint residual ``||A d - b||`` (Euclidean norm, restricted to active rows). Always ``0`` for null-space solvers (CG / CRAIG) where feasibility is enforced structurally; non-zero for ``MinresQLPSolver`` where it reflects the floor of the M-metric range-space projection after iterative refinement. n_proj_refinements: Number of M-metric projection refinement rounds actually applied. Always ``0`` for null-space solvers. At most ``MinresQLPSolver.proj_refine_max_iter``. projected_grad_norm: Norm of the *projected* initial gradient ``W̃_k g`` that the inner solver actually iterated against (HR 2014, Theorem 3.5). This is the noise-aware stationarity proxy: when the outer SQP enables ``use_inexact_stationarity``, the run is allowed to converge once this value drops below ``rtol * max(mu_max, 1)`` (filterSQP eq. 6 with the shared denominator from :func:`slsqp_jax.slsqp.termination.compute_mu_max`). Defaults to ``inf`` so that solvers which do not produce this quantity (i.e. anything other than ``HRInexactSTCG``) cannot accidentally satisfy a ``< rtol`` test even if the user toggles the flag — the inexact path silently degrades to "never converges this way". """ d: Vector multipliers: Float[Array, " m"] converged: Bool[Array, ""] proj_residual: Scalar = jnp.asarray(0.0) n_proj_refinements: Int[Array, ""] = jnp.asarray(0) projected_grad_norm: Scalar = jnp.asarray(jnp.inf)
[docs] class ProjectionContext(NamedTuple): """Reusable bundle of projector + particular-solution + multiplier-recovery closures for an active equality system ``A_active d = b_active``. Existing strategies (``ProjectedCGCholesky``, ``ProjectedCGCraig``) build these inline inside their ``solve`` methods. Composed strategies (e.g. ``HRInexactSTCG``) call ``inner.build_projection_context(...)`` to reuse the projector and multiplier-recovery infrastructure of an underlying null-space solver while running their own CG loop on top. Attributes: project: Inexact null-space projector ``W̃_k(v)``. Maps a vector in the (free) ambient space to its projection onto ``null(A_work)`` using whatever inner approximation the underlying strategy provides (Cholesky for ``ProjectedCGCholesky``, CRAIG for ``ProjectedCGCraig``). d_p: Particular solution. ``A_work @ d_p == b_work`` to inner solver precision; ``d_p`` already incorporates ``d_fixed`` on the bound-fixed coordinates, so it lives in the full ``n``- dimensional space. recover_multipliers: Closure mapping ``(B d + g)`` to a length-``m`` multiplier vector with zeros on inactive rows. Encapsulates both the inversion of ``A_work A_workᵀ`` *and* the active-mask zeroing. HR Algorithm 4.5 calls this once per outer step (after the modified-residual CG iteration converges). hvp_work: Working-subspace HVP. Equal to ``hvp_fn`` when no bound fixing is in effect; otherwise ``v -> _free * hvp_fn(_free * v)`` so the iteration only sees the free coordinates. g_eff: Effective gradient ``g + B @ d_p`` evaluated against ``hvp_work``. HR's notation calls this ``g_k``; it is the input to the projected-residual recurrence. A_work: Already-masked working constraint matrix (active rows, free columns). Surfaced primarily so callers can sanity-check the residual ``||A_work @ d - b_work||``; ``project`` and ``recover_multipliers`` already incorporate it. free_mask: Boolean mask of free variables (always present; equals ``ones(n)`` when no bound-fixing is in effect). d_fixed: Fixed-variable values on bound-active coordinates (zeros elsewhere; zeros everywhere when no bound-fixing). has_fixed: ``True`` iff any coordinate is bound-fixed. Cheap indicator so consumers do not have to redo the mask check. converged: Convergence flag of the inner projector solve that built the context (always ``True`` for Cholesky; carries the CRAIG breakdown / convergence flag for the iterative projector). Composed strategies AND this with their own convergence to surface inner-projector failures upstream. """ project: Callable[[Vector], Vector] d_p: Vector recover_multipliers: Callable[[Vector], Float[Array, " m"]] hvp_work: Callable[[Vector], Vector] g_eff: Vector A_work: Float[Array, "m n"] free_mask: Bool[Array, " n"] d_fixed: Vector has_fixed: bool converged: Bool[Array, ""]
# --------------------------------------------------------------------------- # QP-layer state # ---------------------------------------------------------------------------
[docs] class QPState(eqx.Module): """State for the Active Set QP solver. ``any_inner_failure`` accumulates whether any inner-solver call during the active-set loop reported non-convergence or produced a non-finite direction. The final ``QPSolverResult.converged`` combines this with the active-set completion check. ``last_add_idx`` / ``last_drop_idx`` / ``ping_pong_count`` / ``ping_ponged`` implement an explicit anti-cycling guard on top of EXPAND: when the active-set loop alternately adds and drops the *same* constraint several times in a row (typical signature of multiplier-recovery noise on a degenerate vertex) the loop is short-circuited as ``converged=True`` with the sticky ``ping_ponged`` flag set so the outer solver can surface it through the diagnostic counters. Indices are initialised to ``-1`` to mean "no add/drop yet". """ d: Vector active_set: Bool[Array, " m_ineq"] multipliers_eq: Float[Array, " m_eq"] multipliers_ineq: Float[Array, " m_ineq"] iteration: Int[Array, ""] converged: Bool[Array, ""] any_inner_failure: Bool[Array, ""] last_add_idx: Int[Array, ""] last_drop_idx: Int[Array, ""] ping_pong_count: Int[Array, ""] ping_ponged: Bool[Array, ""] # Latest M-metric projection feasibility residual from the inner # solver (relevant for ``MinresQLPSolver``; always 0 for null-space # CG / CRAIG solvers where feasibility is enforced structurally). proj_residual: Scalar # Cumulative number of M-metric projection refinement rounds across # all inner solves performed inside this active-set loop. n_proj_refinements: Int[Array, ""] # Latest projected-gradient norm ``||W̃_k g_k||`` from the inner # solver (``HRInexactSTCG`` only; defaults to ``inf`` for every # other inner solver). Surfaced through ``QPSolverResult`` for the # opt-in ``use_inexact_stationarity`` outer-loop convergence test. # Stores the *latest* inner-solver value rather than accumulating; # the active-set loop's final inner solve produces the value that # matters. projected_grad_norm: Scalar
[docs] class QPSolverResult(NamedTuple): """Result returned by :func:`slsqp_jax.qp.solve_qp`. The Bundle 1 diagnostic fields ``ping_ponged`` / ``reached_max_iter`` / ``final_working_tol`` are surfaced from the active-set loop so the outer solver can track *why* the QP stopped (clean convergence versus cycling versus iteration-budget exhaustion). They default to ``False`` / ``False`` / ``0.0`` for the trivial QP paths that do not run an active-set loop. This is the *internal* QP-pipeline result. The richer outer-facing :class:`QPResult` (returned by :meth:`SLSQP._solve_qp_subproblem`, after bound-fixing) wraps this and adds bound-handling diagnostics. """ d: Vector multipliers_eq: Float[Array, " m_eq"] multipliers_ineq: Float[Array, " m_ineq"] active_set: Bool[Array, " m_ineq"] converged: Bool[Array, ""] iterations: Int[Array, ""] ping_ponged: Bool[Array, ""] reached_max_iter: Bool[Array, ""] final_working_tol: Scalar # M-metric projection feasibility residual from the inner solver # producing the *final* QP direction. Always 0 for null-space # solvers; surfaces the post-refinement residual of # ``MinresQLPSolver`` so the outer SQP can flag QP solves whose # feasibility floor is dangerously high. proj_residual: Scalar # Cumulative number of M-metric projection refinement rounds taken # across all inner solves performed inside the active-set loop. n_proj_refinements: Int[Array, ""] # Latest projected-gradient norm from the final inner solve (HR # Inexact STCG only; ``inf`` for every other inner solver). # Surfaces the noise-aware stationarity proxy used by the opt-in # ``use_inexact_stationarity`` outer-loop convergence test. projected_grad_norm: Scalar
[docs] class QPResult(eqx.Module): """Outer-facing result of solving the SLSQP QP subproblem. Returned by :meth:`SLSQP._solve_qp_subproblem` after the bound-fixing loop has post-processed the direction returned by :func:`solve_qp`. Attributes: direction: The search direction d from the QP solution. multipliers_eq: Lagrange multipliers for equality constraints. multipliers_ineq: Lagrange multipliers for inequality constraints. active_set: Boolean mask of active inequality constraints at the solution. converged: Whether the QP solver converged successfully. iterations: Number of active-set iterations taken. bound_fix_solves: Number of non-trivial bound-fixing passes that actually ran the reduced-space inner solve (passes where the free mask did not change are short-circuited). Useful for debugging bound-handling overhead; 0 when the problem has no bound constraints. n_bound_fixed: Number of variables pinned to a box bound in the final QP direction (counts both lower- and upper-bound activations). ping_ponged: True when the QP active-set loop short-circuited on a detected add/drop ping-pong cycle. reached_max_iter: True when the QP active-set loop exhausted its iteration budget (``qp_max_iter``). lpeca_bypassed: True when the LPEC-A prediction was skipped for this step (warm-up window or trust gate fired). lpeca_capped: True when the LPEC-A prediction was truncated by the rank-aware size cap. n_lpeca_bounds_prefixed: Number of box-bound variables pre-fixed by LPEC-A before entering the bound-fixing loop. Always 0 when ``active_set_method == "expand"``, when ``lpeca_predict_bounds=False``, or when LPEC-A was bypassed by the warm-up / trust gates. proj_residual: Post-refinement constraint residual ``||A d - b||`` from the inner solver producing the final QP direction (after bound-fixing). Always 0 for null-space CG / CRAIG; relevant for ``MinresQLPSolver`` where it reflects the M-metric range-space projection floor and feeds the outer divergence detector via ``SLSQPDiagnostics.max_proj_residual``. n_proj_refinements: Cumulative number of M-metric projection refinement rounds across all inner solves performed for this QP step (active-set loop + bound-fixing loop). Always 0 for null-space solvers. projected_grad_norm: Latest inner-solver projected-gradient norm (``HRInexactSTCG`` only; ``inf`` otherwise). """ direction: Vector multipliers_eq: Float[Array, " m_eq"] multipliers_ineq: Float[Array, " m_ineq"] active_set: Bool[Array, " m_ineq"] converged: Bool[Array, ""] iterations: Int[Array, ""] bound_fix_solves: Int[Array, ""] n_bound_fixed: Int[Array, ""] ping_ponged: Bool[Array, ""] reached_max_iter: Bool[Array, ""] lpeca_bypassed: Bool[Array, ""] lpeca_capped: Bool[Array, ""] n_lpeca_bounds_prefixed: Int[Array, ""] proj_residual: Scalar n_proj_refinements: Int[Array, ""] projected_grad_norm: Scalar
# --------------------------------------------------------------------------- # Outer-loop diagnostics and state # ---------------------------------------------------------------------------
[docs] class SLSQPDiagnostics(eqx.Module): """Diagnostic counters and statistics accumulated during an SLSQP run. These fields are populated inside ``step()`` without Python-side branching, and can be inspected by the user after a solve to decide whether the ``optimistix`` result code is meaningful (e.g. to detect a ``RESULTS.successful`` that actually came from chronic line-search failure rather than real convergence). Attributes: n_qp_inner_failures: Number of iterations where the QP solver reported ``converged=False``. n_ls_failures: Number of iterations where the line search reported ``success=False``. n_lbfgs_skips: Number of iterations where the L-BFGS append skipped the new curvature pair. n_nan_directions: Number of iterations where the QP returned a non-finite search direction. max_gamma: Maximum L-BFGS ``gamma`` observed across iterations. min_diag: Minimum L-BFGS per-variable diagonal entry observed. max_diag: Maximum L-BFGS per-variable diagonal entry observed. eq_jac_min_sv_est: A lower bound on the smallest singular value of ``J_eq`` estimated from the Cholesky factor of ``J_eq J_eq^T``. Small values indicate near rank-deficiency. ls_alpha_min: Smallest line-search step accepted across the run. tail_ls_failures: Consecutive line-search failure count at termination. Non-zero values suggest stagnation. n_bound_fix_solves: Total number of non-trivial bound-fixing inner solves that actually ran (no-op passes are skipped via ``jax.lax.cond`` and do not count). Growing unboundedly is a sign that the bound active set never stabilises. max_bound_fixed: Largest number of variables pinned to their box bounds across all iterations (from the final per-iteration ``free_mask``). max_active_ineq: Largest number of active general inequalities observed across iterations. n_merit_regressions: Number of iterations where the L1 merit function value *increased* despite the line search reporting success. A non-zero count points to merit-function mis-calibration (too-small ``rho``) or line-search slippage. n_qp_budget_exhausted: Number of SQP steps where the QP active-set loop hit ``qp_max_iter`` (i.e. exited because the budget ran out, not because of clean convergence or a ping-pong short-circuit). A non-zero count usually indicates degeneracy, multiplier-noise cycling, or an LPEC-A over-prediction that was not caught by the trust gate; consider raising ``mult_drop_floor`` or tightening ``lpeca_trust_threshold``. n_qp_ping_pong: Number of SQP steps where the QP loop's anti-cycling ping-pong detector fired. These are *good* short-circuits (the loop avoided wasting its full budget) but a chronic non-zero value still points to a degenerate constraint pair worth investigating. max_qp_iterations: Peak active-set iteration count observed across all SQP steps. Useful for sizing ``qp_max_iter``: if this is consistently equal to ``qp_max_iter`` the budget is too tight. max_qp_active_size: Peak ``|active_set|`` (general inequalities only) observed across all SQP steps. n_lpeca_bypassed: Number of SQP steps where the LPEC-A prediction was skipped, either because of the warm-up window (``step_count < lpeca_warmup_steps``) or because the trust gate vetoed it (``rho_bar > lpeca_trust_threshold``). Always 0 when ``active_set_method == "expand"``. n_lpeca_capped: Number of SQP steps where the LPEC-A rank-aware size cap truncated the prediction. n_lpeca_bounds_prefixed: Cumulative count of box-bound pre-fixes contributed by LPEC-A across all SQP steps (summed over the bound predictions that survived the trust / warm-up gates and were installed as the initial ``free_mask`` for the bound-fixing loop). Always 0 when ``active_set_method == "expand"`` or when ``lpeca_predict_bounds=False``. n_proj_refinements: Cumulative number of M-metric projection refinement rounds taken across all inner solves performed by ``MinresQLPSolver`` over the run. Always 0 for null-space CG / CRAIG. max_proj_residual: High-water mark of the post-refinement constraint residual ``||A d - b||`` reported by ``MinresQLPSolver`` on the *accepted* QP direction across all SQP steps. n_divergence_blowups: Total number of merit blowup events observed across the run, whether or not the patience threshold was eventually reached. divergence_triggered: True when the best-iterate divergence rollback fired at least once during the run. min_projected_grad_norm: Low-water mark of the inner solver's projected-gradient norm ``||W̃_k g_k||`` across the run. Always ``inf`` for inner solvers other than ``HRInexactSTCG``. Surfaced for post-hoc inspection of how close the run actually got to KKT in the inexact-projection sense, regardless of whether ``use_inexact_stationarity`` was on. n_steps_inexact_below_classical: Number of iterations where the inner solver's projected-gradient norm was strictly smaller than the classical Lagrangian gradient norm. A high count indicates that multiplier-recovery noise is the limiting factor and the user might benefit from ``use_inexact_stationarity=True`` paired with ``HRInexactSTCG``. """ n_qp_inner_failures: Int[Array, ""] n_ls_failures: Int[Array, ""] n_lbfgs_skips: Int[Array, ""] n_nan_directions: Int[Array, ""] max_gamma: Scalar min_diag: Scalar max_diag: Scalar eq_jac_min_sv_est: Scalar ls_alpha_min: Scalar tail_ls_failures: Int[Array, ""] n_bound_fix_solves: Int[Array, ""] max_bound_fixed: Int[Array, ""] max_active_ineq: Int[Array, ""] n_merit_regressions: Int[Array, ""] n_qp_budget_exhausted: Int[Array, ""] n_qp_ping_pong: Int[Array, ""] max_qp_iterations: Int[Array, ""] max_qp_active_size: Int[Array, ""] n_lpeca_bypassed: Int[Array, ""] n_lpeca_capped: Int[Array, ""] n_lpeca_bounds_prefixed: Int[Array, ""] n_proj_refinements: Int[Array, ""] max_proj_residual: Scalar n_divergence_blowups: Int[Array, ""] divergence_triggered: Bool[Array, ""] min_projected_grad_norm: Scalar n_steps_inexact_below_classical: Int[Array, ""]
def _init_diagnostics() -> SLSQPDiagnostics: """Construct a zero-initialized ``SLSQPDiagnostics`` container.""" return SLSQPDiagnostics( # ty: ignore[invalid-return-type] n_qp_inner_failures=jnp.array(0), n_ls_failures=jnp.array(0), n_lbfgs_skips=jnp.array(0), n_nan_directions=jnp.array(0), max_gamma=jnp.array(0.0), min_diag=jnp.array(jnp.inf), max_diag=jnp.array(0.0), eq_jac_min_sv_est=jnp.array(jnp.inf), ls_alpha_min=jnp.array(1.0), tail_ls_failures=jnp.array(0), n_bound_fix_solves=jnp.array(0), max_bound_fixed=jnp.array(0), max_active_ineq=jnp.array(0), n_merit_regressions=jnp.array(0), n_qp_budget_exhausted=jnp.array(0), n_qp_ping_pong=jnp.array(0), max_qp_iterations=jnp.array(0), max_qp_active_size=jnp.array(0), n_lpeca_bypassed=jnp.array(0), n_lpeca_capped=jnp.array(0), n_lpeca_bounds_prefixed=jnp.array(0), n_proj_refinements=jnp.array(0), max_proj_residual=jnp.array(0.0), n_divergence_blowups=jnp.array(0), divergence_triggered=jnp.array(False), min_projected_grad_norm=jnp.asarray(jnp.inf), n_steps_inexact_below_classical=jnp.array(0), )
[docs] def get_diagnostics(state: "SLSQPState") -> SLSQPDiagnostics: """Return the ``SLSQPDiagnostics`` accumulator from a final state. Use this after ``optimistix.minimise`` (or a manual ``solve / step`` loop) to inspect solver health indicators that the Optimistix result code alone does not expose. See :class:`SLSQPDiagnostics` for field meanings. """ return state.diagnostics
[docs] class SLSQPState(eqx.Module): """State for the SLSQP solver. This is a JAX PyTree (via eqx.Module) that holds all mutable state needed across SLSQP iterations. Attributes: step_count: Current iteration number. f_val: Current objective function value f(x_k). grad: Gradient of objective at current point. eq_val: Equality constraint values c_eq(x_k). ineq_val: Inequality constraint values c_ineq(x_k). eq_jac: Jacobian of equality constraints at x_k. ineq_jac: Jacobian of inequality constraints at x_k. lbfgs_history: L-BFGS history for matrix-free Hessian approximation. multipliers_eq_qp: QP-recovered equality multipliers ``λ_QP = (A_k A_kᵀ)⁻¹ A_k (B d + g_k)`` from the active-set QP solver. Consumed by Han-Powell's penalty rule (``update_penalty_parameter``), the LPEC-A predictor's ``rho_bar`` proximity measure, and the active-set warm-start of the next QP. Carries ``O(s_f / s_eq · cond(B))`` noise when the L-BFGS Hessian is poorly conditioned or when auto-scaling inflates the multiplier scale; do **not** use for ``∇f − Jᵀλ`` stationarity checks. multipliers_ineq_qp: QP-recovered inequality multipliers (general-inequality block + bound block). The bound block comes from the post-line-search recovery in :func:`slsqp_jax.slsqp.bounds.recover_bound_multipliers` so it agrees with ``multipliers_ineq_ls`` for the bound rows; only the general-inequality block carries the QP noise. Consumed by Han-Powell + LPEC-A + warm-start. multipliers_eq_ls: Hessian-free least-squares multipliers ``λ_LS = (J(x_{k+1}) J(x_{k+1})ᵀ)⁻¹ J(x_{k+1}) ∇f(x_{k+1})`` recovered post-line-search at the accepted iterate (see :mod:`slsqp_jax.slsqp.multipliers`). Independent of ``B``, ``d`` and ``α``. Consumed by the L-BFGS secant pair (so the same vector is used at both endpoints) and by ``_terminate_impl``'s filterSQP-style stationarity denominator (the multipliers feed both the Lagrangian gradient numerator and the ``mu_max`` denominator from :func:`slsqp_jax.slsqp.termination.compute_mu_max`). These are the multipliers exposed via ``Solution.stats["multipliers_eq"]`` because they are the stationarity-quality estimate. At ``init()`` time both ``_qp`` and ``_ls`` are seeded from the same ``lstsq`` solution at ``x_0``. multipliers_ineq_ls: Hessian-free LS inequality multipliers (general-inequality block from :mod:`slsqp_jax.slsqp.multipliers` with active-row ``max(0, ·)`` clamp + bound block from :func:`slsqp_jax.slsqp.bounds.recover_bound_multipliers`). At ``init()`` initialised to zeros — bound blocks only get populated once ``step()`` runs the post-step recovery. kkt_residual_grad: Lagrangian-gradient snapshot consumed by the next QP subproblem as the KKT-residual proxy (``kkt_residual = ||state.kkt_residual_grad||`` inside :meth:`SLSQP._solve_qp_subproblem`). Currently always written equal to :attr:`grad_lagrangian` at the end of each step; the field is kept separate from ``grad_lagrangian`` purely so that future variants (e.g. carrying a *previous* iterate's Lagrangian gradient instead of the current one) can be introduced without re-shaping the state pytree. grad_lagrangian: Current gradient of the Lagrangian evaluated at the accepted iterate using the LS multipliers (matching the L-BFGS secant pair). Reused by ``terminate`` so the stationarity check does not fall out of sync with the L-BFGS secant pair. merit_penalty: Current penalty parameter for L1 merit function. bound_jac: Constant Jacobian for bound constraints (computed once in init). qp_iterations: Total accumulated QP active-set iterations across all steps. qp_converged: Whether the most recent QP solve converged. prev_active_set: Active inequality constraint set from the previous QP solve, used for warm-starting the next QP subproblem. termination_code: Granular ``slsqp_jax.RESULTS`` classification (``successful`` / ``merit_stagnation`` / ``line_search_failure`` / ``iterate_blowup`` / ``infeasible`` / ``qp_subproblem_failure`` / ``nonlinear_max_steps_reached`` / ``nonfinite``). Surfaced via ``Solution.stats["slsqp_result"]``; ``Solution.result`` itself remains the coarse ``optx.RESULTS`` code accepted by optimistix's driver. """ # Iteration tracking step_count: Int[Array, ""] # Current function values and gradients f_val: Scalar grad: Vector # Constraint information eq_val: Float[Array, " m_eq"] ineq_val: Float[Array, " m_ineq"] eq_jac: Float[Array, "m_eq n"] ineq_jac: Float[Array, "m_ineq n"] # L-BFGS history for matrix-free Hessian approximation (O(kn) storage) lbfgs_history: LBFGSHistory # QP-recovered Lagrange multipliers (Han-Powell, LPEC-A, warm-start). # Carry ``O(s_f / s_eq * cond(B))`` noise; do NOT use for stationarity. # The bound block of ``multipliers_ineq_qp`` is the post-step bound # recovery (same as the bound block of ``multipliers_ineq_ls``). multipliers_eq_qp: Float[Array, " m_eq"] multipliers_ineq_qp: Float[Array, " m_ineq"] # Hessian-free least-squares multipliers recovered post-line-search at # the accepted iterate. Independent of B, d, alpha; consumed by the # L-BFGS secant pair and by the convergence-test Lagrangian. These # are the multipliers exposed via ``Solution.stats["multipliers_*"]``. multipliers_eq_ls: Float[Array, " m_eq"] multipliers_ineq_ls: Float[Array, " m_ineq"] # Lagrangian-gradient proxy for the next QP's KKT residual. Always # written equal to ``grad_lagrangian`` at the end of each step; kept # as a separate field so future variants can carry a different # snapshot here without re-shaping the state pytree. Read once per # QP solve as ``kkt_residual = ||state.kkt_residual_grad||`` in # :meth:`SLSQP._solve_qp_subproblem`. kkt_residual_grad: Vector # Current Lagrangian gradient at the accepted iterate, computed with # the LS multipliers (matching the L-BFGS secant pair). Reused by # ``terminate`` for stationarity so the check is consistent with # what ``step`` saw. grad_lagrangian: Vector # Merit function penalty parameter merit_penalty: Scalar # Bound constraint Jacobian (constant, computed once in init) bound_jac: Float[Array, "m_bounds n"] # QP solver statistics qp_iterations: Int[Array, ""] qp_converged: Bool[Array, ""] # Active-set warm-starting: carry the QP active set across iterations # to promote multiplier stability (Wright, SIAM J. Optim., 2002, Sec. 8) prev_active_set: Bool[Array, " m_ineq"] # Consecutive QP failure tracking for escalating L-BFGS recovery consecutive_qp_failures: Int[Array, ""] # Consecutive line search failure tracking for escalating L-BFGS recovery consecutive_ls_failures: Int[Array, ""] # Zero-step convergence detection: consecutive iterations where the QP # converged but returned ||d|| < atol, indicating the current point # satisfies the QP's KKT conditions. consecutive_zero_steps: Int[Array, ""] qp_optimal: Bool[Array, ""] # Merit-based stagnation detection best_merit: Scalar steps_without_improvement: Int[Array, ""] stagnation: Bool[Array, ""] last_alpha: Scalar # Norm of the projected gradient ``||W̃_k g_k||`` from the most # recent inner solve, surfaced through ``QPResult.projected_grad_norm``. # Always ``inf`` for inner solvers that do not produce this value # (everything other than ``HRInexactSTCG``). Used by # ``terminate()`` when ``use_inexact_stationarity=True`` so that # the run can declare convergence at the inner solver's noise # floor instead of waiting for the exact Lagrangian gradient. last_projected_grad_norm: Scalar # Whether the most recent line search reported success (Armijo satisfied # or at least a strictly decreasing merit). Used by ``terminate`` to # distinguish genuine convergence from tiny-alpha stagnation. ls_success: Bool[Array, ""] # Whether the most recent line search failed AND the escalated # identity reset also failed (``consecutive_ls_failures`` exceeded # ``2 * ls_failure_patience``). Triggers early termination with # ``RESULTS.line_search_failure`` to surface chronic LS failure. ls_fatal: Bool[Array, ""] # Whether the QP subproblem has failed for ``2 * qp_failure_patience`` # consecutive iterations. Mirrors ``ls_fatal``: the L-BFGS soft- / # identity-reset escalation could not produce a usable QP direction, # and we should terminate with ``RESULTS.qp_subproblem_failure``. qp_fatal: Bool[Array, ""] # Best-iterate divergence rollback (see :attr:`SLSQPConfig` / # :attr:`ToleranceConfig`). ``best_x`` is the iterate that achieved # ``best_merit``. When the merit blows up by ``divergence_factor`` # or returns NaN/Inf for ``divergence_patience`` consecutive steps, # ``step()`` overwrites the returned iterate with ``best_x`` and # sets ``diverging=True``, which routes ``terminate()`` to # ``RESULTS.iterate_blowup``. best_x: Vector blowup_count: Int[Array, ""] diverging: Bool[Array, ""] # Granular termination classification using ``slsqp_jax.RESULTS`` # (see :mod:`slsqp_jax.results`). Optimistix's ``iterative_solve`` # driver only accepts members of ``optx.RESULTS`` from # ``terminate()``, so the public ``Solution.result`` is the coarse # base-class code. This field carries the finer # ``slsqp_jax.RESULTS`` classification (e.g. ``merit_stagnation``, # ``line_search_failure``, ``iterate_blowup``, # ``qp_subproblem_failure``, ``infeasible``) and is surfaced via # ``Solution.stats["slsqp_result"]`` in :meth:`SLSQP.postprocess`. # Pre-termination its value is ``RESULTS.successful`` (i.e. the # loop is still running, no failure latched). termination_code: Any # Diagnostic accumulators (see :class:`SLSQPDiagnostics`). diagnostics: SLSQPDiagnostics
__all__ = [ "InnerSolveResult", "ProjectionContext", "QPState", "QPSolverResult", "QPResult", "SLSQPDiagnostics", "SLSQPState", "_init_diagnostics", "get_diagnostics", ]