Skip to content

adm

desdeo.adm.base_adm

Base class for Artificial Decision Makers (ADMs).

This module provides the abstract base class that defines the structure and required methods for implementing an artificial decision maker, which generates preference information for interactive multiobjective optimization methods.

BaseADM

Bases: ABC

Abstract base class for Artificial Decision Makers (ADMs).

This class provides the basic structure and required methods for implementing an ADM. Subclasses must implement the abstract methods to define specific ADM behavior.

Attributes:

Name Type Description
problem Problem

The optimization problem to solve.

it_learning_phase int

Number of iterations for the learning phase.

it_decision_phase int

Number of iterations for the decision phase.

iteration_counter int

Counter for the current iteration.

rng Generator

Random number generator used by subclasses.

Properties

max_iterations (int): Total number of iterations (learning + decision).

Source code in desdeo/adm/base_adm.py
class BaseADM(ABC):
    """Abstract base class for Artificial Decision Makers (ADMs).

    This class provides the basic structure and required methods for implementing
    an ADM. Subclasses must implement the abstract methods to define specific ADM behavior.

    Attributes:
        problem (Problem): The optimization problem to solve.
        it_learning_phase (int): Number of iterations for the learning phase.
        it_decision_phase (int): Number of iterations for the decision phase.
        iteration_counter (int): Counter for the current iteration.
        rng (np.random.Generator): Random number generator used by subclasses.

    Properties:
        max_iterations (int): Total number of iterations (learning + decision).
    """

    def __init__(
        self,
        problem: Problem,
        it_learning_phase: int,
        it_decision_phase: int,
        seed: int | None = None,
    ):
        """Initialize the ADM with the given problem and phase lengths.

        Args:
            problem (Problem): The optimization problem to solve.
            it_learning_phase (int): Number of iterations for the learning phase.
            it_decision_phase (int): Number of iterations for the decision phase.
            seed (int | None): Optional seed for the random number generator used
                by subclasses. Defaults to None.
        """
        self.problem = problem
        self.it_learning_phase = it_learning_phase
        self.it_decision_phase = it_decision_phase
        self.iteration_counter = 0
        self.rng = np.random.default_rng(seed)

    @property
    def max_iterations(self):
        """int: Total number of iterations (learning + decision)."""
        return self.it_learning_phase + self.it_decision_phase

    def has_next(self) -> bool:
        """Check if there are more iterations left to run.

        Returns:
            bool: True if more iterations remain, False otherwise.
        """
        return self.iteration_counter < self.max_iterations

    @abstractmethod
    def generate_initial_preference(self):
        """Generate the initial preference information for the ADM.

        This method must be implemented by subclasses.
        """

    @abstractmethod
    def get_next_preference(self):
        """Get the next preference value according to the current phase.

        This method must be implemented by subclasses.
        """

    @abstractmethod
    def generate_preference_learning(self):
        """Generate preference information during the learning phase.

        This method must be implemented by subclasses.
        """

    @abstractmethod
    def generate_preference_decision(self):
        """Generate preference information during the decision phase.

        This method must be implemented by subclasses.
        """
max_iterations property
max_iterations

int: Total number of iterations (learning + decision).

__init__
__init__(
    problem: Problem,
    it_learning_phase: int,
    it_decision_phase: int,
    seed: int | None = None,
)

Initialize the ADM with the given problem and phase lengths.

Parameters:

Name Type Description Default
problem Problem

The optimization problem to solve.

required
it_learning_phase int

Number of iterations for the learning phase.

required
it_decision_phase int

Number of iterations for the decision phase.

required
seed int | None

Optional seed for the random number generator used by subclasses. Defaults to None.

None
Source code in desdeo/adm/base_adm.py
def __init__(
    self,
    problem: Problem,
    it_learning_phase: int,
    it_decision_phase: int,
    seed: int | None = None,
):
    """Initialize the ADM with the given problem and phase lengths.

    Args:
        problem (Problem): The optimization problem to solve.
        it_learning_phase (int): Number of iterations for the learning phase.
        it_decision_phase (int): Number of iterations for the decision phase.
        seed (int | None): Optional seed for the random number generator used
            by subclasses. Defaults to None.
    """
    self.problem = problem
    self.it_learning_phase = it_learning_phase
    self.it_decision_phase = it_decision_phase
    self.iteration_counter = 0
    self.rng = np.random.default_rng(seed)
generate_initial_preference abstractmethod
generate_initial_preference()

Generate the initial preference information for the ADM.

This method must be implemented by subclasses.

Source code in desdeo/adm/base_adm.py
@abstractmethod
def generate_initial_preference(self):
    """Generate the initial preference information for the ADM.

    This method must be implemented by subclasses.
    """
generate_preference_decision abstractmethod
generate_preference_decision()

Generate preference information during the decision phase.

This method must be implemented by subclasses.

Source code in desdeo/adm/base_adm.py
@abstractmethod
def generate_preference_decision(self):
    """Generate preference information during the decision phase.

    This method must be implemented by subclasses.
    """
generate_preference_learning abstractmethod
generate_preference_learning()

Generate preference information during the learning phase.

This method must be implemented by subclasses.

Source code in desdeo/adm/base_adm.py
@abstractmethod
def generate_preference_learning(self):
    """Generate preference information during the learning phase.

    This method must be implemented by subclasses.
    """
get_next_preference abstractmethod
get_next_preference()

Get the next preference value according to the current phase.

This method must be implemented by subclasses.

Source code in desdeo/adm/base_adm.py
@abstractmethod
def get_next_preference(self):
    """Get the next preference value according to the current phase.

    This method must be implemented by subclasses.
    """
has_next
has_next() -> bool

Check if there are more iterations left to run.

Returns:

Name Type Description
bool bool

True if more iterations remain, False otherwise.

Source code in desdeo/adm/base_adm.py
def has_next(self) -> bool:
    """Check if there are more iterations left to run.

    Returns:
        bool: True if more iterations remain, False otherwise.
    """
    return self.iteration_counter < self.max_iterations

desdeo.adm.adm_chen

Artificial decision maker (ADM) based on the approach by Chen et al.

This module implements the artificial decision maker proposed by Chen et al.

References

Chen, L., Miettinen, K., Xin, B., & Ojalehto, V. (2023). Comparing reference point based interactive multiobjective optimization methods without a human decision maker. European Journal of Operational Research, 307(1), 327-345.

IMPORTANT: This module is a work in progress. There are multiple things not clear in the article that need further clarification.

ADMChen

Bases: BaseADM

Artificial Decision Maker implementation based on Chen et al. (2023).

This ADM simulates human decision-making behavior in interactive multiobjective optimization by operating in two phases: learning and decision-making. During the learning phase, it explores the Pareto front by identifying neighboring solutions with maximum normalized Euclidean distance. In the decision phase, it selects solutions based on a utility function that minimizes disutility.

Attributes:

Name Type Description
true_ideal ndarray

True ideal point computed from the problem.

true_nadir ndarray

True nadir point computed from the problem.

num_objectives int

Number of objectives in the problem.

num_variables int

Number of variables in the problem.

preference ndarray

Current reference point preference.

weights ndarray

Objective weights (equal weights by default).

UF_max float

Maximum utility function value on the Pareto front.

UF_opt float

Optimal (minimum) utility function value on the Pareto front.

extreme_solutions ndarray

Extreme solutions from the Pareto front.

Parameters:

Name Type Description Default
problem Problem

The multiobjective optimization problem.

required
it_learning_phase int

Number of iterations for the learning phase.

required
it_decision_phase int

Number of iterations for the decision phase.

required
pareto_front ndarray

Known Pareto front solutions for initialization.

required
initial_reference_point Optional[ndarray]

Initial reference point. If None, a random point between ideal and nadir is generated.

None

Raises:

Type Description
ValueError

If the initial reference point is not between ideal and nadir points.

Example

problem = Problem(...) # Define your problem pareto_front = np.array([[1, 2], [2, 1], [1.5, 1.5]]) adm = ADMChen(problem, it_learning_phase=5, it_decision_phase=3, ... pareto_front=pareto_front) preference = adm.get_next_preference(current_front)

