Source code for sequince.steps.mutation.adaptive.policies.acceptance

"""Choose mutation budgets from observed acceptance probabilities."""

from __future__ import annotations

import math

from .....array_api import to_numpy
from ....base import validate_n_steps
from ....context import StepContext
from ...metrics import MutationMetricsOutput


[docs] class AcceptanceRatePolicy: """Target an expected number of accepted moves per particle. Propose ``ceil(target_steps / max(acceptance, min_acceptance))`` for the next iteration. ``min_acceptance`` (default ``1e-6``) keeps the budget finite for zero or very small acceptance rates. Every returned budget is clipped to the required ``min_steps`` and ``max_steps`` bounds. A block with no attempted moves supplies no acceptance estimate, so its current budget is retained (or increased to one when zero) to allow a subsequent measurement. Acceptance is estimated from the complete block's accepted and attempted counts, including fractional expected counts from recycling algorithms. """ def __init__( self, target_steps: float, *, min_steps: int, max_steps: int, min_acceptance: float = 1e-6, ) -> None: for name, value in ( ("target_steps", target_steps), ("min_acceptance", min_acceptance), ): if isinstance(value, bool): raise TypeError(f"{name} must be a real number.") if not math.isfinite(value) or value <= 0: raise ValueError(f"{name} must be finite and positive.") if min_acceptance > 1: raise ValueError("min_acceptance must be <= 1.") self.min_steps = validate_n_steps(min_steps) self.max_steps = validate_n_steps(max_steps) if self.max_steps < self.min_steps: raise ValueError("max_steps must be >= min_steps.") self.target_steps = float(target_steps) self.min_acceptance = float(min_acceptance) def _bound(self, steps: int) -> int: return min(max(steps, self.min_steps), self.max_steps)
[docs] def initialise(self, *, initial_steps: int, context: StepContext) -> int: return self._bound(validate_n_steps(initial_steps))
[docs] def update( self, *, current_steps: int, metrics: MutationMetricsOutput, context: StepContext, ) -> int: current_steps = validate_n_steps(current_steps) if not isinstance(metrics, MutationMetricsOutput): raise TypeError( "AcceptanceRatePolicy requires MutationMetricsOutput." ) 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 math.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: return self._bound(max(1, current_steps)) acceptance = max(accepted / attempted, self.min_acceptance) # Integer ratios avoid overflow even for very large finite targets. numerator, denominator = self.target_steps.as_integer_ratio() a_numerator, a_denominator = acceptance.as_integer_ratio() numerator *= a_denominator denominator *= a_numerator return self._bound((numerator + denominator - 1) // denominator)
__all__ = ["AcceptanceRatePolicy"]