#!/usr/bin/env python
# Created by "Thieu" at 05:33, 28/09/2023 ----------%
# Email: nguyenthieu2102@gmail.com %
# Github: https://github.com/thieu1995 %
# --------------------------------------------------%
import numpy as np
import numbers as nb
from abc import ABC, abstractmethod
from mealpy.utils import transfer
[docs]class LabelEncoder:
"""
Encode categorical features as integer labels.
Especially, it can encode a list of mixed types include integer, float, and string. Better than scikit-learn module.
"""
def __init__(self):
self.unique_labels = None
self.label_to_index = {}
[docs] def set_y(self, y):
if type(y) not in (list, tuple, np.ndarray):
y = (y,)
return y
[docs] def fit(self, y):
"""
Fit label encoder to a given set of labels.
Parameters:
-----------
y : list, tuple
Labels to encode.
"""
def safe_key(val):
# Chuyển None -> 0, số -> 1, chuỗi -> 2, object khác -> 3
if val is None:
return (0, '')
elif isinstance(val, nb.Number):
return (1, val)
elif isinstance(val, str):
return (2, val)
else:
return (3, str(val))
# self.unique_labels = sorted(set(y), key=lambda x: (isinstance(x, (int, float)), x))
self.unique_labels = sorted(set(y), key=safe_key)
self.label_to_index = {label: i for i, label in enumerate(self.unique_labels)}
return self
[docs]class BaseVar(ABC):
SUPPORTED_ARRAY = [tuple, list, np.ndarray]
def __init__(self, name="variable"):
self.name = name
self.n_vars = None
self.lb, self.ub = None, None
self._seed = None
self.generator = np.random.default_rng()
[docs] def set_n_vars(self, n_vars):
if type(n_vars) is int and n_vars > 0:
self.n_vars = n_vars
else:
raise ValueError(f"Invalid n_vars. It should be integer and > 0.")
@property
def seed(self):
return self._seed
@seed.setter
def seed(self, value: int) -> None:
self._seed = value
self.generator = np.random.default_rng(self._seed)
[docs] @abstractmethod
def encode(self, x):
pass
[docs] @abstractmethod
def decode(self, x):
pass
[docs] @abstractmethod
def correct(self, x):
pass
[docs] @abstractmethod
def generate(self):
pass
[docs] @staticmethod
def round(x):
frac = x - np.floor(x)
t1 = np.floor(x)
t2 = np.ceil(x)
return np.where(frac < 0.5, t1, t2)
[docs]class FloatVar(BaseVar):
def __init__(self, lb=-10., ub=10., name="float"):
super().__init__(name)
self._set_bounds(lb, ub)
def _set_bounds(self, lb, ub):
if isinstance(lb, nb.Number) and isinstance(ub, nb.Number):
self.lb, self.ub = np.array((lb, ), dtype=float), np.array((ub,), dtype=float)
self.n_vars = 1
elif type(lb) in self.SUPPORTED_ARRAY and type(ub) in self.SUPPORTED_ARRAY:
if len(lb) == len(ub):
self.lb, self.ub = np.array(lb, dtype=float), np.array(ub, dtype=float)
self.n_vars = len(lb)
else:
raise ValueError(f"Invalid lb or ub. Length of lb should equal to length of ub.")
else:
raise TypeError(f"Invalid lb or ub. It should be one of following: {self.SUPPORTED_ARRAY}")
[docs] def encode(self, x):
return np.array(x, dtype=float)
[docs] def decode(self, x):
x = self.correct(x)
return np.array(x, dtype=float)
[docs] def correct(self, x):
return np.clip(x, self.lb, self.ub)
[docs] def generate(self):
return self.generator.uniform(self.lb, self.ub)
[docs]class IntegerVar(BaseVar):
def __init__(self, lb=-10, ub=10, name="integer"):
super().__init__(name)
self.eps = 1e-4
self._set_bounds(lb, ub)
def _set_bounds(self, lb, ub):
if isinstance(lb, nb.Number) and isinstance(ub, nb.Number):
lb, ub = int(lb) - 0.5, int(ub) + 0.5 - self.eps
self.lb, self.ub = np.array((lb, ), dtype=float), np.array((ub, ), dtype=float)
self.n_vars = 1
elif type(lb) in self.SUPPORTED_ARRAY and type(ub) in self.SUPPORTED_ARRAY:
if len(lb) == len(ub):
self.lb, self.ub = np.array(lb, dtype=float) - 0.5, np.array(ub, dtype=float) + (0.5 - self.eps)
self.n_vars = len(lb)
else:
raise ValueError(f"Invalid lb or ub. Length of lb should equal to length of ub.")
else:
raise TypeError(f"Invalid lb or ub. It should be one of following: {self.SUPPORTED_ARRAY}")
[docs] def encode(self, x):
return np.array(x, dtype=float)
[docs] def decode(self, x):
x = self.correct(x)
x = self.round(x)
return np.array(x, dtype=int)
[docs] def correct(self, x):
return np.clip(x, self.lb, self.ub)
[docs] def generate(self):
return self.generator.integers(self.lb+0.5, self.ub+0.5+self.eps)
[docs]class StringVar(BaseVar):
def __init__(self, valid_sets=(("",),), name="string"):
super().__init__(name)
self.eps = 1e-4
self._set_bounds(valid_sets)
def _set_bounds(self, valid_sets):
if type(valid_sets) in self.SUPPORTED_ARRAY:
if type(valid_sets[0]) not in self.SUPPORTED_ARRAY:
self.n_vars = 1
self.valid_sets = (tuple(valid_sets),)
le = LabelEncoder().fit(valid_sets)
self.list_le = (le,)
self.lb = np.array([0., ])
self.ub = np.array([len(valid_sets) - self.eps, ])
else:
self.n_vars = len(valid_sets)
if all(len(item) > 1 for item in valid_sets):
self.valid_sets = valid_sets
self.list_le = []
ub = []
for vl_set in valid_sets:
le = LabelEncoder().fit(vl_set)
self.list_le.append(le)
ub.append(len(vl_set) - self.eps)
self.lb = np.zeros(self.n_vars)
self.ub = np.array(ub)
else:
raise ValueError(f"Invalid valid_sets. All variables need to have at least 2 values.")
else:
raise TypeError(f"Invalid valid_sets. It should be {self.SUPPORTED_ARRAY}.")
[docs] def encode(self, x):
return np.array([le.transform(val)[0] for (le, val) in zip(self.list_le, x)], dtype=float)
[docs] def decode(self, x):
x = self.correct(x)
return [le.inverse_transform(val)[0] for (le, val) in zip(self.list_le, x)]
[docs] def correct(self, x):
x = np.clip(x, self.lb, self.ub)
return np.array(x, dtype=int)
[docs] def generate(self):
return [self.generator.choice(np.array(vl_set, dtype=str)) for vl_set in self.valid_sets]
[docs]class CategoricalVar(StringVar):
def __init__(self, valid_sets=(("",),), name="categorical"):
super().__init__(valid_sets, name)
[docs] def generate(self):
return [self.generator.choice(np.array(vl_set, dtype=object)) for vl_set in self.valid_sets]
[docs]class SequenceVar(BaseVar):
def __init__(self, valid_sets, return_type=tuple, name="sequence"):
super().__init__(name)
self.eps = 1e-4
self.valid_sets = [tuple(v) for v in valid_sets] # Normalize to tuples for hashing
self.return_type = return_type
self.label_to_index = {val: i for i, val in enumerate(self.valid_sets)}
self.index_to_label = {i: val for i, val in enumerate(self.valid_sets)}
self.n_vars = 1
self.lb = np.array([0., ])
self.ub = np.array([len(valid_sets) - self.eps, ])
[docs] def encode(self, x):
x_tuple = tuple(x)
if x_tuple not in self.label_to_index:
raise ValueError(f"Unknown sequence for encoding: {x}")
return np.array([self.label_to_index[x_tuple]], dtype=float)
[docs] def decode(self, x):
x = self.correct(x)
val = self.index_to_label[x[0]]
return [self.return_type(val)]
[docs] def correct(self, x):
x = np.clip(x, self.lb, self.ub)
return np.array(x, dtype=int)
[docs] def generate(self):
idx = self.generator.integers(0, len(self.valid_sets))
return self.valid_sets[idx]
[docs]class PermutationVar(BaseVar):
def __init__(self, valid_set=(1, 2), name="permutation"):
super().__init__(name)
self.eps = 1e-4
self._set_bounds(valid_set)
def _set_bounds(self, valid_set):
if type(valid_set) in self.SUPPORTED_ARRAY and len(valid_set) > 1:
self.valid_set = np.array(valid_set)
self.n_vars = len(valid_set)
self.le = LabelEncoder().fit(valid_set)
self.lb = np.zeros(self.n_vars)
self.ub = (self.n_vars - self.eps) * np.ones(self.n_vars)
else:
raise TypeError(f"Invalid valid_set. It should be {self.SUPPORTED_ARRAY} and contains at least 2 variables")
[docs] def encode(self, x):
return np.array(self.le.transform(x), dtype=float)
[docs] def decode(self, x):
x = self.correct(x)
return self.le.inverse_transform(x)
[docs] def correct(self, x):
return np.argsort(x)
[docs] def generate(self):
return self.generator.permutation(self.valid_set)
[docs]class BinaryVar(BaseVar):
def __init__(self, n_vars=1, name="binary"):
super().__init__(name)
self.set_n_vars(n_vars)
self.eps = 1e-4
self.lb = np.zeros(self.n_vars)
self.ub = (2 - self.eps) * np.ones(self.n_vars)
[docs] def encode(self, x):
return np.array(x, dtype=float)
[docs] def decode(self, x):
x = self.correct(x)
return np.array(x, dtype=int)
[docs] def correct(self, x):
return np.clip(x, self.lb, self.ub)
[docs] def generate(self):
return self.generator.integers(0, 2, self.n_vars)
[docs]class TransferBinaryVar(BinaryVar):
SUPPORTED_TF_FUNCS = ["vstf_01", "vstf_02", "vstf_03", "vstf_04", "sstf_01", "sstf_02", "sstf_03", "sstf_04"]
def __init__(self, n_vars=1, name="tf-binary", tf_func="vstf_01", lb=-8., ub=8., all_zeros=True):
super().__init__(n_vars, name)
if tf_func in self.SUPPORTED_TF_FUNCS:
self.tf_name = tf_func
self.tf_func = getattr(transfer, tf_func)
else:
raise ValueError(f"Invalid transfer function! The supported TF funcs are: {self.SUPPORTED_TF_FUNCS}")
self.lb = lb * np.zeros(self.n_vars)
self.ub = ub * np.ones(self.n_vars)
self.all_zeros = all_zeros
def _get_correct_x(self, x):
if self.all_zeros:
return x
else:
if np.sum(x) == 0:
x[self.generator.integers(0, len(x))] = 1
return x
[docs] def correct(self, x):
x = np.clip(x, self.lb, self.ub)
x = self.tf_func(x)
cons = self.generator.random(len(x))
x = np.where(cons < x, 1, 0)
return self._get_correct_x(x)
[docs] def generate(self):
x = self.generator.integers(0, 2, self.n_vars)
return self._get_correct_x(x)
[docs]class BoolVar(BaseVar):
def __init__(self, n_vars=1, name="boolean"):
super().__init__(name)
self.set_n_vars(n_vars)
self.eps = 1e-4
self.lb = np.zeros(self.n_vars)
self.ub = (2 - self.eps) * np.ones(self.n_vars)
[docs] def encode(self, x):
return np.array(x, dtype=float)
[docs] def decode(self, x):
x = self.correct(x)
x = np.array(x, dtype=int)
return x == 1
[docs] def correct(self, x):
return np.clip(x, self.lb, self.ub)
[docs] def generate(self):
return self.generator.choice([True, False], self.n_vars, replace=True)
[docs]class TransferBoolVar(BoolVar):
SUPPORTED_TF_FUNCS = ["vstf_01", "vstf_02", "vstf_03", "vstf_04", "sstf_01", "sstf_02", "sstf_03", "sstf_04"]
def __init__(self, n_vars=1, name="boolean", tf_func="vstf_01", lb=-8., ub=8.):
super().__init__(n_vars, name)
if tf_func in self.SUPPORTED_TF_FUNCS:
self.tf_name = tf_func
self.tf_func = getattr(transfer, tf_func)
else:
raise ValueError(f"Invalid transfer function! The supported TF funcs are: {self.SUPPORTED_TF_FUNCS}")
self.lb = lb * np.zeros(self.n_vars)
self.ub = ub * np.ones(self.n_vars)
[docs] def correct(self, x):
x = np.clip(x, self.lb, self.ub)
x = self.tf_func(x)
cons = self.generator.random(len(x))
return np.where(cons < x, 1, 0)