POUNCE v0.4.0
[0.4.0] — 2026-06-05
Added — pounce.curve_fit (Python)
A scipy.optimize.curve_fit-style nonlinear fitter on top of the
interior-point solver, returning much more than (popt, pcov):
- parameter covariance, standard errors, and Student-t confidence intervals
read pounce-natively from the converged factor's reduced Hessian
(pcov = 2·s²·inv(H_S) = s²·(JᵀJ)⁻¹; matches scipy /pycse.nlinfit). The
t-quantiles use scipy when present and an accurate scipy-free inverse-t
(via the inverse regularized incomplete beta) otherwise, so the CIs are
correct on a numpy-only install even for small samples; - a smooth (C²) loss family — ordinary/weighted least squares plus robust
Cauchy and a smooth pseudo-Huber, exposed under bothsoft_l1andhuber
(the same C² loss: a true piecewise Huber is only C¹, which the IPM can't
use), with a sandwich covariance estimator (non-smooth L1/MAE is
intentionally out of scope for the IPM); - parameter constraints scipy can't express — positivity/negativity/ranges
viabounds, and relations between parameters viaconstraints=; an active
bound/constraint yields a covariance projected onto the free subspace; - data sensitivity
dpopt/ddata(∂params/∂data) from a single batched
back-solve against the same factor (Solver.kkt_solve_many); - a
CurveFitResultwithpredict(),confidence_band()(bothconfidence
andpredictionkinds, heteroscedastic-aware),correlation, R²/χ²/dof,
andsummary().
Derivatives resolve analytic jac → JAX autodiff (the default for
jax.numpy models) → a finite-difference fallback; exact derivatives let the
solve converge cleanly with scaling off, which is what makes the
factor-based covariance and sensitivity exact. Docs:
docs/src/curve-fitting.md; notebook python/notebooks/18_curve_fit.ipynb.
p0 is now optional even without bounds: when omitted, the parameter count is
read from the model signature and the starting point is chosen data-drivenly
(a bound-aware, data-scale candidate sweep scored by the objective) instead of
defaulting to a flat vector of ones — so badly-scaled problems get a far better
seed, while ones (clipped into the bounds) is always among the scored
candidates so the choice is never worse than the old default.
Added — pounce.curve_fit_minima (Python)
curve_fit_minima finds multiple parameter sets that each explain the
data, for the non-convex problems where one fit isn't the whole story
(peak-assignment ambiguity, frequency aliasing in sinusoids, amplitude/decay
trade-offs in sums of exponentials, sign/label symmetry, …).
- drives
pounce.find_minimaover the very same fitting objective as
curve_fit— identicalsigmaweighting, robustloss,f_scale,
constraints, and resolved Jacobian — so the enumerated minima are true
optima of the actual fit, not a separate surrogate; - reuses the model Jacobian as the search gradient and the Gauss-Newton
matrix as the search Hessian, which sharpens the basin escapes and lets
find_minimacertify each point as a minimum (rejecting saddles); - refines every distinct minimum into a full
CurveFitResult(covariance,
CIs, optionaldpopt/ddata) and returns them ranked by SSE, best first; - the
method,n_minima,max_solves,patience,dedup,seed, and
find_minima_kwarguments pass straight through tofind_minima; finite
boundsdefine the box it samples / repels within. Docs:
docs/src/curve-fitting.md.
Added — pounce verify subcommand + signed receipts
A verify subcommand that re-derives feasibility from the canonical .nl
rather than trusting a .sol's status line or the solver/agent that produced
it — the trust anchor when pounce is a tool an agent calls: the agent
proposes a solution, a small deterministic checker disposes.
pounce verify <problem.nl> <claim.sol>evaluatesg(x*)and bounds
against the canonical model, reporting the worst constraint/bound violation
and (when the.solcarries duals) a bound-projected KKT stationarity
residual. Exit 0 = VERIFIED, 20 = REJECTED, 2 = usage/IO. Feasibility
gates; optimality is informational unless--require-optimal.- The JSON receipt content-addresses both inputs by SHA-256 (zero new deps);
withPOUNCE_VERIFY_KEYset it signs the receipt with HMAC-SHA256 over a
float-free preimage so any language can re-derive it. - MCP
verify_solutiontool plus dependency-freeverify_sighelpers and a
stdlib reference signer service.
The check itself (recompute feasibility against the model + a content-addressed
receipt) is ready to use and needs no secrets; the signing / remote-authority
layer is an explicit proof of concept. Docs: docs/src/verify.md.
Added — Debugger load / sweep / multistart
The interactive solver debugger gained three commands for seeding solves
from externally-computed points and for initialization-sensitivity
diagnostics:
load <file> [block]— the inverse ofsave. Reads a block (default
x) into the live iterate from either asaveartifact (JSON; every
block present is loaded) or a plain numeric file
(comma/whitespace/newline-separated). The many-variable escape hatch:
generate a start once (numpy.savetxt) andloadit instead of typing
it. A loadedxbecomes the seed for the next step /resolve.sweep <file>— run one full solve per start in a file (one per line),
then tabulate each terminal status / objective, count distinct minima
(objectives clustered to a relative1e-6), and flag the best solve.multistart <N> [rel]—Nsolves from sampled restarts: each variable
with a finite box[x_Lᵢ, x_Uᵢ]is drawn uniformly in that box;
unbounded variables fall back to a relative jitter±rel·(|xᵢ|+1)
aroundx. Start 0 is the unperturbed point; deterministic (fixed-seed
PRNG), so runs reproduce. Backed by a newDebugCtx::var_bounds()that
reconstructs full-length algorithm-space bounds (post-scaling, with±∞
for absent bounds) from the NLP's reduced bound vectors + expansion
matrices.
Tab completion now also covers filesystem paths (after
load/sweep/save/source, with a trailing / on directories) and
block names for load's optional second argument — available both at the
REPL Tab key and via the programmatic complete command.
Ctrl-C at the prompt is now a working escape hatch: the first press
cancels the current input line (readline convention), a second in a row
stops the solve (a clean UserRequestedStop) — mirroring the running-mode
double-tap, so two Ctrl-Cs always exit whether running or paused.
And a little something for the 2am debugging sessions: an undocumented
coffee command at the prompt. ☕
Both sweep commands build on the existing re-solve machinery and keep each
solve's trajectory observable (breakpoints/events still fire inside a
sweep). JSON mode emits sweep_result per solve and a final
sweep_summary; hello.capabilities advertises load and sweep. For
automated global search with dedup and minimum certification, the Python
find_minima remains the production path. Docs: docs/src/debugger.md
(new "Multi-start and initialization sensitivity" section + scripting
examples).
Added — Sparse (colored) AD for the JAX front-ends (sparse=)
from_jax and JaxProblem gained a sparse=True flag that computes the
constraint Jacobian and the Lagrangian Hessian with CPR-style colored AD
— one JVP/HVP per color (k ≪ n colors) scattered back to the detected
nonzeros — instead of materializing the dense matrix and slicing it
(pounce#83). Per-iteration derivative cost drops from O(n) to O(k)
AD passes on genuinely sparse problems; benchmarked on a banded family at
~560× (Jacobian) / ~200× (Hessian) per eval and 7.6× faster full solve
by n=2000. When the sparsity pattern is value-independent (any
composition of smooth pointwise ops) the reported structure, values, and
solutions are identical to the dense path; the differentiable backward is
unaffected. For value-dependent structure (where / abs / branches) a
random probe can miss a nonzero, and under compression a missed entry aliases
into a same-colored reported entry — silently wrong derivatives — so such
models should hand-specify the pattern via the Problem API or stay on the
dense path. Dense problems see a small bounded overhead, so the flag is opt-in.
- Forward/reverse mode selection (
jacfwdwhenn < m, elsejacrev)
for the dense path / sparsity probe. - Multi-probe sparsity detection (
n_probes=, default 3 under
sparse=True, 1 otherwise) unions several random probes to harden
against value-dependent structure. - Benchmark:
python/benchmarks/bench_sparse_ad_83.py. Docs:
docs/src/python.md(JAX integration → "Sparse Jacobian/Hessian
compression").
Added — Interactive solver debugger (--debug / --debug-json)
A "pdb for the interior-point loop." pounce <problem> --debug opens a
branded REPL that pauses the solve to inspect and mutate live state;
--debug-json speaks a newline-delimited JSON protocol so an LLM agent,
a script, or a visual debugger (VS Code DAP / webview) can drive it.
Full guide in docs/src/debugger.md. Zero effect on the solve when not
attached.
- Checkpoints & stepping: pauses at
iter_start, the sub-iteration
phases (after_mu/after_search_dir/after_step),step_rejected
(line search gave up, before restoration), around restoration
(pre_/post_restoration_entry/exit), andterminated.
step/stepi/continue/run N/stop-at <cp>/detach/
quit. The same debugger steps into the restoration inner IPM
(pauses flaggedin_restoration). - Breakpoints: by iteration (
break N, one-shottbreak N),
conditional with&&/||, on a solver event (break on regularized|resto_entered|tiny_step|ls_rejected|mu_stalled|nan), and
watchpoints (watchpoint x[3]).commands N …auto-runs a list on
hit. - Inspect:
info;printof blocks, search-direction blocks (dx),
scalars (mu obj inf_pr inf_du err compl iter),kkt(inertia +
regularization), andactive;watch/display;diff. - Named-equation diagnostics:
print residualslabels primal/dual
residuals with their original.nlconstraint/variable names;print equation <name|row>renders the source algebra of a named constraint
(by model name or.nlrow index);print rankreports the SVD
numerical rank of the equality Jacobian J_c and names the implicated
rows.diagnose(aliasdiag) runs a panel of heuristics over the
current iterate and emits a named health report — "the worst
constraint residual is c[mass_balance]" rather than "row 13 is
infeasible" — the live counterpart to thepounce-studiodiagnose
tool. - Mutate / what-if:
set mu,set x[i],set opt;goto/restart
(soft rewind) andresolve(re-solve from the current point). - Visualize:
viz kkt/viz L/viz <block>open viapounce-dbg-viz
— an interactive Plotly viewer (spy/heatmap for the KKT matrix & LDLᵀ
factor, bars for vectors);savedumps the iterate.pip install 'pounce-solver[viz]'. - Attach & drive:
--debug-on-error(post-mortem),--debug-on- interrupt/ Ctrl-C / in-band{"cmd":"pause"}(async pause),
--debug-script/source, option discovery + Tab completion,ask
(consult an LLM about the paused state; provider-selectable via
$POUNCE_DBG_LLM=claude/codex/gemini/llmor a custom
command template, default Claude Code), and a branded REPL banner
reusing the project wordmark with a command cheat-sheet. - JSON protocol:
hello→pause→result(withrequest_id) →
progress→terminated. Engine inpounce-algorithm::debug; front
end inpounce-cli::debug_repl. - MCP live-debug proxy:
pounce-studioexposes the debugger over the
Model Context Protocol (debug_start/debug_command/debug_state
/debug_sessions/debug_close), proxying the--debug-json
protocol so an MCP client can start, drive, and inspect a live solve.
Added — read_nl / NlProblem (Python)
pounce.read_nl(path) loads an AMPL .nl file through pounce's own reader
and returns an NlProblem exposing the model's objective, gradient,
hessian, and constraint jacobian at any point — the same evaluation
pipeline the solver uses, available standalone for inspection, finite-
difference checks, or feeding another tool. Exported from pounce
(read_nl, NlProblem are in __all__).
Added — Expanded .nl opcode coverage
The .nl reader now handles conditional/logical opcodes (if-then-else,
comparisons), the n-ary list reducers o11 (MINLIST) / o12 (MAXLIST), and
the remaining smooth transcendentals (inverse and hyperbolic trig). Models
that previously failed to load with an "unsupported opcode" error now parse,
with FD-verified first/second derivatives on the smooth interior.
min/max/if-then-elseare non-smooth: at a kink the gradient is a
subgradient and the Hessian misses the kink curvature, so an iterate landing
on or oscillating across the switch can stall the interior-point solve. The
inverse-trig opcodes (asin/acos/atanh/acosh) have bounded domains
whose derivatives blow up at the edge — bound such variables away from the
boundary. The reader accepts these models; convergence is on you.
Added — pounce --cite and --minima
pounce --cite [REPORT.json]lists the citations to use for pounce (and,
when a solve report is given, any method-specific references it triggered,
e.g. the Byrd restoration paper).--bibtexemits ready-to-paste entries.pounce <problem> --minimaruns the multistart global search from the CLI
with fullfind_minimaparity (method,n_minima, dedup, seed).
Changed
- Default solver trajectory moved on several fronts as the interior-point
method was brought closer to IPOPT. These change which iterates are visited
(and, on a few problems, the iteration count) but not the math being solved:- the barrier parameter
μis now updated inside the monotone reduction
loop, so the relaxed-complementarity error reflects the currentμ. Net
+2 problems reach Optimal on the internal.nlsweep, at a ~2.7% total
iteration-count cost and a regression ondeconvb/gausselm; - under the watchdog, the line search bypasses the acceptor's
alpha_min
floor (mirrors IPOPT) so the full-step watchdog trial actually runs; - the IPOPT safe-slack bound-adjustment mechanism (
slack_move) is ported
and active by default; - NLP gradient-based scaling now lifts fixed variables to their value before
sampling, so the computed scale factors match the operating point.
- the barrier parameter
- Auto-retry on local infeasibility (default on). New option
feral_infeasibility_scaling_retry(defaultyes): when a solve ends in
Infeasible_Problem_Detectedunder a non-MC64 effective scaling, pounce
re-solves once withferal_scaling=mc64(main IPM and restoration sub-IPM).
This rescues problems where a backward-stable scaling choice lands in a
spurious infeasible basin under sensitive dependence (discs.nlis the
canonical case); every individual solve along both trajectories is itself
backward-stable, so an a-priori scaling router can't distinguish them. Set
tonoto restore the single-solve behavior. - New option
feral_scaling(defaultauto, mirrorsferal_ordering):
pins FERAL's diagonal KKT scaling strategy; also settable via the
POUNCE_FERAL_SCALINGenv var. - Dependency:
feralpinned to crates.io0.10.0(was a git rev),
bringing AMF ordering by default and MC64 inertia-guided scaling fallback. - Internal: the
.nlpipeline was extracted into a new leaf crate
pounce-nl(re-exported frompounce-cli; no public API change). pounce-studio-mcp→ 0.1.0 (versioned independently of the0.4.0
core): the MCP server graduated from its0.0.1spike to its first
functional release — analyze / run / explain / citations tools, GAMS
problem tools, a live debug-session proxy, and PyO3 backing via
pounce-studio-core.
Fixed
- Windows build: the debugger's
SIGINT-to-break handler referenced
nix::sys/nix::libc, which the (Unix-only)nixcrate does not expose
on Windows, breaking thepounce-clibuild there. The POSIX handler is now
#[cfg(unix)]-gated with a no-opinstall()stub elsewhere; the rustyline
prompt's Ctrl-C double-tap remains the cross-platform escape hatch. .solbanner no longer goes stale: theparse_solround-trip test
fixture derived itsPOUNCE <version>:message from a hardcoded literal,
which silently drifted on each release (it was still0.3.1). It now
readsCARGO_PKG_VERSION, like the production writer always has, so the
fixture self-updates and never needs a manual bump.- Restoration: the limited-memory (L-BFGS) Hessian is now built in the
iterates' native space, fixing a space mismatch on compound problems (#102);
the cycle detector rolls back to the last acceptable point instead of
erroring out when a usable iterate exists. - KKT: the negative-eigenvalue cache is refreshed on
WrongInertia/
Singularoutcomes (not onlySuccess), matching IPOPT's inertia
pass-through so δ_c regularization routing stays live near a singular KKT (#99). find_minima: the in-bounds test uses a bound-magnitude-relative
tolerance so large-scale boxes aren't spuriously rejected (#101); MLSL is
bounded by a sample budget so it always terminates instead of looping when
its clustering filter rejects every sample (#103).- Bounds length is validated up front across
minimize,find_minima,
find_saddles,find_critical_points,reaction_network, andcurve_fit.
Aboundslist whose length didn't match the variable/parameter count used
to fail silently — a too-short list left trailing variables unbounded, and in
the sampling-based searches a length-1 box could broadcast across every
dimension (sampling all of them from variable 0's interval). It now raises a
clearValueErrorimmediately, like scipy;curve_fit's scipy-style
(lo, hi)tuple form is likewise checked so array sides must be scalar or
length-n_params. - Input validation hardened so imperfect-but-plausible arguments raise a
clearValueErrorup front instead of failing cryptically deep in the solve:minimize/find_minima/find_saddlesnow promote a scalar / 0-d
x0to 1-D (matching scipy), sominimize(f, 1.5)works instead of
raisingiteration over a 0-d array;- a reversed bound (
low > high) is rejected instead of silently
producing an infeasible box (a fixedlow == highis still allowed); - malformed constraint dicts (not a dict, or missing
type/fun, or a
non-callablefun) raise a descriptive error instead of a bareKeyError; curve_fitvalidates its data and weights:xdata/ydatalength must
match and be non-empty and finite,sigmamust be positive and finite,
f_scalemust be positive and finite, and an explicitp0must have one
start per model parameter — each previously surfaced as aLinAlgError,
ZeroDivisionError, back-solveRuntimeError, broadcast error, or a
silently wrong fit;- a model with keyword-only parameters (
f(x, *, a, b)) — which
curve_fitcannot call positionally asf(x, *params)— is rejected with
a clear message instead of a downstreamTypeError; CurveFitResult.confidence_bandchecks thatxhas the same
dimensionality as the fittedxdataand that a prediction-bandsigmais
scalar or matchesx, replacing a cryptic einsum/broadcast error;find_minima/find_saddlesreject a sub-1n_minima/n_saddles/
patience/max_solves, andfind_saddlesrejects a Morseindex
outside[1, n](which previously sliced the step vector wrong and found
the wrong critical points).