# mypy: disable-error-code="return-value"
"""Infrastructure for representing individuals in population-based optimization."""
# public API for when using ``from pymoo.core.individual import *``
__all__ = [
"default_config",
"Individual",
"calc_cv",
"constr_to_cv",
]
import copy
from typing import Optional, Callable, Any
from typing import Union
from warnings import warn
import numpy as np
def default_config() -> dict:
"""Get default constraint violation configuration settings.
Returns:
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."""
# function: function to generate default configuration settings
default_config = default_config
def __init__(
self,
config: Optional[dict] = None,
**kwargs: Any,
) -> None:
"""Constructor for the Individual class.
Args:
config: A dictionary of configuration metadata. If None, uses a class-dependent default.
kwargs: Additional keyword arguments containing data to store in the Individual.
"""
# set decision variable vector to None
self._X: np.ndarray | None = None
# set values objective(s), inequality constraint(s), equality
# contstraint(s) to None
self._F: np.ndarray | None = None
self._G: np.ndarray | None = None
self._H: np.ndarray | None = None
# set first derivatives of objective(s), inequality constraint(s),
# equality contstraint(s) to None
self._dF: np.ndarray | None = None
self._dG: np.ndarray | None = None
self._dH: np.ndarray | None = None
# set second derivatives of objective(s), inequality constraint(s),
# equality contstraint(s) to None
self._ddF: np.ndarray | None = None
self._ddG: np.ndarray | None = None
self._ddH: np.ndarray | None = None
# set constraint violation value to None
self._CV: np.ndarray | None = None
self.evaluated: set[Any] | None = None
# initialize all the local variables
self.reset()
# a local storage for data
self.data: dict[Any, Any] = {}
# 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 objectives, constraints, derivatives, and violation values.
Resets all objective values, inequality/equality constraints, their first
and second derivatives, constraint violations, and metadata to empty values.
Args:
data: Whether to reset metadata associated with the Individual.
"""
# 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.
Args:
key: The key for which to test.
Returns:
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:
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.
Args:
value: The decision variable for the individual.
"""
self._X = value
@property
def F(self) -> np.ndarray:
"""Get the objective function vector for an individual.
Returns:
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.
Args:
value: 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:
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.
Args:
value: 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:
np.ndarray: The equality constraint vector for the individual.
"""
return self._H
@H.setter
def H(self, value: np.ndarray) -> None:
"""Set the equality constraint vector for an individual.
Args:
value: The equality constraint vector for the individual.
"""
self._H = value
@property
def CV(self) -> np.ndarray:
"""Get the constraint violation vector from cache or calculate it.
Returns:
np.ndarray: The constraint violation vector for the 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.
Args:
value: 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:
np.ndarray: An array of feasibility flags for each constraint.
"""
eps = self.config.get("cv_eps", 0.0)
return self.CV <= eps
# -------------------------------------------------------
# Gradients
# -------------------------------------------------------
@property
def dF(self) -> np.ndarray:
"""Get the objective function first derivatives for an individual.
Returns:
np.ndarray: The first derivatives of the objective function vector.
"""
return self._dF
@dF.setter
def dF(self, value: np.ndarray) -> None:
"""Set the objective function first derivatives for an individual.
Args:
value: The first derivatives of the objective function vector.
"""
self._dF = value
@property
def dG(self) -> np.ndarray:
"""Get the inequality constraint first derivatives for an individual.
Returns:
np.ndarray: The first derivatives of the inequality constraints.
"""
return self._dG
@dG.setter
def dG(self, value: np.ndarray) -> None:
"""Set the inequality constraint first derivatives for an individual.
Args:
value: The first derivatives of the inequality constraints.
"""
self._dG = value
@property
def dH(self) -> np.ndarray:
"""Get the equality constraint first derivatives for an individual.
Returns:
np.ndarray: The first derivatives of the equality constraints.
"""
return self._dH
@dH.setter
def dH(self, value: np.ndarray) -> None:
"""Set the equality constraint first derivatives for an individual.
Args:
value: The first derivatives of the equality constraints.
"""
self._dH = value
# -------------------------------------------------------
# Hessians
# -------------------------------------------------------
@property
def ddF(self) -> np.ndarray:
"""Get the objective function second derivatives for an individual.
Returns:
np.ndarray: The second derivatives of the objective function vector.
"""
return self._ddF
@ddF.setter
def ddF(self, value: np.ndarray) -> None:
"""Set the objective function second derivatives for an individual.
Args:
value: The second derivatives of the objective function vector.
"""
self._ddF = value
@property
def ddG(self) -> np.ndarray:
"""Get the inequality constraint second derivatives for an individual.
Returns:
np.ndarray: The second derivatives of the inequality constraints.
"""
return self._ddG
@ddG.setter
def ddG(self, value: np.ndarray) -> None:
"""Set the inequality constraint second derivatives for an individual.
Args:
value: The second derivatives of the inequality constraints.
"""
self._ddG = value
@property
def ddH(self) -> np.ndarray:
"""Get the equality constraint second derivatives for an individual.
Returns:
np.ndarray: The second derivatives of the equality constraints.
"""
return self._ddH
@ddH.setter
def ddH(self, value: np.ndarray) -> None:
"""Set the equality constraint second derivatives for an individual.
Args:
value: The second derivatives of the equality constraints.
"""
self._ddH = value
# -------------------------------------------------------
# Convenience (value instead of array)
# -------------------------------------------------------
@property
def x(self) -> np.ndarray:
"""Get the decision vector for an individual.
Returns:
np.ndarray: The decision variable for the individual.
"""
return self.X
@property
def f(self) -> float:
"""Get the first objective function value for an individual.
Returns:
float: The first objective function value for the individual.
"""
return self.F[0]
@property
def cv(self) -> Union[float, None]:
"""Get the first constraint violation value for an individual.
Returns:
float or None: The first constraint violation value for the individual.
"""
if self.CV is None:
return None
else:
return self.CV[0]
@property
def feas(self) -> bool:
"""Get whether an individual is feasible for the first constraint.
Returns:
bool: Whether the individual is feasible for the first constraint.
"""
return self.FEAS[0]
# -------------------------------------------------------
# Deprecated
# -------------------------------------------------------
@property
def feasible(self) -> np.ndarray:
"""Get whether an individual is feasible for each constraint.
Deprecated. Use FEAS instead.
Returns:
np.ndarray: An array of feasibility flags for each constraint.
"""
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.
Args:
kwargs: 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.
Args:
key: Key of the data for which to set.
value: Value of the data for which to set.
Returns:
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.
Args:
keys: Keys for which to get values.
Returns:
tuple or object: If more than one key provided, returns a tuple of values.
If a single key provided, returns the retrieved value.
"""
ret = []
for key in keys:
if hasattr(self, key):
v = getattr(self, key) # type: ignore
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.
Args:
key: Name of the key to duplicate.
new_key: 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:
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.
Args:
other: The individual to copy. If None, assumed to be self.
deep: Whether to deep copy the individual.
Returns:
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 constraint violations from inequality and equality constraints.
Args:
G: A vector of inequality constraint(s).
H: A vector of equality constraint(s).
config: A dictionary of constraint violation scoring configuration settings.
Returns:
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] # type: ignore
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] # type: ignore
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: Callable = np.mean,
) -> float:
"""Convert a constraint to a constraint violation.
Args:
c: An array of constraint violations.
eps: Error tolerance bounds.
scale: The scale to apply to a constraint violation. If None, no scale is applied.
pow: A power to apply to a constraint violation. If None, no power is applied.
func: A function to convert multiple constraint violations into a single score.
Returns:
float: The constraint violation 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)