from typing import Sequence, Tuple
import numpy as np
from .basestopper import BaseStopper
from ..common.helpers import distance, is_bounds_valid
from ..core.optimizerlogger import BaseLogger
from ...plams.core.settings import Settings
__all__ = ("MaxInteroptimizerDistance",)
[docs]class MaxInteroptimizerDistance(BaseStopper):
"""Terminates optimizers which are too close in parameter space.
:Parameters:
bounds
Bounds of each parameter.
relative_distance
Fraction (between 0 and 1) of the maximum distance in the space (from the point at all lower bounds to the point
at all upper bounds) below which the optimizers are deemed too close and the tested optimizer will be stopped.
test_all
If ``True`` the distance between the tested optimizer and all other optimizers is tested, else only the best and
tested optimizers are compared.
:Returns:
bool
Returns ``True`` if optimizers are calculated to be too close together.
:Notes:
Calculates the maximum Euclidean distance in parameter space between the point of lower bounds and the point of
upper bounds (``max_distance``). Calculates the Euclidean distance between the final iterations of the best
and tested optimizers (``inter_optimizer_distance``). Returns ``True`` if::
inter_optimizer_distance <= max_distance * relative_distance
.. caution::
Use this stopper with care and only in problems where the parameters have been standardised so that every
parameter is dimensionless and on the same order of magnitude.
"""
def __init__(self, bounds: Sequence[Tuple[float, float]], relative_distance: float, test_all: bool = False):
super().__init__()
if isinstance(relative_distance, (float, int)) and relative_distance > 0:
self.relative_distance = relative_distance
else:
raise ValueError("relative_distance should be a positive float.")
if is_bounds_valid(bounds):
lower_pt, upper_pt = tuple(np.transpose(bounds))
self.trans_space_dist = distance(lower_pt, upper_pt)
self.test_all = test_all
def __call__(self, log: BaseLogger, best_opt_id: int, tested_opt_id: int) -> bool:
if self.test_all:
compare_to = range(1, log.n_optimizers + 1)
else:
compare_to = [best_opt_id]
for opt_id in compare_to:
if opt_id != tested_opt_id:
try:
h1 = np.array(log.get_history(opt_id, "x")[-1])
except IndexError:
self.logger.debug("Unable to compare to Opt%d, no points in log", opt_id)
continue
v1 = np.array(log.get_history(tested_opt_id, "x")[-1])
opt_dist = distance(h1, v1)
ratio = opt_dist / self.trans_space_dist
self.last_result = ratio <= self.relative_distance
if self.last_result:
self.logger.debug(
"MaxInteroptimizerDistance: Best=%d, Tested=%d, " "Result=%.2f / %.2f <= %.2f} = " "%s.",
opt_id,
tested_opt_id,
opt_dist,
self.trans_space_dist,
self.relative_distance,
self.last_result,
)
return self.last_result
self.last_result = False
return self.last_result
def __amssettings__(self, s: Settings) -> Settings:
s.input.ams.Stopper.Type = "MaxInteroptimizerDistance"
s.input.ams.Stopper.MaxInteroptimizerDistance.MaxRelativeDistance = self.relative_distance
s.input.ams.Stopper.MaxInteroptimizerDistance.CompareAllOptimizers = self.test_all
return s