Skip to content

Modernize find_MAP - #8429

Merged
ricardoV94 merged 20 commits into
pymc-devs:mainfrom
velochy:port-find-map
Oct 7, 2026
Merged

ricardoV94 merged 20 commits into
pymc-devs:mainfrom
velochy:port-find-map

Conversation

@velochy

@velochy velochy commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

Port find_MAP from pymc-extras into PyMC

Closes #7308.

Summary

pm.find_MAP is rewritten on top of the pymc_extras.find_MAP implementation and aligned with
pm.sample in parameter names, defaults and conventions. The pymc-extras design (fused
loss/gradient/hessian functions compiled over one flat parameter vector, better_optimize for
the scipy interface and progress bar, basinhopping support, optional inverse-Hessian) is taken
as the base; where pymc-extras and pm.sample disagree, pm.sample wins.

Highlights:

  • Same compile pipeline as pm.sample. backend= / compile_kwargs= go through
    resolve_backend_compile_kwargs, so the default backend is whatever PyTensor's default mode is
    (numba), exactly like NUTS. Derivatives are always taken by PyTensor; pymc-extras'
    gradient_backend="jax" option (JAX autodiff on the compiled loss) is dropped as vestigial.
  • Same initial point machinery as pm.sample. initvals=, jitter=, jitter_max_retries=
    and random_seed= use the initial point function pm.sample uses, with the same redraw rule
    for non-finite jittered starts, which also fixes pymc-extras#687 (jitter never applied /
    broken by model freezing).
  • Same output as pm.sample. return_inferencedata=True returns a DataTree
    built by pm.to_inference_data from a one-draw trace, so posterior, observed_data,
    constant_data, deterministics, coords/dims and idata_kwargs (log_likelihood, ...) all
    behave like a sampling run. Two extra groups carry the optimizer output: fit
    (mean_vector, optional covariance_matrix) and optimizer_result (every field of the scipy
    OptimizeResult, per-parameter fields labelled with coordinate-aware names such as
    beta[Intercept]). The classic {name: value} dict stays the default for a few releases,
    with a FutureWarning.
  • Optimizer methods are case-insensitive ("bfgs", "Powell" keep working), use_grad /
    use_hess / use_hessp are user-controllable with method-based defaults, and
    method="basinhopping" is available.

Backward compatibility

Existing find_MAP calls keep working. For release notes:

pm.find_MAP is rebuilt on the pymc-extras implementation. Pass return_inferencedata=True to get an InferenceData with the MAP as a single-draw posterior, and jitter=True to start from a jittered initial point. Both will become the default in a future release; until then the dict output and unjittered start are kept, with a FutureWarning. start, seed and maxeval are renamed to initvals, random_seed and maxiter, and a callable method is no longer supported.

  • Warns (FutureWarning) but keeps working: the dict return (default, or return_inferencedata=False), the unjittered start when jitter is not given, start → initvals, seed → random_seed, maxeval → maxiter, include_transformed= → idata_kwargs, return_raw, a single positional start dict, and progressbar_theme (ignored).
  • Breaks immediately: a callable method raises a TypeError, and everything except method is keyword-only, so find_MAP(start, vars) raises a TypeError.
  • Unchanged: the pm.find_MAP and pymc.tuning.find_MAP import paths, and pm.sample(init="map") / "advi_map", apart from now honouring initvals.

API

find_MAP(
    method="L-BFGS-B", *,
    vars=None, use_grad=None, use_hess=None, use_hessp=None,
    initvals=None, jitter=None, jitter_max_retries=10, random_seed=None,
    progressbar=True, compute_hessian=False,
    return_inferencedata=False, idata_kwargs=None,
    model=None, backend=None, compile_kwargs=None,
    **optimizer_kwargs,
)
old pymc pymc-extras this PR note
start (positional) initvals initvals start= and a positional dict still work with a FutureWarning
seed random_seed random_seed seed= warns
vars – vars kept; others held fixed at the initial point
– jitter_rvs jitter + jitter_max_retries pm.sample semantics; only optimized variables are jittered. Off unless requested for now, with a FutureWarning when not given
return_raw – – warns; result lives in idata.optimizer_result
maxeval maxiter (via kwargs) maxiter (via kwargs) maxeval= warns and is forwarded
progressbar_theme – – warns, ignored (progress bar comes from better_optimize)
include_transformed=True include_transformed=True idata_kwargs={"include_transformed": ...}, default False as in pm.sample; the old keyword warns and is forwarded
– gradient_backend – removed; PyTensor derivatives on every backend
– compile_kwargs backend + compile_kwargs pm.sample convention
dict DataTree dict, DataTree with return_inferencedata=True the dict warns; the default flips in a future release
– compute_hessian same kept from pymc-extras
– freeze_model – removed; pass a model from pm.model.transform.freeze_model instead

