"""
Module containing infrastructure for representing individuals in
population-based optimization algorithms.
"""
# public API for when using ``from pymoo.core.individual import *``
__all__ = [
"default_config",
"Individual",
"calc_cv",
"constr_to_cv",
]
import copy
from typing import Any
from typing import Optional
from typing import Tuple
from typing import Union
from warnings import warn
import numpy as np
def default_config() -> dict:
"""
Get default constraint violation configuration settings.
Returns
-------
out : dict
A dictionary of default constraint violation settings.
"""
return dict(
cache = True,
cv_eps = 0.0,
cv_ieq = dict(scale=None, eps=0.0, pow=None, func=np.sum),
cv_eq = dict(scale=None, eps=1e-4, pow=None, func=np.sum),
)
[docs]
class Individual:
"""
Base class for representing an individual in a population-based
optimization algorithm.
"""
# function: function to generate default configuration settings
default_config = default_config
def __init__(
self,
config: Optional[dict] = None,
**kwargs: Any,
) -> None:
"""
Constructor for the ``Invididual`` class.
Parameters
----------
config : dict, None
A dictionary of configuration metadata.
If ``None``, use a class-dependent default configuration.
kwargs : Any
Additional keyword arguments containing data which is to be stored
in the ``Individual``.
"""
# set decision variable vector to None
self._X = None
# set values objective(s), inequality constraint(s), equality
# contstraint(s) to None
self._F = None
self._G = None
self._H = None
# set first derivatives of objective(s), inequality constraint(s),
# equality contstraint(s) to None
self._dF = None
self._dG = None
self._dH = None
# set second derivatives of objective(s), inequality constraint(s),
# equality contstraint(s) to None
self._ddF = None
self._ddG = None
self._ddH = None
# set constraint violation value to None
self._CV = None
self.evaluated = None
# initialize all the local variables
self.reset()
# a local storage for data
self.data = {}
# the config for this individual
if config is None:
config = Individual.default_config()
self.config = config
for k, v in kwargs.items():
if k in self.__dict__:
self.__dict__[k] = v
elif "_" + k in self.__dict__:
self.__dict__["_" + k] = v
else:
self.data[k] = v
[docs]
def reset(
self,
data: bool = True,
) -> None:
"""
Reset the value of objective(s), inequality constraint(s), equality
constraint(s), their first and second derivatives, the constraint
violation, and the metadata to empty values.
Parameters
----------
data : bool
Whether to reset metadata associated with the ``Individiual``.
"""
# create an empty array to share
empty = np.array([])
# design variables
self._X = empty
# objectives and constraint values
self._F = empty
self._G = empty
self._H = empty
# first order derivation
self._dF = empty
self._dG = empty
self._dH = empty
# second order derivation
self._ddF = empty
self._ddG = empty
self._ddH = empty
# if the constraint violation value to be used
self._CV = None
if data:
self.data = {}
# a set storing what has been evaluated
self.evaluated = set()
[docs]
def has(
self,
key: str,
) -> bool:
"""
Determine whether an individual has a provided key or not.
Parameters
----------
key : str
The key for which to test.
Returns
-------
out : bool
Whether the ``Individual`` has the provided key.
"""
return hasattr(self.__class__, key) or key in self.data
# -------------------------------------------------------
# Values
# -------------------------------------------------------
@property
def X(self) -> np.ndarray:
"""
Get the decision vector for an individual.
Returns
-------
out : np.ndarray
The decision variable for the individual.
"""
return self._X
@X.setter
def X(self, value: np.ndarray) -> None:
"""
Set the decision vector for an individual.
Parameters
----------
value : np.ndarray
The decision variable for the individual.
"""
self._X = value
@property
def F(self) -> np.ndarray:
"""
Get the objective function vector for an individual.
Returns
-------
out : np.ndarray
The objective function vector for the individual.
"""
return self._F
@F.setter
def F(self, value: np.ndarray) -> None:
"""
Set the objective function vector for an individual.
Parameters
----------
value : np.ndarray
The objective function vector for the individual.
"""
self._F = value
@property
def G(self) -> np.ndarray:
"""
Get the inequality constraint vector for an individual.
Returns
-------
out : np.ndarray
The inequality constraint vector for the individual.
"""
return self._G
@G.setter
def G(self, value: np.ndarray) -> None:
"""
Set the inequality constraint vector for an individual.
Parameters
----------
value : np.ndarray
The inequality constraint vector for the individual.
"""
self._G = value
@property
def H(self) -> np.ndarray:
"""
Get the equality constraint vector for an individual.
Returns
-------
out : np.ndarray
The equality constraint vector for the individual.
"""
return self._H
@H.setter
def H(self, value: np.ndarray) -> None:
"""
Get the equality constraint vector for an individual.
Parameters
----------
value : np.ndarray
The equality constraint vector for the individual.
"""
self._H = value
@property
def CV(self) -> np.ndarray:
"""
Get the constraint violation vector for an individual by either reading
it from the cache or calculating it.
Returns
-------
out : np.ndarray
The constraint violation vector for an individual.
"""
config = self.config
cache = config["cache"]
if cache and self._CV is not None:
return self._CV
else:
self._CV = np.array([calc_cv(G=self.G, H=self.H, config=config)])
return self._CV
@CV.setter
def CV(self, value: np.ndarray) -> None:
"""
Set the constraint violation vector for an individual.
Parameters
----------
value : np.ndarray
The constraint violation vector for the individual.
"""
self._CV = value
@property
def FEAS(self) -> np.ndarray:
"""
Get whether an individual is feasible for each constraint.
Returns
-------
out : np.ndarray
An array containing whether each constraint is feasible for an
individual.
"""
eps = self.config.get("cv_eps", 0.0)
return self.CV <= eps
# -------------------------------------------------------
# Gradients
# -------------------------------------------------------
@property
def dF(self) -> np.ndarray:
"""
Get the objective function vector first derivatives for an individual.
Returns
-------
out : np.ndarray
The objective function vector first derivatives for the individual.
"""
return self._dF
@dF.setter
def dF(self, value: np.ndarray) -> None:
"""
Set the objective function vector first derivatives for an individual.
Parameters
----------
value : np.ndarray
The objective function vector first derivatives for the individual.
"""
self._dF = value
@property
def dG(self) -> np.ndarray:
"""
Get the inequality constraint(s) first derivatives for an individual.
Returns
-------
out : np.ndarray
The inequality constraint(s) first derivatives for the individual.
"""
return self._dG
@dG.setter
def dG(self, value: np.ndarray) -> None:
"""
Set the inequality constraint(s) first derivatives for an individual.
Parameters
----------
value : np.ndarray
The inequality constraint(s) first derivatives for the individual.
"""
self._dG = value
@property
def dH(self) -> np.ndarray:
"""
Get the equality constraint(s) first derivatives for an individual.
Returns
-------
out : np.ndarray
The equality constraint(s) first derivatives for the individual.
"""
return self._dH
@dH.setter
def dH(self, value: np.ndarray) -> None:
"""
Set the equality constraint(s) first derivatives for an individual.
Parameters
----------
value : np.ndarray
The equality constraint(s) first derivatives for the individual.
"""
self._dH = value
# -------------------------------------------------------
# Hessians
# -------------------------------------------------------
@property
def ddF(self) -> np.ndarray:
"""
Get the objective function vector second derivatives for an individual.
Returns
-------
out : np.ndarray
The objective function vector second derivatives for the individual.
"""
return self._ddF
@ddF.setter
def ddF(self, value: np.ndarray) -> None:
"""
Set the objective function vector second derivatives for an individual.
Parameters
----------
value : np.ndarray
The objective function vector second derivatives for the individual.
"""
self._ddF = value
@property
def ddG(self) -> np.ndarray:
"""
Get the inequality constraint(s) second derivatives for an individual.
Returns
-------
out : np.ndarray
The inequality constraint(s) second derivatives for the individual.
"""
return self._ddG
@ddG.setter
def ddG(self, value: np.ndarray) -> None:
"""
Set the inequality constraint(s) second derivatives for an individual.
Parameters
----------
value : np.ndarray
The inequality constraint(s) second derivatives for the individual.
"""
self._ddG = value
@property
def ddH(self) -> np.ndarray:
"""
Get the equality constraint(s) second derivatives for an individual.
Returns
-------
out : np.ndarray
The equality constraint(s) second derivatives for the individual.
"""
return self._ddH
@ddH.setter
def ddH(self, value: np.ndarray) -> None:
"""
Set the equality constraint(s) second derivatives for an individual.
Parameters
----------
value : np.ndarray
The equality constraint(s) second derivatives for the individual.
"""
self._ddH = value
# -------------------------------------------------------
# Convenience (value instead of array)
# -------------------------------------------------------
@property
def x(self) -> np.ndarray:
"""
Convenience property. Get the decision vector for an individual.
Returns
-------
out : np.ndarray
The decision variable for the individual.
"""
return self.X
@property
def f(self) -> float:
"""
Convenience property. Get the first objective function value for an individual.
Returns
-------
out : float
The first objective function value for the individual.
"""
return self.F[0]
@property
def cv(self) -> Union[float,None]:
"""
Convenience property. Get the first constraint violation value for an
individual by either reading it from the cache or calculating it.
Returns
-------
out : float, None
The constraint violation vector for an individual.
"""
if self.CV is None:
return None
else:
return self.CV[0]
@property
def feas(self) -> bool:
"""
Convenience property. Get whether an individual is feasible for the
first constraint.
Returns
-------
out : bool
Whether an individual is feasible for the first constraint.
"""
return self.FEAS[0]
# -------------------------------------------------------
# Deprecated
# -------------------------------------------------------
@property
def feasible(self) -> np.ndarray:
"""
Deprecated. Get whether an individual is feasible for each constraint.
Returns
-------
out : np.ndarray
An array containing whether each constraint is feasible for an
individual.
"""
warn(
"The ``feasible`` property for ``pymoo.core.individual.Individual`` is deprecated",
DeprecationWarning,
stacklevel = 2,
)
return self.FEAS
# -------------------------------------------------------
# Other Functions
# -------------------------------------------------------
[docs]
def set_by_dict(
self,
**kwargs: Any
) -> None:
"""
Set an individual's data or metadata using values in a dictionary.
Parameters
----------
kwargs : Any
Keyword arguments defining the data to set.
"""
for k, v in kwargs.items():
self.set(k, v)
[docs]
def set(
self,
key: str,
value: object,
) -> 'Individual':
"""
Set an individual's data or metadata based on a key and value.
Parameters
----------
key : str
Key of the data for which to set.
value : object
Value of the data for which to set.
Returns
-------
out : Individual
A reference to the ``Individual`` for which values were set.
"""
if hasattr(self, key):
setattr(self, key, value)
else:
self.data[key] = value
return self
[docs]
def get(
self,
*keys: str,
) -> Union[tuple,object]:
"""
Get the values for one or more keys for an individual.
Parameters
----------
keys : str
Keys for which to get values.
Returns
-------
out : tuple, object
If more than one key provided, return a ``tuple`` of retrieved values.
If a single key provided, return the retrieved value.
"""
ret = []
for key in keys:
if hasattr(self, key):
v = getattr(self, key)
elif key in self.data:
v = self.data[key]
else:
v = None
ret.append(v)
if len(ret) == 1:
return ret[0]
else:
return tuple(ret)
[docs]
def duplicate(
self,
key: str,
new_key: str,
) -> None:
"""
Duplicate a key to a new key.
Parameters
----------
key : str
Name of the key to duplicated.
new_key : str
Name of the key to which to duplicate the original key.
"""
self.set(new_key, self.get(key))
[docs]
def new(self) -> 'Individual':
"""
Create a new instance of this class.
Returns
-------
out : Individual
A new instance of an ``Individual``.
"""
return self.__class__()
[docs]
def copy(
self,
other: Optional['Individual'] = None,
deep: bool = True,
) -> 'Individual':
"""
Copy an individual.
Parameters
----------
other : Individual, None
The individual to copy. If ``None``, assumed to be self.
deep : bool
Whether to deep copy the individual.
Returns
-------
out : Individual
A copy of the individual.
"""
obj = self.new()
# if not provided just copy yourself
if other is None:
other = self
# the data the new object needs to have
D = other.__dict__
# if it should be a deep copy do it
if deep:
D = copy.deepcopy(D)
for k, v in D.items():
obj.__dict__[k] = v
return obj
def calc_cv(
G: Optional[np.ndarray] = None,
H: Optional[np.ndarray] = None,
config: Optional[dict] = None,
) -> np.ndarray:
"""
Calculate the constraint violation(s) for a set of inequality constraint(s),
equality constraint(s), and a scoring configuration.
Parameters
----------
G : np.ndarray, None
A vector of inequality constraint(s).
H : np.ndarray, None
A vector of equality constraint(s).
config : dict, None
A dictionary of constraint violation scoring configuration settings.
Returns
-------
out : np.ndarray
An array of constraint violations for each objective.
"""
if G is None:
G = np.array([])
if H is None:
H = np.array([])
if config is None:
config = Individual.default_config()
if G is None:
ieq_cv = [0.0]
elif G.ndim == 1:
ieq_cv = constr_to_cv(G, **config["cv_ieq"])
else:
ieq_cv = [constr_to_cv(g, **config["cv_ieq"]) for g in G]
if H is None:
eq_cv = [0.0]
elif H.ndim == 1:
eq_cv = constr_to_cv(np.abs(H), **config["cv_eq"])
else:
eq_cv = [constr_to_cv(np.abs(h), **config["cv_eq"]) for h in H]
return np.array(ieq_cv) + np.array(eq_cv)
def constr_to_cv(
c: Union[np.ndarray,None],
eps: float = 0.0,
scale: Optional[float] = None,
pow: Optional[float] = None,
func: object = np.mean,
) -> float:
"""
Convert a constraint to a constraint violation.
c : np.ndarray
An array of constraint violations.
eps : float
Error tolerance bounds.
scale : float, None
The scale to apply to a constraint violation.
If ``None``, no scale alteration is applied.
pow : float, None
A power to apply to a constraint violation.
If ``None``, no power alteration is applied.
func : function
A function to convert multiple constraint violations into a single score.
"""
if c is None or len(c) == 0:
return 0.0
# subtract eps to allow some violation and then zero out all values less than zero
c = np.maximum(0.0, c - eps)
# apply init_simplex_scale if necessary
if scale is not None:
c = c / scale
# if a pow factor has been provided
if pow is not None:
c = c ** pow
return func(c)