Skip to content

[Scoping] Evaluate POUNCE as a replacement for cyipopt/IPOPT parameter estimation #139

Description

@bernalde

Goal

Evaluate POUNCE as the replacement for
PharmaPy's cyipopt/Ipopt-backed bounded parameter-estimation path and, if the
parity gate below passes, migrate that path without changing the default
Levenberg-Marquardt (LM) behavior.

POUNCE is a pure-Rust port of Ipopt with a Python package published as
pounce-solver (imported as
pounce). Its Python API provides a SciPy-style minimize() facade and
cyipopt-compatible Problem interface without requiring a separate system
Ipopt installation. At the time of this audit, the latest PyPI release is
0.8.0 (July 11, 2026).

Package-wide audit

Audit basis: origin/master at 3ebf71f (186 tracked files, including all 36
production Python modules, tests, documentation, packaging metadata, examples,
and workflows).

There is exactly one direct cyipopt/Ipopt integration:

  • PharmaPy/ParamEstim.py:24-29 optionally imports
    cyipopt.minimize_ipopt and records have_cyipopt.
  • PharmaPy/ParamEstim.py:599-646 selects it with method="IPOPT" and passes
    the scalar objective, analytic gradient, bounds, Ipopt options, and
    kwargs={"out_array": False}.

The surrounding public/downstream surface is:

  • PharmaPy/SimExec.py:368-373: EstimateParams() passes the solver method,
    bounds, and options through to ParameterEstimation.optimize_fn().
  • PharmaPy/StatsModule.py:24-28,409-412: parallel fitting and bootstrap
    re-run the selected opt_method, so migration must cover repeated solves.
  • doc/online_docs/conf.py:24: Sphinx mocks cyipopt.
  • pyproject.toml, requirements files, DEPENDENCIES.md, and CI do not
    declare, install, or exercise cyipopt.
  • No current test invokes a real cyipopt/Ipopt parameter-estimation solve.

No other ipopt or cyipopt calls exist in the package. The SciPy
minimize, root, fsolve, and newton calls elsewhere solve unrelated
distillation/root-finding problems and are not migration targets. Assimulo is
also a separate ODE/DAE integration concern covered by #10 and #134.

Compatibility findings to validate

A direct import rename is close, but not sufficient:

  1. Distribution/import names: install pounce-solver; import
    pounce.minimize.
  2. Call signature: POUNCE's minimize() accepts fun, x0, jac,
    bounds, and Ipopt-style options, but PharmaPy's current
    kwargs={"out_array": False} must not be forwarded as a solver option.
    get_objective() already defaults to out_array=False, so remove that
    argument or use an explicit wrapper.
  3. Results: confirm the returned OptimizeResult preserves everything
    PharmaPy consumes (x) and that PharmaPy's separately assembled
    info={"jac", "fun"} remains correct for covariance/statistics.
  4. Selector compatibility: decide whether the public method becomes
    "POUNCE" with a deprecation window for "IPOPT", or whether "IPOPT"
    remains a compatibility alias backed by POUNCE. Do not silently change the
    meaning without documentation.
  5. Dependency floor: current pounce-solver metadata requires Python
    >=3.9, NumPy >=1.23, and SciPy >=1.11; PharmaPy currently advertises
    Python >=3.9, NumPy >=1.22, and SciPy >=1.9. Decide whether POUNCE is
    an optional extra (preferred for the evaluation) or whether core floors
    should move.
  6. Platform/installability: verify released wheels on the supported
    Linux, macOS, and Windows architectures used by PharmaPy, and document the
    source-build fallback and EPL-2.0 dependency license.
  7. Correct baseline: High: [ParamEstim] Fixed-parameter gradients and IPOPT residual weighting are broken #83 must be fixed before solver parity is judged.
    POUNCE would receive the same currently incorrect residual ordering and
    weighting in get_gradient(). High: [ParamEstim/Calibration] IPOPT result assembly crashes and PCR predict recenters on new data #78 / Fix IPOPT result assembly and PCR prediction centering #137 must also land so post-solve
    result assembly no longer crashes.

Evaluation plan

1. Establish corrected baselines

2. Build an isolated POUNCE adapter spike

  • Import pounce.minimize lazily/optionally with an actionable error when
    pounce-solver is absent.
  • Adapt the current objective, gradient, bounds, verbosity, and options
    without forwarding kwargs as an unknown POUNCE option.
  • Preserve the info["jac"] / info["fun"] contract used by covariance,
    StatisticsClass, bootstrap, and parallel refits.
  • Exercise the public SimExec.EstimateParams() path, not only a direct
    helper call.

3. Compare correctness, robustness, and cost

Record results for corrected cyipopt/Ipopt, POUNCE, and LM where applicable:

  • converged parameters, objective, weighted residual vector, analytic gradient,
    covariance inputs, bound feasibility, termination status, and reproducibility;
  • cold import/startup time, solve time, function/gradient evaluations, and
    repeated/bootstrap solve behavior;
  • behavior for invalid bounds, unavailable optional dependency, non-convergence,
    and solver option errors;
  • installation and CI behavior on the package's supported Python/platform
    matrix.

Use explicit tolerances justified by the problem scale. A successful exit code
alone is not a parity result.

4. Make and implement the decision

  • Post a compact comparison table and a go/no-go recommendation on this
    issue, including the exact POUNCE and cyipopt/Ipopt versions tested.
  • If go, implement the migration in a focused PR:
    • add a documented optional optimization extra and CI lane;
    • implement the chosen method-name/deprecation policy;
    • replace the direct cyipopt import/call and error text;
    • update dependency policy, installation/API docs, Sphinx mocks, and examples;
    • add real POUNCE end-to-end regression coverage (not only a monkeypatched
      result object);
    • remove stale cyipopt/Ipopt references except intentional migration notes.
  • If no-go, document the blocking numerical/API/platform gaps and keep
    the current backend explicitly rather than leaving a partial adapter.

Acceptance criteria

Out of scope

Related

Metadata

Metadata

Assignees

Labels

area:packagingPackaging, dependency metadata, and distributionarea:param-estimationParameter estimation, gradients, and optimization plumbingarea:solversODE/DAE solver integration and solver interfacesarea:testingTest discovery, fixtures, coverage, and test strategyenhancementNew feature or requeststatus:plannedPlanned roadmap or maintenance work, not a verified bug reporttech-debtCode health and refactoring

Type

No type

Projects

Status
No status

Relationships

None yet

Development

No branches or pull requests

Issue actions