Behavioural differences vs. pymc-extras worth knowing:

  • Transformed values go into posterior with idata_kwargs={"include_transformed": True},
    exactly as in pm.sample, instead of a separate unconstrained_posterior group.
  • progressbar accepts pm.sample's string options, which simply enable the optimizer's bar.
  • constant_data is only present when the model has constant data (converter behaviour).
  • Parameter vector order in fit / optimizer_result follows model definition order.
  • compute_hessian=True inverts the exact Hessian of -logp(jacobian=False) over the optimized
    unconstrained value variables, from the fused Hessian or from Hessian-vector products compiled
    after optimization if the optimizer did not need them. pymc-extras reused BFGS / L-BFGS-B's
    hess_inv, which is only an approximation (L-BFGS-B's diagonal can be off by orders of
    magnitude); those approximations are still stored in optimizer_result. Eigenvalues are
    clipped to a relative tolerance (n · eps · max|λ|, as in np.linalg.matrix_rank) instead
    of 0, so numerical noise neither discards the estimate nor makes it singular, and a warning
    fires when the Hessian is clearly indefinite (a saddle point, not a minimum). With discrete
    variables in vars it raises, since the Hessian is undefined there.
  • Gradient-based methods fall back to "powell" with a warning when discrete variables are
    optimized or the model has no gradient, only if use_grad was left at None; explicitly
    asking for gradients raises. Gradient-free methods such as nelder-mead are kept as given.
  • use_grad / use_hess / use_hessp: each None is resolved from what the method uses,
    explicit values are respected, and a flag the method cannot use is ignored with a warning
    (so use_hessp=True on trust-exact no longer drops the Hessian it needs). Of hess and
    hessp only one is used: the explicit one, else hessp.
  • A UserWarning fires when the optimizer reports that it did not converge.
  • Passing a deprecated kwarg together with its replacement (start + initvals, seed +
    random_seed, maxeval + maxiter) raises instead of silently picking one.
  • initvals may be keyed by variables as well as names.
  • Jittered starts are checked for finiteness with the optimizer's own compiled loss, so nothing
    extra is compiled for it. Variables held fixed via vars are shared inputs of that loss.
  • The compiled loss and Hessian-vector product run with trust_input=True, after a dtype cast.
  • Every OptimizeResult field is stored, including method-specific ones (nelder-mead's
    final_simplex is split into two variables, trust-constr's constraint fields keep their own
    dims). Basinhopping reports its own nit / nfev totals, with the best inner run's fields
    underneath.
  • Unknown methods raise ValueError instead of being forwarded to scipy, and a callable
    method raises a TypeError: it is no longer supported.

Large models and the Hessian

MAP estimation is mostly worth reaching for on models too large for full MCMC, so parameter
counts in the tens of thousands are normal here and anything O(n²) is a liability. find_MAP
therefore never materializes a dense n×n matrix unless asked: L-BFGS-B's inverse Hessian is
stored in optimizer_result as its m correction pairs (hess_inv_sk / hess_inv_yk, dims
(lbfgs_corrections, variables)) instead of being densified through n matvecs, which costs
8.7 GB transient at n = 19k for an object that is only 2·m·n floats. A dense
fit.covariance_matrix is only computed with compute_hessian=True, and use_hess defaults to
False for every method that can use hessp instead.

