from dragon.search_space.addons import VarAddon
from abc import ABC, abstractmethod
from collections.abc import Iterable
import math
import numpy as np
import random
import copy
[docs]
@abstractmethod
class Variable(ABC):
"""Variable
Variable is an Abstract class defining what a variable is in a given search space.
Parameters
----------
label : str
Name of the variable.
kwargs : dict
Kwargs will be the different addons you want to add to a :ref:`var`.
Known addons are:
* neighbor : VarNeighborhood
Attributes
----------
label
"""
def __init__(self, label, **kwargs):
assert isinstance(
label, str
), f"""
Label must be a string, got {label}
"""
self.label = label
self.kwargs = kwargs
self._add_addons(**kwargs)
[docs]
@abstractmethod
def random(self, size=None):
pass
[docs]
@abstractmethod
def isconstant(self):
pass
def _add_addons(self, **kwargs):
for k in kwargs:
assert isinstance(
kwargs[k], VarAddon
), f"""
Kwargs must be of type `VarAddon`, got {k}:{kwargs[k]}
"""
if kwargs[k]:
setattr(self, k, copy.copy(kwargs[k]))
addon = getattr(self, k)
addon.target = self
else:
setattr(self, k, kwargs[k])
addon = getattr(self, k)
addon.target = self
def __repr__(self):
return f"{self.__class__.__name__}({self.label}, "
# Discrete
[docs]
class IntVar(Variable):
"""IntVar
`IntVar` defines :ref:`var` discribing Integer variables. The user must specify a :code:`lower` and an :code:`upper` bounds for the Integer variable.
Parameters
----------
label : str
Name of the variable.
lower : int
Lower bound of the variable
upper : int
Upper bound of the variable
sampler : Callable, default=np.random.randint
Function that takes lower bound, upper bound and a size as parameters and defines how the random values for the variable should be sampled.
Attributes
----------
up_bound : int
Lower bound of the variable
: int
Upper bound of the variable
Examples
--------
>>> from dragon.search_space.zellij_variables import IntVar
>>> a = IntVar("test", 0, 5)
>>> print(a)
IntVar(test, [0;5])
>>> a.random()
1
"""
def __init__(
self, label, lower, upper, sampler=np.random.randint, **kwargs
):
super(IntVar, self).__init__(label, **kwargs)
assert isinstance(
upper, (int, np.integer)
), f"""
Upper bound must be an int, got {upper}
"""
assert isinstance(
lower, (int, np.integer)
), f"""
Lower bound must be an int, got {lower}
"""
assert (
lower < upper
), f"""Lower bound must be
strictly inferior to upper bound, got {lower}<{upper}"""
self.low_bound = lower
self.up_bound = upper + 1
self.sampler = sampler
[docs]
def random(self, size=None):
"""random(size=None)
Parameters
----------
size : int, default=None
Number of draws.
Returns
-------
out: int or list[int]
Return an int if :code:`size`=1, a :code:`list[int]` else.
"""
return self.sampler(self.low_bound, self.up_bound, size, dtype=int)
[docs]
def isconstant(self):
"""isconstant()
Returns
-------
out: boolean
Return True, if this :ref:`var` is a constant
(:code:`lower`==:code:`upper`),\
False otherwise.
"""
return self.up_bound == self.low_bound
def __len__(self):
return 1
def __repr__(self):
return (
super(IntVar, self).__repr__()
+ f"[{self.low_bound};{self.up_bound}])"
)
# Real
[docs]
class FloatVar(Variable):
"""FloatVar
`FloatVar` defines :ref:`var` discribing Float variables.
Parameters
----------
label : str
Name of the variable.
lower : {int,float}
Lower bound of the variable
upper : {int,float}
Upper bound of the variable
sampler : Callable, default=np.random.uniform
Function that takes lower bound, upper bound and a size as parameters.
Attributes
----------
up_bound : {int,float}
Lower bound of the variable
low_bound : {int,float}
Upper bound of the variable
Examples
--------
>>> from dragon.search_space.zellij_variables import FloatVar
>>> a = FloatVar("test", 0, 5.0)
>>> print(a)
FloatVar(test, [0;5.0])
>>> a.random()
2.2011985711663056
"""
def __init__(
self,
label,
lower,
upper,
sampler=np.random.uniform,
tolerance=1e-14,
**kwargs,
):
super(FloatVar, self).__init__(label, **kwargs)
assert isinstance(
upper, (float, int, np.integer, np.floating)
), f"""Upper bound must be an int or a float, got {upper}"""
assert isinstance(
lower, (float, int, np.integer, np.floating)
), f"""Lower bound must be an int or a float, got {lower}"""
assert (
lower < upper
), f"""Lower bound must be
strictly inferior to upper bound, got {lower}<{upper}"""
assert tolerance >= 0, f"""Tolerance must be > 0, got{tolerance}"""
self.up_bound = upper
self.low_bound = lower
self.sampler = sampler
self.tolerance = tolerance
[docs]
def random(self, size=None):
"""random(size=None)
Parameters
----------
size : int, default=None
Number of draws.
Returns
-------
out: float or list[float]
Return a float if :code:`size`=1, a :code:`list[float]` else.
"""
return self.sampler(self.low_bound, self.up_bound, size)
[docs]
def isconstant(self):
"""isconstant()
Returns
-------
out: boolean
Return True, if this :ref:`var` is a constant
(:code:`lower`==:code:`upper`),\
False otherwise.
"""
return self.up_bound == self.low_bound
def __len__(self):
return 1
def __repr__(self):
return (
super(FloatVar, self).__repr__()
+ f"[{self.low_bound};{self.up_bound}])"
)
# Categorical
[docs]
class CatVar(Variable):
"""CatVar(Variable)
`CatVar` defines :ref:`var` discribing categorical variables.
Parameters
----------
label : str
Name of the variable.
features : list
List of all choices.
weights : list[float]
Weights associated to each elements of :code:`features`. The sum of all
positive elements of this list, must be equal to 1.
Attributes
----------
features
weights
Examples
--------
>>> from dragon.search_space.zellij_variables import CatVar, IntVar
>>> a = CatVar("test", ['a', 1, 2.56, IntVar("int", 100 , 200)])
>>> print(a)
CatVar(test, ['a', 1, 2.56, IntVar(int, [100;200])])
>>> a.random(10)
['a', 180, 2.56, 'a', 'a', 2.56, 185, 2.56, 105, 1]
"""
def __init__(self, label, features, weights=None, **kwargs):
assert isinstance(
features, list
), f"""
Features must be a list with a length > 0, got{features}
"""
assert (
len(features) > 1
), f"""
Features must be a list with a length > 1,
got length= {len(features)}
"""
self.features = features
assert (
isinstance(weights, (list, np.ndarray)) or weights == None
), f"""`weights` must be a list or equal to None, got {weights}"""
super(CatVar, self).__init__(label, **kwargs)
if weights:
self.weights = weights
else:
self.weights = [1 / len(features)] * len(features)
[docs]
def random(self, size=1):
"""random(size=1)
Parameters
----------
size : int, default=1
Number of draws.
Returns
-------
out: float or list[float]
Return a feature if :code:`size`=1, a :code:`list[features]` else.
Features can be :ref:`var`. When seleted, it will return
a random point from this :ref:`var`.
"""
if size == 1:
res = random.choices(self.features, weights=self.weights, k=size)[0]
if isinstance(res, Variable):
res = res.random()
else:
res = random.choices(self.features, weights=self.weights, k=size)
for i, v in enumerate(res):
if isinstance(v, Variable):
res[i] = v.random()
return res
[docs]
def isconstant(self):
"""isconstant()
Returns
-------
out: boolean
Return True, if this :ref:`var` is a constant
(:code:`len(feature)==1`),\
False otherwise.
"""
return len(self.features) == 1
def __len__(self):
return 1
def __repr__(self):
return super(CatVar, self).__repr__() + f"{self.features})"
# Array of variables
[docs]
class ArrayVar(Variable):
"""ArrayVar(Variable)
:code:`ArrayVar` defines :ref:`var` describing lists of :ref:`var`. This class is
iterable.
Parameters
----------
label : str
Name of the variable.
*args : list[Variable]
Elements of the :code:`ArrayVar`. All elements must be of type :ref:`var`
Examples
--------
>>> from dragon.search_space.zellij_variables import ArrayVar, IntVar, FloatVar, CatVar
>>> a = ArrayVar(IntVar("int_1", 0,8),
... IntVar("int_2", 4,45),
... FloatVar("float_1", 2,12),
... CatVar("cat_1", ["Hello", 87, 2.56]))
>>> print(a)
ArrayVar(, [IntVar(int_1, [0;8]),
IntVar(int_2, [4;45]),
FloatVar(float_1, [2;12]),
CatVar(cat_1, ['Hello', 87, 2.56])])
>>> a.random()
[5, 15, 8.483221226216427, 'Hello']
"""
def __init__(self, *args, label="", **kwargs):
if args and len(args) > 1 and args[0]:
assert all(
isinstance(v, Variable) for v in args
), f"""
All elements must inherit from :ref:`var`,
got {args}
"""
self.values = list(args)
for idx, v in enumerate(self.values):
setattr(v, "_idx", idx)
else:
self.values = []
super(ArrayVar, self).__init__(label, **kwargs)
[docs]
def random(self, size=1):
"""random(size=1)
Parameters
----------
size : int, default=None
Number of draws.
Returns
-------
out: float or list[float]
Return a list composed of the values returned by each :ref:`var` of
:code:`ArrayVar`. If :code:`size`>1, return a list of list
"""
if size == 1:
for v in self.values:
v.random()
return [v.random() for v in self.values]
else:
res = []
for _ in range(size):
res.append([v.random() for v in self.values])
return res
[docs]
def isconstant(self):
"""isconstant()
Returns
-------
out: boolean
Return True, if this :ref:`var` is a constant (all elements are
constants), False otherwise.
"""
return all(v.isconstant for v in self.values)
[docs]
def index(self, value):
"""index(value)
Return the index inside the :code::code:`ArrayVar` of a given :code:`value`.
Parameters
----------
value : Variable
Targeted Variable in the ArrayVar
Returns
-------
int
Index of :code:`value`.
"""
return value._idx
[docs]
def append(self, v):
"""append(v)
Append a :ref:`Variables` to the :code::code:`ArrayVar`.
Parameters
----------
v : Variable
Variable to be added to the :code:`ArrayVar`
"""
if isinstance(v, Variable):
setattr(v, "_idx", len(self.values))
self.values.append(v)
else:
raise ValueError(
f"""
Cannot append a {type(v)} to ArrayVar.
Tried to append {v} to {self}.
"""
)
def __iter__(self):
self.index = 0
return self
def __next__(self):
if self.index >= len(self.values):
raise StopIteration
res = self.values[self.index]
self.index += 1
return res
def __getitem__(self, item):
return self.values[item]
def __len__(self):
return len(self.values)
def __repr__(self):
values_reprs = ""
for v in self.values:
values_reprs += v.__repr__() + ","
return super(ArrayVar, self).__repr__() + f"[{values_reprs[:-1]}])"
# Block of variable, fixed size
[docs]
class Block(Variable):
"""Block(Variable)
`Block` defines :ref:`var` which will repeat multiple times a :ref:`var`.
Parameters
----------
label : str
Name of the variable.
value : Variable
:ref:`var` that will be repeated
repeat : int
Number of repeats.
Examples
--------
>>> from dragon.search_space.zellij_variables import Block, ArrayVar, FloatVar, IntVar
>>> content = ArrayVar("test",
... IntVar("int_1", 0,8),
... IntVar("int_2", 4,45),
... FloatVar("float_1", 2,12))
>>> a = Block("size 3 Block", content, 3)
>>> print(a)
Block(size 3 Block, [IntVar(int_1, [0;8]),
IntVar(int_2, [4;45]),
FloatVar(float_1, [2;12]),])
>>> a.random(3)
[[[7, 22, 6.843164591359903],
[5, 18, 10.608957810018786],
[4, 21, 10.999649079045858]],
[[5, 9, 9.773288692746476],
[1, 12, 6.1909724243671445],
[4, 12, 9.404313234593669]],
[[4, 10, 2.72648188721585],
[1, 44, 5.319257221471118],
[4, 24, 9.153357213126071]]]
"""
def __init__(self, label, value, repeat, **kwargs):
self.value = value
super(Block, self).__init__(label, **kwargs)
assert isinstance(
value, Variable
), f"""
Value must inherit from :ref:`var`, got {value}
"""
assert (
isinstance(repeat, int) and repeat > 0
), f"""
`repeat` must be a strictly positive int, got {repeat}.
"""
self.repeat = repeat
[docs]
def random(self, size=1):
"""random(size=1)
Parameters
----------
size : int, default=None
Number of draws.
Returns
-------
out: float or list[float]
Return a list composed of the results from the :ref:`var` `random()`
method, repeated `repeat` times. If size > 1, return a list of list.
"""
res = []
if size > 1:
for _ in range(size):
block = []
for _ in range(self.repeat):
if isinstance(self.value, list) and (len(self.value) > 0):
block.append([v.random() for v in self.value])
else:
block.append(self.value.random())
res.append(block)
else:
for _ in range(self.repeat):
if isinstance(self.value, list) and (len(self.value) > 0):
res.append([v.random() for v in self.value])
else:
res.append(self.value.random())
return res
[docs]
def isconstant(self):
"""isconstant()
Returns
-------
out: boolean
Return True, if this :ref:`var` is a constant (the repeated
:ref:`var` is constant), False otherwise.
"""
return self.value.isconstant()
def __repr__(self):
values_reprs = ""
if isinstance(self.value, Iterable) and (len(self.value) > 0):
for v in self.value:
values_reprs += v.__repr__() + ","
return super(Block, self).__repr__() + f"[{values_reprs}])"
else:
return super(Block, self).__repr__() + f"{self.value.__repr__()}"
# Block of variables, with random size.
[docs]
class DynamicBlock(Block):
"""DynamicBlock(Block)
A `DynamicBlock` is a `Block` with a random number of repeats.
Parameters
----------
label : str
Name of the variable.
value : Variable
:ref:`var` that will be repeated
repeat : int
Maximum number of repeats.
Examples
--------
>>> from dragon.search_space.zellij_variables import DynamicBlock, ArrayVar, FloatVar, IntVar
>>> content = ArrayVar(IntVar("int_1", 0,8),
... IntVar("int_2", 4,45),
... FloatVar("float_1", 2,12))
>>> a = DynamicBlock("max size 10 Block", content, 10)
>>> print(a)
DynamicBlock(max size 10 Block, [IntVar(int_1, [0;8]),
IntVar(int_2, [4;45]),
FloatVar(float_1, [2;12]),])
>>> a.random()
[[[3, 12, 10.662362255103403],
[7, 9, 5.496860842510198],
[3, 37, 7.25449459082227],
[4, 28, 4.912883181322568]],
[[3, 23, 5.150228671772998]],
[[6, 30, 6.1181372194738515]]]
"""
def __init__(self, label, value, repeat, **kwargs):
self.value = value
super(DynamicBlock, self).__init__(label, value, repeat, **kwargs)
[docs]
def random(self, size=1, n_repeat=None):
"""random(size=1)
Parameters
----------
size : int, default=None
Number of draws.
n_repeat : max size of randomly generated block
Returns
-------
out: float or list[float]
Return a list composed of the results from the :ref:`var` `random()`
method, repeated `repeat` times. If size > 1, return a list of list.
"""
res = []
if size > 1:
for _ in range(size):
block = []
if n_repeat is None:
n_repeat = np.random.randint(1, self.repeat)
for _ in range(n_repeat):
if isinstance(self.value, list) and (len(self.value) > 0):
block.append([v.random() for v in self.value])
else:
block.append(self.value.random())
res.append(block)
else:
if n_repeat is None:
n_repeat = np.random.randint(1, self.repeat)
for _ in range(n_repeat):
if isinstance(self.value, list) and (len(self.value) > 0):
res.append([v.random() for v in self.value])
else:
res.append(self.value.random())
return res
[docs]
def isconstant(self):
"""isconstant()
Returns
-------
out: False
Return False, a dynamic block cannot be constant. (It is a binary)
"""
return False
# Constant
[docs]
class Constant(Variable):
"""Constant
:code:`Constant` is a :ref:`var` discribing a constant of any type.
Parameters
----------
label : str
Name of the variable.
value : object
Constant value
Attributes
----------
label : str
Name of the variable.
value : object
Constant value
Examples
--------
>>> from dragon.search_space.zellij_variables import Constant
>>> a = Constant("test", 5)
>>> print(a)
Constant(test, 5)
>>> a.random()
5
"""
def __init__(self, label, value, **kwargs):
super(Constant, self).__init__(label, **kwargs)
assert not isinstance(
value, Variable
), f"Element must not be of Variable type, got {value}"
self.value = value
[docs]
def random(self, size=1):
"""random(size=None)
Parameters
----------
size : int, default=None
Number of draws.
Returns
-------
out: int or list[int]
Return an int if :code:`size`=1, a :code:`list[self.value]` else.
"""
if size > 1:
return [self.value] * size
else:
return self.value
[docs]
def isconstant(self):
"""isconstant()
Returns
-------
out: boolean
Return True
"""
return True
def __repr__(self):
return super(Constant, self).__repr__() + f"{self.value})"