Source code in desdeo/adm/adm_chen.py
class ADMChen(BaseADM):
    """Artificial Decision Maker implementation based on Chen et al. (2023).

    This ADM simulates human decision-making behavior in interactive multiobjective
    optimization by operating in two phases: learning and decision-making. During the
    learning phase, it explores the Pareto front by identifying neighboring solutions
    with maximum normalized Euclidean distance. In the decision phase, it selects
    solutions based on a utility function that minimizes disutility.

    Attributes:
        true_ideal (np.ndarray): True ideal point computed from the problem.
        true_nadir (np.ndarray): True nadir point computed from the problem.
        num_objectives (int): Number of objectives in the problem.
        num_variables (int): Number of variables in the problem.
        preference (np.ndarray): Current reference point preference.
        weights (np.ndarray): Objective weights (equal weights by default).
        UF_max (float): Maximum utility function value on the Pareto front.
        UF_opt (float): Optimal (minimum) utility function value on the Pareto front.
        extreme_solutions (np.ndarray): Extreme solutions from the Pareto front.

    Args:
        problem (Problem): The multiobjective optimization problem.
        it_learning_phase (int): Number of iterations for the learning phase.
        it_decision_phase (int): Number of iterations for the decision phase.
        pareto_front (np.ndarray): Known Pareto front solutions for initialization.
        initial_reference_point (Optional[np.ndarray]): Initial reference point.
            If None, a random point between ideal and nadir is generated.

    Raises:
        ValueError: If the initial reference point is not between ideal and nadir points.

    Example:
        >>> problem = Problem(...)  # Define your problem
        >>> pareto_front = np.array([[1, 2], [2, 1], [1.5, 1.5]])
        >>> adm = ADMChen(problem, it_learning_phase=5, it_decision_phase=3,
        ...               pareto_front=pareto_front)
        >>> preference = adm.get_next_preference(current_front)
    """

    def __init__(
        self,
        problem: Problem,
        it_learning_phase: int,
        it_decision_phase: int,
        pareto_front: np.ndarray,
        initial_reference_point: np.ndarray | None = None,
        seed: int | None = None,
    ):
        """Initialize the artificial decision maker proposed by Chen et al.

        Args:
            problem (Problem): The multiobjective optimization problem.
            it_learning_phase (int): Number of iterations for the learning phase.
            it_decision_phase (int): Number of iterations for the decision phase.
            pareto_front (np.ndarray): Known Pareto front solutions for initialization.
            initial_reference_point (np.ndarray | None): Initial reference point. If
                None, a random point between the ideal and nadir is generated.
            seed (int | None): Optional seed for the random number generator. Defaults to None.
        """
        # Initialize problem with true ideal and nadir points
        self.true_ideal, self.true_nadir = payoff_table_method(problem)
        problem = problem.update_ideal_and_nadir(new_ideal=self.true_ideal, new_nadir=self.true_nadir)
        super().__init__(problem, it_learning_phase, it_decision_phase, seed=seed)

        # Store problem dimensions
        self.num_objectives = len(problem.objectives)
        self.num_variables = len(problem.variables)

        # Generate initial preference
        self.preference = self.generate_initial_preference(initial_reference_point)
        self.iteration_counter += 1

        # Initialize equal weights for all objectives
        # NOTE: In the original article, weights were set manually
        self.weights = np.ones(self.num_objectives) / self.num_objectives

        # Compute utility function bounds on the Pareto front
        self.UF_max = np.max(
            [self.utility_function(sol, self.true_ideal, self.true_nadir, self.weights) for sol in pareto_front]
        )
        self.UF_opt = np.min(
            [self.utility_function(sol, self.true_ideal, self.true_nadir, self.weights) for sol in pareto_front]
        )

        # Store extreme solutions from the
        self.extreme_solutions = self.get_extreme_solutions(pareto_front)

    def generate_initial_preference(self, initial_reference_point: np.ndarray | None = None) -> np.ndarray:
        """Generate the initial reference point for the ADM.

        If an initial reference point is provided, it validates that the point lies
        between the ideal and nadir points. Otherwise, generates a random point
        within the feasible objective space.

        Args:
            initial_reference_point (Optional[np.ndarray]): User-specified initial
                reference point. Must be between ideal and nadir points.

        Returns:
            np.ndarray: Valid initial reference point.

        Raises:
            ValueError: If the provided reference point is outside the valid range.
        """
        if initial_reference_point is not None:
            if not (self.true_ideal <= initial_reference_point <= self.true_nadir).all():
                raise ValueError("Initial reference point must be between the ideal and nadir points.")
            return initial_reference_point
        # Generate random reference point between ideal and nadir
        return np.array(
            [
                self.rng.uniform(min_val, max_val)
                for min_val, max_val in zip(
                    self.problem.get_ideal_point().values(),
                    self.problem.get_nadir_point().values(),
                    strict=True,
                )
            ]
        )

    def get_next_preference(self, front: np.ndarray) -> np.ndarray:
        """Get the next preference (reference point) based on the current iteration phase.

        This method determines whether the ADM is in the learning or decision phase
        and calls the appropriate preference generation method.

        Args:
            front (np.ndarray): Current Pareto front approximation with shape
                (n_solutions, n_objectives).

        Returns:
            np.ndarray: Next reference point for the interactive method.
        """
        if self.iteration_counter < self.it_learning_phase:
            self.preference = self.generate_preference_learning(front)
        else:
            self.preference = self.generate_preference_decision(front)
        self.iteration_counter += 1
        return self.preference

    def get_extreme_solutions(self, front: np.ndarray) -> np.ndarray:
        """Extract extreme solutions from the Pareto front.

        An extreme solution is defined as the objective vector that minimizes
        one of the objective functions on the Pareto front. These solutions
        represent the boundaries of the achievable objective space.

        Args:
            front (np.ndarray): Pareto front with shape (n_solutions, n_objectives).

        Returns:
            np.ndarray: Array of extreme solutions with shape (n_objectives, n_objectives).
                Each row represents an extreme solution for one objective.

        Example:
            For a 2-objective problem with front = [[1, 3], [2, 2], [3, 1]]:
            - Extreme for obj 1: [1, 3] (min value 1 in first objective)
            - Extreme for obj 2: [3, 1] (min value 1 in second objective)
        """
        extreme_solutions = []
        for i in range(front.shape[1]):  # For each objective
            idx_min_i = np.argmin(front[:, i])  # Find solution with minimum value
            extreme_solutions.append(front[idx_min_i])
        return np.array(extreme_solutions)

    @staticmethod
    def normalized_euclidean_distance(
        za: np.ndarray, zb: np.ndarray, znad: np.ndarray, zstar: np.ndarray, eps: float | None = None
    ) -> float:
        """Compute normalized Euclidean distance between two solutions.

        The normalization is performed using the range between the utopian point
        (ideal - eps) and the nadir point. This ensures that the distance metric
        is scale-independent across different objectives.

        Args:
            za (np.ndarray): First solution vector of shape (n_objectives,).
            zb (np.ndarray): Second solution vector of shape (n_objectives,).
            znad (np.ndarray): Nadir point (worst values) of shape (n_objectives,).
            zstar (np.ndarray): Ideal point (best values) of shape (n_objectives,).
            eps (Optional[float]): Small positive value for utopian shift.
                Defaults to 1e-6 if None.

        Returns:
            float: Normalized Euclidean distance between za and zb.

        Note:
            The utopian point is computed as zstar - eps to ensure strict
            improvement over the ideal point. Division by zero is avoided
            by replacing zero denominators with 1e-12.
        """
        za, zb, znad, zstar = map(np.asarray, (za, zb, znad, zstar))
        if eps is None:
            eps = 1e-6
        if np.isscalar(eps):
            eps = np.full_like(zstar, eps, dtype=float)

        # Compute utopian point (strictly better than ideal)
        z_utopian = zstar - eps

        # Compute normalization denominator
        denom = znad - z_utopian

        # Avoid division by zero when nadir ≈ utopian
        denom = np.where(denom <= 0, 1e-12, denom)

        # Compute normalized difference and Euclidean distance
        diff = (za - zb) / denom
        return np.sqrt(np.sum(diff**2))

    @staticmethod
    def are_neighbors(za: np.ndarray, zb: np.ndarray, solutions: np.ndarray) -> bool:
        """Check if two solutions are neighbors in the context of a solution set.

        Two solutions za and zb are considered neighbors if their componentwise
        minimum is not dominated by any other solution in the set.

        Args:
            za (np.ndarray): First solution vector of shape (n_objectives,).
            zb (np.ndarray): Second solution vector of shape (n_objectives,).
            solutions (np.ndarray): Complete solution set with shape
                (n_solutions, n_objectives).

        Returns:
            bool: True if za and zb are neighbors, False otherwise.

        Note:
            The componentwise minimum z_ab = min(za, zb) represents a point
            that is at least as good as both za and zb in all objectives.
            If any other solution dominates z_ab, then za and zb are not
            considered neighbors.
        """
        za = np.asarray(za)
        zb = np.asarray(zb)
        z_ab = np.minimum(za, zb)  # Componentwise minimum

        for i in range(len(solutions)):
            zc = solutions[i]
            # Skip comparing to za and zb themselves
            if np.array_equal(zc, za) or np.array_equal(zc, zb):
                continue
            if nds.dominates(z_ab, zc):
                return False
        return True

    def generate_preference_learning(self, front: np.ndarray) -> np.ndarray:
        """Generate preference during the learning phase through systematic exploration.

        The learning phase explores the Pareto front by identifying neighboring
        solution pairs with the maximum normalized Euclidean distance.

        Args:
            front (np.ndarray): Current Pareto front with shape (n_solutions, n_objectives).

        Returns:
            np.ndarray: New reference point derived from the most distant neighbors.

        Note:
            The reference point is set as the componentwise minimum of the
            most distant neighboring pair, which represents an aspirational
            point that is better than both neighbors in all objectives.

        Todo:
            Validate that the same region has not been selected before to
            avoid redundant exploration.
        """
        # Extend front with extreme solutions for comprehensive exploration
        extended_set = np.append(front, self.extreme_solutions, axis=0)

        neighbors_1 = []
        neighbors_2 = []
        euclidean_distances = []

        # Find all neighboring pairs and compute their distances
        for i in range(extended_set.shape[0] - 1):
            z1 = extended_set[i, :]
            for j in range(i + 1, extended_set.shape[0]):
                z2 = extended_set[j, :]
                if self.are_neighbors(z1, z2, extended_set):
                    neighbors_1.append(z1)
                    neighbors_2.append(z2)
                    euclidean_distances.append(
                        self.normalized_euclidean_distance(z1, z2, self.true_nadir, self.true_ideal)
                    )

        # Select the pair with maximum distance for exploration
        max_distance_idx = np.argmax(euclidean_distances)

        # Generate reference point as componentwise minimum of the distant pair
        return np.minimum(neighbors_1[max_distance_idx], neighbors_2[max_distance_idx])

    def utility_function(
        self,
        z: np.ndarray,
        zstar: np.ndarray,
        znad: np.ndarray,
        weight: np.ndarray,
        utility_type: str = "deterministic",
        eps: float | None = None,
    ) -> float:
        """Compute the utility function value for a given solution.

        The utility function measures the maximum weighted normalized distance
        from the utopian point. Lower values indicate better solutions.

        Args:
            z (np.ndarray): Solution vector of shape (n_objectives,).
            zstar (np.ndarray): Ideal point of shape (n_objectives,).
            znad (np.ndarray): Nadir point of shape (n_objectives,).
            weight (np.ndarray): Objective weights of shape (n_objectives,).
            utility_type (str): Type of utility function. Options: 'deterministic', 'random'.
                Defaults to 'deterministic'.
            eps (float | None): Small positive value for utopian shift.
                Defaults to 1e-6.

        Returns:
            float: Utility function value (lower is better).

        Note:
            When utility_type='random', Gaussian noise is added with standard deviation
            that decreases over iterations to simulate learning behavior.
            The noise magnitude is based on the utility function range.
        """
        z, znad, zstar, weight = map(np.asarray, (z, znad, zstar, weight))
        if eps is None:
            eps = 1e-6
        if np.isscalar(eps):
            eps = np.full_like(zstar, eps, dtype=float)

        # Compute utopian point (strictly better than ideal)
        z_utopian = zstar - eps

        # Compute normalization denominator
        denom = znad - z_utopian

        # Avoid division by zero
        denom = np.where(denom <= 0, 1e-12, denom)

        # Compute weighted normalized distances
        diff = weight * ((z - z_utopian) / denom)
        u_minus = np.max(diff)  # Chebyshev scalarization

        # Add random component if requested
        if utility_type == "random":
            # Noise decreases over iterations to simulate learning
            sigma = (self.UF_max - self.UF_opt) * 0.2 / (2 ** (self.it_decision_phase - 1))
            noise = self.rng.uniform(low=0, high=sigma)
            u_minus = noise + u_minus

        return u_minus

    def generate_preference_decision(self, front: np.ndarray) -> np.ndarray:
        """Generate preference during the decision phase by selecting the best solution.

        In the decision phase, the ADM acts more decisively by evaluating all
        solutions in the current front using the utility function and selecting
        the one with minimum disutility (best utility value). This represents
        the final decision-making behavior after the learning phase.

        Args:
            front (np.ndarray): Current Pareto front with shape (n_solutions, n_objectives).

        Returns:
            np.ndarray: The solution with minimum disutility as the preferred reference point.

        Note:
            The returned solution represents the ADM's final preference and
            should be close to the decision maker's most preferred solution.
        """
        min_disutility = np.inf
        preferred_solution = None

        # Evaluate all solutions and find the one with minimum disutility
        for solution in front:
            disutility = self.utility_function(solution, self.true_ideal, self.true_nadir, self.weights)
            if disutility < min_disutility:
                min_disutility = disutility
                preferred_solution = solution

        return preferred_solution
