Source code for sequince.steps.transition
from __future__ import annotations
from typing import Any
from orng import RandomGenerator
from ..array_api import namespace
from ..filtering import ImportanceKernel
from ..state import ParticleState
from ..target import EvaluationCounts
from .base import SMCStep
from .context import StepContext
from .weighting import _apply_log_weight_increment, _weights_as_1d_array
[docs]
class ImportanceTransition(SMCStep):
"""Propagate particles and apply a joint importance-weight correction.
The supplied :class:`~sequince.filtering.ImportanceKernel` samples one
child per parent and computes the associated importance log-weight
increment from the parent and child populations.
The transition is one-to-one: it must preserve the particle population
size. Moving particles invalidates any cached target evaluation while
preserving their existing ancestry indices.
"""
def __init__(
self,
kernel: ImportanceKernel,
*,
normalize_after: bool = True,
) -> None:
if not callable(getattr(kernel, "sample", None)) or not callable(
getattr(kernel, "log_increment", None)
):
raise TypeError(
"kernel must implement sample() and log_increment()."
)
self._kernel = kernel
self._normalize_after = normalize_after
@property
def kernel(self) -> ImportanceKernel:
"""The transition kernel applied by this step."""
return self._kernel
[docs]
def apply(
self,
state: ParticleState,
context: StepContext,
rng: "RandomGenerator",
) -> tuple[ParticleState, StepContext, dict[str, Any]]:
previous_particles = state.particles
new_particles = self._kernel.sample(
previous_particles,
context,
rng,
)
if new_particles.shape[0] != state.log_weights.shape[0]:
raise ValueError(
"Transition must return one particle per input particle."
)
increment = self._kernel.log_increment(
previous_particles,
new_particles,
context,
)
xp = namespace(state.log_weights)
context = context.add_evaluation_counts(
EvaluationCounts(
log_increment_evaluations=xp.asarray(new_particles.shape[0])
)
)
increments = _weights_as_1d_array(
increment,
xp=xp,
like=state.log_weights,
)
if increments.shape[0] != state.log_weights.shape[0]:
raise ValueError(
"Increment function must return one value per particle."
)
moved_state = state.replace_particles(new_particles)
updated_state, _ = _apply_log_weight_increment(
moved_state,
increments,
normalize=self._normalize_after,
)
return (
updated_state,
context,
{
"log_weight_increment": increments,
},
)