import hashlib
from addict import Dict
import numpy as np
from CADETProcess import CADETProcessError
from CADETProcess.dataStructure import Structure
from CADETProcess.dataStructure import Bool, Float, Vector
def hash_array(array: np.ndarray) -> str:
"""Compute a hash value for an array of floats using the sha256 hash function.
Parameters
----------
array : numpy.ndarray
An array of floats.
Returns
-------
str
A hash value as a string of hexadecimal characters.
Examples
--------
>>> import numpy as np
>>> hash_array(np.array([1, 2.0]))
'3dfc9d56e04dcd01590f48b1b57c9ed9fecb1e94e11d3c3f13cf0fd97b7a9f0f'
"""
array = np.asarray(array)
return hashlib.sha256(array.tobytes()).hexdigest()
[docs]
class Individual(Structure):
"""
Set of variables evaluated during Optimization.
Attributes
----------
id : str
UUID for individual.
x : np.ndarray
Variable values in untransformed space.
x_transformed : np.ndarray
Independent variable values in transformed space.
cv_bounds : np.ndarray
Vound constraint violations.
cv_lincon : np.ndarray
Linear constraint violations.
cv_lineqcon : np.ndarray
Linear equality constraint violations.
f : np.ndarray
Objective values.
f_min : np.ndarray
Minimized objective values.
g : np.ndarray
Nonlinear constraint values.
cv_nonlincon : np.ndarray
Nonlinear constraints violation.
m : np.ndarray
Meta score values.
m_min : np.ndarray
Minimized meta score values.
is_feasible : bool
True, if individual fulfills all constraints.
See Also
--------
CADETProcess.optimization.Population
"""
x = Vector()
x_transformed = Vector()
cv_bounds = Vector()
cv_lincon = Vector()
cv_lineqcon = Vector()
f = Vector()
f_min = Vector()
g = Vector()
cv_nonlincon = Vector()
cv_nonlincon_tol = Float()
m = Vector()
m_min = Vector()
is_feasible = Bool()
def __init__(
self,
x,
f=None,
g=None,
f_min=None,
x_transformed=None,
cv_bounds=None,
cv_lincon=None,
cv_lineqcon=None,
cv_nonlincon=None,
m=None,
m_min=None,
independent_variable_names=None,
objective_labels=None,
nonlinear_constraint_labels=None,
meta_score_labels=None,
variable_names=None,
is_feasible=True,
):
self.x = x
if x_transformed is None:
x_transformed = x
independent_variable_names = variable_names
self.x_transformed = x_transformed
self.cv_bounds = cv_bounds
self.cv_lincon = cv_lincon
self.cv_lineqcon = cv_lineqcon
self.f = f
if f_min is None:
f_min = f
self.f_min = f_min
self.g = g
if g is not None and cv_nonlincon is None:
cv_nonlincon = g
self.cv_nonlincon = cv_nonlincon
self.m = m
if m_min is None:
m_min = m
self.m_min = m_min
if isinstance(variable_names, np.ndarray):
variable_names = [s.decode() for s in variable_names]
self.variable_names = variable_names
if isinstance(independent_variable_names, np.ndarray):
independent_variable_names = [s.decode() for s in independent_variable_names]
self.independent_variable_names = independent_variable_names
if isinstance(objective_labels, np.ndarray):
objective_labels = [s.decode() for s in objective_labels]
self.objective_labels = objective_labels
if isinstance(nonlinear_constraint_labels, np.ndarray):
nonlinear_constraint_labels = [
s.decode() for s in nonlinear_constraint_labels
]
self.nonlinear_constraint_labels = nonlinear_constraint_labels
if isinstance(meta_score_labels, np.ndarray):
meta_score_labels = [s.decode() for s in meta_score_labels]
self.meta_score_labels = meta_score_labels
self.id = hash_array(self.x)
self.is_feasible = is_feasible
@property
def id_short(self) -> str:
"""str: Id shortened to the first seven digits."""
return self.id[0:7]
@property
def is_evaluated(self) -> bool:
"""bool: Return True if individual has been evaluated. False otherwise."""
if self.f is None:
return False
else:
return True
@property
def n_x(self) -> int:
"""int: Number of variables."""
return len(self.x)
@property
def n_f(self) -> int:
"""int: Number of objectives."""
if self.f is None:
return 0
return len(self.f)
@property
def n_g(self) -> int:
"""int: Number of nonlinear constraints."""
if self.g is None:
return 0
else:
return len(self.g)
@property
def n_m(self) -> int:
"""int: Number of meta scores."""
if self.m is None:
return 0
else:
return len(self.m)
@property
def cv(self) -> np.ndarray:
"""
All constraint violations combined.
(cv_bounds, cv_lincon, cv_lineqcon, cv_nonlincon)
Returns
-------
np.ndarray
All constraint violations combined.
"""
cvs = (self.cv_bounds, self.cv_lincon, self.cv_lineqcon, self.cv_nonlincon)
return np.concatenate([cv for cv in cvs if cv is not None])
@property
def dimensions(self) -> tuple[int]:
"""tuple: Individual dimensions (n_x, n_f, n_g, n_m)."""
return (self.n_x, self.n_f, self.n_g, self.n_m)
@property
def objectives_minimization_factors(self) -> np.ndarray:
"""np.ndarray: Array indicating objectives transformed to minimization."""
return self.f_min / self.f
@property
def meta_scores_minimization_factors(self) -> np.ndarray:
"""np.ndarray: Array indicating meta sorces transformed to minimization."""
return self.m_min / self.m
[docs]
def dominates(self, other: "Individual") -> bool:
"""Determine if individual dominates other.
Parameters
----------
other : Individual
Other individual
Returns
-------
dominates : bool
True if objectives of "self" are not strictly worse than the
corresponding objectives of "other" and at least one objective is
strictly better. False otherwise
"""
if not self.is_evaluated:
raise CADETProcessError("Individual needs to be evaluated first.")
if not other.is_evaluated:
raise CADETProcessError("Other individual needs to be evaluated first.")
if self.is_feasible and not other.is_feasible:
return True
if not self.is_feasible and other.is_feasible:
return False
if not self.is_feasible and not other.is_feasible:
if np.any(self.cv < other.cv):
better_in_all = np.all(self.cv <= other.cv)
strictly_better_in_one = np.any(self.cv < other.cv)
return better_in_all and strictly_better_in_one
if self.m is not None:
self_values = self.m
other_values = other.m
else:
self_values = self.f_min
other_values = other.f_min
if np.any(self_values > other_values):
return False
if np.any(self_values < other_values):
return True
return False
[docs]
def is_similar(self, other: "Individual", tol: float = 1e-1) -> bool:
"""Determine if individual is similar to other.
Parameters
----------
other : Individual
Other individual
tol : float, optional
Relative tolerance parameter.
To reduce number of entries, a rather high rtol is chosen.
Returns
-------
is_similar : bool
True if individuals are close to each other. False otherwise
"""
if tol is None:
return False
similar_x = self.is_similar_x(other, tol)
similar_f = self.is_similar_f(other, tol)
if self.g is not None:
similar_g = self.is_similar_g(other, tol)
else:
similar_g = True
if self.m is not None:
similar_m = self.is_similar_m(other, tol)
else:
similar_m = True
return similar_x and similar_f and similar_g and similar_m
[docs]
def is_similar_x(
self,
other: "Individual",
tol: float = 1e-1,
use_transformed: bool = False,
) -> bool:
"""Determine if individual is similar to other based on parameter values.
Parameters
----------
other : Individual
Other individual
tol : float
Relative tolerance parameter.
To reduce number of entries, a rather high rtol is chosen.
use_transformed : bool
If True, use independent transformed space.
The default is False.
Returns
-------
is_similar : bool
True if parameters are close to each other. False otherwise
"""
similar_x = np.allclose(self.x, other.x, rtol=tol)
return similar_x
[docs]
def is_similar_f(self, other: "Individual", tol: float = 1e-1) -> bool:
"""Determine if individual is similar to other based on objective values.
Parameters
----------
other : Individual
Other individual
tol : float
Relative tolerance parameter.
To reduce number of entries, a rather high rtol is chosen.
Returns
-------
is_similar : bool
True if parameters are close to each other. False otherwise
"""
similar_f = np.allclose(self.f, other.f, rtol=tol)
return similar_f
[docs]
def is_similar_g(self, other: "Individual", tol: float | None = 1e-1) -> bool:
"""Determine if individual is similar to other based on constraint values.
Parameters
----------
other : Individual
Other individual
tol : float
Relative tolerance parameter.
To reduce number of entries, a rather high rtol is chosen.
Returns
-------
is_similar : bool
True if parameters are close to each other. False otherwise
"""
similar_g = np.allclose(self.g, other.g, rtol=tol)
return similar_g
[docs]
def is_similar_m(self, other: "Individual", tol: float | None = 1e-1) -> bool:
"""Determine if individual is similar to other based on meta score values.
Parameters
----------
other : Individual
Other individual
tol : float
Relative tolerance parameter.
To reduce number of entries, a rather high rtol is chosen.
Returns
-------
is_similar : bool
True if parameters are close to each other. False otherwise
"""
similar_m = np.allclose(self.m, other.m, rtol=tol)
return similar_m
def __str__(self) -> str:
return str(list(self.x))
def __repr__(self) -> str:
if self.g is None:
return f'{self.__class__.__name__}({self.x}, {self.f})'
else:
return f'{self.__class__.__name__}({self.x}, {self.f}, {self.g})'
[docs]
def to_dict(self) -> dict:
"""Convert individual to a dictionary.
Returns
-------
dict: A dictionary representation of the individual's attributes.
"""
data = Dict()
data.x = self.x
data.x_transformed = self.x_transformed
data.cv_bounds = self.cv_bounds
data.cv_lincon = self.cv_lincon
data.cv_lineqcon = self.cv_lineqcon
data.f = self.f
data.f_min = self.f_min
if self.g is not None:
data.g = self.g
data.cv_nonlincon = self.cv_nonlincon
if self.m is not None:
data.m = self.m
data.m_min = self.m_min
data.variable_names = self.variable_names
data.independent_variable_names = self.independent_variable_names
if self.objective_labels is not None:
data.objective_labels = self.objective_labels
if self.nonlinear_constraint_labels is not None:
data.nonlinear_constraint_labels = self.nonlinear_constraint_labels
if self.meta_score_labels is not None:
data.meta_score_labels = self.meta_score_labels
data.is_feasible = self.is_feasible
return data
[docs]
@classmethod
def from_dict(cls, data: dict) -> "Individual":
"""Create Individual from dictionary representation of its attributes.
Parameters
----------
data : dict
A dictionary representation of the individual's attributes.
Returns
-------
individual
Individual idual created from the dictionary.
"""
return cls(**data)