Frequently Asked Questions¶
What is the ObservationModel class / do I need it?¶
ObservationModel is a convenience class that wraps a callable (x, u, t) -> Distribution into a standard interface with log_prob and sample methods. You don't strictly need it: you can pass any callable that returns a NumPyro distribution to DynamicalModel's observation_model argument. The built-in LinearGaussianObservation and DiracIdentityObservation implement this interface for common cases. See the observations API reference for details.
Why isn't the math rendering on this website?¶
We don't know, but usually a refresh fixes it :)
What are the most common ways to condition models on data for system identification?¶
Say you have a dynestyx model model that accepts obs_times, obs_values
(and optionally controls and prediction times) and passes them to dsx.sample:
def model(
obs_times=None,
obs_values=None,
ctrl_times=None,
ctrl_values=None,
predict_times=None,
):
params = numpyro.sample(...)
dynamics = dsx.DynamicalModel(...)
dsx.sample(
"f",
dynamics,
obs_times=obs_times,
obs_values=obs_values,
ctrl_times=ctrl_times,
ctrl_values=ctrl_values,
predict_times=predict_times,
)
Omit ctrl_times and ctrl_values when the model has no controls.
- HMM: Use the HMM filter with an
HMMConfig.
from dynestyx.inference.filters import HMMConfig
with dsx.Filter(filter_config=HMMConfig()):
model(obs_times=obs_times, obs_values=obs_values)
See the HMM inference tutorial.
- Discrete-time: Use
LatentPathBuilderfor explicit joint inference over parameters and latent states, or aFilterto marginalize the latent states while inferring parameters.dsx.Filter()defaults to an EnKF for a discrete-time model; pass a filter config to select another algorithm.
# Explicit latent path
with dsx.LatentPathBuilder():
model(obs_times=obs_times, obs_values=obs_values)
# Marginalized latent path
with dsx.Filter():
model(obs_times=obs_times, obs_values=obs_values)
See the filtering and marginal-likelihood tutorial.
- Continuous-time stochastic differential equation: Use a
DiscretizerwithLatentPathBuilderfor explicit latent-state inference, or aFilterfor marginalized inference. The continuous-time EnKF is the default filter.
# Explicit latent path
with dsx.LatentPathBuilder():
with dsx.Discretizer(discretize=dsx.euler_maruyama):
model(obs_times=obs_times, obs_values=obs_values)
# Marginalized latent path
with dsx.Filter():
model(obs_times=obs_times, obs_values=obs_values)
Use a particle filter for non-Gaussian observations; see SDE inference with non-Gaussian observations. See the continuous-time tutorial for the full workflow.
With high-frequency, low-noise data, the explicit-path approach can be
especially attractive when it is reasonable to treat measurements as exact
using dsx.DiracIdentityObservation, dramatically simplifying inference and
improving its efficiency. For example, if the model selects that observation
model when dirac_observation=True:
with dsx.LatentPathBuilder():
with dsx.Discretizer(discretize=dsx.euler_maruyama):
model(
obs_times=obs_times,
obs_values=obs_values,
dirac_observation=True,
)
In this Dirac setting, observed state coordinates are fixed exactly while unobserved or missing coordinates remain latent. Full observation is not required, although the largest speedups occur when most of the path is observed. See the Discretizer reference and the Dirac-observation deep dive.
- Continuous-time ordinary differential equation: Use
LatentPathBuilderfor explicit latent-state inference or aFilterfor marginalized inference.
# Explicit latent path
with dsx.LatentPathBuilder():
model(obs_times=obs_times, obs_values=obs_values)
# Marginalized latent path
with dsx.Filter():
model(obs_times=obs_times, obs_values=obs_values)
Despite the deterministic nature of an ODE, sometimes a filtering-algorithm helps a lot (especially for long timeseries rollouts, partial/noisy observations, systems with large sensitivities to intial conditions). Continuous-time filters work directly with diffusion equal to zero, but you can modify the model definition to have a small diffusion coefficient to "relax" the ODE problem to an SDE. See the ODE inference tutorial.
You can modify the model definition to have a small diffusion coefficient to "relax" the ODE problem to an SDE.
Finally, wrap any dsx.Filter() configuration around MCMC to infer the
parameters in model:
import jax.random as jr
from dynestyx.inference.configs.mcmc import NUTSConfig
from dynestyx.inference.mcmc import MCMCInference
with dsx.Filter(filter_config=my_filter_config):
inference = MCMCInference(
mcmc_config=NUTSConfig(
num_samples=1_000,
num_warmup=1_000,
num_chains=1,
mcmc_source="numpyro",
),
model=model,
)
posterior_samples = inference.run(
rng_key=jr.PRNGKey(0),
obs_times=obs_times,
obs_values=obs_values,
)
The filter supplies the marginal likelihood used by MCMC, so the sampler targets model parameters without explicitly sampling the latent state path.
You can also use NumPyro's MCMC classes directly:
from numpyro.infer import MCMC, NUTS
with dsx.Filter(filter_config=my_filter_config):
mcmc = MCMC(
NUTS(model),
num_warmup=1_000,
num_samples=1_000,
)
mcmc.run(
jr.PRNGKey(0),
obs_times=obs_times,
obs_values=obs_values,
)
posterior_samples = mcmc.get_samples()
See the filtering with NUTS
tutorial and
the MCMCInference API
reference.
What if my data has missingness?¶
All primary inference workflows support missing data. Use jnp.nan for missing
entries in obs_values; filters, smoothers, LatentPathBuilder, HMM inference,
and direct dsx.log_prob scoring can handle fully missing time points and
partially missing observation coordinates. The exact treatment and compatible
backend depend on the observation distribution.
See the tutorials on missing observations with filters and
smoothers,
LatentPathBuilder,
and HMMs.
How do I simulate multiple trajectories?¶
For a concrete dynamics object, use
dsx.simulate(...)
and set
n_simulations:
import jax.random as jr
result = dsx.simulate(
dynamics,
rng_key=jr.PRNGKey(0),
predict_times=times,
n_simulations=100,
)
states = result.states # (100, T, state_dim)
This returns a
SimulatedResult.
dsx.simulate auto-routes to the discrete-time, ODE, or SDE backend. For a
differential equation, pass an
ODESimulatorConfig or
SDESimulatorConfig
through simulator_config. The same n_simulations contract holds for all
three lower-level simulators.
For prior or posterior Predictive workflows, use the
dsx.Simulator handler
in the same way:
from numpyro.infer import Predictive
with dsx.Simulator(n_simulations=100):
samples = Predictive(
model, num_samples=1, exclude_deterministic=False
)(jr.PRNGKey(0), predict_times=times)
Here n_simulations draws trajectories conditional on each parameter
realization, while Predictive(num_samples=...) controls the number of NumPyro
model executions. The corresponding trajectory shape is
(num_samples, n_simulations, T, *event_shape).
What if I do not want to use NumPyro or Predictive?¶
Call the result-returning APIs directly on a dynamics object constructed at
concrete parameter values. For simulation, use
dsx.simulate(...)
as shown above; filtering, smoothing, and latent-path scoring use:
# Auto-routed filtering and its marginal log likelihood.
with dsx.Filter():
filtered = dsx.condition(
"f",
dynamics,
obs_times=obs_times,
obs_values=obs_values,
)
# Auto-routed smoothing and its marginal log likelihood.
with dsx.Smoother():
smoothed = dsx.condition(
"f",
dynamics,
obs_times=obs_times,
obs_values=obs_values,
)
# Joint density of fixed latent-state parameters and observations.
joint_log_prob = dsx.log_prob(
dynamics,
state_path_params=state_path_params,
state_path_param_times=state_path_param_times,
obs_times=obs_times,
obs_values=obs_values,
)
dsx.condition returns a
ConditionedResult
containing the
marginal log likelihood and state summaries requested by the active handler.
Pass a filter or smoother config when you need a specific algorithm.
dsx.log_prob returns the joint density of a fixed latent path. For a native
SDE latent-path workflow, first choose a
Discretizer; see the
discretized latent-path
tutorial.
These APIs are compatible with jax.jit, jax.vmap, and jax.grad when the
selected algorithm is itself differentiable. They do not provide NumPyro
priors or parameter inference: construct dynamics at concrete parameter
values and use the optimizer or inference library of your choice.
For NumPyro-free system identification, put an optimizer or sampler around a
loss that constructs dynamics from the current parameters. With marginalized
latent states, minimize the negative marginal log likelihood returned by a
filter:
def filter_loss(parameters):
dynamics = build_dynamics(parameters)
with dsx.Filter(filter_config=my_filter_config):
result = dsx.condition(
"f",
dynamics,
obs_times=obs_times,
obs_values=obs_values,
)
return -result.marginal_loglik
# Pseudocode: use an optimizer such as Optax or SciPy.
parameters = optimizer_loop(filter_loss, initial_parameters)
For explicit latent-path system identification, optimize or sample parameters
and the latent path using dsx.log_prob:
def latent_path_loss(parameters, state_path):
dynamics = build_dynamics(parameters)
return -dsx.log_prob(
dynamics,
state_path_params=state_path,
state_path_param_times=state_times,
obs_times=obs_times,
obs_values=obs_values,
)
# Pseudocode: use the optimizer or sampler of your choice.
parameters, state_path = optimizer_or_sampler_loop(
latent_path_loss,
initial_parameters,
initial_state_path,
)
See the NumPyro-free filtering and marginal-likelihood
tutorial, the
direct filtering and smoothing
example, and the
NumPyro-free differentiable optimization
tutorial. The result-type
reference documents
SimulatedResult and ConditionedResult.
What about hierarchical models?¶
Hierarchical models are supported by the dsx.plate primitive! This allows for multiple levels of hierarchy (e.g., modelling populations, treatment arms, and individuals within each treatment arm), or simple multi-trajectory inference. You can see an example here.
What about neural nets?¶
We will put examples up soon. See CD-Dynamax's Lorenz 63 neural drift tutorial to convince yourself that this will work well.
What about SINDy?¶
See our Sparse system identification deep dive. TL;DR: pick a Laplace or Spike-and-Slab prior and do everything else the dynestyx-way.
Why are particle filters underperforming?¶
Yes, they are worse than we thought in pseudo-marginal settings too. This is an area of active research. If you know how to do things better, please tell us!
How can I contribute?¶
Open an issue or submit a Pull Request on GitHub.