Source code for zellij.core.geometry

# @Author: Thomas Firmin <ThomasFirmin>
# @Date:   2022-05-03T15:41:48+02:00
# @Email:  thomas.firmin@univ-lille.fr
# @Project: Zellij
# @Last modified by:   tfirmin
# @Last modified time: 2022-11-08T16:31:09+01:00
# @License: CeCILL-C (http://www.cecill.info/index.fr.html)


import numpy as np
import abc
import copy

import time

from zellij.strategies.tools.spoke_dart import (
    randomMuller,
    Hyperplane,
    HalfLine,
)
from zellij.strategies.tools.scoring import Min
from zellij.core.search_space import Searchspace
from zellij.core.variables import FloatVar, Constant
from zellij.strategies.tools.direct_utils import SigmaInf

import logging

logger = logging.getLogger("zellij.geometry")


[docs]class Fractal(Searchspace): """Fractal Fractal is an abstract class used in DBA. This class is used to build a new kind of search space. Fractals are constrained continuous subspaces. Attributes ---------- id : int Identifier of a fractal. father : Fractal Reference to the parent of the current fractal. children : list[Fractal] References to all children of the current fractal. score : {float, int} Heuristic value is computed by an Heuristic. level : int Current level of the fractal in the partition tree. See Tree_search. See Also -------- :ref:`lf` : Defines what a loss function is Tree_search : Defines how to explore and exploit a fractal partition tree. :ref:`sp` : Initial search space used to build fractal. Hypercube : Inherited Fractal type Hypersphere : Inherited Fractal type """ _instances_count = {} def __init__( self, values, loss, heuristic=Min(), **kwargs, ): """__init__(self,values,loss,father="root",level=0,id=0,children=[],score=None,**kwargs) Parameters ---------- values : Variables Defines the decision variables. See :ref:`var`. loss : LossFunc Defines the loss function. See :ref:`lf`. heuristic : Heuristic Function that defines how promising a space is according to sampled points. It is similar to the acquisition function in BO. """ super(Fractal, self).__init__(values, loss, **kwargs) self.children = [] self.heuristic = heuristic self.score = float("inf") self.level = 0 self.father = "root" def __new__(cls, *args, **kwargs): obj = super(Fractal, cls).__new__(cls) if cls not in cls._instances_count: cls._instances_count[cls] = 0 else: cls._instances_count[cls] += 1 obj.id = cls._instances_count[cls] obj._god = obj return obj
[docs] @abc.abstractmethod def create_children(self): """create_children(self) Abstract method. It defines the partition function. Determines how children of the current space should be created. """ pass
[docs] def compute_score(self, idx): self.score = self.heuristic(self, idx)
@property def father(self): return self._father @father.setter def father(self, f): assert f != self, f"Father of a Fractal cannot be `self`" self._father = f
[docs] def subspace(self, low_bounds, up_bounds, use_god=False, **kwargs): if use_god: new = self._god.subspace( low_bounds, up_bounds, **kwargs, use_god=False ) else: new = super(Fractal, self).subspace(low_bounds, up_bounds, **kwargs) new.father = self new.level = self.level + 1 new._god = self._god return new
[docs]class Hypercube(Fractal): """Hypercube The hypercube is a basic hypervolume used to decompose the :ref:`sp`. It is also one of the most computationally inefficient in high dimension. The partition complexity of an Hypercube is equal to :math:`O(2^d)`. See Also -------- LossFunc : Defines what a loss function is Tree_search : Defines how to explore and exploit a fractal partition tree. SearchSpace : Initial search space used to build fractal. Fractal : Parent class. Basic object to define what a fractal is. Hypersphere : Another hypervolume, with different properties """ def __init__( self, values, loss, heuristic=Min(), **kwargs, ): """__init__(self,values,loss,father="root",level=0,id=0,children=[],score=None,**kwargs) Parameters ---------- values : Variables Defines the decision variables. See :ref:`var`. loss : LossFunc Defines the loss function. See :ref:`lf`. heuristic : Heuristic, default=Min() Function that defines how promising a space is according to sampled points. It is similar to the acquisition function in BO. """ super(Hypercube, self).__init__( values, loss, heuristic=heuristic, **kwargs ) continuous = True for v in self.values: if not ( isinstance(v, FloatVar) or (isinstance(v, Constant) and isinstance(v.value, float)) ): continuous = False if continuous: self.lo_bounds = np.zeros(self.size) self.up_bounds = np.zeros(self.size) for i, v in enumerate(self.values): if isinstance(v, FloatVar): self.lo_bounds[i] = v.low_bound self.up_bounds[i] = v.up_bound else: self.lo_bounds[i] = v.value self.up_bounds[i] = v.value self.to_convert = False elif all(hasattr(v, "to_continuous") for v in self.values): logger.warning( f"""Be carefull, for {self.__class__.__name__} with mixed variables, the Searchspace wil be approximated by the unit hypercube. Upper and lower bounds will be between [0,1], the `to_continuous` conversion method must take this into account. For example, Minmax converter can be used.""" ) assert hasattr( self, "to_continuous" ), f"""When {self.__class__.__name__} as mixed variables, a `to_continuous` method must be implemented. Use the `to_continuous` kwargs when defining the :ref:`Searchspace` """ self.lo_bounds = np.array([0.0] * self.size) self.up_bounds = np.array([1.0] * self.size) self.to_convert = True else: raise ValueError( f""" For {self.__class__.__name__}, all variables must be `FloatVar`, or all variables must have a `to_continuous` method added at the initialization of the variable. Got {self.values}. ex:\n>>> FloatVar("test",-5,5,to_continuous=...). """ )
[docs] def create_children(self): """create_children(self) Partition function. """ up_m_lo = self.up_bounds - self.lo_bounds radius = np.abs(up_m_lo / 2) bounds = [[self.lo_bounds, self.up_bounds]] # Hyperplan bisecting next_b = [] for i in range(self.size): next_b = [] for b in bounds: # First part up = np.copy(b[1]) up[i] = b[0][i] + radius[i] next_b.append([np.copy(b[0]), np.copy(up)]) # Second part low = np.copy(b[0]) low[i] = b[1][i] - radius[i] next_b.append([np.copy(low), np.copy(b[1])]) bounds = copy.deepcopy(next_b) # Create Hypercube if self.to_convert: for b in bounds: h = self.subspace( self.to_continuous.reverse(b[0]), self.to_continuous.reverse(b[1]), ) self.children.append(h) else: for b in bounds: h = self.subspace(list(b[0]), list(b[1])) self.children.append(h)
def __repr__(self): return f""" {super(Hypercube, self).__repr__()}\n BOUNDS:\n {self.lo_bounds}\n | {self.up_bounds}\n """
[docs]class Hypersphere(Fractal): """Hypersphere The Hypersphere is a basic hypervolume used to decompose the :ref:`sp`. The partition complexity is equal to :math:`2d`, but it does fully covers the initial :ref:`sp`. Attributes ---------- dim : int Number of dimensions inflation : float Inflation rate of hyperspheres. If >0 and <1 it reduces the hypersphere. If >1, it inflates the hypersphere. center : list[float] List of floats containing the coordinates of the center of the hypersphere. radius : float Radius of the hypersphere. See Also -------- LossFunc : Defines what a loss function is Tree_search : Defines how to explore and exploit a fractal partition tree. SearchSpace : Initial search space used to build fractal. Fractal : Parent class. Basic object to define what a fractal is. Hypercube : Another hypervolume, with different properties """ def __init__( self, values, loss, heuristic=Min(), inflation=1.75, force_convert=False, compute_bounds=False, **kwargs, ): """__init__(self,values,loss,father="root",level=0,id=0,children=[],score=None,**kwargs) Parameters ---------- values : Variables Defines the decision variables. See :ref:`var`. loss : LossFunc Defines the loss function. See :ref:`lf`. heuristic : Heuristic, default=Min() Function that defines how promising a space is according to sampled points. It is similar to the acquisition function in BO. inflation : float, default=1.75 Inflation rate of hyperspheres. If >0 and <1 it reduces the hypersphere. If >1, it inflates the hypersphere. force_convert : bool, default=False Force the convertion of all :ref:`var`, even continuous ones. It allows for example, to consider the unit hypercube, instead of the defined space. compute_bounds : bool, default=False If True, computes, the bounds of the circumscribed hypercube. """ super(Hypersphere, self).__init__( values, loss, heuristic=heuristic, **kwargs ) self.inflation = inflation self.force_convert = force_convert self.compute_bounds = compute_bounds self.to_convert = False count_constant = 0 continuous = True for v in self.values: if isinstance(v, Constant): count_constant += 1 if not isinstance(v.value, float): continuous = False elif not isinstance(v, FloatVar): continuous = False if count_constant == len(self.values): self.is_constant = True else: self.is_constant = False if continuous and not self.force_convert: if self.level == 0 or self.compute_bounds: self.lo_bounds = np.zeros(self.size) self.up_bounds = np.zeros(self.size) for i, v in enumerate(self.values): if isinstance(v, FloatVar): self.lo_bounds[i] = v.low_bound self.up_bounds[i] = v.up_bound else: self.lo_bounds[i] = v.value self.up_bounds[i] = v.value self.to_convert = False elif all(hasattr(v, "to_continuous") for v in self.values): # logger.warning( # f""" # Be carefull, for {self.__class__.__name__} with # mixed variables, the Searchspace wil be approximated by the unit # hypercube. Upper and lower bounds will be between [0,1], # the `to_continuous` conversion method must take this into account. # For example, Minmax converter can be used. # """ # ) assert hasattr( self, "to_continuous" ), f""" When {self.__class__.__name__} as mixed variables, a `to_continuous` method must be implemented. Use the `to_continuous` kwargs when defining the :ref:`Searchspace` """ lo = np.zeros(self.size) up = np.zeros(self.size) for i, v in enumerate(self.values): if isinstance(v, FloatVar): lo[i] = v.low_bound up[i] = v.up_bound else: lo[i] = v.value up[i] = v.value self.lo_bounds = np.array( self.to_continuous.convert(lo, sub_values=True) ) self.up_bounds = np.array( self.to_continuous.convert(up, sub_values=True) ) self.to_convert = True else: raise ValueError( f""" For {self.__class__.__name__}, all variables must be `FloatVar`, or all variables must have a `to_continuous` method added at the initialization of the variable. Got {self.values}. ex:\n>>> FloatVar("test",-5,5,to_continuous=...). """ ) if self.level == 0: up_m_lo = self.up_bounds - self.lo_bounds self.center = self.lo_bounds + (up_m_lo) / 2 self.radius = up_m_lo[0] / 2 else: self.center = None self.radius = None
[docs] def create_children(self): """create_children(self) Partition function """ r_p = self.radius / (1 + np.sqrt(2)) for i in range(self.size): center = np.copy(self.center) center[i] += self.radius - r_p center[i] = max(center[i], self._god.lo_bounds[i]) center[i] = min(center[i], self._god.up_bounds[i]) lo = np.maximum(center - r_p, self._god.lo_bounds) up = np.minimum(center + r_p, self._god.up_bounds) if self.to_convert: h = self.subspace( self.to_continuous.reverse(lo, sub_values=True), self.to_continuous.reverse(up, sub_values=True), use_god=True, ) else: h = self.subspace(lo, up, use_god=True) h.center = center h.radius = r_p h.level = self.level + 1 self.children.append(h) center = np.copy(self.center) center[i] -= self.radius - r_p center[i] = max(center[i], self._god.lo_bounds[i]) center[i] = min(center[i], self._god.up_bounds[i]) lo = np.maximum(center - r_p, self._god.lo_bounds) up = np.minimum(center + r_p, self._god.up_bounds) if self.to_convert: h = self.subspace( self.to_continuous.reverse(lo, sub_values=True), self.to_continuous.reverse(up, sub_values=True), use_god=True, ) else: h = self.subspace(lo, up, use_god=True) h.center = center h.radius = r_p self.children.append(h)
[docs] def subspace(self, low_bounds, up_bounds, use_god=False): new = super(Hypersphere, self).subspace(low_bounds, up_bounds, use_god) new.inflation = self.inflation new.heuristic = self.heuristic return new
def __repr__(self): if type(self.father) == str: id = "root" else: id = str(self.father.id) return f""" {super(Hypersphere, self).__repr__()}\n Center|radius:\n {self.center}\n | {self.radius}\n """
[docs]class Section(Fractal): """Section Performs a n-Section of the search space. Attributes ---------- section : int Defines in how many equal sections the space should be decompose. See Also -------- LossFunc : Defines what a loss function is Tree_search : Defines how to explore and exploit a fractal partition tree. SearchSpace : Initial search space used to build fractal. Fractal : Parent class. Basic object to define what a fractal is. Hypercube : Another hypervolume, with different properties """ def __init__( self, values, loss, heuristic=Min(), section=2, **kwargs, ): """__init__(self,values,loss,father="root",level=0,id=0,children=[],score=None,**kwargs) Parameters ---------- values : Variables Defines the decision variables. See :ref:`var`. loss : LossFunc Defines the loss function. See :ref:`lf`. heuristic : Heuristic, default=Min() Function that defines how promising a space is according to sampled points. It is similar to the acquisition function in BO. section : int, default=2 Defines in how many equal sections the space should be decompose. """ super(Section, self).__init__( values, loss, heuristic=heuristic, **kwargs ) assert section > 1, logger.error( f"{section}-Section is not possible, section must be > 1" ) self.section = section continuous = True for v in self.values: if not ( isinstance(v, FloatVar) or (isinstance(v, Constant) and isinstance(v.value, float)) ): continuous = False if continuous: self.lo_bounds = np.zeros(self.size) self.up_bounds = np.zeros(self.size) for i, v in enumerate(self.values): if isinstance(v, FloatVar): self.lo_bounds[i] = v.low_bound self.up_bounds[i] = v.up_bound else: self.lo_bounds[i] = v.value self.up_bounds[i] = v.value self.to_convert = False elif all(hasattr(v, "to_continuous") for v in self.values): logger.warning( f"""Be carefull, for {self.__class__.__name__} with mixed variables, the Searchspace wil be approximated by the unit hypercube. Upper and lower bounds will be between [0,1], the `to_continuous` conversion method must take this into account. For example, Minmax converter can be used.""" ) assert hasattr( self, "to_continuous" ), f"""When {self.__class__.__name__} as mixed variables, a `to_continuous` method must be implemented. Use the `to_continuous` kwargs when defining the :ref:`Searchspace` """ self.lo_bounds = np.array([0.0] * self.size) self.up_bounds = np.array([1.0] * self.size) self.to_convert = True else: raise ValueError( f"""For {self.__class__.__name__}, all variables must be `FloatVar`, or all variables must have a `to_continuous` method added at the initialization of the variable. Got {self.values}. ex:\n>>> FloatVar("test",-5,5,to_continuous=...).""" ) up_m_lo = self.up_bounds - self.lo_bounds self.longest = np.argmax(up_m_lo) self.width = up_m_lo[self.longest] self.center = up_m_lo / 2 self.length = self.width
[docs] def create_children(self): """create_children() Partition function. """ new_val = self.width / self.section lo = np.copy(self.lo_bounds) up = np.copy(self.up_bounds) up[self.longest] = lo[self.longest] + new_val for i in range(self.section): if self.to_convert: h = self.subspace( self.to_continuous.reverse(lo, sub_values=True), self.to_continuous.reverse(up, sub_values=True), section=self.section, ) else: h = self.subspace(lo, up, section=self.section) self.children.append(h) lo = np.copy(h.lo_bounds) up = np.copy(h.up_bounds) lo[self.longest] += new_val up[self.longest] += new_val
def __repr__(self): if type(self.father) == str: id = "root" else: id = str(self.father.id) return f""" {super(Section, self).__repr__()}\n BOUNDS:\n {self.lo_bounds}\n | {self.up_bounds}\n """
[docs]class Direct(Fractal): """Direct DIRECT geometry. Attributes ---------- sigma : Direct_size Sigma function. Determines a measurement of the size of a subspace. longest : list[int] Index of the dimensions with the longest side of the space. width : float Value of the longest side of the space. center : list[float] Center of the space. See Also -------- LossFunc : Defines what a loss function is Tree_search : Defines how to explore and exploit a fractal partition tree. SearchSpace : Initial search space used to build fractal. Fractal : Parent class. Basic object to define what a fractal is. Hypercube : Another hypervolume, with different properties """ def __init__( self, values, loss, max_calls, heuristic=Min(), force_convert=False, sigma=SigmaInf(), **kwargs, ): """__init__(values, loss, max_calls, heuristic=Min(), force_convert=False, sigma=SigmaInf(), **kwargs,) Parameters ---------- values : Variables Defines the decision variables. See :ref:`var`. loss : LossFunc Defines the loss function. See :ref:`lf`. heuristic : Heuristic, default=Min() Function that defines how promising a space is according to sampled points. It is similar to the acquisition function in BO. force_convert : bool, default=False Force the convertion of all :ref:`var`, even continuous ones. It allows for example, to consider the unit hypercube, instead of the defined space. sigma : Direct_size, default, Sigma2() Sigma function. Determines a measurement of the size of a subspace. """ super(Direct, self).__init__( values, loss, heuristic=heuristic, **kwargs ) self.force_convert = force_convert count_constant = 0 continuous = True for v in self.values: if isinstance(v, Constant): count_constant += 1 if not isinstance(v.value, float): continuous = False elif not isinstance(v, FloatVar): continuous = False if count_constant == len(self.values): self.is_constant = True else: self.is_constant = False if continuous and not self.force_convert: self.lo_bounds = np.zeros(self.size) self.up_bounds = np.zeros(self.size) for i, v in enumerate(self.values): if isinstance(v, FloatVar): self.lo_bounds[i] = v.low_bound self.up_bounds[i] = v.up_bound else: self.lo_bounds[i] = v.value self.up_bounds[i] = v.value self.to_convert = False elif all(hasattr(v, "to_continuous") for v in self.values): # logger.warning( # f""" # Be carefull, for {self.__class__.__name__} with # mixed variables, the Searchspace wil be approximated by the unit # hypercube. Upper and lower bounds will be between [0,1], # the `to_continuous` conversion method must take this into account. # For example, Minmax converter can be used. # """ # ) assert hasattr( self, "to_continuous" ), f""" When {self.__class__.__name__} as mixed variables, a `to_continuous` method must be implemented. Use the `to_continuous` kwargs when defining the :ref:`Searchspace` """ lo = np.zeros(self.size) up = np.zeros(self.size) for i, v in enumerate(self.values): if isinstance(v, FloatVar): lo[i] = v.low_bound up[i] = v.up_bound else: lo[i] = v.value up[i] = v.value self.lo_bounds = np.array( self.to_continuous.convert(lo, sub_values=True) ) self.up_bounds = np.array( self.to_continuous.convert(up, sub_values=True) ) self.to_convert = True else: raise ValueError( f""" For {self.__class__.__name__}, all variables must be `FloatVar`, or all variables must have a `to_continuous` method added at the initialization of the variable. Got {self.values}. ex:\n>>> FloatVar("test",-5,5,to_continuous=...). """ ) up_m_lo = self.up_bounds - self.lo_bounds self.longest = np.argmax(up_m_lo) self.width = up_m_lo[self.longest] self.center = (self.lo_bounds + self.up_bounds) * 0.5 if self.level == 0: if self.to_convert: self.score = self.loss( self.to_continuous.reverse([self.center], sub_values=True) )[0] else: self.score = self.loss([self.center])[0] self.length = 1.0 else: self.length = None self.set_i = np.where(up_m_lo == up_m_lo[self.longest])[0] assert max_calls > 3, logger.error( f"{max_calls} must be greater than 3" ) self.max_calls = max_calls self.stage = 0 self.sigma = sigma
[docs] def create_children(self): """create_children() Partition function """ section_length = self.width / 3 dim = 0 points = np.empty((0, self.size), dtype=float) # While there is dimensions of equal length or remaining calls to loss while ( dim < len(self.set_i) and self.loss.calls + dim * 2 <= self.max_calls ): new_p = np.repeat([self.center], 2, axis=0) new_p[0][self.set_i[dim]] -= section_length new_p[1][self.set_i[dim]] += section_length points = np.append(points, new_p, axis=0) dim += 1 if len(points) > 0: if self.to_convert: scores = self.loss( self.to_continuous.reverse(points, sub_values=True) ) else: scores = self.loss(points) scores = np.reshape(scores, (-1, 2)) scores_dim = scores.min(axis=1) argsort = np.argsort(scores_dim) current_section = self for stage, arg in enumerate(argsort): lo = np.copy(current_section.lo_bounds) up = np.copy(current_section.up_bounds) up[self.set_i[arg]] = lo[self.set_i[arg]] + section_length up = np.minimum(up, self._god.up_bounds) children = [] # Build sections for i in range(3): if self.to_convert: h = current_section.subspace( current_section.to_continuous.reverse( lo, sub_values=True ), current_section.to_continuous.reverse( up, sub_values=True ), max_calls=self.max_calls, force_convert=self.force_convert, sigma=self.sigma, ) else: h = current_section.subspace( lo, up, max_calls=self.max_calls, force_convert=self.force_convert, sigma=self.sigma, ) if not h.is_constant: h.father = self h.length = self.sigma(h) h.level = int( ( np.log( self._god.up_bounds[h.longest] - self._god.lo_bounds[h.longest] ) - np.log(h.width) ) / np.log(3) ) h.stage = self.size - self.level children.append(h) lo[self.set_i[arg]] += section_length up[self.set_i[arg]] += section_length if len(children) == 3: children[0].score, children[2].score = scores[arg] self.children.append(children[0]) self.children.append(children[2]) children[1].score = self.score current_section = children[1] if current_section != self: self.children.append(current_section)
def __repr__(self): if type(self.father) == str: id = "root" else: id = str(self.father.id) return f""" {super(Direct, self).__repr__()}\n BOUNDS:\n {self.lo_bounds}\n | {self.up_bounds}\n """
[docs]class Soo(Fractal): """Soo Performs a n-Section of the search space. Attributes ---------- dim : int Number of dimensions See Also -------- LossFunc : Defines what a loss function is Tree_search : Defines how to explore and exploit a fractal partition tree. SearchSpace : Initial search space used to build fractal. Fractal : Parent class. Basic object to define what a fractal is. Hypercube : Another hypervolume, with different properties """ def __init__( self, values, loss, max_calls, heuristic=Min(), section=3, force_convert=False, **kwargs, ): """__init__(values, loss, max_calls, heuristic=Min(), force_convert=False, sigma=SigmaInf(), **kwargs,) Parameters ---------- values : Variables Defines the decision variables. See :ref:`var`. loss : LossFunc Defines the loss function. See :ref:`lf`. heuristic : Heuristic, default=Min() Function that defines how promising a space is according to sampled points. It is similar to the acquisition function in BO. section : int, default=3 Defines in how many equal sections the space should be decompose. force_convert : bool, default=False Force the convertion of all :ref:`var`, even continuous ones. It allows for example, to consider the unit hypercube, instead of the defined space. """ super(Soo, self).__init__(values, loss, heuristic=heuristic, **kwargs) assert ( section > 1 ), f""" Cannot divide a hypercube into {section} parts. Section must be >1 """ self.section = section self.force_convert = force_convert count_constant = 0 continuous = True for v in self.values: if isinstance(v, Constant): count_constant += 1 if not isinstance(v.value, float): continuous = False elif not isinstance(v, FloatVar): continuous = False if count_constant == len(self.values): self.is_constant = True else: self.is_constant = False if continuous and not self.force_convert: self.lo_bounds = np.zeros(self.size) self.up_bounds = np.zeros(self.size) for i, v in enumerate(self.values): if isinstance(v, FloatVar): self.lo_bounds[i] = v.low_bound self.up_bounds[i] = v.up_bound else: self.lo_bounds[i] = v.value self.up_bounds[i] = v.value self.to_convert = False elif all(hasattr(v, "to_continuous") for v in self.values): # logger.warning( # f""" # Be carefull, for {self.__class__.__name__} with # mixed variables, the Searchspace wil be approximated by the unit # hypercube. Upper and lower bounds will be between [0,1], # the `to_continuous` conversion method must take this into account. # For example, Minmax converter can be used. # """ # ) assert hasattr( self, "to_continuous" ), f""" When {self.__class__.__name__} as mixed variables, a `to_continuous` method must be implemented. Use the `to_continuous` kwargs when defining the :ref:`Searchspace` """ lo = np.zeros(self.size) up = np.zeros(self.size) for i, v in enumerate(self.values): if isinstance(v, FloatVar): lo[i] = v.low_bound up[i] = v.up_bound else: lo[i] = v.value up[i] = v.value self.lo_bounds = np.array( self.to_continuous.convert(lo, sub_values=True) ) self.up_bounds = np.array( self.to_continuous.convert(up, sub_values=True) ) self.to_convert = True else: raise ValueError( f""" For {self.__class__.__name__}, all variables must be `FloatVar`, or all variables must have a `to_continuous` method added at the initialization of the variable. Got {self.values}. ex:\n>>> FloatVar("test",-5,5,to_continuous=...). """ ) up_m_lo = self.up_bounds - self.lo_bounds self.longest = np.argmax(up_m_lo) self.width = up_m_lo[self.longest] self.center = (self.lo_bounds + self.up_bounds) * 0.5 self.length = self.width if self.level == 0: if self.to_convert: self.score = self.loss( self.to_continuous.reverse([self.center], sub_values=True) )[0] else: self.score = self.loss([self.center])[0] assert max_calls > 3, logger.error( f"{max_calls} must be greater than 3" ) self.max_calls = max_calls
[docs] def create_children(self): new_val = self.width / self.section lo = np.copy(self.lo_bounds) up = np.copy(self.up_bounds) up[self.longest] = lo[self.longest] + new_val up = np.minimum(up, self._god.up_bounds) children = [] i = 0 while i < self.section and self.loss.calls < self.max_calls: i += 1 if self.to_convert: h = self.subspace( self.to_continuous.reverse(lo, sub_values=True), self.to_continuous.reverse(up, sub_values=True), max_calls=self.max_calls, section=self.section, force_convert=self.force_convert, ) else: h = self.subspace( lo, up, max_calls=self.max_calls, section=self.section, force_convert=self.force_convert, ) if not h.is_constant: children.append(h) lo[self.longest] += new_val up[self.longest] += new_val if self.section % 2 == 0: centers = [child.center for child in children] if self.to_convert: scores = self.loss( self.to_continuous.reverse(centers, sub_values=True) ) else: scores = self.loss(centers) for child, s in zip(children, scores): child.score = s else: mid = self.section // 2 centers = [ child.center for i, child in enumerate(children) if i != mid ] if self.to_convert: scores = self.loss( self.to_continuous.reverse(centers, sub_values=True) ) else: scores = self.loss(centers) p = 0 for idx, child in enumerate(children): if idx == mid: p = 1 child.score = self.score else: child.score = scores[idx - p] self.children = children
def __repr__(self): if type(self.father) == str: id = "root" else: id = str(self.father.id) return f""" {super(Soo, self).__repr__()}\n BOUNDS:\n {self.lo_bounds}\n | {self.up_bounds}\n """
################# # VORONOI BASED # #################
[docs]class Voronoi(Fractal): """ DEPRECATED. Must be updated with the new :ref:`sp`. """ def __init__( self, lo_bounds, up_bounds, father="root", level=0, id=0, children=[], score=None, seed="random", n_seeds=None, ): """ DEPRECATED. Must be updated with the new :ref:`sp`. """ super().__init__( lo_bounds, up_bounds, father, level, id, children, score ) self.dim = len(self.up_bounds) if n_seeds is None: self.n_seeds = 2 * self.dim else: self.n_seeds = n_seeds self.next_seeds = [] if isinstance(seed, str) and seed == "random": self.all_seeds = [] self.next_seeds = list( np.random.random((self.n_seeds, self.dim)) * (np.array(self.up_bounds) - np.array(self.lo_bounds)) + np.array(self.lo_bounds) ) self.seed = "root" self.hyperplanes = [] else: self.all_seeds = self.father.all_seeds self.hyperplanes = [] self.seed = seed self.center = self.seed self.xlist = np.zeros(2 * self.dim)
[docs] @abc.abstractmethod def create_children(self): pass
[docs] @abc.abstractmethod def update(self): pass
[docs] def randomSpoke(self, s): p = randomMuller(1, self.dim)[0] pfar = p + s return HalfLine(s, pfar)
[docs] def dimSpoke(self, s, dim, r=1): p = np.zeros(self.dim) p[dim] = r pfar = p + s return HalfLine(s, pfar)
[docs] def shiftBorder(self, s, p): l = HalfLine(s, p) return l.point(np.random.random())
[docs] def clipBorder(self, line, upma, loma): self.xlist[: self.dim] = loma / (line.v * (1 + 1e-10)) self.xlist[self.dim :] = upma / (line.v * (1 + 1e-10)) x = np.nanmin(np.where(self.xlist > 0, self.xlist, np.inf)) res = line.point(x) return res
[docs]class DynamicVoronoi(Voronoi): """ DEPRECATED. Must be updated with the new :ref:`sp`. """ def __init__( self, lo_bounds, up_bounds, father="root", level=0, id=0, children=[], score=None, seed="random", spokes=2, n_seeds=5, ): """ DEPRECATED. Must be updated with the new :ref:`sp`. """ super().__init__( lo_bounds, up_bounds, father, level, id, children, score, seed=seed, n_seeds=n_seeds, ) self.n_dim = 2 * self.dim self.spokes = spokes self.sampled_bounds = []
[docs] def create_children(self): update_neighbors = False if not isinstance(self.father, str): # Current cell will be it's own children selfchild = DynamicVoronoi( self.lo_bounds, self.up_bounds, self, self.level + 1, self.id, seed=self.seed, spokes=self.spokes, n_seeds=self.n_seeds, ) self.children.append(selfchild) # Replace previous cell by new cell self.all_seeds[self.id] = selfchild for i in self.next_seeds: child = DynamicVoronoi( self.lo_bounds, self.up_bounds, self, self.level + 1, len(self.all_seeds), seed=i, spokes=self.spokes, n_seeds=self.n_seeds, ) self.children.append(child) self.all_seeds.append(child) for child in self.children: for cell in self.all_seeds: cell.sampled_bounds = [] cell.next_seeds = [] if child.id != cell.id: try: h = Hyperplane(self.children[i], self.children[j]) self.children[i].hyperplanes.append(h) self.children[j].hyperplanes.append(h) except AssertionError as e: logger.warning(f"Hyperplane building aborted: {e}") for i, c in enumerate(self.all_seeds): logger.info(f"Building children n°{i}/{len(self.children)}") if len(c.hyperplanes) > 0: c.update() else: self.children.pop(i)
[docs] def update(self): upma = self.up_bounds - self.seed loma = self.lo_bounds - self.seed cell_idx = [False] * len(self.hyperplanes) # Fixed points (2 for each dimension) for d in range(self.n_dim): l = self.dimSpoke(self.seed, d % self.dim) inter = np.empty((0, self.dim)) for j, h in enumerate(self.hyperplanes): on, pfar = h.intersection(l) if on: # Clip line to bounds if np.any(pfar > self.up_bounds) or np.any( pfar < self.lo_bounds ): pfar_clipped = self.clipBorder(l, upma, loma) inter = np.append(inter, [pfar_clipped], axis=0) cell_idx[j] = False else: inter = np.append(inter, [pfar], axis=0) cell_idx[j] = True else: cell_idx[j] = False if len(inter) == 0: pfar_clipped = self.clipBorder(l, upma, loma) self.sampled_bounds.append(np.copy(pfar_clipped)) else: dist = np.linalg.norm(inter - l.A, axis=1) minidx = np.argmin(dist) a = np.copy(inter[minidx]) if cell_idx[minidx]: self.sampled_hyperplanes.add(minidx) if self.hyperplanes[minidx].cellX != self: self.hyperplanes[minidx].cellX.sampled_bounds.append(a) else: self.hyperplanes[minidx].cellY.sampled_bounds.append(a) self.sampled_bounds.append(a) # Random points for d in range(self.spokes): l = self.randomSpoke(self.seed) inter = np.empty((0, self.dim)) for j, h in enumerate(self.hyperplanes): on, pfar = h.intersection(l) if on: # Clip line to bounds if np.any(pfar > self.up_bounds) or np.any( pfar < self.lo_bounds ): pfar_clipped = self.clipBorder(l, upma, loma) inter = np.append(inter, [pfar_clipped], axis=0) cell_idx[j] = False else: inter = np.append(inter, [pfar], axis=0) cell_idx[j] = True else: cell_idx[j] = False if len(inter) == 0: pfar_clipped = self.clipBorder(l, upma, loma) self.sampled_bounds.append(np.copy(pfar_clipped)) else: dist = np.linalg.norm(inter - l.A, axis=1) minidx = np.argmin(dist) a = np.copy(inter[minidx]) if cell_idx[minidx]: self.sampled_hyperplanes.add(minidx) if self.hyperplanes[minidx].cellX != self: self.hyperplanes[minidx].cellX.sampled_bounds.append(a) else: self.hyperplanes[minidx].cellY.sampled_bounds.append(a) self.sampled_bounds.append(a) dist = np.linalg.norm(np.array(self.sampled_bounds) - self.seed, axis=1) dist = np.nan_to_num(dist) sum = np.sum(dist) if sum != 0: p = dist / sum choosen = np.random.choice( list(range(len(self.sampled_bounds))), np.minimum(np.count_nonzero(p), self.n_seeds), replace=False, p=p, ) for c in choosen: self.next_seeds.append( self.shiftBorder(self.seed, self.sampled_bounds[c]) )
[docs]class FixedVoronoi(Voronoi): """ DEPRECATED. Must be updated with the new :ref:`sp`. """ def __init__( self, lo_bounds, up_bounds, father="root", level=0, id=0, children=[], score=None, seed="random", spokes=2, n_seeds=5, ): """ DEPRECATED. Must be updated with the new :ref:`sp`. """ super().__init__( lo_bounds, up_bounds, father, level, id, children, score, seed=seed, n_seeds=n_seeds, ) self.dim = len(self.up_bounds) self.n_dim = 2 * self.dim self.spokes = spokes self.sampled_bounds = []
[docs] def create_children(self): logger.info(f"Creating children of n°{self.id}") # if not isinstance(self.father, str): # # Current cell will be it's own children # selfchild = FixedVoronoi(self.lo_bounds, self.up_bounds, self, self.level + 1, self.id, seed=self.seed, spokes=self.spokes, n_seeds=self.n_seeds) # selfchild.hyperplanes = self.hyperplanes[:] # self.children.append(selfchild) # # # Replace previous cell by new cell # self.all_seeds.append(selfchild) for i in self.next_seeds: child = FixedVoronoi( self.lo_bounds, self.up_bounds, self, self.level + 1, len(self.all_seeds), seed=i, spokes=self.spokes, n_seeds=self.n_seeds, ) child.hyperplanes = self.hyperplanes[:] self.children.append(child) self.all_seeds.append(child) for i in range(len(self.children) - 1): for j in range(i + 1, len(self.children)): try: h = Hyperplane(self.children[i], self.children[j]) self.children[i].hyperplanes.append(h) self.children[j].hyperplanes.append(h) except AssertionError as e: logger.warning(f"Hyperplane building aborted: {e}") for i, c in enumerate(self.children): logger.info(f"Building children n°{i}/{len(self.children)}") if len(c.hyperplanes) > 0: c.update() else: self.children.pop(i)
[docs] def update(self): upma = self.up_bounds - self.seed loma = self.lo_bounds - self.seed cell_idx = [False] * len(self.hyperplanes) # Fixed points (2 for each dimension) for d in range(self.n_dim): l = self.dimSpoke(self.seed, d % self.dim) inter = np.empty((0, self.dim)) for j, h in enumerate(self.hyperplanes): on, pfar = h.intersection(l) if on: # Clip line to bounds if np.any(pfar > self.up_bounds) or np.any( pfar < self.lo_bounds ): pfar_clipped = self.clipBorder(l, upma, loma) inter = np.append(inter, [pfar_clipped], axis=0) cell_idx[j] = False else: inter = np.append(inter, [pfar], axis=0) cell_idx[j] = True else: cell_idx[j] = False if len(inter) == 0: pfar_clipped = self.clipBorder(l, upma, loma) self.sampled_bounds.append(np.copy(pfar_clipped)) else: dist = np.linalg.norm(inter - l.A, axis=1) minidx = np.argmin(dist) a = np.copy(inter[minidx]) if cell_idx[minidx]: if self.hyperplanes[minidx].cellX != self: self.hyperplanes[minidx].cellX.sampled_bounds.append(a) else: self.hyperplanes[minidx].cellY.sampled_bounds.append(a) self.sampled_bounds.append(a) # Random points for d in range(self.spokes): l = self.randomSpoke(self.seed) inter = np.empty((0, self.dim)) for j, h in enumerate(self.hyperplanes): on, pfar = h.intersection(l) if on: # Clip line to bounds if np.any(pfar > self.up_bounds) or np.any( pfar < self.lo_bounds ): pfar_clipped = self.clipBorder(l, upma, loma) inter = np.append(inter, [pfar_clipped], axis=0) cell_idx[j] = False else: inter = np.append(inter, [pfar], axis=0) cell_idx[j] = True else: cell_idx[j] = False if len(inter) == 0: pfar_clipped = self.clipBorder(l, upma, loma) self.sampled_bounds.append(np.copy(pfar_clipped)) else: dist = np.linalg.norm(inter - l.A, axis=1) minidx = np.argmin(dist) a = np.copy(inter[minidx]) if cell_idx[minidx]: if self.hyperplanes[minidx].cellX != self: self.hyperplanes[minidx].cellX.sampled_bounds.append(a) else: self.hyperplanes[minidx].cellY.sampled_bounds.append(a) self.sampled_bounds.append(a) dist = np.linalg.norm(np.array(self.sampled_bounds) - self.seed, axis=1) dist = np.nan_to_num(dist) sum = np.sum(dist) if sum != 0: p = dist / sum choosen = np.random.choice( list(range(len(self.sampled_bounds))), np.minimum(np.count_nonzero(p), self.n_seeds), replace=False, p=p, ) for c in choosen: self.next_seeds.append( self.shiftBorder(self.seed, self.sampled_bounds[c]) )
[docs]class LightFixedVoronoi(Voronoi): """ DEPRECATED. Must be updated with the new :ref:`sp`. """ def __init__( self, lo_bounds, up_bounds, father="root", level=0, id=0, children=[], score=None, seed="random", spokes=2, n_seeds=5, ): """ DEPRECATED. Must be updated with the new :ref:`sp`. """ super().__init__( lo_bounds, up_bounds, father, level, id, children, score, seed=seed, n_seeds=n_seeds, ) self.dim = len(self.up_bounds) self.n_dim = 2 * self.dim self.spokes = spokes self.sampled_bounds = []
[docs] def create_children(self): logger.info(f"Creating children of n°{self.id}") # if not isinstance(self.father, str): # # Current cell will be it's own children # selfchild = LightFixedVoronoi(self.lo_bounds, self.up_bounds, self, self.level + 1, self.id, seed=self.seed, spokes=self.spokes, n_seeds=self.n_seeds) # selfchild.hyperplanes = self.hyperplanes[:] # self.children.append(selfchild) for i in self.next_seeds: child = LightFixedVoronoi( self.lo_bounds, self.up_bounds, self, self.level + 1, len(self.children), seed=i, spokes=self.spokes, n_seeds=self.n_seeds, ) child.hyperplanes = self.hyperplanes[:] self.children.append(child) for i in range(len(self.children) - 1): for j in range(i + 1, len(self.children)): try: h = Hyperplane(self.children[i], self.children[j]) self.children[i].hyperplanes.append(h) self.children[j].hyperplanes.append(h) except AssertionError as e: logger.warning(f"Hyperplane building aborted: {e}") for i, c in enumerate(self.children): logger.info(f"Building children n°{i}/{len(self.children)}") if len(c.hyperplanes) > 0: c.update() else: self.children.pop(i)
[docs] def update(self): sampled_hyperplanes = set() upma = self.up_bounds - self.seed loma = self.lo_bounds - self.seed cell_idx = [False] * len(self.hyperplanes) # Fixed points (2 for each dimension) for d in range(self.n_dim): l = self.dimSpoke(self.seed, d % self.dim) inter = np.empty((0, self.dim)) for j, h in enumerate(self.hyperplanes): on, pfar = h.intersection(l) if on: # Clip line to bounds if np.any(pfar > self.up_bounds) or np.any( pfar < self.lo_bounds ): pfar_clipped = self.clipBorder(l, upma, loma) inter = np.append(inter, [pfar_clipped], axis=0) cell_idx[j] = False else: inter = np.append(inter, [pfar], axis=0) cell_idx[j] = True else: cell_idx[j] = False if len(inter) == 0: pfar_clipped = self.clipBorder(l, upma, loma) self.sampled_bounds.append(np.copy(pfar_clipped)) else: dist = np.linalg.norm(inter - l.A, axis=1) minidx = np.argmin(dist) a = np.copy(inter[minidx]) if cell_idx[minidx]: sampled_hyperplanes.add(minidx) self.sampled_bounds.append(a) # Random points for d in range(self.spokes): l = self.randomSpoke(self.seed) inter = np.empty((0, self.dim)) for j, h in enumerate(self.hyperplanes): on, pfar = h.intersection(l) if on: # Clip line to bounds if np.any(pfar > self.up_bounds) or np.any( pfar < self.lo_bounds ): pfar_clipped = self.clipBorder(l, upma, loma) inter = np.append(inter, [pfar_clipped], axis=0) cell_idx[j] = False else: inter = np.append(inter, [pfar], axis=0) cell_idx[j] = True else: cell_idx[j] = False if len(inter) == 0: pfar_clipped = self.clipBorder(l, upma, loma) self.sampled_bounds.append(np.copy(pfar_clipped)) else: dist = np.linalg.norm(inter - l.A, axis=1) minidx = np.argmin(dist) a = np.copy(inter[minidx]) if cell_idx[minidx]: sampled_hyperplanes.add(minidx) self.sampled_bounds.append(a) self.hyperplanes = [ self.hyperplanes[idx] for idx in sampled_hyperplanes ] dist = np.linalg.norm(np.array(self.sampled_bounds) - self.seed, axis=1) dist = np.nan_to_num(dist) sum = np.sum(dist) if sum != 0: p = dist / sum choosen = np.random.choice( list(range(len(self.sampled_bounds))), np.minimum(np.count_nonzero(p), self.n_seeds), replace=False, p=p, ) for c in choosen: self.next_seeds.append( self.shiftBorder(self.seed, self.sampled_bounds[c]) )
[docs]class BoxedVoronoi(Voronoi): """ DEPRECATED. Must be updated with the new :ref:`sp`. """ def __init__( self, lo_bounds, up_bounds, father="root", level=0, id=0, children=[], score=None, seed="random", spokes=2, n_seeds=5, ): """ DEPRECATED. Must be updated with the new :ref:`sp`. """ super().__init__( lo_bounds, up_bounds, father, level, id, children, score, seed=seed, n_seeds=n_seeds, ) self.dim = len(self.up_bounds) self.n_dim = 2 * self.dim self.spokes = spokes
[docs] def create_children(self): logger.info(f"Creating children of n°{self.id}") self.next_seeds = np.random.uniform( self.lo_bounds, self.up_bounds, (self.n_seeds, self.dim) ) for i in self.next_seeds: child = BoxedVoronoi( self.lo_bounds, self.up_bounds, self, self.level + 1, len(self.children), seed=i, spokes=self.spokes, n_seeds=self.n_seeds, ) self.children.append(child) for i in range(len(self.children) - 1): for j in range(i + 1, len(self.children)): try: h = Hyperplane(self.children[i], self.children[j]) self.children[i].hyperplanes.append(h) self.children[j].hyperplanes.append(h) except AssertionError as e: logger.warning(f"Hyperplane building aborted: {e}") for i, c in enumerate(self.children): logger.info(f"Building children n°{i}/{len(self.children)}") if len(c.hyperplanes) > 0: c.update() else: self.children.pop(i)
[docs] def update(self): sampled_bounds = [] upma = self.up_bounds - self.seed loma = self.lo_bounds - self.seed cell_idx = [False] * len(self.hyperplanes) # Fixed points (2 for each dimension) for d in range(self.n_dim): l = self.dimSpoke(self.seed, d % self.dim) inter = np.empty((0, self.dim)) for j, h in enumerate(self.hyperplanes): on, pfar = h.intersection(l) if on: # Clip line to bounds if np.any(pfar > self.up_bounds) or np.any( pfar < self.lo_bounds ): pfar_clipped = self.clipBorder(l, upma, loma) inter = np.append(inter, [pfar_clipped], axis=0) cell_idx[j] = False else: inter = np.append(inter, [pfar], axis=0) cell_idx[j] = True else: cell_idx[j] = False if len(inter) == 0: pfar_clipped = self.clipBorder(l, upma, loma) sampled_bounds.append(np.copy(pfar_clipped)) else: dist = np.linalg.norm(inter - l.A, axis=1) minidx = np.argmin(dist) a = np.copy(inter[minidx]) sampled_bounds.append(a) # Random points for d in range(self.spokes): l = self.randomSpoke(self.seed) inter = np.empty((0, self.dim)) for j, h in enumerate(self.hyperplanes): on, pfar = h.intersection(l) if on: # Clip line to bounds if np.any(pfar > self.up_bounds) or np.any( pfar < self.lo_bounds ): pfar_clipped = self.clipBorder(l, upma, loma) inter = np.append(inter, [pfar_clipped], axis=0) cell_idx[j] = False else: inter = np.append(inter, [pfar], axis=0) cell_idx[j] = True else: cell_idx[j] = False if len(inter) == 0: pfar_clipped = self.clipBorder(l, upma, loma) sampled_bounds.append(np.copy(pfar_clipped)) else: dist = np.linalg.norm(inter - l.A, axis=1) minidx = np.argmin(dist) a = np.copy(inter[minidx]) sampled_bounds.append(a) self.lo_bounds = np.nanmin(sampled_bounds, axis=0) self.up_bounds = np.nanmax(sampled_bounds, axis=0) del self.hyperplanes