Source code for sequince.steps.mutation.adaptive.policies.rho
"""Policies selecting MiniPCN step sizes from completed mutation blocks."""
from __future__ import annotations
from collections.abc import Mapping
from dataclasses import asdict, dataclass
from typing import Any, Protocol
import numpy as np
from .....array_api import to_numpy
from ....context import StepContext
from ...metrics import MutationMetricsOutput
def _validate_rho(value: Any) -> float:
"""Validate the kernel's step-size domain independently of policy bounds."""
array = to_numpy(value)
if array.size != 1:
raise TypeError("rho must be scalar.")
rho = float(array.reshape(()))
if not np.isfinite(rho) or not 0 < rho < 1:
raise ValueError(
"rho must be finite and strictly between zero and one."
)
return rho
[docs]
class MiniPCNRhoPolicy(Protocol):
"""Choose rho before a block and from its completed metrics.
Decisions must be scalar, finite and strictly between zero and one.
Stateful policies may supply ``state_dict`` and ``load_state_dict``;
the wrapper forwards these when checkpointing. Policies never see
intermediate rounds or mutate the sampler.
"""
[docs]
def initialise(self, *, initial_rho: float, context: StepContext) -> float:
"""Choose the first iteration's step size."""
...
[docs]
def update(
self,
*,
current_rho: float,
metrics: MutationMetricsOutput,
context: StepContext,
) -> float:
"""Choose the next iteration's step size from one complete block."""
...
[docs]
@dataclass(frozen=True)
class AcceptanceRateRhoPolicy:
"""Tune rho toward a target acceptance using a bounded multiplicative update.
Parameters
----------
target_acceptance : float, optional
Desired acceptance fraction. Default 0.234.
adaptation_rate : float, optional
Rate in ``rho * exp(rate * (acceptance - target))``. Default 0.1.
Zero retains rho exactly after initial clipping.
min_rho, max_rho : float, optional
Bounds strictly between zero and one. Defaults 0.001 and 0.99.
Initial rho is clipped to these bounds.
Notes
-----
Higher acceptance increases the next block's proposal step size. Updates
use finalized accepted and attempted counts; fractional counts are valid.
Empty blocks retain rho. This policy has no evolving state; checkpoint
configuration is validated when restoring into a reconstructed policy.
"""
target_acceptance: float = 0.234
adaptation_rate: float = 0.1
min_rho: float = 1e-3
max_rho: float = 0.99
def __post_init__(self) -> None:
if not all(np.isfinite(value) for value in asdict(self).values()):
raise ValueError("Rho policy parameters must be finite.")
if not 0 < self.min_rho <= self.max_rho < 1:
raise ValueError("Require 0 < min_rho <= max_rho < 1.")
if not 0 <= self.target_acceptance <= 1 or self.adaptation_rate < 0:
raise ValueError(
"Require target_acceptance in [0, 1] and adaptation_rate >= 0."
)
[docs]
def initialise(self, *, initial_rho: float, context: StepContext) -> float:
return float(
np.clip(_validate_rho(initial_rho), self.min_rho, self.max_rho)
)
[docs]
def update(
self,
*,
current_rho: float,
metrics: MutationMetricsOutput,
context: StepContext,
) -> float:
rho = self.initialise(initial_rho=current_rho, context=context)
counts = []
for name in ("n_attempted", "n_accepted"):
array = to_numpy(getattr(metrics, name))
if array.size != 1:
raise TypeError(f"{name} must be scalar.")
value = float(array.reshape(()))
if not np.isfinite(value) or value < 0:
raise ValueError(f"{name} must be finite and nonnegative.")
counts.append(value)
attempted, accepted = counts
if accepted > attempted:
raise ValueError("n_accepted must not exceed n_attempted.")
if attempted == 0 or self.adaptation_rate == 0:
return rho
log_rho = np.log(rho) + self.adaptation_rate * (
accepted / attempted - self.target_acceptance
)
# Clip before exponentiation to avoid overflow, and afterward to
# keep rounding at a boundary inside the configured interval.
return float(
np.clip(
np.exp(
np.clip(
log_rho, np.log(self.min_rho), np.log(self.max_rho)
)
),
self.min_rho,
self.max_rho,
)
)
[docs]
def state_dict(self) -> dict[str, Any]:
return asdict(self)
[docs]
def load_state_dict(self, state: Mapping[str, Any]) -> None:
if dict(state) != self.state_dict():
raise ValueError(
"Rho policy checkpoint configuration does not match."
)
__all__ = ["MiniPCNRhoPolicy", "AcceptanceRateRhoPolicy"]