Source code for optilab.metamodels.top_half_metamodel

"""
Top half metamodel. Estimates all points using the surrogate function, then
evaluates the top mu (typically half) points with the objective function.
"""

# pylint: disable=duplicate-code

from ..data_classes import PointList
from ..functions import ObjectiveFunction
from ..functions.surrogate import SurrogateObjectiveFunction


[docs] class TopHalfMetamodel: """ Top-half metamodel. Estimates all points using the surrogate function, then calculates the real value for the top mu (typically half, hence the name) using the actual objective function. The remaining points get a value that's guaranteed to be worse than the rest of the points. """
[docs] def __init__( self, population_size: int, mu: int, objective_function: ObjectiveFunction, surrogate_function: SurrogateObjectiveFunction, *, buffer_size: int | None = None, ) -> None: """ Class constructor. Args: population_size: The population size (lambda). mu: Number of elite solutions (typically population_size // 2). objective_function: The real objective function. surrogate_function: Surrogate used to pre-screen candidates. buffer_size: If set, only the last *buffer_size* real evaluations are used as the surrogate training set. """ self.population_size = population_size self.mu = mu self.train_set = PointList(points=[]) self.objective_function = objective_function self.surrogate_function = surrogate_function self.buffer_size = buffer_size self._adapted_results: PointList | None = None
[docs] def __call__(self, points: PointList) -> PointList: """ Return evaluated / estimated values for *points*. If ``adapt()`` was called immediately before, returns the combined results (real values for top half, penalised for bottom half) and clears the internal cache. Otherwise falls back to pure surrogate estimation. Args: points: Candidate solutions. Returns: PointList with y-values set for every point. """ if self._adapted_results is not None: result = self._adapted_results self._adapted_results = None return result return PointList(points=[self.surrogate_function(x) for x in points])
[docs] def adapt(self, xs: PointList) -> None: """ Screen *xs* with the surrogate, evaluate the best half with the real objective, and cache a combined result for the next ``__call__()``. Args: xs: Candidate solutions (must have length == population_size). Raises: ValueError: If the number of points does not match population_size. """ if len(xs) != self.population_size: raise ValueError(f"Expected {self.population_size} points, got {len(xs)}.") if len(self.train_set) < self.population_size: self._adapted_results = self.evaluate(xs) return estimated = PointList(points=[self.surrogate_function(x) for x in xs]) estimated.rank() evaluated = self.evaluate(estimated[: self.mu]) evaluated.rank() # penalize worst points penalty_y = evaluated[-1].y + 1 not_evaluated = estimated[self.mu :] for point in not_evaluated: point.y = penalty_y evaluated.extend(not_evaluated) self._adapted_results = evaluated
[docs] def train_surrogate(self) -> None: """ Retrain the surrogate on the real evaluations. """ if self.buffer_size: self.surrogate_function.train(self.train_set[-self.buffer_size :]) else: self.surrogate_function.train(self.train_set)
[docs] def evaluate(self, xs: PointList) -> PointList: """ Evaluate *xs* with the real objective, extend the training set, and retrain the surrogate. Args: xs: Points to evaluate. Returns: PointList of evaluated points. """ result = PointList( points=[self.objective_function(point) for point in xs.points] ) self.train_set.extend(result) self.train_surrogate() return result
[docs] def get_log(self) -> PointList: """ Return all points evaluated with the real objective so far. """ return self.train_set