Files

  • pymc/tuning/scipy_interface.py (new): compile helpers ported from pymc-extras
    (scipy_optimize_funcs_from_loss, set_optimizer_function_defaults,
    _compute_inverse_hessian). get_nearest_psd is folded into _compute_inverse_hessian.
  • pymc/tuning/starting.py: the rewrite plus the idata helpers. better_optimize and
    scipy.optimize are imported on first use so import pymc stays light.
  • pymc/sampling/mcmc.py: init="map" / "jitter+map" / "advi_map" get the point from the
    private _find_MAP_point, forwarding compile_kwargs, progressbar and pm.sample's
    initvals (previously ignored; with per-chain initvals the single MAP search uses chain 0's).
    map and advi_map keep their old unjittered start.
  • pymc/model/transform/conditioning.py, pymc/gp/gp.py, pymc/sampling/forward.py:
    docstrings no longer present find_MAP's output as a point dict.
  • ARCHITECTURE.md: lists scipy.optimize / better-optimize under functionality not in PyMC.
  • .github/workflows/tests.yml: registers tests/tuning/test_scipy_interface.py.
  • pyproject.toml, requirements-dev.txt, conda-envs/*.yml: new dependency better-optimize>=0.4.2,<0.5
    (conda-forge and PyPI; depends only on numpy/scipy/rich/pandas/joblib/threadpoolctl, all
    already required or trivially available).
  • Tests: tests/tuning/test_starting.py (existing tests adapted, pymc-extras tests ported, new
    tests for jitter/seeding, legacy kwargs, idata_kwargs, vars subsets, invalid start),
    tests/tuning/test_scipy_interface.py (new). Other callers in tests/gp, tests/distributions
    and tests/model/transform read the point from the returned InferenceData.

Decisions for reviewers

  1. better-optimize becomes a hard dependency. It is maintained by @jessegrabowski and is
    what pymc-extras builds on (progress bar, fused-function detection, maxiter / tolerance
    defaults, early stopping, basinhopping). Vendoring the parts find_MAP needs would be
    roughly 150 lines of duplicated code, so the dependency looked like the more maintainable
    option. It is only imported on first use. If a new dependency is not acceptable, the
    fallback is to vendor a minimal minimize wrapper.
  2. The dict return is deprecated but stays the default for a few releases, with a
    FutureWarning; return_inferencedata=True opts in to the InferenceData. On the dict
    path transformed values are included, as before. Nothing inside PyMC uses the dict:
    the MAP-based init options get the point from the private _find_MAP_point, and the
    docstrings and tests read it from the InferenceData.
  3. include_transformed moves into idata_kwargs, default False for the InferenceData, as in pm.sample. Both old
    pymc and pymc-extras had it as a keyword defaulting to True; the keyword still works with a
    FutureWarning.
  4. jitter=True will become the default, like pm.sample, so the default initial point
    cannot park the optimizer on a saddle point (pymc-extras#687). For a few releases the start
    stays unjittered unless jitter=True is passed, with a FutureWarning when jitter is not
    given. Results are reproducible with random_seed.
    Variables held fixed via vars are not jittered.
  5. init="map" keeps its old unjittered start, and init="jitter+map" is new. The latter
    searches for the MAP from a jittered point and honours jitter_max_retries, following the
    adapt_diag / jitter+adapt_diag pattern.
  6. No freeze_model option, unlike pymc-extras and like pm.sample. The model is used as
    given: a model from pm.model.transform.freeze_model gets constant folding and compiled
    functions cached across calls. The JAX backend works on unfrozen models too.
  7. progressbar_theme is dropped. better_optimize owns the progress bar and does not take a
    rich theme.

Follow-ups (separate PRs)

  • pymc-extras: make pymc_extras.find_MAP re-export pm.find_MAP, import
    scipy_optimize_funcs_from_loss / set_optimizer_function_defaults /
    _compute_inverse_hessian from pymc.tuning.scipy_interface in fit_laplace and DADVI, and
    read idata.posterior instead of idata.unconstrained_posterior.
  • Docs: the find_MAP example notebooks still index the returned dict.

Checks run locally

  • tests/tuning, tests/test_root_namespace.py, tests/sampling/test_mcmc.py,
    tests/sampling/test_forward.py, tests/model/transform/test_conditioning.py,
    tests/distributions/test_truncated.py and tests/gp/test_gp.py all pass with the numba
    default backend. JAX paths are exercised where jax is installed. Both tuning modules have
    100% line coverage. test_starting.py compiles with pymc.testing.fast_unstable_sampling_mode
    like the step-method tests (the default backend stays covered by the init, GP and JAX
    tests), so tests/tuning takes about 40 s, against 32 s on main with 13 tests.
  • pre-commit run over the branch is clean; mypy is clean on the tuning modules and adds no
    errors to mcmc.py.

🤖 Generated with Claude Code

@velochy

velochy commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

CI note: the two failing test jobs (tests/dims/distributions/test_core.py, tests/model/test_core.py) fail with KeyError: '__pyx_fuse_0__pyx_f_5scipy_7special_14cython_special_erfcx' inside numba-compiled scipy special functions. Nothing in this PR touches that path. The only package difference vs. the last green run on main is scipy 1.18.0 → 1.18.1 (same numba 0.66.0 / pytensor 3.3.0 / llvmlite 0.48.0), so this looks like a fresh conda-forge scipy release breaking pytensor's numba cython_special dispatch; main should hit it on its next run. The pre-commit failure (new test file not in the CI matrix) is fixed in the second commit.

@velochy

velochy commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

My AI friend doing most of the work, as always - but I am very much here babysitting it. So far this arrangement has been ok so I hope it remains so :)

@read-the-docs-community

read-the-docs-community Bot commented Sep 12, 2026 •

Copy link
Copy Markdown

@codecov

codecov Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.06%. Comparing base (c2cb98d) to head (8f87b56).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #8429      +/-   ##
==========================================
+ Coverage   91.95%   92.06%   +0.11%     
==========================================
  Files         128      129       +1     
  Lines       21276    21453     +177     
==========================================
+ Hits        19564    19751     +187     
+ Misses       1712     1702      -10     
Files with missing lines Coverage Δ
pymc/gp/gp.py 94.71% <ø> (ø)
pymc/model/transform/conditioning.py 95.83% <ø> (ø)
pymc/sampling/forward.py 96.78% <ø> (ø)
pymc/sampling/mcmc.py 91.94% <100.00%> (+0.02%) ⬆️
pymc/tuning/scipy_interface.py 100.00% <100.00%> (ø)
pymc/tuning/starting.py 100.00% <100.00%> (+8.77%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@velochy

velochy commented Sep 29, 2026

Copy link
Copy Markdown
Contributor Author

@jessegrabowski have you had a chance to take a look?

@jessegrabowski jessegrabowski left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAP points match pymc_extras.find_MAP to 1e-10 for L-BFGS-B, BFGS, Newton-CG, trust-ncg and powell, and the tuning, init="map" and GP suites pass locally. The blockers are the optimizer_result crashes for nelder-mead and trust-constr.

Comment thread pymc/tuning/scipy_interface.py Outdated
if method == "L-BFGS-B" and optimizer_result is not None:
if hess_inv is None:
return None
return np.stack([hess_inv(basis[:, i]) for i in range(n_vars)], axis=-1)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not a blocker, existing bug from pymc-extras _compute_inverse_hessian. optional to fix here or in a follow-up. This densifies L-BFGS's 10-pair approximation, not the inverse Hessian. On a 6-parameter regression the diagonal is off by ~1400x vs trust-exact. With compute_hessian=True, compile f_hessp and take the exact route, like the powell case already does.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done here rather than in a follow-up (074dd79): compute_hessian always inverts the exact Hessian (fused or hessp); the optimizers' approximations only live in optimizer_result.

Comment thread pymc/tuning/scipy_interface.py Outdated
hess_inv = getattr(inner_result, "hess_inv", None)

if method == "BFGS" and optimizer_result is not None:
return hess_inv

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not a blocker, same pymc-extras origin as line 277: BFGS hess_inv is also an approximation, ~1% off in the same model

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same fix as the L-BFGS-B one (074dd79): BFGS's hess_inv is no longer used for fit.covariance_matrix.

Comment thread pymc/tuning/scipy_interface.py Outdated
return None
return np.stack([hess_inv(basis[:, i]) for i in range(n_vars)], axis=-1)
if f_hessp is not None:
H = np.stack([f_hessp(x_star, basis[:, i]) for i in range(n_vars)], axis=-1)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

raises TypeError under floatX="float32": x_star and basis are float64 and compile doesn't downcast. cast both to floatX

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 074dd79: inputs are cast to floatX, with a float32 test.

Comment thread pymc/tuning/starting.py

if "lowest_optimization_result" in result:
# basinhopping nests the inner optimizer's result; flatten it over the outer fields
result = OptimizeResult(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

merge outer over inner. this replaces basinhopping's totals with one inner run's, nit/nfev go from 5/18 to 2/3 on a 5-hop run

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 074dd79: basinhopping's outer totals now win over the inner run's; the test checks nit.

Comment thread pymc/tuning/starting.py Outdated
)
vec: tuple[str, ...] = ("variables",)
mat: tuple[str, ...] = ("variables", "variables_aux")
data = {"method": xr.DataArray(method)}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

trust-constr returns its own method key, which overwrites this

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 074dd79: our method is written last, so trust-constr's sub-method no longer overwrites it.

Comment thread pymc/tuning/starting.py Outdated
and values[value.name].shape == values[rv.name].shape
}
idata_kwargs = {**idata_kwargs, "dims": dims | idata_kwargs.get("dims", {})}
idata = to_inference_data(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this adds an empty sample_stats group

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 074dd79: the empty group is dropped.

Comment thread pymc/sampling/mcmc.py Outdated
potential = quadpotential.QuadPotentialDiag(cov, rng=random_seed_list[0])
elif init == "map":
start = pm.find_MAP(include_transformed=True, seed=random_seed_list[0])
start = pm.find_MAP(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

init="map" used to start from the unjittered initial point. intended?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say so. 0 can be a saddle point for some common and sensible models (like factor analysis or IRT), so jitter tends to be safer and more robust to beginners in my opinion

@jessegrabowski jessegrabowski Sep 29, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider init = "jitter+map" for consistency/clarity and warn on init == "map" that this is renamed to jitter+map and use that to silence the warning?

Maybe too much, just a suggestion.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but for unified syntax, I made it have jitter+map and map separately.

Comment thread tests/tuning/test_starting.py Outdated
find_MAP()
with pytest.warns(UserWarning, match="Gradient not available"):
find_MAP(model=model, progressbar=False)
with pytest.raises(Exception):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pytest.raises(NotImplementedError)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 074dd79.

Comment thread tests/tuning/test_starting.py Outdated
("BFGS", True, False, False),
("L-BFGS-B", True, False, False),
("trust-exact", True, True, False),
("powell", False, False, False),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add nelder-mead and trust-constr

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added both to the parametrized test in 074dd79.

posterior = idata.posterior.dataset.squeeze(["chain", "draw"])
assert posterior["mu"].shape == () and posterior["sigma"].shape == ()
assert ("sigma_log__" in posterior) == include_transformed
assert ("covariance_matrix" in idata.fit) == compute_hessian

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not a blocker: check the values against the exact inverse Hessian, not only presence

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 074dd79: the covariance is compared against the inverse of compile_d2logp(jacobian=False) at the optimum.

@velochy

velochy commented Oct 1, 2026

Copy link
Copy Markdown
Contributor Author

My AI has made all the fixes you requested. Want to do another round @jessegrabowski ?

@velochy
velochy marked this pull request as ready for review October 1, 2026 06:34

@jessegrabowski jessegrabowski left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Want to do another round @jessegrabowski ?

Be careful what you wish for.

Comment thread pymc/tuning/scipy_interface.py Outdated
H = np.stack([np.asarray(f_hessp(x_star, e)) for e in basis], axis=-1)
else:
raise ValueError("Either `f_hessp` or a fused hessian (`use_hess=True`) is required.")
return np.linalg.inv(get_nearest_psd(np.asarray(H, dtype="float64")))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that this is the exact Hessian it can be indefinite. Clipping negative eigenvalues to 0 then calling inv returns entries around 1e16, or raises LinAlgError after the whole optimization, when the optimum isn't a minimum. Warn and return NaNs (or raise a clear error) when H isn't PD.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Real jesse here: This bot comment slipped past me. get_nearest_psd is doing the right thing. The robot was upset because it clips negative eigenvalues to exactly zero, which can result in a singular/non-invertible covariance matrix. Some thoughts:

  1. it's an existing thing so not necessarily your problem.
  2. It might be right to remove it, but I kind of like it. But I also wrote it so another view would be nice.
  3. My original logic was that if we have some small numerical noise, it's a shame to throw away the entire (expensive) estimation. Instead we should just give the best possible answer
  4. If you agree, we should consider clipping the negative eigenvalues to np.spacing(1, dtype=floatX) or something, and add a test (as requested below)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, kept the projection (38a4586). Eigenvalues are now clipped to n * eps(floatX) * max|λ| (the np.linalg.matrix_rank tolerance) instead of 0, so noise neither discards the estimate nor makes it singular. A warning fires when the Hessian is clearly indefinite (smallest eigenvalue below -tol), i.e. the point is a saddle rather than a minimum. get_nearest_psd is folded into _compute_inverse_hessian.



@pytest.mark.parametrize("use_hess", [True, False])
def test_compute_inverse_hessian_is_exact(use_hess):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add an indefinite-H case

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added test_compute_inverse_hessian_indefinite in 38a4586.

Comment thread pymc/tuning/starting.py Outdated
)
var_names = [str(var.name) for var in model.value_vars if var.name in names]
if freeze_model:
model = freeze_dims_and_data(model)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

initvals keyed by a TensorVariable are silently ignored once the model is frozen: {x: -50} starts from 0. Convert the keys to names before freezing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 38a4586: initvals keys are converted to names before freezing; test added.

Comment thread pymc/sampling/mcmc.py
potential = quadpotential.QuadPotentialDiag(cov, rng=random_seed_list[0])
elif init == "advi_map":
start = pm.find_MAP(include_transformed=True, seed=random_seed_list[0])
start = _find_MAP_point(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jitter=False. advi_map was unjittered before, and only jitter+map should jitter.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 38a4586.

Comment thread tests/sampling/test_mcmc.py Outdated
monkeypatch.setattr(
pm.sampling.mcmc,
"_find_MAP_point",
lambda **kwargs: jitters.append(kwargs["jitter"]) or find_map_point(**kwargs),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add advi_map here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 38a4586.

Comment thread pymc/tuning/starting.py
else:
cost_func = CostFuncWrapper(maxeval, progressbar, progressbar_theme, logp_func)
compute_gradient = False
res = minimize(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warn when res.success is False. maxiter=1 returns a point far from the optimum with no warning.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 38a4586: a UserWarning with the optimizer's message.

Comment thread pymc/tuning/starting.py
f"`{old}` is deprecated, use `{new}` instead.", FutureWarning, stacklevel=2
)
optimizer_kwargs[new] = optimizer_kwargs.pop(old)
initvals = optimizer_kwargs.pop("initvals", initvals)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

start= silently overrides an explicit initvals=. Raise when both are passed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this (and the next comment): The principle should be to harmonize arguments between different fit methods. Take pm.sample as the "truth" and rename stuff? Open to pushback here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed that pm.sample is the reference: start became initvals, seed became random_seed, and maxeval maps to scipy's maxiter. Passing an old name together with its replacement now raises (38a4586).

Comment thread pymc/tuning/starting.py
)
optimizer_kwargs[new] = optimizer_kwargs.pop(old)
initvals = optimizer_kwargs.pop("initvals", initvals)
random_seed = optimizer_kwargs.pop("random_seed", random_seed)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same for seed= / random_seed=, and maxeval= / maxiter=

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same fix (38a4586): all three pairs raise.

Comment thread pymc/tuning/scipy_interface.py Outdated
use_grad = use_grad if use_grad is not None else method_info["uses_grad"]

if use_hessp is not None and use_hess is None:
use_hess = not use_hessp

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not a blocker: use_hessp=False alone turns on the dense Hessian, even for L-BFGS-B. Leave use_hess at the method default instead.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the use_** machinery is a bit scuffed in pymc-extras, it can be rewritten to be less terrible.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More than the use_hessp=False case: setting one Hessian flag flips the other regardless of method. use_hess=False on L-BFGS-B compiles a hessp it can't use. use_hessp=True on trust-exact drops the Hessian it requires, and find_MAP returns the unoptimized start point without raising. Resolve each None flag from the method independently and respect explicit values:

method_info = MINIMIZE_MODE_KWARGS[method]
hess_was_none, hessp_was_none = use_hess is None, use_hessp is None

use_grad = method_info["uses_grad"] if use_grad is None else use_grad
use_hess = method_info["uses_hess"] if hess_was_none else use_hess
use_hessp = method_info["uses_hessp"] if hessp_was_none else use_hessp

if use_hess and use_hessp:
    if hessp_was_none and not hess_was_none:
        use_hessp = False
    else:
        use_hess = False

Warn when an explicit True names something the method doesn't use.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented your resolution in 38a4586, with one addition: a flag the method can't use is ignored with a warning, otherwise use_hessp=True on trust-exact would still drop the Hessian it needs. use_grad gets the same treatment.

Comment thread pymc/tuning/starting.py Outdated
include_transformed : bool, default False
Whether to also return the values of transformed (unconstrained) variables, e.g. ``sigma_log__``.
compute_hessian : bool, default False
Compute the inverse hessian at the optimum and store it as ``fit.covariance_matrix``.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The covariance is the inverse Hessian of logp(jacobian=False) over the unconstrained value variables, which isn't the Laplace covariance in that space. State the space and drop the Laplace sentence.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 38a4586: the docstring states the space, and the Laplace sentence is gone.

Comment thread pymc/tuning/starting.py Outdated
# Variable keys would not match the frozen model's variables, so key by name
initvals = initvals and {getattr(k, "name", k): v for k, v in initvals.items()}
if freeze_model:
if freeze_model and not isinstance(model, FrozenModel):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not in love with the freeze model option, I'm willing to wager that @ricardoV94 will object on the grounds that if a user wants it, he can just do it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 9206e69. The model is used as given, so passing one from pm.model.transform.freeze_model gets constant folding and cached compiled functions. The JAX backend works on unfrozen models too.

Comment thread pymc/tuning/scipy_interface.py Outdated

"""Compile model log-densities into the callables expected by ``scipy.optimize``."""

from __future__ import annotations

@jessegrabowski jessegrabowski Oct 1, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this being used? llms love this import but it doesn't do anything 99% of the time

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer, removed in 9206e69.

Comment thread pymc/tuning/starting.py
model : Model (optional if in ``with`` context)
backend : str, optional
Computational backend, one of "numba", "c" or "jax". Defaults to the PyTensor default mode.
compile_kwargs : dict, optional

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i recommend just removing the gradient_backend thing. It is a vestigial feature from when our Op coverage was less complete.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 9206e69: derivatives always come from PyTensor, and backend="jax" still compiles via JAX. That also let the private compile helper fold into scipy_optimize_funcs_from_loss.

Comment thread conda-envs/environment-docs.yml Outdated
dependencies:
# Base dependencies
- arviz>=1.1.0,<2.0
- better-optimize>=0.4.2,<1.0

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pin this <0.5 everywhere, 1.0 is too aggressive

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pinned to >=0.4.2,<0.5 everywhere in a4e415c.

@ricardoV94 ricardoV94 left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small question and suggestion.

Can you give me a one sentence about backward incompatible changes and how users should change their code to include in the release notes?

And clarify if this PR breaks compat immediately or only issues the warning but keeps working.

Comment thread pymc/sampling/mcmc.py Outdated
Comment thread pymc/sampling/mcmc.py Outdated
* advi: Run ADVI to estimate posterior mean and diagonal mass matrix.
* advi_map: Initialize ADVI with MAP and use MAP as starting point.
* map: Use the MAP as starting point. This is discouraged.
* map: Use the MAP, searched for from the test value, as starting point. This is

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what "test_value"?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wording copied from the neighbouring entries; it now says "the model's initial point" (4a531ee). The older adapt_diag / adapt_full entries still say "test value". Happy to fix those here too if you want.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah let's fix

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the remaining "test value" entries in init_nuts and _init_jitter (8af31ef).

Comment thread pymc/tuning/scipy_interface.py Outdated
outputs.append(grad)
if use_hess:
outputs.append(pytensor.gradient.jacobian(grad, [flat_input])[0])
f_fused = compile([flat_input], outputs if len(outputs) > 1 else loss, **compile_kwargs)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

want to do some trust_input=True, somewhere?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 4a531ee: the loss and hessp run with trust_input=True, behind a cast to the input dtype since scipy hands over float64.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that defeats part of the trust_input part I guess, but still cheaper? OTOH isn't it always a single input function, why the comprehension?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still cheaper: 8.3 µs per call with checked inputs against 5.1 µs trusted with the cast, the same as without it. The comprehension was for hessp's (x, p); the casts are now explicit per function (8af31ef).

assert_allclose([r["x"], r["y"], r["det"]], [50, 50, 100])


def test_find_MAP_frozen_model(normal_model):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move these tests (and map functionality) out of tuning/starting.py, unless they are specifically abound starting at the map from pm.sample?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to pymc/optimization/{map,scipy_interface}.py, with tests in tests/optimization (77acf5c). pymc.find_MAP and pymc.tuning.find_MAP still work. The name is my pick, so let me know if you prefer another home.

@ricardoV94 ricardoV94 added major Include in major changes release notes section samplers labels Oct 6, 2026
@ricardoV94

Copy link
Copy Markdown
Member

The jitter retry check uses a cheap FAST_COMPILE log-density instead of a second full
backend compile of the model.

Can't this reuse the function that find_MAP used?

@ricardoV94

ricardoV94 commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

progressbar_theme is dropped. better_optimize owns the progress bar and does not take a rich theme.

Can we change this / take control over the progressbar? Not a blocker (nor very urgent). @jessegrabowski

@velochy

velochy commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor Author

@ricardoV94 thanks for the review. Inline comments are answered in their threads; the rest is here.

Release notes

pm.find_MAP now returns an InferenceData with the MAP point as a single-draw posterior instead of a dict, starts from a jittered initial point, and no longer returns transformed values by default: use pm.find_MAP().posterior["x"], pass random_seed (or jitter=False) for reproducible results, and idata_kwargs={"include_transformed": True} for transformed values.

Does it break immediately or only warn? Mixed:

  • Breaks immediately:
    • the default return type, so find_MAP()["x"] raises KeyError
    • jitter is on by default
    • transformed values are off by default
    • everything except method is keyword-only, so find_MAP(start, vars) raises TypeError
    • a callable method is rejected
    • better-optimize is a new required dependency
  • Warns (FutureWarning) but keeps working: return_inferencedata=False (returns the old dict), start → initvals, seed → random_seed, maxeval → maxiter, include_transformed= → idata_kwargs, return_raw, a single positional start dict, and progressbar_theme (ignored).
  • Unchanged: the pm.find_MAP and pymc.tuning.find_MAP import paths, and pm.sample(init="map") / "advi_map", apart from now honouring initvals.

"Can't this reuse the function that find_MAP used?" Yes, done in 4a531ee. Jittered starts are checked with the optimizer's own compiled loss, with held-fixed variables as shared inputs so one function serves every redraw. No separate logp is compiled, and _init_jitter is untouched again.

Progress bar. Feasible as a follow-up: better_optimize.minimize already accepts a rich Progress instead of a bool, so PyMC could pass its own themed bar. basinhopping only takes a bool today.

Three things I'd like your guidance on

  1. Module location. You asked to move the MAP code out of tuning/starting.py without naming a destination. I put it in pymc/optimization/ (map.py, scipy_interface.py, tests in tests/optimization). Is that the home you want, or do you prefer another name?
  2. Warning period for the return type. The dict → InferenceData default is the one hard break with no deprecation cycle. The alternative is to keep returning the dict for one release with a FutureWarning and flip the default afterwards. Which do you prefer?
  3. Callable method. The old find_MAP documented "method: string or callable" and forwarded it to scipy.optimize.minimize; the new one rejects callables, because both find_MAP and better_optimize.minimize look the method up by name. Options: (a) support callables by calling scipy.optimize.minimize directly for them, without the progress bar; (b) accept callables in better_optimize (@jessegrabowski); (c) leave it out and say so in the release notes. Real Margus says we have no preference.

@ricardoV94

ricardoV94 commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

The alternative is to keep returning the dict for one release with a FutureWarning and flip the default afterwards. Which do you prefer?

I have to revisit my categorical reasoning 101, but I feel there ought to be more alternatives than returning the dict for one release alone

Yes, I'd say keep the old behavior of dict and no jitter by default with the warning for a few releases. The callable bit seems a bit more niche and we can deprecate immediately. Raise an informative error if one was provided.

Also to nit with the bot, adding a dependency is not breaking anything. Must revisit causal analysis 101 as well I guess

@ricardoV94

Copy link
Copy Markdown
Member

Re module location... nhe leave it for now. We should probably get rid of the whole tuning and then we'll get the new home. nothing here depends on that

@ricardoV94

Copy link
Copy Markdown
Member

@velochy didn't hear back from you on suggestion to make transition gradual and not break now

@velochy

velochy commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

@ricardoV94 done, thanks for the guidance.

  • Defaults. The dict return and the unjittered start stay the defaults, each with a FutureWarning; return_inferencedata=True and jitter=True opt in to the future behaviour (8af31ef). Old-style calls run unchanged, including find_hessian(find_MAP()).
  • Callable method. Raises an informative TypeError (8af31ef).
  • Module location. Moved back to pymc/tuning/starting.py and tests/tuning (df427e4), until tuning gets its new home.
  • Release notes. The PR description has a "Backward compatibility" section with the updated sentence and what warns versus what breaks. The new dependency is no longer listed as breaking.

CI: the one red job is test_fit_oo[ASVGD-full] in tests/variational/test_inference.py on Windows, a stochastic test that missed its 20% tolerance (0.120 vs 0.095). It does not touch find_MAP and passed on the previous pushes, so it should clear on a re-run, which I can't trigger myself.

velochy and others added 20 commits October 7, 2026 16:17
Rebuild pm.find_MAP on the pymc-extras implementation: fused loss/grad/hess
functions over one flat parameter vector, better_optimize for the scipy
interface, basinhopping and optional inverse-Hessian. Parameters, defaults and
conventions follow pm.sample (backend/compile_kwargs, initvals/jitter/
random_seed via _init_jitter, return_inferencedata via to_inference_data).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ensifying it

optimizer_result turned scipy's LbfgsInvHessProduct into an n x n matrix via n matvecs:
O(n^2) memory (8.7 GB transient at n = 19k) for an object that is 2 m n floats. Keep it as
hess_inv_sk / hess_inv_yk with dims (lbfgs_corrections, variables); a generic LinearOperator
is still densified as before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`import pymc` eagerly imports pymc.tuning, so module-level better_optimize imports pulled in
scipy.optimize/linalg/special/stats/interpolate/spatial and broke
test_eager_import_heavy_dependencies.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…l fallback

- compute_hessian always uses the exact Hessian (fused or hessp), never BFGS/L-BFGS-B's
  approximate hess_inv; floatX-safe under float32
- optimizer_result: split ragged tuples (nelder-mead final_simplex), label "variables" only when
  sizes match (trust-constr's empty jac), keep our method over trust-constr's, and let
  basinhopping's outer totals win over the inner run's
- only fall back to powell for discrete vars when the method uses gradients
- drop the empty sample_stats group
- tests: nelder-mead/trust-constr, exact covariance values, float32, NotImplementedError

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…d_MAP_point

return_inferencedata=False keeps working but raises a FutureWarning. The optimization moves
into _fit_MAP, and init="map"/"advi_map" read the point from the private _find_MAP_point.
Docstrings and tests read the point from the returned InferenceData instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The MAP search starts from a jittered point, so name it like the other jitter+ inits and
forward jitter_max_retries. Also pin the discrete find_MAP test's randomness, which the
jitter default had made flaky.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- use_* flags: resolve each None from the method independently, respect explicit values,
  and warn about and ignore flags the method cannot use
- inverse Hessian: clip eigenvalues to a relative tolerance instead of 0 and warn when the
  Hessian is not positive definite (saddle point) rather than raising or returning ~1e16
- warn when the optimizer does not converge
- raise when a deprecated kwarg is passed together with its replacement
- key initvals by name so Variable keys survive model freezing
- advi_map does not jitter; MAP-based inits receive pm.sample's initvals
- compute_hessian docstring states the space the covariance lives in

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- compute_hessian raises for discrete variables instead of returning garbage rows
- post-hoc hessp honours gradient_backend="jax" for gradient-free methods
- drop the generic LinearOperator densification (only L-BFGS-B returns one, handled compactly)
- error message lists canonical method names; shorter private docstrings
- jitter_rvs typed as TensorVariable (no new mypy errors in mcmc.py)
- gp.predict / sample_posterior_predictive docstrings no longer imply find_MAP returns a dict
- ARCHITECTURE.md lists scipy.optimize / better-optimize
- tests: compute_hessian once per Hessian route (fewer compiles), discrete-hessian and jax-hessp tests

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e in tests

The jitter retry loop compiled a full backend logp just to check a few starts for
finiteness; a FAST_COMPILE one suffices. test_starting.py compiles with
fast_unstable_sampling_mode like the step-method tests: tests/tuning drops from ~55 s
to ~29 s (main: ~32 s).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Refreezing a FrozenModel turned it into a plain Model and dropped its cached functions.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A user who wants a frozen model can pass one from pm.model.transform.freeze_model, which is
used as is. Derivatives are always taken by PyTensor; the JAX-autodiff path was vestigial.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- include_transformed goes through idata_kwargs as in pm.sample; the keyword warns and is forwarded
- progressbar accepts pm.sample's string options
- transformed values no longer get their RV's dims, matching pm.sample
- compute_hessian compiles hessp alongside the loss instead of recompiling the loss
- _fit_MAP has no defaults, so they live only on find_MAP; _find_MAP_point states its settings

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
find_MAP draws its start from the initial point function pm.sample uses and tests it against
the loss it compiles anyway, with held-fixed variables as shared inputs. That drops the extra
logp compile and the _init_jitter change in mcmc.py. The compiled functions use trust_input.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MAP estimation is not about NUTS tuning. find_MAP and its scipy interface now live in
pymc/optimization, with tests in tests/optimization. pymc.find_MAP is unchanged and
pymc.tuning.find_MAP stays importable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…FutureWarning

Both stay for a few releases; return_inferencedata=True and jitter=True opt in to the future
defaults. A callable method raises an informative TypeError. The trust_input casts are
explicit per function, and the remaining "test value" wording in init_nuts is fixed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The module's new home is to be decided when pymc.tuning is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ricardoV94
ricardoV94 merged commit 2394fd4 into pymc-devs:main Oct 7, 2026
42 checks passed
@ricardoV94 ricardoV94 changed the title Port find_MAP from pymc-extras and align it with pm.sample Modernize find_MAP Oct 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor/Upgrade find_MAP

3 participants