Source code for CADETProcess.optimization.individual

import hashlib
import warnings
from typing import Optional

import numpy as np
import numpy.typing as npt
from addict import Dict

from CADETProcess import CADETProcessError
from CADETProcess.dataStructure import Bool, Float, Structure, Vector

__all__ = ["hash_array", "Individual"]


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_minimized : 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_minimized : 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_minimized = Vector() g = Vector() cv_nonlincon = Vector() cv_nonlincon_tol = Float() m = Vector() m_minimized = Vector() is_feasible = Bool() def __init__( self, x: npt.ArrayLike, f: Optional[npt.ArrayLike] = None, g: Optional[npt.ArrayLike] = None, f_minimized: Optional[npt.ArrayLike] = None, x_transformed: Optional[npt.ArrayLike] = None, cv_bounds: Optional[npt.ArrayLike] = None, cv_lincon: Optional[npt.ArrayLike] = None, cv_lineqcon: Optional[npt.ArrayLike] = None, cv_nonlincon: Optional[npt.ArrayLike] = None, m: Optional[npt.ArrayLike] = None, m_minimized: Optional[npt.ArrayLike] = None, independent_variable_names: list[str] = None, objective_labels: list[str] = None, nonlinear_constraint_labels: list[str] = None, meta_score_labels: list[str] = None, variable_names: list[str] = None, is_feasible: bool = True, f_min: Optional[npt.ArrayLike] = None, m_min: Optional[npt.ArrayLike] = None, ) -> None: """Initialize Individual Object.""" 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 not None: warnings.warn( "The 'f_min' argument is deprecated and will be removed in a future version. " "Use 'f_minimized' instead.", DeprecationWarning, stacklevel=2 ) if f_minimized is not None: raise ValueError("Cannot specify both 'f_min' and 'f_minimized'.") f_minimized = f_min if f_minimized is None: f_minimized = f self.f_minimized = f_minimized 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 not None: warnings.warn( "The 'm_min' argument is deprecated and will be removed in a future version. " "Use 'm_minimized' instead.", DeprecationWarning, stacklevel=2 ) if m_minimized is not None: raise ValueError("Cannot specify both 'm_min' and 'm_minimized'.") m_minimized = m_min if m_minimized is None: m_minimized = m self.m_minimized = m_minimized 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 np.where(np.signbit(self.f_minimized) == np.signbit(self.f), 1, -1) @property def meta_scores_minimization_factors(self) -> np.ndarray | None: """np.ndarray: Array indicating meta sorces transformed to minimization.""" if self.m is not None: return np.where(np.signbit(self.m_minimized) == np.signbit(self.m), 1, -1)
[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. """ dominates_f = self.dominates_f(other) if self.m is not None: dominates_m = self.dominates_m(other) else: dominates_m = True return dominates_f and dominates_m
def _dominates(self, other: "Individual", attr: str) -> bool: """ Determine if individual dominates other in terms of some value. Parameters ---------- other : Individual Other individual Returns ------- dominates : bool True if values of "self" are not strictly worse than the corresponding values of "other" and at least one value is strictly better. False otherwise. """ # Evaluation checks 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.") # Feasibility check 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: 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 # Objective comparison self_value = getattr(self, attr) other_value = getattr(other, attr) better_in_all = np.all(self_value <= other_value) strictly_better_in_one = np.any(self_value < other_value) return better_in_all and strictly_better_in_one
[docs] def dominates_f(self, other: "Individual") -> bool: """ Determine if individual dominates other in terms of objectives. 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. """ return self._dominates(other, "f_minimized")
[docs] def dominates_m(self, other: "Individual") -> bool: """ Determine if individual dominates other in terms of meta scores. Parameters ---------- other : Individual Other individual Returns ------- dominates : bool True if meta scores of "self" are not strictly worse than the corresponding objectives of "other" and at least one score is strictly better. False otherwise. """ return self._dominates(other, "m_minimized")
[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 not tol: 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: """str: String representation of the individual.""" return str(list(self.x)) def __repr__(self) -> str: """str: String representation of the individual.""" 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_minimized = self.f_minimized 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_minimized = self.m_minimized 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)