[0.8.0] - 2026-07-11
Added — declared-parameter sensitivity for Pyomo (Python / Pyomo)
pyomo_pouncesensitivity interface. Declare parameters while
building the model (declare_sens_param(m.p)— a flag, no perturbed
values), solve normally withSolverFactory('pounce'), then query:
gradient(m.x, wrt=m.p)(exact dx*/dp; equality constraints give
their multiplier's derivative), container/Jacobian access via the
Gradientobject, andestimate(m, [(m.p, value)])for
first-order perturbed-solution estimates with bound clamping and an
active-set warning. When declarations are present the solve runs
in-process through thepounce.Solversession (read_nl+ callback
bridge) and keeps the converged KKT factorization; models without
declarations use the CLI path unchanged.- Python
Solversession additions:parametric_step_full
(full KKT-space step, exposing multiplier sensitivities) and
multiplier_rows(map constraint indices to theiry_crows), with
matching Rust methodsSolver::parametric_step_full/
Solver::g_multiplier_rowsinpounce-sensitivity.
Added — structure-aware KKT hooks (#180)
- Caller-supplied KKT ordering (item 1). A structure-aware presolve can
now hand pounce a precomputed fill-reducing permutation for the KKT linear
solver — a block-triangular / Schur ordering (Parker, Garcia & Bent,
arXiv:2602.17968) or a tearing ordering from equation-oriented
decomposition — that the built-in AMD/METIS pass cannot derive.
Python:Problem.set_ordering(perm)/get_ordering()/clear_ordering();
Rust:IpoptApplication::set_external_ordering(perm).permis a 0-based
new-to-old permutation whose length equals the augmented KKT dimension;
FERAL validates it as a bijection and fails the factorization (never a wrong
answer) on a bad permutation. Maps to FERAL's newOrderingMethod::External
(feral#107); honored by the default FERAL backend only. - Per-solve linear-algebra / callback timing (item 3).
Problem.solve's
infodict now carriesinfo["wall_time"]and aninfo["timing"]
breakdown (overall total, the linear-algebra factorization-vs-back-solve
split, and the per-callback objective / gradient / constraint / Jacobian /
Lagrangian-Hessian eval time);pounce.minimizemirrors these as
res.wall_time/res.timing. Lets a caller attribute a reduced-space
solve's runtime (e.g. densified-Hessian eval cost) directly. The detailed
breakdown is opt-in viatiming_statistics="yes"(see #190 under Fixed);
without it,wall_time/overall_algare populated and the per-subsystem
entries read0.0. - Block-triangular / Schur KKT solve (item 2). A structure-aware presolve
can hand pounce the reducible block of the KKT system; that block is
Schur-complemented out and only the two diagonal blocks are factorized, with
full-system inertia recovered a priori via Sylvester's law (Parker, Garcia &
Bent, arXiv:2602.17968). Python:Problem.set_kkt_schur_block(indices)/
get_kkt_schur_block()/clear_kkt_schur_block(); Rust:
IpoptApplication::set_kkt_schur_block(indices).indicesare KKT-space
(x, slack, eq-dual, ineq-dualblock order). The Schur solver
(FeralSchurSolver+SchurAugSystemSolver) uses only feral's stable
factor/solve, and falls back to the standard full-space solver transparently
when the partition is unsuitable (too large a fraction, malformed, or a
singular diagonal block), so a stray hook never breaks a solve. Beneficial
only when the Schur block is much smaller than the eliminated block; honored
on the default feral + exact-Hessian path.
Changed
- feral 0.12.0 → 0.14.0. 0.13.0 adds
OrderingMethod::External(Vec<usize>)
(feral#107), which backs the caller-supplied KKT ordering hook (#180); the
enum is no longerCopy(theExternalarm carries a heap permutation), so
pounce clones it where a copy was previously implicit. 0.14.0 is a pure-perf
release targeting the IPM warm-refactor workload (one KKT pattern, thousands
of factorizations): it splits the symbolic ordering races so only the winning
candidate pays the expensive tail (feral#127), reuses the permute cache on the
parallel numeric driver so a warm re-factor scatters in O(nnz) instead of
rebuilding and re-sorting triplets every iteration (feral#124), and fuses the
D-block solve into forward substitution for ~14–29% faster warm solves
(feral#126), plus an opt-in tree-parallel sparse solve (feral#131) and
analysis-time assembly maps (feral#125). Numerics are bit-identical; no
breaking API change beyond the 0.13.0Copyremoval noted above.
Fixed
-
pounce.minimize(..., args=...)now works with convex routing. The extra
objectiveargswere applied on the NLP path but not bound into the copies of
fun/jac/hesshanded to the LP/QP/SOCP routers, which probe them as bare
f(x). A parameterized convex objective therefore either silently never
routed (solver_selection=autofell back to NLP) or was wrongly rejected as
"not convex" under a forcedsolver_selection.argsare now bound into the
router probes, so a parameterized convex QP/LP/QCQP routes to the specialized
solver as expected. -
Post-optimal requests are no longer silently dropped on the specialized
solve paths (#196). When an.nldeclared the sIPOPT sensitivity suffixes
(sens_state_1/sens_state_value_1/sens_init_constr) or the solve
asked for a reduced Hessian (--compute-red-hessian), the request was
honored only on the general NLP filter-IPM path. Three fast paths bypassed it
without a word:- Convex LP/QP/QCQP routing. Under
solver_selection=auto, a problem
that classifies as LP / convex-QP was sent to the pounce-convex solver,
which has no sensitivity / reduced-Hessian machinery, so no
sens_sol_state_1was written.autonow declines the fast path and
routes such a solve to the NLP path (which honors the request); an
explicit convexsolver_selectionstill runs convex but now warns that
the request is skipped. --minimamultistart. The--minimaearly-return skipped the
post-optimal step entirely; it now warns that sensitivity / reduced-Hessian
is not available in a multistart search.--minima.solduals. The multistart.solwrote a zero placeholder
for the constraint multipliers; it now recovers the real base-problem duals
at each reported minimum (via a clean re-solve, so a point accepted from an
augmented penalty/tunnel solve still gets the base problem's multipliers).- Python
pounce.minimizeconvex route. A usercallbackcannot fire on
the convex/SOCP route (the solver consumes the extracted quadratic form and
never calls back into Python); it is now surfaced in the dropped-options
warning rather than silently ignored.
- Convex LP/QP/QCQP routing. Under
-
Registered-but-unread algorithmic tuning options are now honored (#191).
A range of options were registered but never read, so the solver always ran
with the hard-coded defaults and any user-set value was silently dropped.
Now wired throughAlgorithmBuilder:kappa_sigma(default1e10) — bounds how far the bound multipliers may
deviate from their primal estimates via a clamp applied after every
accepted step, including the documented< 1value that disables the
correction.kappa_d(default1e-5) — weight of the linear damping term for
one-sided bounds in the barrier objective/gradient.- Filter switching / Armijo / margin constants for the filter line search:
eta_phi,theta_min_fact,theta_max_fact,gamma_phi,gamma_theta,
s_phi,s_theta,alpha_min_frac,obj_max_inc. - Second-order-correction constants:
max_soc(incl.0to disable SOC),
kappa_soc,soc_method. - Filter-reset heuristic:
max_filter_resets(incl.0to disable),
filter_reset_trigger. - Tiny-step and divergence guards on the algorithm:
tiny_step_tol,
tiny_step_y_tol,diverging_iterates_tol. - Inertia-correction / Jacobian-regularization constants on the
perturbation handler:max_hessian_perturbation,
min_hessian_perturbation,first_hessian_perturbation,
perturb_inc_fact_first,perturb_inc_fact,perturb_dec_fact,
jacobian_regularization_value,jacobian_regularization_exponent,
perturb_always_cd. - Iterative-refinement constants on the KKT full-space solver:
min_refinement_steps,max_refinement_steps,residual_ratio_max,
residual_ratio_singular,residual_improvement_factor. - Restoration-phase constants:
bound_mult_reset_threshold,
constr_mult_reset_threshold,resto_penalty_parameter,
resto_proximity_weight. The outer builder carries these (read from the
options list) and propagates them into the restoration builder when the
restoration factory is minted, so all frontends honor them without
per-frontend plumbing.
Every default equals the previously-hard-coded value, so runs that don't set
these options are unchanged. The only options still not wired are ones whose
underlying behavior is not yet implemented (e.g.neg_curv_test_tol's
non-zero branch,expect_infeasible_problem,start_with_resto,
alpha_for_y_tol); wiring those would be misleading until the feature
lands. -
timing_statistics=nono longer runs the detailed timers every iteration
(#190). EveryTimedTask::start/endpair callsgetrusage(RUSAGE_SELF)
(twice — once each), and the per-subsystem / per-callback timers wrap hot
paths (each objective/gradient/constraint/Jacobian/Hessian evaluation, plus
every solve phase). Upstream Ipopt gates these detailed timers on
timing_statistics(defaultno), but pounce mirrored the timers without
the gating — so the syscalls were paid unconditionally, measuring at 16–20%
of busy CPU on fast-objective, high-iteration NLPs. The detailed timers are
now disabled unlesstiming_statistics yes(orprint_timing_statistics yes, which implies it) is set.OverallAlgorithmstays live regardless: it
feeds themax_cpu_timeconvergence check and its total is always reported. -
pip GAMS link honors
json_output/json_detail(#187). The pure-Python
GAMS link parsed those option-file keys and then discarded them, so a
pounce.optrequesting apounce.solve-report/v1JSON was a silent no-op on
the pip route even thoughdocs/src/gams.mdadvertises it (only the native C
link implemented it).Problem.solvenow takes optionalreport_path/
report_detail("summary"|"full") kwargs that emit the report through
the canonical Rust writer (pounce-solve-report, the same schema/serializer
as the CLI's--json-output) — no report format is reimplemented in Python.
The GAMS link threads the two link options into that surface (Full detail
enables the per-iteration trace thepounce-studio/MCP post-mortem tools
consume), and the writer is now available to any Python caller. -
Convex QP/LP path now honors
max_iter=0(#186). A Pyomo/AMPL solve
auto-routed to thepounce-convexinterior-point path reported Optimal
Solution Found even withmax_iter=0, because the routed problem was solved
by presolve or a direct step that ignored the iteration cap — violating the
AMPL/Ipopt contract that zero iterations cannot reach optimality (the NLP
path already reportedMaximumIterationsExceeded). The convex QP and SOCP
dispatch now short-circuit to an iteration-limit result before any solve when
max_iter=0, andmax_iteris forwarded to the convex driver for the=0
case (previously dropped). CI now runspytest pyomo-pounce/tests
end-to-end (not just an import smoke test), which is how this regressed
silently. -
reaction_networkmode-aware dedup on flat eigenmodes (#183). When the
PES has a genuine zero (flat) Hessian eigenmode — rigid translation/rotation
of any molecule, or an intrinsically flat coordinate — the minima dedup
compared full-coordinate distance, so copies of the same basin displaced
along the flat direction counted as distinct minima and exhausted the
n_statesbudget before flooding reached other basins (a whole basin, and
its connections, silently missed).reaction_networknow deduplicates
minima, saddles, and saddle→basin descent matches in the non-null subspace
of the Hessian, quotienting out any eigenmode beloweig_tol. Reduces
exactly to the previous scaled-Euclidean metric when no null modes are
present, so well-conditioned surfaces are unaffected.find_saddlesgains an
optionaldistanceoverride for the same purpose.