Source code for optilab.data_classes.bounds

"""
Class representing bounds of the search space.
"""

from copy import deepcopy
from dataclasses import dataclass
from typing import List

import numpy as np

from .point import Point
from .point_list import PointList


[docs] @dataclass class Bounds: """ Class representing bounds of the search space. """ lower: float "The lower bound of search space." upper: float "The upper bound of search space."
[docs] def to_list(self) -> List[float]: """ Return the bounds as a list of two floats. Returns: List containing the lower and upper bound. """ return [self.lower, self.upper]
[docs] def __len__(self) -> float: """ Returns the width of the search space - the distance between the lower and upper bound. Returns: The width of the search space. """ return self.upper - self.lower
[docs] def __str__(self) -> str: """ Express the bounds as a string. Returns: Simple, printable string representation of this object. """ return f"{self.lower} {self.upper}"
[docs] def is_valid(self) -> bool: """ Check if the bounds are valid, i.e. if lower bound is below upper bound. Returns: True if bounds are valid, false otherwise. """ return self.lower < self.upper
[docs] def __contains__(self, point: Point) -> bool: """ Check if a point lies in the bounds. This method overrides the "in" operator. Returns: True if point lies in the bounds. """ return np.all((point.x >= self.lower) & (point.x <= self.upper))
[docs] def random_point(self, dim: int) -> Point: """ Sample the bounds for a random point of given dimensionality. Args: dim: The dimensionality of the point. Returns: Randomly sampled point from the search space. """ return Point(np.random.uniform(low=self.lower, high=self.upper, size=dim))
[docs] def random_point_list(self, num_points: int, dim: int) -> PointList: """ Sample the bounds for a list of random points of given dimensionality. Args: num_points: The number of points to sample. dim: The dimensionality of the points. Returns: List of randomly sampled points from the search space. """ return PointList([self.random_point(dim) for _ in range(num_points)])
# search space bounds handling methods
[docs] def reflect(self, point: Point) -> Point: """ Handle bounds by reflecting the point back into the search area. Args: point: The point to handle. Returns: Reflected point. """ new_x = [] for val in point.x: if val < self.lower or val > self.upper: val -= self.lower remainder = val % (self.upper - self.lower) relative_distance = val // (self.upper - self.lower) if relative_distance % 2 == 0: new_x.append(self.lower + remainder) else: new_x.append(self.upper - remainder) else: new_x.append(val) new_point = deepcopy(point) new_point.x = np.array(new_x, dtype=np.float64) return new_point
[docs] def wrap(self, point: Point) -> Point: """ Handle bounds by wrapping the point around the search area. Args: point: The point to handle. Returns: Wrapped point. """ new_x = [] for val in point.x: if val < self.lower or val > self.upper: val -= self.lower val %= self.upper - self.lower val += self.lower new_x.append(val) else: new_x.append(val) new_point = deepcopy(point) new_point.x = np.array(new_x, dtype=np.float64) return new_point
[docs] def project(self, point: Point) -> Point: """ Handle bounds by projecting the point onto the bounds of the search area. Args: point: The point to handle. Returns: Projected point. """ new_point = deepcopy(point) new_point.x = np.clip(point.x, self.lower, self.upper) return new_point
[docs] def handle_bounds(self, point: Point, mode: str) -> Point: """ Function to choose the bound handling method by name of the method. Args: point: The point to handle. mode: Bound handling mode to use, choose from reflect, wrap or project. Returns: Handled point. """ methods = {"reflect": self.reflect, "wrap": self.wrap, "project": self.project} try: return methods[mode](point) except KeyError as err: raise ValueError(f"Invalid mode {mode} in Bounds.handle_bounds!") from err