Source code for mimirax.inference.objective

"""Composing a forward model, an observable, a likelihood and priors into one objective.

This is the one place the pieces meet. Everything else in the package is
either a pluggable implementation of one protocol or a method that consumes the
scalar functions defined here.
"""

from __future__ import annotations

from dataclasses import dataclass

import jax.numpy as jnp
from jax import Array

from mimirax._typing import PyTree, Scalar
from mimirax.protocols import ForwardModel, Likelihood, Observable, Prior

__all__ = ["InferenceProblem"]


[docs] @dataclass(frozen=True) class InferenceProblem: """A posterior over parameters, assembled from protocol-satisfying parts. ``log_posterior(params) = likelihood.log_prob(observable(forward(params)), observed) + sum(prior.log_prob(params))``. Every method is differentiable in ``params`` provided the parts are, which the protocols require. Attributes ---------- forward : ForwardModel Parameters to state. observable : Observable State to data space. likelihood : Likelihood Data-space comparison. observed : Array The data. priors : tuple[Prior, ...] Zero or more priors and regularizers, summed. """ forward: ForwardModel observable: Observable likelihood: Likelihood observed: Array priors: tuple[Prior, ...] = ()
[docs] def predict(self, params: PyTree) -> Array: """Run the forward model and the observable. Parameters ---------- params : PyTree The free parameters. Returns ------- Array The prediction in data space. """ return self.observable(self.forward(params))
[docs] def log_likelihood(self, params: PyTree) -> Scalar: """Return the log-likelihood of the data at ``params``. Parameters ---------- params : PyTree The free parameters. Returns ------- Scalar ``likelihood.log_prob(predict(params), observed)``. """ return self.likelihood.log_prob(self.predict(params), self.observed)
[docs] def log_prior(self, params: PyTree) -> Scalar: """Return the summed log prior density at ``params``. Parameters ---------- params : PyTree The free parameters. Returns ------- Scalar Zero when there are no priors. """ total = jnp.zeros(()) for prior in self.priors: total = total + prior.log_prob(params) return total
[docs] def log_posterior(self, params: PyTree) -> Scalar: """Return the unnormalized log posterior at ``params``. Parameters ---------- params : PyTree The free parameters. Returns ------- Scalar ``log_likelihood + log_prior``. """ return self.log_likelihood(params) + self.log_prior(params)
[docs] def negative_log_posterior(self, params: PyTree) -> Scalar: """Return ``-log_posterior``, the objective an optimizer minimizes. Parameters ---------- params : PyTree The free parameters. Returns ------- Scalar The negative log posterior. """ return -self.log_posterior(params)