Skip to content

Commit 19a1fbb

Browse files
juanitorduzclaude
andauthored
Improve integration with external samplers (#2203)
* init * improvements * update flowmc * improvements * type hints * feedback 1 * missing fix * cleanup * docs * rerun nb * simplify * rerun nb * Address review: remove get_log_density_fn, inline via initialize_model Per fehiepsi's review on PR #2203, drop the get_log_density_fn helper, its LogDensityInfo return type, and the PositionDict alias; build the log density inline by negating model_info.potential_fn (initialize_model already returns a single-position potential_fn/postprocess_fn). - numpyro/infer/util.py: remove get_log_density_fn + LogDensityInfo; narrow w.message to Warning in find_valid_initial_params so util.py type-checks under ty (newly added to [tool.ty.src]). - numpyro/_typing.py, numpyro/infer/__init__.py, docs/source/utilities.rst: drop the removed symbols. - notebooks/source/other_samplers.ipynb: inline the pattern and re-execute. - test/infer/test_external_helpers.py: inline the pattern; keep constrain_fn and end-to-end MCMCKernel coverage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * rerun nb * docs: clarify log-density wording in notebook Reword the markdown lead-in to the Pathfinder log-density cell to say we build the log-density function by negating the potential energy, avoiding the awkward doubled 'negated' phrasing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * prune --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 36dddd2 commit 19a1fbb

5 files changed

Lines changed: 2153 additions & 1217 deletions

File tree

notebooks/source/other_samplers.ipynb

Lines changed: 1807 additions & 1191 deletions
Large diffs are not rendered by default.

numpyro/infer/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@
2727
from numpyro.infer.mixed_hmc import MixedHMC
2828
from numpyro.infer.sa import SA
2929
from numpyro.infer.svi import SVI
30-
from numpyro.infer.util import Predictive, log_likelihood
30+
from numpyro.infer.util import (
31+
Predictive,
32+
initialize_model,
33+
log_likelihood,
34+
)
3135

3236
from . import autoguide, calibration, reparam
3337

@@ -41,6 +45,7 @@
4145
"init_to_sample",
4246
"init_to_uniform",
4347
"init_to_value",
48+
"initialize_model",
4449
"log_likelihood",
4550
"psis_diagnostic",
4651
"reparam",

numpyro/infer/util.py

Lines changed: 59 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -191,44 +191,74 @@ def transform_fn(transforms, params, invert=False):
191191
return {k: transforms[k](v) if k in transforms else v for k, v in params.items()}
192192

193193

194-
def constrain_fn(model, model_args, model_kwargs, params, return_deterministic=False):
194+
def constrain_fn(
195+
model,
196+
model_args,
197+
model_kwargs,
198+
params,
199+
return_deterministic=False,
200+
batch_ndims=0,
201+
):
195202
"""
196203
(EXPERIMENTAL INTERFACE) Gets value at each latent site in `model` given
197-
unconstrained parameters `params`. The `transforms` is used to transform these
198-
unconstrained parameters to base values of the corresponding priors in `model`.
199-
If a prior is a transformed distribution, the corresponding base value lies in
200-
the support of base distribution. Otherwise, the base value lies in the support
201-
of the distribution.
204+
unconstrained parameters `params`. Each unconstrained value is pushed through
205+
the inverse bijection of the corresponding prior's support to recover the
206+
constrained value. If a prior is a transformed distribution, the corresponding
207+
base value lies in the support of the base distribution. Otherwise, the base
208+
value lies in the support of the distribution.
209+
210+
``batch_ndims`` declares how many leading sample dimensions each leaf of
211+
``params`` carries, so the transforms are ``jax.vmap``-ed the correct number
212+
of times. The common layouts are: ``batch_ndims=0`` (a single unconstrained
213+
position, the default), ``batch_ndims=1`` (a single chain of samples), and
214+
``batch_ndims=2`` (``num_chains x num_samples``, matching
215+
:meth:`MCMC.get_samples(group_by_chain=True)
216+
<numpyro.infer.MCMC.get_samples>`). This is useful to map a batch of
217+
unconstrained samples produced by an external sampler back to the
218+
constrained space.
202219
203220
:param model: a callable containing NumPyro primitives.
204221
:param tuple model_args: args provided to the model.
205222
:param dict model_kwargs: kwargs provided to the model.
206223
:param dict params: dictionary of unconstrained values keyed by site
207-
names.
224+
names. Leading dimensions are batch dimensions (see ``batch_ndims``).
208225
:param bool return_deterministic: whether to return the value of `deterministic`
209226
sites from the model. Defaults to `False`.
227+
:param int batch_ndims: number of leading batch dimensions on each leaf of
228+
``params``. Defaults to ``0`` (a single position).
210229
:return: `dict` of transformed params.
211230
"""
231+
if batch_ndims < 0:
232+
raise ValueError(
233+
f"batch_ndims must be a non-negative integer, got {batch_ndims}."
234+
)
212235

213-
def substitute_fn(site):
214-
if site["name"] in params:
215-
if site["type"] == "sample":
216-
with helpful_support_errors(site):
217-
return biject_to(site["fn"].support)(params[site["name"]])
218-
elif site["type"] == "param":
219-
constraint = site["kwargs"].pop("constraint", constraints.real)
220-
with helpful_support_errors(site):
221-
return biject_to(constraint)(params[site["name"]])
222-
else:
223-
return params[site["name"]]
236+
def single(position):
237+
def substitute_fn(site):
238+
if site["name"] in position:
239+
if site["type"] == "sample":
240+
with helpful_support_errors(site):
241+
return biject_to(site["fn"].support)(position[site["name"]])
242+
elif site["type"] == "param":
243+
constraint = site["kwargs"].pop("constraint", constraints.real)
244+
with helpful_support_errors(site):
245+
return biject_to(constraint)(position[site["name"]])
246+
else:
247+
return position[site["name"]]
224248

225-
substituted_model = substitute(model, substitute_fn=substitute_fn)
226-
model_trace = trace(substituted_model).get_trace(*model_args, **model_kwargs)
227-
return {
228-
k: v["value"]
229-
for k, v in model_trace.items()
230-
if (k in params) or (return_deterministic and (v["type"] == "deterministic"))
231-
}
249+
substituted_model = substitute(model, substitute_fn=substitute_fn)
250+
model_trace = trace(substituted_model).get_trace(*model_args, **model_kwargs)
251+
return {
252+
k: v["value"]
253+
for k, v in model_trace.items()
254+
if (k in position)
255+
or (return_deterministic and (v["type"] == "deterministic"))
256+
}
257+
258+
fn = single
259+
for _ in range(batch_ndims):
260+
fn = jax.vmap(fn)
261+
return fn(params)
232262

233263

234264
def get_transforms(model, model_args, model_kwargs, params):
@@ -779,6 +809,10 @@ def initialize_model(
779809
site["fn"]._validate_sample(site["value"])
780810
if len(ws) > 0:
781811
for w in ws:
812+
# `catch_warnings(record=True)` stores `Warning`
813+
# instances; narrow the `Warning | str` type so
814+
# `.args` access type-checks.
815+
assert isinstance(w.message, Warning)
782816
# at site information to the warning message
783817
w.message.args = (
784818
"Site {}: {}".format(

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ include = [
234234
"numpyro/examples/datasets.py",
235235
"numpyro/handlers.py",
236236
"numpyro/infer/elbo.py",
237+
"numpyro/infer/util.py",
237238
"numpyro/optim.py",
238239
"numpyro/primitives.py",
239240
"numpyro/patch.py",

0 commit comments

Comments
 (0)