__init__
__init__(
    problem: Problem,
    it_learning_phase: int,
    it_decision_phase: int,
    pareto_front: ndarray,
    initial_reference_point: ndarray | None = None,
    seed: int | None = None,
)

Initialize the artificial decision maker proposed by Chen et al.

Parameters:

Name Type Description Default
problem Problem

The multiobjective optimization problem.

required
it_learning_phase int

Number of iterations for the learning phase.

required
it_decision_phase int

Number of iterations for the decision phase.

required
pareto_front ndarray

Known Pareto front solutions for initialization.

required
initial_reference_point ndarray | None

Initial reference point. If None, a random point between the ideal and nadir is generated.

None
seed int | None

Optional seed for the random number generator. Defaults to None.

None
Source code in desdeo/adm/adm_chen.py
def __init__(
    self,
    problem: Problem,
    it_learning_phase: int,
    it_decision_phase: int,
    pareto_front: np.ndarray,
    initial_reference_point: np.ndarray | None = None,
    seed: int | None = None,
):
    """Initialize the artificial decision maker proposed by Chen et al.

    Args:
        problem (Problem): The multiobjective optimization problem.
        it_learning_phase (int): Number of iterations for the learning phase.
        it_decision_phase (int): Number of iterations for the decision phase.
        pareto_front (np.ndarray): Known Pareto front solutions for initialization.
        initial_reference_point (np.ndarray | None): Initial reference point. If
            None, a random point between the ideal and nadir is generated.
        seed (int | None): Optional seed for the random number generator. Defaults to None.
    """
    # Initialize problem with true ideal and nadir points
    self.true_ideal, self.true_nadir = payoff_table_method(problem)
    problem = problem.update_ideal_and_nadir(new_ideal=self.true_ideal, new_nadir=self.true_nadir)
    super().__init__(problem, it_learning_phase, it_decision_phase, seed=seed)

    # Store problem dimensions
    self.num_objectives = len(problem.objectives)
    self.num_variables = len(problem.variables)

    # Generate initial preference
    self.preference = self.generate_initial_preference(initial_reference_point)
    self.iteration_counter += 1

    # Initialize equal weights for all objectives
    # NOTE: In the original article, weights were set manually
    self.weights = np.ones(self.num_objectives) / self.num_objectives

    # Compute utility function bounds on the Pareto front
    self.UF_max = np.max(
        [self.utility_function(sol, self.true_ideal, self.true_nadir, self.weights) for sol in pareto_front]
    )
    self.UF_opt = np.min(
        [self.utility_function(sol, self.true_ideal, self.true_nadir, self.weights) for sol in pareto_front]
    )

    # Store extreme solutions from the
    self.extreme_solutions = self.get_extreme_solutions(pareto_front)
are_neighbors staticmethod
are_neighbors(
    za: ndarray, zb: ndarray, solutions: ndarray
) -> bool

Check if two solutions are neighbors in the context of a solution set.

Two solutions za and zb are considered neighbors if their componentwise minimum is not dominated by any other solution in the set.

Parameters:

Name Type Description Default
za ndarray

First solution vector of shape (n_objectives,).

required
zb ndarray

Second solution vector of shape (n_objectives,).

required
solutions ndarray

Complete solution set with shape (n_solutions, n_objectives).

required

Returns:

Name Type Description
bool bool

True if za and zb are neighbors, False otherwise.

Note

The componentwise minimum z_ab = min(za, zb) represents a point that is at least as good as both za and zb in all objectives. If any other solution dominates z_ab, then za and zb are not considered neighbors.

Source code in desdeo/adm/adm_chen.py
@staticmethod
def are_neighbors(za: np.ndarray, zb: np.ndarray, solutions: np.ndarray) -> bool:
    """Check if two solutions are neighbors in the context of a solution set.

    Two solutions za and zb are considered neighbors if their componentwise
    minimum is not dominated by any other solution in the set.

    Args:
        za (np.ndarray): First solution vector of shape (n_objectives,).
        zb (np.ndarray): Second solution vector of shape (n_objectives,).
        solutions (np.ndarray): Complete solution set with shape
            (n_solutions, n_objectives).

    Returns:
        bool: True if za and zb are neighbors, False otherwise.

    Note:
        The componentwise minimum z_ab = min(za, zb) represents a point
        that is at least as good as both za and zb in all objectives.
        If any other solution dominates z_ab, then za and zb are not
        considered neighbors.
    """
    za = np.asarray(za)
    zb = np.asarray(zb)
    z_ab = np.minimum(za, zb)  # Componentwise minimum

    for i in range(len(solutions)):
        zc = solutions[i]
        # Skip comparing to za and zb themselves
        if np.array_equal(zc, za) or np.array_equal(zc, zb):
            continue
        if nds.dominates(z_ab, zc):
            return False
    return True
generate_initial_preference
generate_initial_preference(
    initial_reference_point: ndarray | None = None,
) -> np.ndarray

Generate the initial reference point for the ADM.

If an initial reference point is provided, it validates that the point lies between the ideal and nadir points. Otherwise, generates a random point within the feasible objective space.

Parameters:

Name Type Description Default
initial_reference_point Optional[ndarray]

User-specified initial reference point. Must be between ideal and nadir points.

None

Returns:

Type Description
ndarray

np.ndarray: Valid initial reference point.

Raises:

Type Description
ValueError

If the provided reference point is outside the valid range.

Source code in desdeo/adm/adm_chen.py
def generate_initial_preference(self, initial_reference_point: np.ndarray | None = None) -> np.ndarray:
    """Generate the initial reference point for the ADM.

    If an initial reference point is provided, it validates that the point lies
    between the ideal and nadir points. Otherwise, generates a random point
    within the feasible objective space.

    Args:
        initial_reference_point (Optional[np.ndarray]): User-specified initial
            reference point. Must be between ideal and nadir points.

    Returns:
        np.ndarray: Valid initial reference point.

    Raises:
        ValueError: If the provided reference point is outside the valid range.
    """
    if initial_reference_point is not None:
        if not (self.true_ideal <= initial_reference_point <= self.true_nadir).all():
            raise ValueError("Initial reference point must be between the ideal and nadir points.")
        return initial_reference_point
    # Generate random reference point between ideal and nadir
    return np.array(
        [
            self.rng.uniform(min_val, max_val)
            for min_val, max_val in zip(
                self.problem.get_ideal_point().values(),
                self.problem.get_nadir_point().values(),
                strict=True,
            )
        ]
    )
generate_preference_decision
generate_preference_decision(front: ndarray) -> np.ndarray

Generate preference during the decision phase by selecting the best solution.

In the decision phase, the ADM acts more decisively by evaluating all solutions in the current front using the utility function and selecting the one with minimum disutility (best utility value). This represents the final decision-making behavior after the learning phase.

Parameters:

Name Type Description Default
front ndarray

Current Pareto front with shape (n_solutions, n_objectives).

required

Returns:

Type Description
ndarray

np.ndarray: The solution with minimum disutility as the preferred reference point.

Note

The returned solution represents the ADM's final preference and should be close to the decision maker's most preferred solution.

Source code in desdeo/adm/adm_chen.py
def generate_preference_decision(self, front: np.ndarray) -> np.ndarray:
    """Generate preference during the decision phase by selecting the best solution.

    In the decision phase, the ADM acts more decisively by evaluating all
    solutions in the current front using the utility function and selecting
    the one with minimum disutility (best utility value). This represents
    the final decision-making behavior after the learning phase.

    Args:
        front (np.ndarray): Current Pareto front with shape (n_solutions, n_objectives).

    Returns:
        np.ndarray: The solution with minimum disutility as the preferred reference point.

    Note:
        The returned solution represents the ADM's final preference and
        should be close to the decision maker's most preferred solution.
    """
    min_disutility = np.inf
    preferred_solution = None

    # Evaluate all solutions and find the one with minimum disutility
    for solution in front:
        disutility = self.utility_function(solution, self.true_ideal, self.true_nadir, self.weights)
        if disutility < min_disutility:
            min_disutility = disutility
            preferred_solution = solution

    return preferred_solution
generate_preference_learning
generate_preference_learning(front: ndarray) -> np.ndarray

Generate preference during the learning phase through systematic exploration.

The learning phase explores the Pareto front by identifying neighboring solution pairs with the maximum normalized Euclidean distance.

Parameters:

Name Type Description Default
front ndarray

Current Pareto front with shape (n_solutions, n_objectives).

required

Returns:

Type Description
ndarray

np.ndarray: New reference point derived from the most distant neighbors.

Note

The reference point is set as the componentwise minimum of the most distant neighboring pair, which represents an aspirational point that is better than both neighbors in all objectives.

Todo

Validate that the same region has not been selected before to avoid redundant exploration.

Source code in desdeo/adm/adm_chen.py
def generate_preference_learning(self, front: np.ndarray) -> np.ndarray:
    """Generate preference during the learning phase through systematic exploration.

    The learning phase explores the Pareto front by identifying neighboring
    solution pairs with the maximum normalized Euclidean distance.

    Args:
        front (np.ndarray): Current Pareto front with shape (n_solutions, n_objectives).

    Returns:
        np.ndarray: New reference point derived from the most distant neighbors.

    Note:
        The reference point is set as the componentwise minimum of the
        most distant neighboring pair, which represents an aspirational
        point that is better than both neighbors in all objectives.

    Todo:
        Validate that the same region has not been selected before to
        avoid redundant exploration.
    """
    # Extend front with extreme solutions for comprehensive exploration
    extended_set = np.append(front, self.extreme_solutions, axis=0)

    neighbors_1 = []
    neighbors_2 = []
    euclidean_distances = []

    # Find all neighboring pairs and compute their distances
    for i in range(extended_set.shape[0] - 1):
        z1 = extended_set[i, :]
        for j in range(i + 1, extended_set.shape[0]):
            z2 = extended_set[j, :]
            if self.are_neighbors(z1, z2, extended_set):
                neighbors_1.append(z1)
                neighbors_2.append(z2)
                euclidean_distances.append(
                    self.normalized_euclidean_distance(z1, z2, self.true_nadir, self.true_ideal)
                )

    # Select the pair with maximum distance for exploration
    max_distance_idx = np.argmax(euclidean_distances)

    # Generate reference point as componentwise minimum of the distant pair
    return np.minimum(neighbors_1[max_distance_idx], neighbors_2[max_distance_idx])
