Source code for mealpy.utils.space

#!/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] def transform(self, y): """ Transform labels to encoded integer labels. Parameters: ----------- y : list, tuple Labels to encode. Returns: -------- encoded_labels : list Encoded integer labels. """ if self.unique_labels is None: raise ValueError("Label encoder has not been fit yet.") y = self.set_y(y) return [self.label_to_index[label] for label in y]
[docs] def fit_transform(self, y): """Fit label encoder and return encoded labels. Parameters ---------- y : list, tuple Target values. Returns ------- y : list Encoded labels. """ y = self.set_y(y) self.fit(y) return self.transform(y)
[docs] def inverse_transform(self, y): """ Transform integer labels to original labels. Parameters: ----------- y : list, tuple Encoded integer labels. Returns: -------- original_labels : list Original labels. """ if self.unique_labels is None: raise ValueError("Label encoder has not been fit yet.") y = self.set_y(y) return [self.unique_labels[i] if i in self.label_to_index.values() else "unknown" for i in y]
[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)