Source code for superstats.prior.joint_prior

"""Joint priors over time-varying and time-invariant parameters."""

from typing import Any, Dict

import numpy as np

from superstats.diagnostics.plots.prior_samples import (
    plot_joint_prior,
    plot_time_invariant_prior,
    plot_time_varying_prior,
)
from .prior import Prior
from superstats.transition import DeterministicTransition, StochasticTransition


[docs] class JointPrior: """Joint prior over multiple model parameters. Parameters ---------- **kwargs : StochasticTransition, DeterministicTransition, Prior, float, int Named model parameters. Use `StochasticTransition` for stochastic time-varying parameters with hyperparameters, `DeterministicTransition` for deterministic time-varying parameters, `Prior` for inferred time-invariant parameters, and scalar values for fixed parameters. Notes ----- Sample outputs are grouped into: - `local_params`: stochastic time-varying parameters (inferred). - `deterministic_params`: deterministic time-varying parameters (no inferred). - `hyper_params`: hyperparameters for transition models (inferred). - `shared_params`: time-invariant parameters (inferred). - `fixed_params`: fixed parameters (no inferred). """ def __init__(self, **kwargs: StochasticTransition | DeterministicTransition | Prior | float | int): self.params = kwargs self._last_hyper_param_groups = {}
[docs] def sample(self, batch_size: int, num_steps: int) -> Dict[str, Any]: """Draw a joint parameter sample. Parameters ---------- batch_size : int Number of independent samples to draw. num_steps : int Number of time steps per trajectory. Returns ------- result : dict - sampled parameter groups `local_params`, `deterministic_params` `hyper_params`, `shared_params`, and `fixed_params`. Raises ------ ValueError If batch_size or num_steps is not a positive integer. """ if batch_size <= 0: raise ValueError("batch_size must be a positive integer") if num_steps <= 0: raise ValueError("num_steps must be a positive integer") local_params = {} deterministic_params = {} hyper_params = {} shared_params = {} fixed_params = {} hyper_param_groups = {} for name, param in self.params.items(): if isinstance(param, StochasticTransition): target = local_params sample_key = "local_params" elif isinstance(param, DeterministicTransition): target = deterministic_params sample_key = "deterministic_params" elif isinstance(param, Prior): shared_params[name] = param.sample(batch_size=batch_size) continue elif np.isscalar(param): fixed_params[name] = param continue else: raise TypeError(f"Unknown parameter type for '{name}': {type(param).__name__}") samples = param.sample(batch_size=batch_size, num_steps=num_steps) target[name] = samples[sample_key] hyper_param_groups[name] = [] for key, value in samples["hyper_params"].items(): full_key = f"{name}_{key}" hyper_params[full_key] = value hyper_param_groups[name].append(full_key) for key, value in samples["fixed_params"].items(): fixed_params[f"{name}_{key}"] = value self._last_hyper_param_groups = hyper_param_groups return { "local_params": local_params, "deterministic_params": deterministic_params, "hyper_params": hyper_params, "shared_params": shared_params, "fixed_params": fixed_params, }
def _param_bounds(self) -> dict: """Collect y-axis bounds declared on the underlying parameter objects. Returns ------- bounds : dict - mapping from parameter name to its `bounds` attribute, for parameters that define one """ return { name: obj.bounds for name, obj in self.params.items() if hasattr(obj, "bounds") and obj.bounds is not None } def _mixture_names(self) -> dict: """Collect mixture component names declared on the underlying parameter objects. Returns ------- names : dict - mapping from parameter name to its `names` attribute, for parameters that define one """ return {name: obj.names for name, obj in self.params.items() if hasattr(obj, "names")}
[docs] def plot_time_varying_prior(self, num_steps: int = 200, num_trajectories: int = 20, **kwargs): """Plot sampled time-varying prior trajectories. Parameters ---------- num_steps : int, optional, default: 200 Number of time steps to sample per trajectory. num_trajectories : int, optional, default: 20 Number of trajectories to draw. **kwargs : dict, optional, default: {} Further optional keyword arguments propagated to the underlying `plot_time_varying_prior` plotting function. Returns ------- fig : plt.Figure - the generated figure """ samples = self.sample(batch_size=num_trajectories, num_steps=num_steps) local_params = {**samples["local_params"], **samples["deterministic_params"]} return plot_time_varying_prior( local_params=local_params, param_bounds=self._param_bounds(), **kwargs, )
[docs] def plot_time_invariant_prior(self, num_draws: int = 1000, **kwargs): """Plot marginal distributions for time-invariant prior parameters. Parameters ---------- num_draws : int, optional, default: 1000 Number of draws used to sample `hyper_params` and `shared_params`. **kwargs : dict, optional, default: {} Further optional keyword arguments propagated to the underlying `plot_time_invariant_prior` plotting function. Returns ------- fig : plt.Figure - the generated figure """ samples = self.sample(batch_size=num_draws, num_steps=1) return plot_time_invariant_prior( hyper_params=samples["hyper_params"], shared_params=samples["shared_params"], mixture_names=self._mixture_names(), **kwargs, )
[docs] def plot_joint_prior(self, num_steps: int = 200, num_trajectories: int = 20, num_draws: int = 1000, **kwargs): """Plot joint prior diagnostics across local and shared parameters. Parameters ---------- num_steps : int, optional, default: 200 Number of time steps for local trajectory sampling. num_trajectories : int, optional, default: 20 Number of local trajectories to plot. num_draws : int, optional, default: 1000 Number of draws used for time-invariant parameter sampling. **kwargs : dict, optional, default: {} Further optional keyword arguments propagated to the underlying `plot_joint_prior` plotting function. Returns ------- fig : plt.Figure - the generated figure """ samples = self.sample(batch_size=num_draws, num_steps=num_steps) all_local_params = {**samples["local_params"], **samples["deterministic_params"]} local_params = {k: v[:num_trajectories] for k, v in all_local_params.items()} return plot_joint_prior( local_params=local_params, hyper_params=samples["hyper_params"], shared_params=samples["shared_params"], param_bounds=self._param_bounds(), mixture_names=self._mixture_names(), hyper_param_groups=self._last_hyper_param_groups, **kwargs, )