get_extreme_solutions
get_extreme_solutions(front: ndarray) -> np.ndarray

Extract extreme solutions from the Pareto front.

An extreme solution is defined as the objective vector that minimizes one of the objective functions on the Pareto front. These solutions represent the boundaries of the achievable objective space.

Parameters:

Name Type Description Default
front ndarray

Pareto front with shape (n_solutions, n_objectives).

required

Returns:

Type Description
ndarray

np.ndarray: Array of extreme solutions with shape (n_objectives, n_objectives). Each row represents an extreme solution for one objective.

Example

For a 2-objective problem with front = [[1, 3], [2, 2], [3, 1]]: - Extreme for obj 1: [1, 3] (min value 1 in first objective) - Extreme for obj 2: [3, 1] (min value 1 in second objective)

Source code in desdeo/adm/adm_chen.py
def get_extreme_solutions(self, front: np.ndarray) -> np.ndarray:
    """Extract extreme solutions from the Pareto front.

    An extreme solution is defined as the objective vector that minimizes
    one of the objective functions on the Pareto front. These solutions
    represent the boundaries of the achievable objective space.

    Args:
        front (np.ndarray): Pareto front with shape (n_solutions, n_objectives).

    Returns:
        np.ndarray: Array of extreme solutions with shape (n_objectives, n_objectives).
            Each row represents an extreme solution for one objective.

    Example:
        For a 2-objective problem with front = [[1, 3], [2, 2], [3, 1]]:
        - Extreme for obj 1: [1, 3] (min value 1 in first objective)
        - Extreme for obj 2: [3, 1] (min value 1 in second objective)
    """
    extreme_solutions = []
    for i in range(front.shape[1]):  # For each objective
        idx_min_i = np.argmin(front[:, i])  # Find solution with minimum value
        extreme_solutions.append(front[idx_min_i])
    return np.array(extreme_solutions)
get_next_preference
get_next_preference(front: ndarray) -> np.ndarray

Get the next preference (reference point) based on the current iteration phase.

This method determines whether the ADM is in the learning or decision phase and calls the appropriate preference generation method.

Parameters:

Name Type Description Default
front ndarray

Current Pareto front approximation with shape (n_solutions, n_objectives).

required

Returns:

Type Description
ndarray

np.ndarray: Next reference point for the interactive method.

Source code in desdeo/adm/adm_chen.py
def get_next_preference(self, front: np.ndarray) -> np.ndarray:
    """Get the next preference (reference point) based on the current iteration phase.

    This method determines whether the ADM is in the learning or decision phase
    and calls the appropriate preference generation method.

    Args:
        front (np.ndarray): Current Pareto front approximation with shape
            (n_solutions, n_objectives).

    Returns:
        np.ndarray: Next reference point for the interactive method.
    """
    if self.iteration_counter < self.it_learning_phase:
        self.preference = self.generate_preference_learning(front)
    else:
        self.preference = self.generate_preference_decision(front)
    self.iteration_counter += 1
    return self.preference
normalized_euclidean_distance staticmethod
normalized_euclidean_distance(
    za: ndarray,
    zb: ndarray,
    znad: ndarray,
    zstar: ndarray,
    eps: float | None = None,
) -> float

Compute normalized Euclidean distance between two solutions.

The normalization is performed using the range between the utopian point (ideal - eps) and the nadir point. This ensures that the distance metric is scale-independent across different objectives.

Parameters:

Name Type Description Default
za ndarray

First solution vector of shape (n_objectives,).

required
zb ndarray

Second solution vector of shape (n_objectives,).

required
znad ndarray

Nadir point (worst values) of shape (n_objectives,).

required
zstar ndarray

Ideal point (best values) of shape (n_objectives,).

required
eps Optional[float]

Small positive value for utopian shift. Defaults to 1e-6 if None.

None

Returns:

Name Type Description
float float

Normalized Euclidean distance between za and zb.

Note

The utopian point is computed as zstar - eps to ensure strict improvement over the ideal point. Division by zero is avoided by replacing zero denominators with 1e-12.

Source code in desdeo/adm/adm_chen.py
@staticmethod
def normalized_euclidean_distance(
    za: np.ndarray, zb: np.ndarray, znad: np.ndarray, zstar: np.ndarray, eps: float | None = None
) -> float:
    """Compute normalized Euclidean distance between two solutions.

    The normalization is performed using the range between the utopian point
    (ideal - eps) and the nadir point. This ensures that the distance metric
    is scale-independent across different objectives.

    Args:
        za (np.ndarray): First solution vector of shape (n_objectives,).
        zb (np.ndarray): Second solution vector of shape (n_objectives,).
        znad (np.ndarray): Nadir point (worst values) of shape (n_objectives,).
        zstar (np.ndarray): Ideal point (best values) of shape (n_objectives,).
        eps (Optional[float]): Small positive value for utopian shift.
            Defaults to 1e-6 if None.

    Returns:
        float: Normalized Euclidean distance between za and zb.

    Note:
        The utopian point is computed as zstar - eps to ensure strict
        improvement over the ideal point. Division by zero is avoided
        by replacing zero denominators with 1e-12.
    """
    za, zb, znad, zstar = map(np.asarray, (za, zb, znad, zstar))
    if eps is None:
        eps = 1e-6
    if np.isscalar(eps):
        eps = np.full_like(zstar, eps, dtype=float)

    # Compute utopian point (strictly better than ideal)
    z_utopian = zstar - eps

    # Compute normalization denominator
    denom = znad - z_utopian

    # Avoid division by zero when nadir ≈ utopian
    denom = np.where(denom <= 0, 1e-12, denom)

    # Compute normalized difference and Euclidean distance
    diff = (za - zb) / denom
    return np.sqrt(np.sum(diff**2))
utility_function
utility_function(
    z: ndarray,
    zstar: ndarray,
    znad: ndarray,
    weight: ndarray,
    utility_type: str = "deterministic",
    eps: float | None = None,
) -> float

Compute the utility function value for a given solution.

The utility function measures the maximum weighted normalized distance from the utopian point. Lower values indicate better solutions.

Parameters:

Name Type Description Default
z ndarray

Solution vector of shape (n_objectives,).

required
zstar ndarray

Ideal point of shape (n_objectives,).

required
znad ndarray

Nadir point of shape (n_objectives,).

required
weight ndarray

Objective weights of shape (n_objectives,).

required
utility_type str

Type of utility function. Options: 'deterministic', 'random'. Defaults to 'deterministic'.

'deterministic'
eps float | None

Small positive value for utopian shift. Defaults to 1e-6.

None

Returns:

Name Type Description
float float

Utility function value (lower is better).

Note

When utility_type='random', Gaussian noise is added with standard deviation that decreases over iterations to simulate learning behavior. The noise magnitude is based on the utility function range.

Source code in desdeo/adm/adm_chen.py
def utility_function(
    self,
    z: np.ndarray,
    zstar: np.ndarray,
    znad: np.ndarray,
    weight: np.ndarray,
    utility_type: str = "deterministic",
    eps: float | None = None,
) -> float:
    """Compute the utility function value for a given solution.

    The utility function measures the maximum weighted normalized distance
    from the utopian point. Lower values indicate better solutions.

    Args:
        z (np.ndarray): Solution vector of shape (n_objectives,).
        zstar (np.ndarray): Ideal point of shape (n_objectives,).
        znad (np.ndarray): Nadir point of shape (n_objectives,).
        weight (np.ndarray): Objective weights of shape (n_objectives,).
        utility_type (str): Type of utility function. Options: 'deterministic', 'random'.
            Defaults to 'deterministic'.
        eps (float | None): Small positive value for utopian shift.
            Defaults to 1e-6.

    Returns:
        float: Utility function value (lower is better).

    Note:
        When utility_type='random', Gaussian noise is added with standard deviation
        that decreases over iterations to simulate learning behavior.
        The noise magnitude is based on the utility function range.
    """
    z, znad, zstar, weight = map(np.asarray, (z, znad, zstar, weight))
    if eps is None:
        eps = 1e-6
    if np.isscalar(eps):
        eps = np.full_like(zstar, eps, dtype=float)

    # Compute utopian point (strictly better than ideal)
    z_utopian = zstar - eps

    # Compute normalization denominator
    denom = znad - z_utopian

    # Avoid division by zero
    denom = np.where(denom <= 0, 1e-12, denom)

    # Compute weighted normalized distances
    diff = weight * ((z - z_utopian) / denom)
    u_minus = np.max(diff)  # Chebyshev scalarization

    # Add random component if requested
    if utility_type == "random":
        # Noise decreases over iterations to simulate learning
        sigma = (self.UF_max - self.UF_opt) * 0.2 / (2 ** (self.it_decision_phase - 1))
        noise = self.rng.uniform(low=0, high=sigma)
        u_minus = noise + u_minus

    return u_minus

desdeo.adm.adm_afsar

Artificial decision maker (ADM) based on the approach by Afsar et al.

ADMAfsar

Bases: BaseADM

Adaptive Decision Maker using the AFSAR approach.

This ADM generates preferences for interactive evolutionary multiobjective optimization based on the method described in:

Afsar, B., Miettinen, K., & Ruiz, A. B. (2021). An Artificial Decision Maker for Comparing Reference Point Based Interactive Evolutionary Multiobjective Optimization Methods. In: Ishibuchi, H., et al. Evolutionary Multi-Criterion Optimization. EMO 2021. Lecture Notes in Computer Science, vol 12654. Springer, Cham.

Afsar, B., Ruiz, A. B., & Miettinen, K. (2023). Comparing interactive evolutionary multiobjective optimization methods with an artificial decision maker. Complex & Intelligent Systems, Volume 9, pages 1165-1181. Springer.

Attributes:

Name Type Description
composite_front list

Stores the composite front of solutions.

max_assigned_vector int or None

Index of the vector with the maximum assigned solutions.

reference_vectors ndarray

Array of reference vectors.

preference dict

Current preference information.

