Source code for seqme.metrics.hypervolume

from collections.abc import Callable
from typing import Literal

import moocore
import numpy as np
from scipy.spatial import ConvexHull, QhullError

from seqme.core.base import Metric, MetricResult


[docs] class Hypervolume(Metric): """ Computes the Hypervolume metric for multi-objective optimization. This metric evaluates how well the generated sequences cover the approximate Pareto front by computing the hypervolume of the sequences' properties as evaluated by the predictors. Higher hypervolume indicates better coverage of the Pareto front. Two types of hypervolume computation are supported: - Hypervolume indicator [1] - Convex-hull References: [1] Zitler, E., and Thiele, L., "Multiobjective Evolutionary Algorithms: A Comparative Case Study and the Strength Pareto Approach," 1999 (https://www.cse.unr.edu/~sushil/class/gas/papers/StrengthParetoEA.pdf) """
[docs] def __init__( self, predictors: list[Callable[[list[str]], np.ndarray]], *, method: Literal["hvi", "convex-hull"] = "hvi", nadir: np.ndarray | None = None, ideal: np.ndarray | None = None, strict: bool = True, name: str = "Hypervolume", ): """ Initialize the metric. Args: predictors: A list of functions. Each function maps a sequence to a numeric value aimed to be maximized. method: Which Hypervolume computation method to use - ``'hvi'``: Hypervolume indicator - ``'convex-hull'``: Volume of the convex-hull nadir: Smallest (worst) value in each objective dimension. If ``None``, set to zero vector. ideal: Largest (best) value in each objective dimension (used for normalizing points to [0;1]). strict: If ``True`` and values < ``nadir`` (or values > ``ideal``) raise an exception. name: Metric name. """ self.predictors = predictors self.method = method self.nadir = nadir if nadir is not None else np.zeros(len(predictors)) self.ideal = ideal self.strict = strict self._name = name if len(predictors) < 2: raise ValueError("Expected at least two predictors to compute the hypervolume.") if self.nadir.shape[0] != len(predictors): raise ValueError( f"Expected nadir to have {len(predictors)} elements, but only has {self.nadir.shape[0]} elements." ) if self.ideal is not None: if self.ideal.shape[0] != len(predictors): raise ValueError( f"Expected ideal to have {len(predictors)} elements, but only has {self.ideal.shape[0]} elements." ) if (self.ideal < self.nadir).any(): raise ValueError("Expected nadir <= ideal.")
[docs] def __call__(self, sequences: list[str]) -> MetricResult: """Compute hypervolume for the predicted properties of the input sequences. Args: sequences: Sequences to evaluate. Returns: MetricResult: Hypervolume. """ values = np.stack([predictor(sequences) for predictor in self.predictors], axis=1) hypervolume = calculate_hypervolume(values, self.nadir, self.ideal, self.method, self.strict) return MetricResult(hypervolume)
@property def name(self) -> str: return self._name @property def objective(self) -> Literal["minimize", "maximize"]: return "maximize"
def calculate_hypervolume( points: np.ndarray, nadir: np.ndarray, ideal: np.ndarray | None = None, method: Literal["hvi", "convex-hull"] = "hvi", strict: bool = True, ) -> float: """ Compute hypervolume from a set of points in objective space. Args: points: Array of shape [N, D] with objective values. nadir: Reference point (worse than or equal to all actual points). ideal: Best value in each objective dimension (used for normalizing points to [0;1]). method: Either hypervolume indicator ("hvi") or "convex-hull". strict: If ``True``, if values < ``nadir`` (or values > ``ideal``) raise an exception. Returns: Hypervolume """ if points.shape[1] != nadir.shape[0]: raise ValueError("Points must have the same number of dimensions as the reference point.") # replace NaN values with nadir points = np.where(np.isnan(points), nadir, points) if strict: min_elements = points.min(axis=0) if (nadir > min_elements).any(): raise ValueError(f"Value smaller than nadir. Point: {min_elements}. nadir: {nadir}") if ideal is not None: max_elements = points.max(axis=0) if (ideal < max_elements).any(): raise ValueError(f"Value larger than ideal. Point: {max_elements}. ideal: {ideal}") points = np.maximum(points, nadir) if ideal is not None: points = np.minimum(points, ideal) points = points - nadir ref_point = np.zeros(points.shape[1]) if ideal is not None: points = points / (ideal - nadir) if method == "hvi": hypervolume = moocore.hypervolume(points, ref=ref_point, maximise=True) elif method == "convex-hull": all_points = np.vstack((points, ref_point)) try: hypervolume = ConvexHull(all_points).volume except QhullError: hypervolume = float("nan") # Return NaN if hull can't be formed else: raise ValueError(f"Unknown method: {method}") return hypervolume