Skip to content

pounce 0.8.0

Latest

Choose a tag to compare

@jkitchin jkitchin released this 11 Jul 23:49
d2b7ab6

[0.8.0] - 2026-07-11

Added — declared-parameter sensitivity for Pyomo (Python / Pyomo)

  • pyomo_pounce sensitivity interface. Declare parameters while
    building the model (declare_sens_param(m.p) — a flag, no perturbed
    values), solve normally with SolverFactory('pounce'), then query:
    gradient(m.x, wrt=m.p) (exact dx*/dp; equality constraints give
    their multiplier's derivative), container/Jacobian access via the
    Gradient object, and estimate(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 the pounce.Solver session (read_nl + callback
    bridge) and keeps the converged KKT factorization; models without
    declarations use the CLI path unchanged.
  • Python Solver session additions: parametric_step_full
    (full KKT-space step, exposing multiplier sensitivities) and
    multiplier_rows (map constraint indices to their y_c rows), with
    matching Rust methods Solver::parametric_step_full /
    Solver::g_multiplier_rows in pounce-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). perm is 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 new OrderingMethod::External
    (feral#107); honored by the default FERAL backend only.
  • Per-solve linear-algebra / callback timing (item 3). Problem.solve's
    info dict now carries info["wall_time"] and an info["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.minimize mirrors 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 via timing_statistics="yes" (see #190 under Fixed);
    without it, wall_time / overall_alg are populated and the per-subsystem
    entries read 0.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). indices are KKT-space
    (x, slack, eq-dual, ineq-dual block 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 longer Copy (the External arm 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.0 Copy removal noted above.

Fixed

  • pounce.minimize(..., args=...) now works with convex routing. The extra
    objective args were applied on the NLP path but not bound into the copies of
    fun/jac/hess handed to the LP/QP/SOCP routers, which probe them as bare
    f(x). A parameterized convex objective therefore either silently never
    routed (solver_selection=auto fell back to NLP) or was wrongly rejected as
    "not convex" under a forced solver_selection. args are 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 .nl declared 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_1 was written. auto now declines the fast path and
      routes such a solve to the NLP path (which honors the request); an
      explicit convex solver_selection still runs convex but now warns that
      the request is skipped.
    • --minima multistart. The --minima early-return skipped the
      post-optimal step entirely; it now warns that sensitivity / reduced-Hessian
      is not available in a multistart search.
    • --minima .sol duals. The multistart .sol wrote 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.minimize convex route. A user callback cannot 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.
  • 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 through AlgorithmBuilder:

    • kappa_sigma (default 1e10) — bounds how far the bound multipliers may
      deviate from their primal estimates via a clamp applied after every
      accepted step, including the documented < 1 value that disables the
      correction.
    • kappa_d (default 1e-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. 0 to disable SOC),
      kappa_soc, soc_method.
    • Filter-reset heuristic: max_filter_resets (incl. 0 to 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=no no longer runs the detailed timers every iteration
    (#190).
    Every TimedTask::start/end pair calls getrusage(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 (default no), 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 unless timing_statistics yes (or print_timing_statistics yes, which implies it) is set. OverallAlgorithm stays live regardless: it
    feeds the max_cpu_time convergence 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.opt requesting a pounce.solve-report/v1 JSON was a silent no-op on
    the pip route even though docs/src/gams.md advertises it (only the native C
    link implemented it). Problem.solve now takes optional report_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 the pounce-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 the pounce-convex interior-point path reported Optimal
    Solution Found
    even with max_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 reported MaximumIterationsExceeded). The convex QP and SOCP
    dispatch now short-circuit to an iteration-limit result before any solve when
    max_iter=0, and max_iter is forwarded to the convex driver for the =0
    case (previously dropped). CI now runs pytest pyomo-pounce/tests
    end-to-end (not just an import smoke test), which is how this regressed
    silently.

  • reaction_network mode-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_states budget before flooding reached other basins (a whole basin, and
    its connections, silently missed). reaction_network now deduplicates
    minima, saddles, and saddle→basin descent matches in the non-null subspace
    of the Hessian, quotienting out any eigenmode below eig_tol. Reduces
    exactly to the previous scaled-Euclidean metric when no null modes are
    present, so well-conditioned surfaces are unaffected. find_saddles gains an
    optional distance override for the same purpose.