Source code in desdeo/adm/adm_afsar.py
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
class ADMAfsar(BaseADM):
    """Adaptive Decision Maker using the AFSAR approach.

    This ADM generates preferences for interactive evolutionary multiobjective optimization
    based on the method described in:

    > Afsar, B., Miettinen, K., & Ruiz, A. B. (2021). An Artificial Decision Maker
    > for Comparing Reference Point Based Interactive Evolutionary Multiobjective
    > Optimization Methods. In: Ishibuchi, H., et al. Evolutionary Multi-Criterion
    > Optimization. EMO 2021. Lecture Notes in Computer Science, vol 12654.
    > Springer, Cham.

    > Afsar, B., Ruiz, A. B., & Miettinen, K. (2023).
    > Comparing interactive evolutionary multiobjective optimization methods with an artificial decision maker.
    > Complex & Intelligent Systems, Volume 9, pages 1165-1181. Springer.

    Attributes:
        composite_front (list): Stores the composite front of solutions.
        max_assigned_vector (int or None): Index of the vector with the maximum assigned solutions.
        reference_vectors (np.ndarray): Array of reference vectors.
        preference (dict): Current preference information.
    """

    def __init__(
        self,
        problem: Problem,
        it_learning_phase: int,
        it_decision_phase: int,
        lattice_resolution: int | None = None,
        number_of_vectors: int | None = None,
        seed: int | None = None,
    ):
        """Initialize the artificial decision maker proposed by Afsar et al.

        Args:
            problem (Problem): The optimization problem to solve.
            it_learning_phase (int): Number of iterations for the learning phase.
            it_decision_phase (int): Number of iterations for the decision phase.
            lattice_resolution (int, optional): Lattice resolution for reference vectors.
            number_of_vectors (int, optional): Number of reference vectors.
            seed (int | None): Optional seed for the random number generator. Defaults to None.
        """
        self.true_ideal, self.true_nadir = payoff_table_method(problem)
        problem = problem.update_ideal_and_nadir(new_ideal=self.true_ideal, new_nadir=self.true_nadir)
        super().__init__(problem, it_learning_phase, it_decision_phase, seed=seed)
        self.composite_front = []
        self.max_assigned_vector = None
        self.preference_type = "reference_point"
        number_of_objectives = len(problem.objectives)

        self.reference_vectors = create_simplex(number_of_objectives, lattice_resolution, number_of_vectors)
        self.true_ideal, self.true_nadir = payoff_table_method(problem)

        self.generate_initial_preference()

    def generate_initial_preference(self):
        """Generate the initial preference as a random point between the ideal and nadir points.

        The preference is stored in self.preference as a numpy array.
        """
        self.preference = np.array(
            [
                self.rng.uniform(min_val, max_val)
                for min_val, max_val in zip(
                    self.problem.get_ideal_point().values(),
                    self.problem.get_nadir_point().values(),
                    strict=True,
                )
            ]
        )

    def get_next_preference(self, *fronts: np.ndarray, preference_type: str = "reference_point") -> np.ndarray:
        """Generate the next preference based on the current phase and provided solution fronts.

        Args:
            *fronts: One or more solution fronts (arrays) to be considered.
            preference_type (str): The type of preference to generate.

        Returns:
            np.ndarray: The generated preference information.
        """
        self.preference_type = preference_type
        if len(self.composite_front) == 0:
            self.composite_front = self.generate_composite_front(*fronts)
        else:
            self.composite_front = self.generate_composite_front(self.composite_front, *fronts)
        ideal_point = self.composite_front.min(axis=0)
        translated_front = self.translate_front(self.composite_front, ideal_point)
        normalized_front = self.normalize_front(self.composite_front, translated_front)
        assigned_vectors = self.assign_vectors(normalized_front)
        if self.iteration_counter < self.it_learning_phase:
            self.preference = self.generate_preference_learning(ideal_point, translated_front, assigned_vectors)
        else:
            if self.iteration_counter == self.it_learning_phase:
                self.max_assigned_vector = self.get_max_assigned_vector(assigned_vectors)
            self.preference = self.generate_preference_decision(
                ideal_point,
                translated_front,
                assigned_vectors,
                self.max_assigned_vector,
            )
        self.iteration_counter += 1
        return self.preference

    def assign_vectors(self, front) -> np.ndarray:
        """Assign each solution in the front to the closest reference vector using cosine similarity.

        Args:
            front (np.ndarray): The normalized solution front.

        Returns:
            np.ndarray: Indices of the assigned reference vectors for each solution.
        """
        cosine = np.dot(front, np.transpose(self.reference_vectors))
        if cosine[np.where(cosine > 1)].size:
            cosine[np.where(cosine > 1)] = 1
        if cosine[np.where(cosine < 0)].size:
            cosine[np.where(cosine < 0)] = 0

        return np.argmax(cosine, axis=1)

    def normalize_front(self, front, translated_front) -> np.ndarray:
        """Normalize the translated front so that each solution has unit length.

        Args:
            front (np.ndarray): The original solution front.
            translated_front (np.ndarray): The translated solution front.

        Returns:
            np.ndarray: The normalized solution front.
        """
        translated_norm = np.linalg.norm(translated_front, axis=1)
        translated_norm = np.repeat(translated_norm, len(translated_front[0, :])).reshape(len(front), len(front[0, :]))

        translated_norm[translated_norm == 0] = np.finfo(float).eps
        return np.divide(translated_front, translated_norm)

    def generate_composite_front(self, *fronts: np.ndarray) -> np.ndarray:
        """Generate the composite front by stacking and extracting the non-dominated solutions.

        Args:
            *fronts: One or more solution fronts (arrays).

        Returns:
            np.ndarray: The composite non-dominated front.
        """
        _fronts = np.vstack(fronts)
        return _fronts[nds(_fronts)]

    def translate_front(self, front, ideal) -> np.ndarray:
        """Translate the front by subtracting the ideal point from each solution.

        Args:
            front (np.ndarray): The solution front.
            ideal (np.ndarray): The ideal point.

        Returns:
            np.ndarray: The translated front.
        """
        return np.subtract(front, ideal)

    def generate_preference_learning(self, ideal_point, translated_front, assigned_vectors) -> np.ndarray:
        """Generate preference information during the learning phase.

        The preference is generated according to the selected preference type:
        - 'reference_point': Returns a reference point.
        - 'preferred_ranges': Returns a preferred range.
        - 'preferred_solutions': Returns preferred solutions.

        Args:
            ideal_point (np.ndarray): The ideal point.
            translated_front (np.ndarray): The translated solution front.
            assigned_vectors (np.ndarray): Indices of assigned reference vectors.

        Returns:
            np.ndarray: The generated preference information.
        """
        if self.preference_type == "reference_point":
            return self.generate_reference_point_learning(ideal_point, translated_front, assigned_vectors)
        if self.preference_type == "preferred_ranges":
            return self.generate_ranges_learning(ideal_point, translated_front, assigned_vectors)
        if self.preference_type == "preferred_solutions":
            return self.generate_preferred_solutions_learning(ideal_point, translated_front, assigned_vectors)

        raise ValueError(
            f"Invalid preference type: {self.preference_type}. "
            "Valid options are 'reference_point', 'preferred_ranges', "
            "'preferred_solutions', or 'non_preferred_solutions'."
        )

    def generate_preference_decision(
        self, ideal_point, translated_front, assigned_vectors, max_assigned_vector
    ) -> np.ndarray:
        """Generate preference information during the decision phase.

        The preference is generated according to the selected preference type:
        - 'reference_point': Returns a reference point.
        - 'preferred_ranges': Returns a preferred range.
        - 'preferred_solutions': Returns preferred solutions.

        Args:
            ideal_point (np.ndarray): The ideal point.
            translated_front (np.ndarray): The translated solution front.
            assigned_vectors (np.ndarray): Indices of assigned reference vectors.
            max_assigned_vector (int): Index of the reference vector with the maximum assigned solutions.

        Returns:
            np.ndarray: The generated preference information.
        """
        if self.preference_type == "reference_point":
            return self.generate_reference_point_decision(
                ideal_point,
                translated_front,
                assigned_vectors,
                max_assigned_vector,
            )
        if self.preference_type == "preferred_ranges":
            return self.generate_ranges_decision(ideal_point, translated_front, assigned_vectors, max_assigned_vector)
        if self.preference_type == "preferred_solutions":
            return self.generate_preferred_solutions_decision(
                ideal_point, translated_front, assigned_vectors, max_assigned_vector
            )
        raise ValueError(
            f"Invalid preference type: {self.preference_type}. "
            "Valid options are 'reference_point', 'preferred_ranges', "
            "'preferred_solutions', or 'non_preferred_solutions'."
        )

    def get_max_assigned_vector(self, assigned_vectors) -> np.ndarray:
        """Find the reference vector with the maximum number of assigned solutions.

        Args:
            assigned_vectors (np.ndarray): Indices of assigned reference vectors.

        Returns:
            np.ndarray: Indices of the reference vector(s) with the maximum assignments.
        """
        number_assigned = np.bincount(assigned_vectors)
        return np.atleast_1d(
            np.squeeze(np.where(number_assigned == np.max(number_assigned[np.nonzero(number_assigned)])))
        )

    def generate_reference_point_learning(self, ideal_point, translated_front, assigned_vectors) -> np.ndarray:
        """Generate a reference point for the learning phase.

        The reference point is based on the solution assigned to the reference vector with the minimum
        number of assigned solutions and closest to the origin.

        Args:
            ideal_point (np.ndarray): The ideal point.
            translated_front (np.ndarray): The translated solution front.
            assigned_vectors (np.ndarray): Indices of assigned reference vectors.

        Returns:
            np.array: The generated reference point.
        """
        ideal_cf = ideal_point
        translated_cf = translated_front
        number_assigned = np.bincount(assigned_vectors)
        min_assigned_vector = np.atleast_1d(
            np.squeeze(np.where(number_assigned == np.min(number_assigned[np.nonzero(number_assigned)])))
        )
        sub_population_index = np.atleast_1d(np.squeeze(np.where(assigned_vectors == min_assigned_vector[0])))
        sub_population_fitness = translated_cf[sub_population_index]
        sub_pop_fitness_magnitude = np.sqrt(np.sum(np.power(sub_population_fitness, 2), axis=1))
        minidx = np.where(sub_pop_fitness_magnitude == np.nanmin(sub_pop_fitness_magnitude))
        distance_selected = sub_pop_fitness_magnitude[minidx]
        reference_point = distance_selected[0] * self.reference_vectors[min_assigned_vector[0]]
        return np.squeeze(reference_point + ideal_cf)

    def generate_reference_point_decision(
        self, ideal_point, translated_front, assigned_vectors, max_assigned_vector
    ) -> np.ndarray:
        """Generate a reference point for the decision phase.

        The reference point is based on the solution assigned to the reference vector with the maximum
        number of assigned solutions and closest to the origin.

        Args:
            ideal_point (np.ndarray): The ideal point.
            translated_front (np.ndarray): The translated solution front.
            assigned_vectors (np.ndarray): Indices of assigned reference vectors.
            max_assigned_vector (int): Index of the reference vector with the maximum assigned solutions.

        Returns:
            np.ndarray: The generated reference point.
        """
        ideal_cf = ideal_point
        translated_cf = translated_front
        sub_population_index = np.atleast_1d(np.squeeze(np.where(assigned_vectors == max_assigned_vector)))
        sub_population_fitness = translated_cf[sub_population_index]
        sub_pop_fitness_magnitude = np.sqrt(np.sum(np.power(sub_population_fitness, 2), axis=1))
        minidx = np.where(sub_pop_fitness_magnitude == np.nanmin(sub_pop_fitness_magnitude))
        distance_selected = sub_pop_fitness_magnitude[minidx]
        reference_point = distance_selected[0] * self.reference_vectors[max_assigned_vector]
        return np.squeeze(reference_point + ideal_cf)

    def generate_ranges_learning(self, ideal_point, translated_front, assigned_vectors) -> np.ndarray:
        """Generate the preferred ranges for the learning phase.

        Args:
            ideal_point (np.ndarray): The ideal point.
            translated_front (np.ndarray): The translated solution front.
            assigned_vectors (np.ndarray): Indices of assigned reference vectors.

        Returns:
            np.ndarray: an array of ranges.
        """
        number_assigned = np.bincount(assigned_vectors)
        min_assigned_vector = np.atleast_1d(
            np.squeeze(np.where(number_assigned == np.min(number_assigned[np.nonzero(number_assigned)])))
        )
        sub_population_index = np.atleast_1d(np.squeeze(np.where(assigned_vectors == min_assigned_vector[0])))
        sub_population_fitness = translated_front[sub_population_index]
        sub_pop_fitness_magnitude = np.sqrt(np.sum(np.power(sub_population_fitness, 2), axis=1))
        minidx = np.where(sub_pop_fitness_magnitude == np.nanmin(sub_pop_fitness_magnitude))
        distance_selected = sub_pop_fitness_magnitude[minidx]
        reference_point = distance_selected[0] * self.reference_vectors[min_assigned_vector[0]]
        distance = min(np.linalg.norm(reference_point - i) for i in sub_population_fitness)
        reference_point = np.squeeze(reference_point + ideal_point)
        temp = reference_point - distance
        temp2 = reference_point + distance

        true_ideal = np.array(list(self.problem.get_ideal_point().values()))
        true_nadir = np.array(list(self.problem.get_nadir_point().values()))

        for i in range(reference_point.shape[0]):
            reference_point[i] = max(reference_point[i], true_ideal[i])
            reference_point[i] = min(reference_point[i], true_nadir[i])
            temp[i] = max(temp[i], true_ideal[i])
            temp[i] = min(temp[i], true_nadir[i])
            temp2[i] = max(temp2[i], true_ideal[i])
            temp2[i] = min(temp2[i], true_nadir[i])

        # TODO (giomara): return the reference point in some other place
        return np.vstack((temp, temp2)).T

    def generate_ranges_decision(
        self, ideal_point, translated_front, assigned_vectors, max_assigned_vector
    ) -> np.ndarray:
        """Generate the preferred ranges for the decision phase.

        Args:
            ideal_point (np.ndarray): The ideal point.
            translated_front (np.ndarray): The translated solution front.
            assigned_vectors (np.ndarray): Indices of assigned reference vectors.
            max_assigned_vector (int): Index of the reference vector with the maximum assigned solutions.

        Returns:
            np.ndarray: an array of ranges.
        """
        sub_population_index = np.atleast_1d(np.squeeze(np.where(assigned_vectors == max_assigned_vector)))
        sub_population_fitness = translated_front[sub_population_index]
        sub_pop_fitness_magnitude = np.sqrt(np.sum(np.power(sub_population_fitness, 2), axis=1))
        minidx = np.where(sub_pop_fitness_magnitude == np.nanmin(sub_pop_fitness_magnitude))
        distance_selected = sub_pop_fitness_magnitude[minidx]
        reference_point = distance_selected[0] * self.reference_vectors[max_assigned_vector]
        distance = min(np.linalg.norm(reference_point - i) for i in sub_population_fitness)
        reference_point = np.squeeze(reference_point + ideal_point)
        reference_point = np.squeeze(reference_point - distance)
        temp = reference_point - distance
        temp2 = reference_point + distance

        true_ideal = np.array(list(self.problem.get_ideal_point().values()))
        true_nadir = np.array(list(self.problem.get_nadir_point().values()))

        for i in range(reference_point.shape[0]):
            reference_point[i] = max(reference_point[i], true_ideal[i])
            reference_point[i] = min(reference_point[i], true_nadir[i])
            temp[i] = max(temp[i], true_ideal[i])
            temp[i] = min(temp[i], true_nadir[i])
            temp2[i] = max(temp2[i], true_ideal[i])
            temp2[i] = min(temp2[i], true_nadir[i])

        return np.vstack((temp, temp2)).T

    def generate_preferred_solutions_learning(self, ideal_point, translated_front, assigned_vectors) -> np.ndarray:
        """Generate the preferred solutions during the learning phase.

        Args:
            ideal_point (np.ndarray): The ideal point.
            translated_front (np.ndarray): The translated solution front.
            assigned_vectors (np.ndarray): Indices of assigned reference vectors.

        Returns:
            np.ndarray: The preferred solutions.
        """
        number_assigned = np.bincount(assigned_vectors)
        min_assigned_vector = np.atleast_1d(
            np.squeeze(np.where(number_assigned == np.min(number_assigned[np.nonzero(number_assigned)])))
        )
        sub_population_index = np.atleast_1d(np.squeeze(np.where(assigned_vectors == min_assigned_vector[0])))
        sub_population_fitness = translated_front[sub_population_index]
        return np.squeeze(sub_population_fitness + ideal_point)

    def generate_preferred_solutions_decision(
        self, ideal_point, translated_front, assigned_vectors, max_assigned_vector
    ) -> np.ndarray:
        """Generate the preferred solutions during the decision phase.

        Args:
            ideal_point (np.ndarray): The ideal point.
            translated_front (np.ndarray): The translated solution front.
            assigned_vectors (np.ndarray): Indices of assigned reference vectors.
            max_assigned_vector (int): Index of the reference vector with the maximum assigned solutions.

        Returns:
            np.ndarray: The preferred solutions.
        """
        sub_population_index = np.atleast_1d(np.squeeze(np.where(assigned_vectors == max_assigned_vector)))
        sub_population_fitness = translated_front[sub_population_index]
        sub_pop_fitness_magnitude = np.sqrt(np.sum(np.power(sub_population_fitness, 2), axis=1))
        minidx = np.argpartition(sub_pop_fitness_magnitude, 4)
        solution_selected = sub_population_fitness[minidx[:4]]
        return np.squeeze(solution_selected + ideal_point)
__init__
__init__(
    problem: Problem,
    it_learning_phase: int,
    it_decision_phase: int,
    lattice_resolution: int | None = None,
    number_of_vectors: int | None = None,
    seed: int | None = None,
)

Initialize the artificial decision maker proposed by Afsar et al.

Parameters:

Name Type Description Default
problem Problem

The optimization problem to solve.

required
it_learning_phase int

Number of iterations for the learning phase.

required
it_decision_phase int

Number of iterations for the decision phase.

required
lattice_resolution int

Lattice resolution for reference vectors.

None
number_of_vectors int

Number of reference vectors.

None
seed int | None

Optional seed for the random number generator. Defaults to None.

None
Source code in desdeo/adm/adm_afsar.py
def __init__(
    self,
    problem: Problem,
    it_learning_phase: int,
    it_decision_phase: int,
    lattice_resolution: int | None = None,
    number_of_vectors: int | None = None,
    seed: int | None = None,
):
    """Initialize the artificial decision maker proposed by Afsar et al.

    Args:
        problem (Problem): The optimization problem to solve.
        it_learning_phase (int): Number of iterations for the learning phase.
        it_decision_phase (int): Number of iterations for the decision phase.
        lattice_resolution (int, optional): Lattice resolution for reference vectors.
        number_of_vectors (int, optional): Number of reference vectors.
        seed (int | None): Optional seed for the random number generator. Defaults to None.
    """
    self.true_ideal, self.true_nadir = payoff_table_method(problem)
    problem = problem.update_ideal_and_nadir(new_ideal=self.true_ideal, new_nadir=self.true_nadir)
    super().__init__(problem, it_learning_phase, it_decision_phase, seed=seed)
    self.composite_front = []
    self.max_assigned_vector = None
    self.preference_type = "reference_point"
    number_of_objectives = len(problem.objectives)

    self.reference_vectors = create_simplex(number_of_objectives, lattice_resolution, number_of_vectors)
    self.true_ideal, self.true_nadir = payoff_table_method(problem)

    self.generate_initial_preference()
assign_vectors
assign_vectors(front) -> np.ndarray

Assign each solution in the front to the closest reference vector using cosine similarity.

Parameters:

Name Type Description Default
front ndarray

The normalized solution front.

required

Returns:

Type Description
ndarray

np.ndarray: Indices of the assigned reference vectors for each solution.

Source code in desdeo/adm/adm_afsar.py
def assign_vectors(self, front) -> np.ndarray:
    """Assign each solution in the front to the closest reference vector using cosine similarity.

    Args:
        front (np.ndarray): The normalized solution front.

    Returns:
        np.ndarray: Indices of the assigned reference vectors for each solution.
    """
    cosine = np.dot(front, np.transpose(self.reference_vectors))
    if cosine[np.where(cosine > 1)].size:
        cosine[np.where(cosine > 1)] = 1
    if cosine[np.where(cosine < 0)].size:
        cosine[np.where(cosine < 0)] = 0

    return np.argmax(cosine, axis=1)
generate_composite_front
generate_composite_front(*fronts: ndarray) -> np.ndarray

Generate the composite front by stacking and extracting the non-dominated solutions.

Parameters:

Name Type Description Default
*fronts ndarray

One or more solution fronts (arrays).

()

Returns:

Type Description
ndarray

np.ndarray: The composite non-dominated front.

Source code in desdeo/adm/adm_afsar.py
def generate_composite_front(self, *fronts: np.ndarray) -> np.ndarray:
    """Generate the composite front by stacking and extracting the non-dominated solutions.

    Args:
        *fronts: One or more solution fronts (arrays).

    Returns:
        np.ndarray: The composite non-dominated front.
    """
    _fronts = np.vstack(fronts)
    return _fronts[nds(_fronts)]
generate_initial_preference
generate_initial_preference()

Generate the initial preference as a random point between the ideal and nadir points.

The preference is stored in self.preference as a numpy array.

Source code in desdeo/adm/adm_afsar.py
def generate_initial_preference(self):
    """Generate the initial preference as a random point between the ideal and nadir points.

    The preference is stored in self.preference as a numpy array.
    """
    self.preference = np.array(
        [
            self.rng.uniform(min_val, max_val)
            for min_val, max_val in zip(
                self.problem.get_ideal_point().values(),
                self.problem.get_nadir_point().values(),
                strict=True,
            )
        ]
    )
generate_preference_decision
generate_preference_decision(
    ideal_point,
    translated_front,
    assigned_vectors,
    max_assigned_vector,
) -> np.ndarray

Generate preference information during the decision phase.

The preference is generated according to the selected preference type: - 'reference_point': Returns a reference point. - 'preferred_ranges': Returns a preferred range. - 'preferred_solutions': Returns preferred solutions.

Parameters:

Name Type Description Default
ideal_point ndarray

The ideal point.

required
translated_front ndarray

The translated solution front.

required
assigned_vectors ndarray

Indices of assigned reference vectors.

required
max_assigned_vector int

Index of the reference vector with the maximum assigned solutions.

required

Returns:

Type Description
ndarray

np.ndarray: The generated preference information.

Source code in desdeo/adm/adm_afsar.py
def generate_preference_decision(
    self, ideal_point, translated_front, assigned_vectors, max_assigned_vector
) -> np.ndarray:
    """Generate preference information during the decision phase.

    The preference is generated according to the selected preference type:
    - 'reference_point': Returns a reference point.
    - 'preferred_ranges': Returns a preferred range.
    - 'preferred_solutions': Returns preferred solutions.

    Args:
        ideal_point (np.ndarray): The ideal point.
        translated_front (np.ndarray): The translated solution front.
        assigned_vectors (np.ndarray): Indices of assigned reference vectors.
        max_assigned_vector (int): Index of the reference vector with the maximum assigned solutions.

    Returns:
        np.ndarray: The generated preference information.
    """
    if self.preference_type == "reference_point":
        return self.generate_reference_point_decision(
            ideal_point,
            translated_front,
            assigned_vectors,
            max_assigned_vector,
        )
    if self.preference_type == "preferred_ranges":
        return self.generate_ranges_decision(ideal_point, translated_front, assigned_vectors, max_assigned_vector)
    if self.preference_type == "preferred_solutions":
        return self.generate_preferred_solutions_decision(
            ideal_point, translated_front, assigned_vectors, max_assigned_vector
        )
    raise ValueError(
        f"Invalid preference type: {self.preference_type}. "
        "Valid options are 'reference_point', 'preferred_ranges', "
        "'preferred_solutions', or 'non_preferred_solutions'."
    )
generate_preference_learning
generate_preference_learning(
    ideal_point, translated_front, assigned_vectors
) -> np.ndarray

Generate preference information during the learning phase.

The preference is generated according to the selected preference type: - 'reference_point': Returns a reference point. - 'preferred_ranges': Returns a preferred range. - 'preferred_solutions': Returns preferred solutions.

Parameters:

Name Type Description Default
ideal_point ndarray

The ideal point.

required
translated_front ndarray

The translated solution front.

required
assigned_vectors ndarray

Indices of assigned reference vectors.

required

Returns:

Type Description
ndarray

np.ndarray: The generated preference information.

Source code in desdeo/adm/adm_afsar.py
def generate_preference_learning(self, ideal_point, translated_front, assigned_vectors) -> np.ndarray:
    """Generate preference information during the learning phase.

    The preference is generated according to the selected preference type:
    - 'reference_point': Returns a reference point.
    - 'preferred_ranges': Returns a preferred range.
    - 'preferred_solutions': Returns preferred solutions.

    Args:
        ideal_point (np.ndarray): The ideal point.
        translated_front (np.ndarray): The translated solution front.
        assigned_vectors (np.ndarray): Indices of assigned reference vectors.

    Returns:
        np.ndarray: The generated preference information.
    """
    if self.preference_type == "reference_point":
        return self.generate_reference_point_learning(ideal_point, translated_front, assigned_vectors)
    if self.preference_type == "preferred_ranges":
        return self.generate_ranges_learning(ideal_point, translated_front, assigned_vectors)
    if self.preference_type == "preferred_solutions":
        return self.generate_preferred_solutions_learning(ideal_point, translated_front, assigned_vectors)

    raise ValueError(
        f"Invalid preference type: {self.preference_type}. "
        "Valid options are 'reference_point', 'preferred_ranges', "
        "'preferred_solutions', or 'non_preferred_solutions'."
    )
generate_preferred_solutions_decision
generate_preferred_solutions_decision(
    ideal_point,
    translated_front,
    assigned_vectors,
    max_assigned_vector,
) -> np.ndarray

Generate the preferred solutions during the decision phase.

Parameters:

Name Type Description Default
ideal_point ndarray

The ideal point.

required
translated_front ndarray

The translated solution front.

required
assigned_vectors ndarray

Indices of assigned reference vectors.

required
max_assigned_vector int

Index of the reference vector with the maximum assigned solutions.

required

Returns:

Type Description
ndarray

np.ndarray: The preferred solutions.

Source code in desdeo/adm/adm_afsar.py
def generate_preferred_solutions_decision(
    self, ideal_point, translated_front, assigned_vectors, max_assigned_vector
) -> np.ndarray:
    """Generate the preferred solutions during the decision phase.

    Args:
        ideal_point (np.ndarray): The ideal point.
        translated_front (np.ndarray): The translated solution front.
        assigned_vectors (np.ndarray): Indices of assigned reference vectors.
        max_assigned_vector (int): Index of the reference vector with the maximum assigned solutions.

    Returns:
        np.ndarray: The preferred solutions.
    """
    sub_population_index = np.atleast_1d(np.squeeze(np.where(assigned_vectors == max_assigned_vector)))
    sub_population_fitness = translated_front[sub_population_index]
    sub_pop_fitness_magnitude = np.sqrt(np.sum(np.power(sub_population_fitness, 2), axis=1))
    minidx = np.argpartition(sub_pop_fitness_magnitude, 4)
    solution_selected = sub_population_fitness[minidx[:4]]
    return np.squeeze(solution_selected + ideal_point)
generate_preferred_solutions_learning
generate_preferred_solutions_learning(
    ideal_point, translated_front, assigned_vectors
) -> np.ndarray

Generate the preferred solutions during the learning phase.

Parameters:

Name Type Description Default
ideal_point ndarray

The ideal point.

required
translated_front ndarray

The translated solution front.

required
assigned_vectors ndarray

Indices of assigned reference vectors.

required

Returns:

Type Description
ndarray

np.ndarray: The preferred solutions.

Source code in desdeo/adm/adm_afsar.py
def generate_preferred_solutions_learning(self, ideal_point, translated_front, assigned_vectors) -> np.ndarray:
    """Generate the preferred solutions during the learning phase.

    Args:
        ideal_point (np.ndarray): The ideal point.
        translated_front (np.ndarray): The translated solution front.
        assigned_vectors (np.ndarray): Indices of assigned reference vectors.

    Returns:
        np.ndarray: The preferred solutions.
    """
    number_assigned = np.bincount(assigned_vectors)
    min_assigned_vector = np.atleast_1d(
        np.squeeze(np.where(number_assigned == np.min(number_assigned[np.nonzero(number_assigned)])))
    )
    sub_population_index = np.atleast_1d(np.squeeze(np.where(assigned_vectors == min_assigned_vector[0])))
    sub_population_fitness = translated_front[sub_population_index]
    return np.squeeze(sub_population_fitness + ideal_point)
generate_ranges_decision
generate_ranges_decision(
    ideal_point,
    translated_front,
    assigned_vectors,
    max_assigned_vector,
) -> np.ndarray

Generate the preferred ranges for the decision phase.

Parameters:

Name Type Description Default
ideal_point ndarray

The ideal point.

required
translated_front ndarray

The translated solution front.

required
assigned_vectors ndarray

Indices of assigned reference vectors.

required
max_assigned_vector int

Index of the reference vector with the maximum assigned solutions.

required

Returns:

Type Description
ndarray

np.ndarray: an array of ranges.

Source code in desdeo/adm/adm_afsar.py
def generate_ranges_decision(
    self, ideal_point, translated_front, assigned_vectors, max_assigned_vector
) -> np.ndarray:
    """Generate the preferred ranges for the decision phase.

    Args:
        ideal_point (np.ndarray): The ideal point.
        translated_front (np.ndarray): The translated solution front.
        assigned_vectors (np.ndarray): Indices of assigned reference vectors.
        max_assigned_vector (int): Index of the reference vector with the maximum assigned solutions.

    Returns:
        np.ndarray: an array of ranges.
    """
    sub_population_index = np.atleast_1d(np.squeeze(np.where(assigned_vectors == max_assigned_vector)))
    sub_population_fitness = translated_front[sub_population_index]
    sub_pop_fitness_magnitude = np.sqrt(np.sum(np.power(sub_population_fitness, 2), axis=1))
    minidx = np.where(sub_pop_fitness_magnitude == np.nanmin(sub_pop_fitness_magnitude))
    distance_selected = sub_pop_fitness_magnitude[minidx]
    reference_point = distance_selected[0] * self.reference_vectors[max_assigned_vector]
    distance = min(np.linalg.norm(reference_point - i) for i in sub_population_fitness)
    reference_point = np.squeeze(reference_point + ideal_point)
    reference_point = np.squeeze(reference_point - distance)
    temp = reference_point - distance
    temp2 = reference_point + distance

    true_ideal = np.array(list(self.problem.get_ideal_point().values()))
    true_nadir = np.array(list(self.problem.get_nadir_point().values()))

    for i in range(reference_point.shape[0]):
        reference_point[i] = max(reference_point[i], true_ideal[i])
        reference_point[i] = min(reference_point[i], true_nadir[i])
        temp[i] = max(temp[i], true_ideal[i])
        temp[i] = min(temp[i], true_nadir[i])
        temp2[i] = max(temp2[i], true_ideal[i])
        temp2[i] = min(temp2[i], true_nadir[i])

    return np.vstack((temp, temp2)).T
generate_ranges_learning
generate_ranges_learning(
    ideal_point, translated_front, assigned_vectors
) -> np.ndarray

Generate the preferred ranges for the learning phase.

Parameters:

Name Type Description Default
ideal_point ndarray

The ideal point.

required
translated_front ndarray

The translated solution front.

