Skip to content

Add Powell's method as a second fit-refinement method (complement to Simplex) #403

@wshlavacek

Description

@wshlavacek

Summary

PyBNF's refine = 1 post-fit polish currently offers exactly one local method — Nelder–Mead Simplex (_refine_best_fit in pybnf/pybnf.pySimplexAlgorithm). Add Powell's method as a second, user-selectable refiner that complements (does not replace) Simplex.

Why Powell (and why these constraints)

The refiner's role is local polish from an already-good point found by the global search. PyBNF objectives are typically black-box (simulator outputs, no analytic gradient) and often noisy (SSA / replicate-averaged), which rules out gradient local-optimizers (BFGS/L-BFGS) unless finite-differenced — fragile under simulation noise. Powell's method is derivative-free (conjugate-direction) like Nelder–Mead, fits the same black-box/noisy regime, and frequently converges better on smooth-ish objectives. Both are available via scipy.optimize.minimize(method='Powell'|'Nelder-Mead').

(Levenberg–Marquardt was considered as a third option — highest value for least-squares objectives — but it is gradient-based and objective-restricted to sum-of-squares (chi_sq/sos); meaningless for kl/neg_bin/likelihood objfuncs. Out of scope here; revisit separately if requested.)

Design notes (from the #399 grill, 2026-06-04)

  • This is the second refiner, which is the event that clears ADR-0009's ≥2-user bar for a general cross-method-dependency mechanism. M2.1 Stage (c): narrow each fit_type's effective config to its own keys (per-method narrowing, ADR-0006 #1) #399 deliberately keeps the refine→simplex reach as an explicit one-off (ADR-0006 Use scipy.stats functions for defining distributions #5) BUT routes it through a single _REFINER_SCHEMA / _refine_pulls_in(d) seam in config.py so adding a refiner is a localized change, not a rewrite.
  • Likely surface: a refine_method = sim | powell config key (default sim, backward-compatible). The selected refiner's config schema is what _build_config conditionally pulls in (generalizing today's hardwired SimplexConfig overlay to "the chosen refiner's schema") — at which point the registry cross-dependency mechanism (ADR-0006 considered-option (b)) may finally be worth building.
  • Keep .conf backward compatibility: refine = 1 with no refine_method ⇒ Simplex, byte-identical to today.

Scope / acceptance (rough — to be planned in its own session)

  • refine_method config key + grammar/schema; default sim.
  • A Powell refiner path in _refine_best_fit (likely a thin scipy.optimize.minimize(method='Powell') driver respecting bounds / parameter scales).
  • Generalize the _REFINER_SCHEMA seam from M2.1 Stage (c): narrow each fit_type's effective config to its own keys (per-method narrowing, ADR-0006 #1) #399 to dispatch on refine_method.
  • End-to-end test: refine_method = powell on a non-sim fit over an AnalyticalModel target reaches the mode (mirrors the Simplex refine test).
  • Docs: refinement section lists both methods + when to prefer each.

Depends on #399 (the refiner seam). Separate session. Any new runtime dep → add to .github/actions/setup-pybnf (scipy is already a dep).

🤖 Filed with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions