import warnings
import numpy as np
from .basestopper import BaseStopper
from ..core.optimizerlogger import BaseLogger
from ...plams.core.settings import Settings
__all__ = ("CurrentFunctionValueUnmoving",)
[docs]class CurrentFunctionValueUnmoving(BaseStopper):
"""Considers function values the optimizers are currently exploring.
Used to terminate an optimizer when its function evaluations are unchanging, usually indicating that it is
approaching some convergence. Best used with a stopper which monitors step size to ensure a widely exploring
optimizer is not stopped.
:Parameters:
calls
Number of function evaluations between comparison points.
tol
Tolerance fraction between 0 and 1.
:Returns:
bool
Returns ``True`` if the standard deviation of the last ``calls`` function evaluations is below
``tol * abs(latest_f_eval)``.
"""
def __init__(self, calls: int, tol: float = 0):
super().__init__()
self.calls = calls
self.tol = tol
def __call__(self, log: BaseLogger, best_opt_id: int, tested_opt_id: int) -> bool:
vals = log.get_history(tested_opt_id, "fx")
n_calls = log.len(tested_opt_id)
try:
assert n_calls > self.calls # If there are insufficient iterations the stopper will return False
with warnings.catch_warnings():
warnings.filterwarnings(
"error", "invalid value encountered in subtract", RuntimeWarning, module="numpy.core._methods"
) # Caught if there are infs in the results
st_dev = np.std(vals[-self.calls :])
self.last_result = st_dev <= np.abs(vals[-1] * self.tol)
except (AssertionError, RuntimeWarning):
self.last_result = False
return self.last_result
def __amssettings__(self, s: Settings) -> Settings:
s.input.ams.Stopper.Type = "CurrentFunctionValueUnmoving"
s.input.ams.Stopper.CurrentFunctionValueUnmoving.NumberOfFunctionCalls = self.calls
s.input.ams.Stopper.CurrentFunctionValueUnmoving.Tolerance = self.tol
return s