required
assigned_vectors ndarray

Indices of assigned reference vectors.

required

Returns:

Type Description
ndarray

np.ndarray: an array of ranges.

Source code in desdeo/adm/adm_afsar.py
def generate_ranges_learning(self, ideal_point, translated_front, assigned_vectors) -> np.ndarray:
    """Generate the preferred ranges for the learning phase.

    Args:
        ideal_point (np.ndarray): The ideal point.
        translated_front (np.ndarray): The translated solution front.
        assigned_vectors (np.ndarray): Indices of assigned reference vectors.

    Returns:
        np.ndarray: an array of ranges.
    """
    number_assigned = np.bincount(assigned_vectors)
    min_assigned_vector = np.atleast_1d(
        np.squeeze(np.where(number_assigned == np.min(number_assigned[np.nonzero(number_assigned)])))
    )
    sub_population_index = np.atleast_1d(np.squeeze(np.where(assigned_vectors == min_assigned_vector[0])))
    sub_population_fitness = translated_front[sub_population_index]
    sub_pop_fitness_magnitude = np.sqrt(np.sum(np.power(sub_population_fitness, 2), axis=1))
    minidx = np.where(sub_pop_fitness_magnitude == np.nanmin(sub_pop_fitness_magnitude))
    distance_selected = sub_pop_fitness_magnitude[minidx]
    reference_point = distance_selected[0] * self.reference_vectors[min_assigned_vector[0]]
    distance = min(np.linalg.norm(reference_point - i) for i in sub_population_fitness)
    reference_point = np.squeeze(reference_point + ideal_point)
    temp = reference_point - distance
    temp2 = reference_point + distance

    true_ideal = np.array(list(self.problem.get_ideal_point().values()))
    true_nadir = np.array(list(self.problem.get_nadir_point().values()))

    for i in range(reference_point.shape[0]):
        reference_point[i] = max(reference_point[i], true_ideal[i])
        reference_point[i] = min(reference_point[i], true_nadir[i])
        temp[i] = max(temp[i], true_ideal[i])
        temp[i] = min(temp[i], true_nadir[i])
        temp2[i] = max(temp2[i], true_ideal[i])
        temp2[i] = min(temp2[i], true_nadir[i])

    # TODO (giomara): return the reference point in some other place
    return np.vstack((temp, temp2)).T
generate_reference_point_decision
generate_reference_point_decision(
    ideal_point,
    translated_front,
    assigned_vectors,
    max_assigned_vector,
) -> np.ndarray

Generate a reference point for the decision phase.

The reference point is based on the solution assigned to the reference vector with the maximum number of assigned solutions and closest to the origin.

Parameters:

Name Type Description Default
ideal_point ndarray

The ideal point.

required
translated_front ndarray

The translated solution front.

required
assigned_vectors ndarray

Indices of assigned reference vectors.

required
max_assigned_vector int

Index of the reference vector with the maximum assigned solutions.

required

Returns:

Type Description
ndarray

np.ndarray: The generated reference point.

Source code in desdeo/adm/adm_afsar.py
def generate_reference_point_decision(
    self, ideal_point, translated_front, assigned_vectors, max_assigned_vector
) -> np.ndarray:
    """Generate a reference point for the decision phase.

    The reference point is based on the solution assigned to the reference vector with the maximum
    number of assigned solutions and closest to the origin.

    Args:
        ideal_point (np.ndarray): The ideal point.
        translated_front (np.ndarray): The translated solution front.
        assigned_vectors (np.ndarray): Indices of assigned reference vectors.
        max_assigned_vector (int): Index of the reference vector with the maximum assigned solutions.

    Returns:
        np.ndarray: The generated reference point.
    """
    ideal_cf = ideal_point
    translated_cf = translated_front
    sub_population_index = np.atleast_1d(np.squeeze(np.where(assigned_vectors == max_assigned_vector)))
    sub_population_fitness = translated_cf[sub_population_index]
    sub_pop_fitness_magnitude = np.sqrt(np.sum(np.power(sub_population_fitness, 2), axis=1))
    minidx = np.where(sub_pop_fitness_magnitude == np.nanmin(sub_pop_fitness_magnitude))
    distance_selected = sub_pop_fitness_magnitude[minidx]
    reference_point = distance_selected[0] * self.reference_vectors[max_assigned_vector]
    return np.squeeze(reference_point + ideal_cf)
generate_reference_point_learning
generate_reference_point_learning(
    ideal_point, translated_front, assigned_vectors
) -> np.ndarray

Generate a reference point for the learning phase.

The reference point is based on the solution assigned to the reference vector with the minimum number of assigned solutions and closest to the origin.

Parameters:

Name Type Description Default
ideal_point ndarray

The ideal point.

required
translated_front ndarray

The translated solution front.

required
assigned_vectors ndarray

Indices of assigned reference vectors.

required

Returns:

Type Description
ndarray

np.array: The generated reference point.

Source code in desdeo/adm/adm_afsar.py
def generate_reference_point_learning(self, ideal_point, translated_front, assigned_vectors) -> np.ndarray:
    """Generate a reference point for the learning phase.

    The reference point is based on the solution assigned to the reference vector with the minimum
    number of assigned solutions and closest to the origin.

    Args:
        ideal_point (np.ndarray): The ideal point.
        translated_front (np.ndarray): The translated solution front.
        assigned_vectors (np.ndarray): Indices of assigned reference vectors.

    Returns:
        np.array: The generated reference point.
    """
    ideal_cf = ideal_point
    translated_cf = translated_front
    number_assigned = np.bincount(assigned_vectors)
    min_assigned_vector = np.atleast_1d(
        np.squeeze(np.where(number_assigned == np.min(number_assigned[np.nonzero(number_assigned)])))
    )
    sub_population_index = np.atleast_1d(np.squeeze(np.where(assigned_vectors == min_assigned_vector[0])))
    sub_population_fitness = translated_cf[sub_population_index]
    sub_pop_fitness_magnitude = np.sqrt(np.sum(np.power(sub_population_fitness, 2), axis=1))
    minidx = np.where(sub_pop_fitness_magnitude == np.nanmin(sub_pop_fitness_magnitude))
    distance_selected = sub_pop_fitness_magnitude[minidx]
    reference_point = distance_selected[0] * self.reference_vectors[min_assigned_vector[0]]
    return np.squeeze(reference_point + ideal_cf)
get_max_assigned_vector
get_max_assigned_vector(assigned_vectors) -> np.ndarray

Find the reference vector with the maximum number of assigned solutions.

Parameters:

Name Type Description Default
assigned_vectors ndarray

Indices of assigned reference vectors.

required

Returns:

Type Description
ndarray

np.ndarray: Indices of the reference vector(s) with the maximum assignments.

Source code in desdeo/adm/adm_afsar.py
def get_max_assigned_vector(self, assigned_vectors) -> np.ndarray:
    """Find the reference vector with the maximum number of assigned solutions.

    Args:
        assigned_vectors (np.ndarray): Indices of assigned reference vectors.

    Returns:
        np.ndarray: Indices of the reference vector(s) with the maximum assignments.
    """
    number_assigned = np.bincount(assigned_vectors)
    return np.atleast_1d(
        np.squeeze(np.where(number_assigned == np.max(number_assigned[np.nonzero(number_assigned)])))
    )
get_next_preference
get_next_preference(
    *fronts: ndarray,
    preference_type: str = "reference_point",
) -> np.ndarray

Generate the next preference based on the current phase and provided solution fronts.

Parameters:

Name Type Description Default
*fronts ndarray

One or more solution fronts (arrays) to be considered.

()
preference_type str

The type of preference to generate.

'reference_point'

Returns:

Type Description
ndarray

np.ndarray: The generated preference information.

Source code in desdeo/adm/adm_afsar.py
def get_next_preference(self, *fronts: np.ndarray, preference_type: str = "reference_point") -> np.ndarray:
    """Generate the next preference based on the current phase and provided solution fronts.

    Args:
        *fronts: One or more solution fronts (arrays) to be considered.
        preference_type (str): The type of preference to generate.

    Returns:
        np.ndarray: The generated preference information.
    """
    self.preference_type = preference_type
    if len(self.composite_front) == 0:
        self.composite_front = self.generate_composite_front(*fronts)
    else:
        self.composite_front = self.generate_composite_front(self.composite_front, *fronts)
    ideal_point = self.composite_front.min(axis=0)
    translated_front = self.translate_front(self.composite_front, ideal_point)
    normalized_front = self.normalize_front(self.composite_front, translated_front)
    assigned_vectors = self.assign_vectors(normalized_front)
    if self.iteration_counter < self.it_learning_phase:
        self.preference = self.generate_preference_learning(ideal_point, translated_front, assigned_vectors)
    else:
        if self.iteration_counter == self.it_learning_phase:
            self.max_assigned_vector = self.get_max_assigned_vector(assigned_vectors)
        self.preference = self.generate_preference_decision(
            ideal_point,
            translated_front,
            assigned_vectors,
            self.max_assigned_vector,
        )
    self.iteration_counter += 1
    return self.preference
normalize_front
normalize_front(front, translated_front) -> np.ndarray

Normalize the translated front so that each solution has unit length.

Parameters:

Name Type Description Default
front ndarray

The original solution front.

required
translated_front ndarray

The translated solution front.

required

Returns:

Type Description
ndarray

np.ndarray: The normalized solution front.

Source code in desdeo/adm/adm_afsar.py
def normalize_front(self, front, translated_front) -> np.ndarray:
    """Normalize the translated front so that each solution has unit length.

    Args:
        front (np.ndarray): The original solution front.
        translated_front (np.ndarray): The translated solution front.

    Returns:
        np.ndarray: The normalized solution front.
    """
    translated_norm = np.linalg.norm(translated_front, axis=1)
    translated_norm = np.repeat(translated_norm, len(translated_front[0, :])).reshape(len(front), len(front[0, :]))

    translated_norm[translated_norm == 0] = np.finfo(float).eps
    return np.divide(translated_front, translated_norm)
translate_front
translate_front(front, ideal) -> np.ndarray

Translate the front by subtracting the ideal point from each solution.

Parameters:

Name Type Description Default
front ndarray

The solution front.

required
ideal ndarray

The ideal point.

required

Returns:

Type Description
ndarray

np.ndarray: The translated front.

Source code in desdeo/adm/adm_afsar.py
def translate_front(self, front, ideal) -> np.ndarray:
    """Translate the front by subtracting the ideal point from each solution.

    Args:
        front (np.ndarray): The solution front.
        ideal (np.ndarray): The ideal point.

    Returns:
        np.ndarray: The translated front.
    """
    return np.subtract(front, ideal)