Source code for CADETProcess.optimization.individual

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)