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

"""Choose the next mutation budget from completed relative jump distances."""

from __future__ import annotations

import math
from collections.abc import Mapping
from dataclasses import dataclass

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


[docs] @dataclass(frozen=True) class RelativeJumpDistancePolicy: """Increase or decrease a fixed budget using the fraction of RJD > 1. Parameters ---------- target_fraction : float Lower threshold for the fraction exceeding the radius, default 0.5. factor : float Multiplicative budget adjustment, greater than one, default 1.1. Below the lower threshold, multiply and round up; above the upper threshold, divide and round down. AdaptMutationSteps supplies bounds. metric_name : str Key containing RelativeJumpDistanceMetric results, default "rjd". upper_target_fraction : float or None Decrease only above this fraction. None uses target_fraction. The inclusive interval between thresholds retains the budget. Notes ----- This population-level controller follows the increase/decrease idea in Buchner (2024), https://arxiv.org/abs/2402.11936. The factor and aggregation are implementation choices, not a convergence guarantee from that paper. Decisions affect the next SMC iteration, never stop the current block. No measured transitions retains the budget (at least one). """ target_fraction: float = 0.5 factor: float = 1.1 metric_name: str = "rjd" upper_target_fraction: float | None = None def __post_init__(self) -> None: if isinstance(self.target_fraction, bool) or not ( math.isfinite(self.target_fraction) and 0 < self.target_fraction < 1 ): raise ValueError("target_fraction must be finite and in (0, 1).") if self.upper_target_fraction is not None and ( isinstance(self.upper_target_fraction, bool) or not math.isfinite(self.upper_target_fraction) or not self.target_fraction <= self.upper_target_fraction < 1 ): raise ValueError( "upper_target_fraction must be finite, >= target_fraction " "and < 1." ) if isinstance(self.factor, bool) or not ( math.isfinite(self.factor) and self.factor > 1 ): raise ValueError("factor must be finite and greater than one.") if not isinstance(self.metric_name, str) or not self.metric_name: raise ValueError("metric_name must be a nonempty string.")
[docs] def initialise(self, *, initial_steps: int, context: StepContext) -> int: """Retain the supplied initial budget.""" return validate_n_steps(initial_steps)
[docs] def update( self, *, current_steps: int, metrics: MutationMetricsOutput, context: StepContext, ) -> int: """Propose an integer budget from completed population diagnostics.""" current_steps = validate_n_steps(current_steps) if float(to_numpy(metrics.n_steps)) == 0: return max(1, current_steps) values = metrics.values.get(self.metric_name) if ( not isinstance(values, Mapping) or "fraction_above_one" not in values ): raise ValueError( f"RelativeJumpDistancePolicy requires {self.metric_name!r} " "metrics with fraction_above_one." ) fraction_array = to_numpy(values["fraction_above_one"]) if fraction_array.size != 1: raise ValueError("fraction_above_one must be scalar.") fraction = float(fraction_array.reshape(())) if not math.isfinite(fraction) or not 0 <= fraction <= 1: raise ValueError( "fraction_above_one must be finite and in [0, 1]." ) numerator, denominator = self.factor.as_integer_ratio() if fraction < self.target_fraction: scaled = current_steps * self.factor # Fall back to integer arithmetic if float multiplication overflows. return max( 1, math.ceil(scaled) if math.isfinite(scaled) else (current_steps * numerator + denominator - 1) // denominator, ) upper = ( self.target_fraction if self.upper_target_fraction is None else self.upper_target_fraction ) if fraction > upper: return max(1, math.floor(current_steps / self.factor)) return current_steps
__all__ = ["RelativeJumpDistancePolicy"]