# Copyright 2024 - present The PyMC Developers
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Modified from original implementation by Dominik Wabersich (2013)
import numpy as np
from rich.progress import TextColumn
from rich.table import Column
from pymc.blocking import RaveledVars, StatsType
from pymc.initial_point import PointType
from pymc.model import modelcontext
from pymc.pytensorf import compile, join_nonshared_inputs, make_shared_replacements
from pymc.step_methods.arraystep import ArrayStepShared
from pymc.step_methods.compound import Competence, StepMethodState
from pymc.step_methods.state import dataclass_state
from pymc.util import get_value_vars_from_user_vars
from pymc.vartypes import continuous_types
__all__ = ["Slice"]
@dataclass_state
class SliceState(StepMethodState):
w: np.ndarray
tune: bool
n_tunes: float
max_steps: int
[docs]
class Slice(ArrayStepShared):
"""
Univariate slice sampler step method.
Parameters
----------
vars : list, optional
List of value variables for sampler.
w : float, default 1.0
Initial width of slice.
tune : bool, default True
Flag for tuning.
model : Model, optional
Optional model for sampling step. It will be taken from the context if not provided.
max_steps : int, default 100
Maximum interval width as a multiple of ``w``. Must be a positive integer.
At most ``max_steps - 1`` stepping-out expansions are performed per coordinate,
randomly divided between the left and right endpoints. Sampling and shrinkage
proceed when this budget is exhausted, even if an endpoint is still inside
the slice. The budget applies during both tuning and sampling. Set to 1 to
disable stepping out.
rng: RandomGenerator
An object that can produce be used to produce the step method's
:py:class:`~numpy.random.Generator` object. Refer to
:py:func:`pymc.util.get_random_generator` for more information.
References
----------
.. [1] Neal, R. M. (2003). Slice sampling. The Annals of Statistics, 31(3),
705-767. Stepping-out and shrinkage procedures in Figures 3 and 5.
https://doi.org/10.1214/aos/1056562461
"""
name = "slice"
default_blocked = False
stats_dtypes_shapes = {
"nstep_out": (int, []),
"nstep_in": (int, []),
}
_state_class = SliceState
[docs]
def __init__(
self,
vars=None,
*,
w=1.0,
tune=True,
model=None,
max_steps: int = 100,
rng=None,
initial_point: PointType | None = None,
compile_kwargs: dict | None = None,
blocked: bool = False, # Could be true since tuning is independent across dims?
):
model = modelcontext(model)
self.w = np.asarray(w).copy()
self.tune = tune
self.n_tunes = 0.0
if not isinstance(max_steps, int | np.integer) or max_steps < 1:
raise ValueError("max_steps must be a positive integer")
self.max_steps = int(max_steps)
if vars is None:
vars = model.continuous_value_vars
else:
vars = get_value_vars_from_user_vars(vars, model)
if initial_point is None:
initial_point = model.initial_point()
shared = make_shared_replacements(initial_point, vars, model)
[logp], raveled_inp = join_nonshared_inputs(
point=initial_point, outputs=[model.logp()], inputs=vars, shared_inputs=shared
)
if compile_kwargs is None:
compile_kwargs = {}
self.logp = compile([raveled_inp], logp, **compile_kwargs)
self.logp.trust_input = True
super().__init__(vars, shared, blocked=blocked, rng=rng)
[docs]
def astep(self, apoint: RaveledVars) -> tuple[RaveledVars, StatsType]:
# The arguments are determined by the list passed via `super().__init__(..., fs, ...)`
q0_val = apoint.data
if q0_val.shape != self.w.shape:
self.w = np.resize(self.w, len(q0_val)) # this is a repmat
nstep_out = nstep_in = 0
q = np.copy(q0_val)
ql = np.copy(q0_val) # l for left boundary
qr = np.copy(q0_val) # r for right boundary
logp = self.logp
for i, wi in enumerate(self.w):
# uniformly sample from 0 to p(q), but in log space
y = logp(q) - self.rng.standard_exponential()
# Create initial interval
ql[i] = q[i] - self.rng.uniform() * wi # q[i] + r * w
qr[i] = ql[i] + wi # Equivalent to q[i] + (1-r) * w
# Randomly split the expansion budget (Neal, 2003, Figure 3).
# This random allocation is required for a reversible transition.
max_steps_left = self.rng.integers(self.max_steps)
max_steps_right = self.max_steps - 1 - max_steps_left
# Stepping out procedure
cnt = 0
while cnt < max_steps_left and y <= logp(ql):
ql[i] -= wi
cnt += 1
nstep_out += cnt
cnt = 0
while cnt < max_steps_right and y <= logp(qr):
qr[i] += wi
cnt += 1
nstep_out += cnt
cnt = 0
q[i] = self.rng.uniform(ql[i], qr[i])
while y > logp(q): # Changed leq to lt, to accommodate for locally flat posteriors
# Sample uniformly from slice
if q[i] > q0_val[i]:
qr[i] = q[i]
elif q[i] < q0_val[i]:
ql[i] = q[i]
q[i] = self.rng.uniform(ql[i], qr[i])
cnt += 1
nstep_in += cnt
if self.tune:
# Update the mean interval width during tuning only.
self.w[i] = wi * (self.n_tunes / (self.n_tunes + 1)) + (qr[i] - ql[i]) / (
self.n_tunes + 1
)
# Set qr and ql to the accepted points (they matter for subsequent iterations)
qr[i] = ql[i] = q[i]
if self.tune:
self.n_tunes += 1
stats = {
"nstep_out": nstep_out,
"nstep_in": nstep_in,
}
return RaveledVars(q, apoint.point_map_info), [stats]
[docs]
@staticmethod
def competence(var, has_grad):
if var.dtype in continuous_types:
if not has_grad and var.ndim == 0:
return Competence.PREFERRED
return Competence.COMPATIBLE
return Competence.INCOMPATIBLE
@staticmethod
def _progressbar_config(n_chains=1):
columns = [
TextColumn("{task.fields[nstep_out]}", table_column=Column("Steps out", ratio=1)),
TextColumn("{task.fields[nstep_in]}", table_column=Column("Steps in", ratio=1)),
]
stats = {"nstep_out": [0] * n_chains, "nstep_in": [0] * n_chains}
return columns, stats
@staticmethod
def _make_progressbar_update_functions():
def update_stats(step_stats):
return {key: step_stats[key] for key in {"nstep_out", "nstep_in"}}
return (update_stats,)