python/dpt/stochastic_recovery.py

Generated from the full canonical file for this source snapshot. Line numbers match the library source.

Source SHA256: 65736264d79aae216fb492212fa878ee0b93d0026aab6b1cc1ac8e972463630c

1"""Independent-batch squared-expected-signal optimisation in a fixed chart.23The acceptance band uses an estimated standard error, not a distribution-free4confidence bound. Sampling uncertainty can force batch growth or exhaustion;5it never turns an unresolved trial into an accepted decrease. General nonlinear6losses need different estimators and are outside this interface's contract.7"""89from __future__ import annotations1011import math12from collections.abc import Callable13from dataclasses import dataclass, replace14from functools import partial15from typing import Literal, Protocol, TypeVar, cast1617from dpt.contracts import ContractError, NumericalError, TrialDomainError, finite_scalar, integer18from dpt.registration import Vector19from dpt.statistics import mean_standard_error20from dpt.stochastic_models import (21    Matrix,22    QuadraticProposal,23    quadratic_proposal,24    quadratic_reduction,25    validate_curvature,26)27from dpt.transport.rng import HistoryBatch282930class IndependentSquaredOracle(Protocol):31    """Original histories are IID within each caller-specified batch.3233    Source sampling must be keyed by the provided history identity too. The two34    independent batches supply distinct mean and derivative estimates. Replaying35    an original history to obtain its derivative does not create a new sample.36    """3738    def gradient_replicate(39        self, parameters: Vector, mean_batch: HistoryBatch, derivative_batch: HistoryBatch40    ) -> Vector:41        """Return (estimated mean - observed) times an independent mean derivative."""42        ...4344    def change_replicate(45        self, before: Vector, after: Vector, first: HistoryBatch, second: HistoryBatch46    ) -> float:47        """Return an unbiased squared-expected-signal loss difference.4849        Each pose uses the product of independent residual-mean estimates.50        Reuse the specified streams between poses for common random numbers;51        batches first and second must stay independent within either pose.52        """53        ...545556class QuadraticSquaredOracle(IndependentSquaredOracle, Protocol):57    def model_replicate(58        self, parameters: Vector, mean_batch: HistoryBatch, derivative_batch: HistoryBatch59    ) -> tuple[Vector, Matrix]:60        """Return independent-product gradient and PSD estimated-Jacobian metric."""61        ...626364@dataclass(frozen=True, slots=True)65class StochasticPolicy:66    iterations: int = 10067    replicates: int = 868    initial_batch: int = 102469    maximum_batch: int = 1_048_57670    unique_history_budget: int = 100_000_00071    initial_radius: float = 0.172    minimum_radius: float = 1e-773    maximum_radius: float = 1.074    acceptance_fraction: float = 0.175    standard_error_multiplier: float = 2.076    gradient_tolerance: float = 1e-577    relative_gradient_uncertainty: float = 0.578    proposal: Literal["linear", "quadratic"] = "linear"79    numerical_gradient_allowance: float = 0.080    final_validation_batch: int | None = None81    damping_relative: float = 1e-1282    rank_tolerance: float = 1e-1083    radius_growth_agreement: float = 0.758485    def __post_init__(self) -> None:86        for name in ("iterations", "initial_batch", "maximum_batch", "unique_history_budget"):87            integer(getattr(self, name), name, minimum=1, maximum=2**63 - 1)88        integer(self.replicates, "replicates", minimum=2)89        if self.initial_batch > self.maximum_batch or self.maximum_batch >= 2**31:90            raise ContractError("batch sizes must be ordered positive signed-32-bit counts")91        for name in (92            "initial_radius",93            "minimum_radius",94            "maximum_radius",95            "acceptance_fraction",96            "standard_error_multiplier",97            "gradient_tolerance",98            "relative_gradient_uncertainty",99        ):100            if finite_scalar(getattr(self, name), name, minimum=0.0) == 0:101                raise ContractError(f"{name} must be positive")102        if not self.minimum_radius <= self.initial_radius <= self.maximum_radius:103            raise ContractError("trust radii must satisfy minimum <= initial <= maximum")104        if not 0 < self.acceptance_fraction < 1:105            raise ContractError("acceptance fraction must lie in (0,1)")106        if self.proposal not in ("linear", "quadratic"):107            raise ContractError("proposal must be linear or quadratic")108        finite_scalar(self.numerical_gradient_allowance, "numerical_gradient_allowance", minimum=0)109        finite_scalar(self.damping_relative, "damping_relative", minimum=0)110        finite_scalar(self.rank_tolerance, "rank_tolerance", minimum=0)111        finite_scalar(self.radius_growth_agreement, "radius_growth_agreement", minimum=0)112        if not 0 < self.rank_tolerance < 1 or not 0 < self.radius_growth_agreement <= 1:113            raise ContractError("rank tolerance and radius agreement must lie in (0,1] (rank < 1)")114        if self.final_validation_batch is not None:115            integer(self.final_validation_batch, "final_validation_batch", minimum=1)116            if self.final_validation_batch > self.maximum_batch:117                raise ContractError("final validation batch exceeds prepared batch capacity")118119120@dataclass(frozen=True, slots=True)121class StochasticStep:122    iteration: int123    accepted: bool124    mean_change: float125    change_standard_error: float126    predicted_decrease: float127    radius: float128    batch_size: int129    first_history: int130    histories_used: int131132133@dataclass(frozen=True, slots=True)134class GradientAttempt:135    """Host statistics for one complete set of independent gradient replicates."""136137    iteration: int138    parameters: Vector139    batch_size: int140    first_history: int141    histories_used: int142    gradient: Vector143    standard_error: Vector144    gradient_norm: float145    standard_error_norm: float146    decision: Literal["zero_sample", "gradient_band", "resolved", "relative_uncertainty"]147    model_id: int | None = None148    pool: str = "proposal"149    band_method: str = "heuristic_euclidean_marginal_se"150    numerical_allowance: float = 0.0151    replicate_gradients: tuple[Vector, ...] = ()152153154@dataclass(frozen=True, slots=True)155class WorkReservation:156    """Random identities reserved, distinct from completed oracle calls/work.157158    A failed call may have executed partial CUDA work. Only the operator's work159    counters can report that; completed_replicates does not infer replay costs.160    No identity from a failed reservation is reused.161    """162163    operation: str164    iteration: int165    first_history: int166    required_histories: int167    reserved_histories: int168    checkpoint_histories: int169    pairs: tuple[tuple[HistoryBatch, HistoryBatch], ...]170    status: str171    attempted_replicates: int = 0172    completed_replicates: int = 0173174175@dataclass(frozen=True, slots=True)176class LocalModel:177    model_id: int178    parameters: Vector179    gradient: Vector180    curvature: Matrix181    first_history: int182    histories_used: int183    batch_size: int184185186@dataclass(frozen=True, slots=True)187class AcceptanceAttempt:188    iteration: int189    incumbent: Vector190    candidate: Vector191    model_id: int | None192    step: Vector193    predicted_decrease: float194    radius: float195    batch_size: int196    first_history: int197    histories_used: int198    required_histories: int199    replicate_changes: tuple[float, ...]200    mean_change: float | None201    standard_error: float | None202    band_method: str203    look_index: int204    threshold: float205    outcome: str206    agreement: float | None207    boundary: bool208    proposal_diagnostics: QuadraticProposal | None = None209210211@dataclass(frozen=True, slots=True)212class StochasticRecoveryResult:213    parameters: Vector214    reason: Literal[215        "gradient_band", "sampling_unresolved", "history_budget", "radius_limit", "iteration_budget"216    ]217    unique_histories: int218    steps: tuple[StochasticStep, ...]219    domain_rejections: tuple[DomainRejection, ...] = ()220    gradient_attempts: tuple[GradientAttempt, ...] = ()221    termination_detail: str = ""222    diagnostic_version: int = 2223    acceptance_attempts: tuple[AcceptanceAttempt, ...] = ()224    reservations: tuple[WorkReservation, ...] = ()225    local_models: tuple[LocalModel, ...] = ()226    final_validation_attempts: tuple[GradientAttempt, ...] = ()227    sampling_classification: str = "stochastic"228    proposal_policy: str = "linear"229    uncertainty_policy: str = "heuristic_euclidean_marginal_se"230    numerical_gradient_allowance: float = 0.0231    final_validation_batch: int | None = None232    final_validation_trigger: str | None = None233234235@dataclass(frozen=True, slots=True)236class DomainRejection:237    iteration: int238    radius: float239    message: str240241242_Replicate = TypeVar("_Replicate")243244245def _gradient_attempt(246    replicates: tuple[Vector, ...],247    parameters: Vector,248    selected: StochasticPolicy,249    deterministic: bool,250    *,251    iteration: int,252    batch_size: int,253    first_history: int,254    histories_used: int,255    model_id: int | None,256    pool: str = "proposal",257) -> GradientAttempt:258    if any(len(value) != len(parameters) for value in replicates):259        raise ContractError("gradient replicate dimension differs from the active chart")260    statistics = tuple(261        mean_standard_error(tuple(row[j] for row in replicates)) for j in range(len(parameters))262    )263    gradient = tuple(mean for mean, _ in statistics)264    norm = math.hypot(*gradient)265    empirical_error = math.hypot(*(error for _, error in statistics))266    if not math.isfinite(norm) or not math.isfinite(empirical_error):267        raise NumericalError("gradient or uncertainty norm exceeds the finite chart range")268    if deterministic and empirical_error > selected.numerical_gradient_allowance:269        raise NumericalError("verified deterministic oracle exceeds its numerical allowance")270    error_norm = 0.0 if deterministic else empirical_error271    allowance = selected.numerical_gradient_allowance if deterministic else 0.0272    decision: Literal["zero_sample", "gradient_band", "resolved", "relative_uncertainty"]273    if not deterministic and norm == 0.0 and error_norm == 0.0:274        decision = "zero_sample"275    elif (276        norm + selected.standard_error_multiplier * error_norm + allowance277        <= selected.gradient_tolerance278    ):279        decision = "gradient_band"280    elif norm > 0 and error_norm + allowance <= selected.relative_gradient_uncertainty * norm:281        decision = "resolved"282    else:283        decision = "relative_uncertainty"284    return GradientAttempt(285        iteration,286        parameters,287        batch_size,288        first_history,289        histories_used,290        gradient,291        (0.0,) * len(parameters) if deterministic else tuple(error for _, error in statistics),292        norm,293        error_norm,294        decision,295        model_id,296        pool,297        "deterministic_numerical_allowance" if deterministic else "heuristic_euclidean_marginal_se",298        allowance,299        replicates,300    )301302303# region book:stochastic-independent-acceptance304def recover_expected_signal(305    oracle: IndependentSquaredOracle,306    initial: Vector,307    *,308    seed: int,309    policy: StochasticPolicy | None = None,310) -> StochasticRecoveryResult:311    """Fresh proposal/acceptance pools with an optional held-out final checkpoint.312313    Quadratic proposals cache a PSD metric at the unchanged incumbent; rejected314    radii use fresh acceptance identities. Final sample size is fixed here before315    observing any samples. A failed final checkpoint terminates unresolved and316    never feeds a new proposal. ``linear`` with no explicit final batch retains317    the original trajectory for ablation, including its heuristic stopping rule.318319    The explicit ``deterministic_sampling`` oracle flag must be established from320    immutable physical/source properties. Empirical variance never establishes321    that classification. Its numerical allowance requires independent validation322    by the caller. Stochastic norm bands are heuristic, not simultaneous-vector323    or repeated-look confidence bounds. Unique identities are not replay costs.324    """325    selected: StochasticPolicy = policy or StochasticPolicy()326    quadratic = selected.proposal == "quadratic"327    integer(seed, "seed", maximum=2**64 - 1)328    parameters: Vector = tuple(finite_scalar(value, "initial parameter") for value in initial)329    if not parameters:330        raise ContractError("a stochastic inverse problem needs active parameters")331    if quadratic and len(parameters) > 16:332        raise ContractError("dense stochastic models support at most 16 active parameters")333    if quadratic and not callable(getattr(oracle, "model_replicate", None)):334        raise ContractError("quadratic proposals require an oracle model_replicate operation")335    classification = getattr(oracle, "deterministic_sampling", False)336    if type(classification) is not bool:337        raise ContractError("deterministic_sampling must be an explicit boolean contract")338    deterministic = classification339    final_size = selected.final_validation_batch340    if quadratic and final_size is None:341        final_size = selected.initial_batch342    final_cost = 0 if final_size is None else 2 * final_size * selected.replicates343    first_history = 0344    radius = selected.initial_radius345    batch_size = selected.initial_batch346    damping_relative = selected.damping_relative347    rank_tolerance = selected.rank_tolerance348    steps: list[StochasticStep] = []349    domain_rejections: list[DomainRejection] = []350    gradient_attempts: list[GradientAttempt] = []351    acceptance_attempts: list[AcceptanceAttempt] = []352    reservations: list[WorkReservation] = []353    local_models: list[LocalModel] = []354    validation_attempts: list[GradientAttempt] = []355    validation_trigger: str | None = None356    cached: list[LocalModel] = []357    band_method = (358        "deterministic_numerical_allowance" if deterministic else "heuristic_euclidean_marginal_se"359    )360361    def reserve(size: int, operation: str, iteration: int, holdback: int = 0) -> int | None:362        nonlocal first_history363        required = 2 * size * selected.replicates364        start = first_history365        if first_history + required + holdback > selected.unique_history_budget:366            reservations.append(367                WorkReservation(368                    operation, iteration, start, required, 0, holdback, (), "budget_denied"369                )370            )371            return None372        pairs: list[tuple[HistoryBatch, HistoryBatch]] = []373        for _ in range(selected.replicates):374            left = HistoryBatch(seed, first_history, size, f"inverse-source-{first_history}")375            first_history += size376            right = HistoryBatch(seed, first_history, size, f"inverse-source-{first_history}")377            first_history += size378            pairs.append((left, right))379        reservations.append(380            WorkReservation(381                operation, iteration, start, required, required, holdback, tuple(pairs), "reserved"382            )383        )384        return len(reservations) - 1385386    def execute(387        index: int, operation: Callable[[HistoryBatch, HistoryBatch], _Replicate]388    ) -> tuple[_Replicate, ...]:389        outputs: list[_Replicate] = []390        for left, right in reservations[index].pairs:391            reservations[index] = replace(392                reservations[index], attempted_replicates=len(outputs) + 1393            )394            try:395                outputs.append(operation(left, right))396            except Exception as error:397                reservations[index] = replace(398                    reservations[index],399                    status="domain_error"400                    if isinstance(error, TrialDomainError)401                    else "oracle_error",402                    completed_replicates=len(outputs),403                )404                raise405            reservations[index] = replace(reservations[index], completed_replicates=len(outputs))406        reservations[index] = replace(reservations[index], status="complete")407        return tuple(outputs)408409    def result(410        reason: Literal[411            "gradient_band",412            "sampling_unresolved",413            "history_budget",414            "radius_limit",415            "iteration_budget",416        ],417        detail: str = "",418    ) -> StochasticRecoveryResult:419        return StochasticRecoveryResult(420            parameters,421            reason,422            first_history,423            tuple(steps),424            tuple(domain_rejections),425            tuple(gradient_attempts),426            detail or reason,427            acceptance_attempts=tuple(acceptance_attempts),428            reservations=tuple(reservations),429            local_models=tuple(local_models),430            final_validation_attempts=tuple(validation_attempts),431            sampling_classification="verified_deterministic_expectation"432            if deterministic433            else "stochastic",434            proposal_policy=selected.proposal,435            uncertainty_policy=band_method,436            numerical_gradient_allowance=selected.numerical_gradient_allowance,437            final_validation_batch=final_size,438            final_validation_trigger=validation_trigger,439        )440441    def record_exception(error: Exception) -> None:442        # Preserve the normal numerical exception contract, with a serialisable443        # snapshot for callers recording a failed run. Partial device work is444        # deliberately unknown here; the production operator records its cost.445        error.__dict__["recovery_diagnostics"] = result("sampling_unresolved", "oracle_exception")446        error.add_note("Recovery reservation/attempt snapshot is in recovery_diagnostics.")447448    def log_acceptance(449        base: AcceptanceAttempt,450        changes: list[float],451        outcome: str,452        change: float | None = None,453        error: float | None = None,454    ) -> None:455        ratio = None if change is None else -change / base.predicted_decrease456        acceptance_attempts.append(457            replace(458                base,459                outcome=outcome,460                replicate_changes=tuple(changes),461                mean_change=change,462                standard_error=error,463                agreement=ratio if ratio is None or math.isfinite(ratio) else None,464            )465        )466467    def finish_stationarity(468        iteration: int, trigger: str = "proposal_gradient_band"469    ) -> StochasticRecoveryResult:470        nonlocal validation_trigger471        if final_size is None:472            return result("gradient_band")473        validation_trigger = trigger474        index = reserve(final_size, "final_validation", iteration)475        if index is None:476            return result("history_budget", "final_validation_reservation_failed")477        try:478            values = execute(index, partial(oracle.gradient_replicate, parameters))479            attempt = _gradient_attempt(480                values,481                parameters,482                selected,483                deterministic,484                iteration=iteration,485                batch_size=final_size,486                first_history=reservations[index].first_history,487                histories_used=reservations[index].reserved_histories,488                model_id=None,489                pool="final_validation",490            )491        except Exception as error:492            record_exception(error)493            raise494        validation_attempts.append(attempt)495        if attempt.decision == "gradient_band":496            return result("gradient_band", "held_out_gradient_band")497        return result("sampling_unresolved", "final_validation_failed")498499    def finish_normal(500        reason: Literal["sampling_unresolved", "radius_limit"], detail: str, iteration: int501    ) -> StochasticRecoveryResult:502        # A development decision can be unresolved even when the fixed incumbent503        # is stationary. Spend its already reserved, independent checkpoint once.504        # Final failure terminates; none of these samples select another point.505        if final_size is not None and first_history + final_cost <= selected.unique_history_budget:506            return finish_stationarity(iteration, detail or reason)507        return result(reason, detail)508509    for iteration in range(selected.iterations):510        if not cached:511            # Fresh-batch growth is deliberately retained: there is no pooling512            # across adaptive epochs or different parameter values.513            while True:514                checkpoint = final_cost515                if final_size is not None:516                    checkpoint += 2 * batch_size * selected.replicates517                index = reserve(518                    batch_size,519                    "model" if quadratic else "gradient",520                    iteration,521                    checkpoint,522                )523                if index is None:524                    if final_size is not None and any(value.accepted for value in steps):525                        return finish_stationarity(526                            iteration, "proposal_checkpoint_reservation_failed"527                        )528                    return result(529                        "history_budget",530                        "proposal_checkpoint_reservation_failed"531                        if checkpoint532                        else "history_budget",533                    )534                model_id = len(local_models) if quadratic else None535                try:536                    curvature: Matrix = ()537                    if quadratic:538                        model_oracle = cast(QuadraticSquaredOracle, oracle)539                        models = execute(index, partial(model_oracle.model_replicate, parameters))540                        replicates = tuple(value[0] for value in models)541                        size = len(parameters)542                        if any(543                            len(value[1]) != size or any(len(row) != size for row in value[1])544                            for value in models545                        ):546                            raise ContractError(547                                "model curvature dimension differs from the active chart"548                            )549                        curvature = tuple(550                            tuple(551                                mean_standard_error(tuple(value[1][i][j] for value in models))[0]552                                for j in range(size)553                            )554                            for i in range(size)555                        )556                        validate_curvature(curvature, size)557                    else:558                        replicates = execute(index, partial(oracle.gradient_replicate, parameters))559                    attempt = _gradient_attempt(560                        replicates,561                        parameters,562                        selected,563                        deterministic,564                        iteration=iteration,565                        batch_size=batch_size,566                        first_history=reservations[index].first_history,567                        histories_used=reservations[index].reserved_histories,568                        model_id=model_id,569                    )570                except Exception as error:571                    record_exception(error)572                    raise573                gradient_attempts.append(attempt)574                if model_id is not None:575                    local_models.append(576                        LocalModel(577                            model_id,578                            parameters,579                            attempt.gradient,580                            curvature,581                            attempt.first_history,582                            attempt.histories_used,583                            batch_size,584                        )585                    )586                if attempt.decision == "zero_sample":587                    if batch_size == selected.maximum_batch:588                        return finish_normal(589                            "sampling_unresolved",590                            "zero_gradient_and_variance_at_maximum_batch",591                            iteration,592                        )593                    batch_size = min(2 * batch_size, selected.maximum_batch)594                    continue595                if attempt.decision == "gradient_band":596                    return finish_stationarity(iteration)597                if attempt.decision == "resolved":598                    gradient, norm = attempt.gradient, attempt.gradient_norm599                    if model_id is not None:600                        cached[:] = [local_models[-1]]601                    break602                if batch_size == selected.maximum_batch:603                    return finish_normal(604                        "sampling_unresolved", "gradient_uncertainty_at_maximum_batch", iteration605                    )606                batch_size = min(2 * batch_size, selected.maximum_batch)607        else:608            gradient, norm = cached[0].gradient, math.hypot(*cached[0].gradient)609        proposal: QuadraticProposal | None = None610        active_curvature: Matrix = ()611        if quadratic:612            assert cached613            active_curvature = cached[0].curvature614            proposal = quadratic_proposal(615                gradient,616                active_curvature,617                radius,618                damping_relative=damping_relative,619                rank_tolerance=rank_tolerance,620            )621            step, predicted, boundary = (622                proposal.step,623                proposal.predicted_decrease,624                proposal.boundary,625            )626        else:627            step = tuple(-radius * (value / norm) for value in gradient)628            predicted, boundary = radius * norm, True629        candidate: Vector = tuple(a + b for a, b in zip(parameters, step, strict=True))630        if not math.isfinite(predicted) or predicted <= 0 or not all(map(math.isfinite, candidate)):631            raise NumericalError("stochastic proposal exceeds the finite chart range")632        if candidate == parameters:633            return finish_normal(634                "sampling_unresolved", "proposal_below_chart_resolution", iteration635            )636        # Addition in a large finite chart can round the requested displacement.637        # Log the realised move and assess its actual quadratic prediction. The638        # legacy linear prediction/decisions remain unchanged for the ablation.639        step = tuple(b - a for a, b in zip(parameters, candidate, strict=True))640        if proposal is not None:641            actual_norm = math.hypot(*step)642            if actual_norm > radius * (1 + 1e-12):643                return finish_normal(644                    "sampling_unresolved", "chart_rounding_exceeds_trust_radius", iteration645                )646            predicted = quadratic_reduction(gradient, active_curvature, step)647            boundary = proposal.boundary and actual_norm >= radius * (1 - 1e-12)648            proposal = replace(proposal, step=step, predicted_decrease=predicted, boundary=boundary)649        look = 0650        while True:651            look += 1652            start = first_history653            index = reserve(batch_size, "acceptance", iteration, final_cost)654            changes: list[float] = []655            threshold = -selected.acceptance_fraction * predicted656657            base = AcceptanceAttempt(658                iteration,659                parameters,660                candidate,661                None if not cached else cached[0].model_id,662                step,663                predicted,664                radius,665                batch_size,666                start,667                first_history - start,668                2 * batch_size * selected.replicates,669                (),670                None,671                None,672                band_method,673                look,674                threshold,675                "reserved",676                None,677                boundary,678                proposal,679            )680681            if index is None:682                log_acceptance(base, changes, "budget_denied")683                return result(684                    "history_budget",685                    "acceptance_checkpoint_reservation_failed" if final_cost else "history_budget",686                )687688            def change_operation(689                a: HistoryBatch,690                b: HistoryBatch,691                *,692                before: Vector = parameters,693                after: Vector = candidate,694                outputs: list[float] = changes,695            ) -> float:696                value = oracle.change_replicate(before, after, a, b)697                if not math.isfinite(value):698                    raise NumericalError("objective-change replicate is nonfinite")699                outputs.append(value)700                return value701702            try:703                execute(index, change_operation)704                change, standard_error = mean_standard_error(changes)705                band = selected.standard_error_multiplier * standard_error706                if not math.isfinite(band):707                    raise NumericalError("objective-change uncertainty band exceeds finite range")708            except TrialDomainError as error:709                log_acceptance(base, changes, "domain_error")710                domain_rejections.append(DomainRejection(iteration, radius, str(error)))711                radius *= 0.5712                if radius < selected.minimum_radius:713                    return finish_normal("radius_limit", "radius_limit", iteration)714                break715            except Exception as error:716                log_acceptance(base, changes, "oracle_error")717                record_exception(error)718                raise719            if change + band < threshold:720                log_acceptance(base, changes, "accepted", change, standard_error)721                steps.append(722                    StochasticStep(723                        iteration,724                        True,725                        change,726                        standard_error,727                        predicted,728                        radius,729                        batch_size,730                        start,731                        first_history - start,732                    )733                )734                parameters = candidate735                cached.clear()736                if not quadratic or (737                    boundary and -change / predicted >= selected.radius_growth_agreement738                ):739                    radius = min(2 * radius, selected.maximum_radius)740                break741            if change - band >= threshold:742                log_acceptance(base, changes, "rejected", change, standard_error)743                steps.append(744                    StochasticStep(745                        iteration,746                        False,747                        change,748                        standard_error,749                        predicted,750                        radius,751                        batch_size,752                        start,753                        first_history - start,754                    )755                )756                radius *= 0.5757                if radius < selected.minimum_radius:758                    return finish_normal("radius_limit", "radius_limit", iteration)759                break760            log_acceptance(base, changes, "ambiguous", change, standard_error)761            if batch_size == selected.maximum_batch:762                return finish_normal(763                    "sampling_unresolved", "acceptance_uncertainty_at_maximum_batch", iteration764                )765            batch_size = min(2 * batch_size, selected.maximum_batch)766    if final_size is not None:767        return finish_stationarity(selected.iterations, "iteration_budget")768    return result("iteration_budget")769770771# endregion book:stochastic-independent-acceptance772