Skip to content

Pareto

Multi-objective Pareto front analysis.

trade_study.extract_front(scores, directions, weights=None)

Extract Pareto-optimal indices from a score matrix.

Parameters:

Name Type Description Default
scores NDArray[floating[Any]]

Array of shape (n_trials, n_objectives).

required
directions list[Direction]

Optimization direction for each objective.

required
weights list[float] | None

Optional objective scaling. Strictly positive weights preserve dominance and front membership; they do not encode preferences.

None

Returns:

Type Description
NDArray[intp]

Integer array of row indices on the Pareto front.

Examples:

>>> scores = np.array([[1.0, 1.0], [2.0, 2.0], [0.5, 3.0]])
>>> directions = [Direction.MINIMIZE, Direction.MINIMIZE]
>>> sorted(extract_front(scores, directions).tolist())
[0, 2]
Source code in src/trade_study/_pareto.py
def extract_front(
    scores: NDArray[np.floating[Any]],
    directions: list[Direction],
    weights: list[float] | None = None,
) -> NDArray[np.intp]:
    """Extract Pareto-optimal indices from a score matrix.

    Args:
        scores: Array of shape (n_trials, n_objectives).
        directions: Optimization direction for each objective.
        weights: Optional objective scaling. Strictly positive weights preserve
            dominance and front membership; they do not encode preferences.

    Returns:
        Integer array of row indices on the Pareto front.

    Examples:
        >>> scores = np.array([[1.0, 1.0], [2.0, 2.0], [0.5, 3.0]])
        >>> directions = [Direction.MINIMIZE, Direction.MINIMIZE]
        >>> sorted(extract_front(scores, directions).tolist())
        [0, 2]
    """
    _require_pymoo()
    from pymoo.util.nds.non_dominated_sorting import (  # type: ignore[import-untyped]
        NonDominatedSorting,
    )

    obj = _normalize_objectives(scores, directions, weights)
    nds = NonDominatedSorting()
    fronts = nds.do(obj)
    return np.asarray(fronts[0], dtype=np.intp)

trade_study.pareto_rank(scores, directions, weights=None)

Assign Pareto rank to each trial (0 = front, 1 = next layer, ...).

Parameters:

Name Type Description Default
scores NDArray[floating[Any]]

Array of shape (n_trials, n_objectives).

required
directions list[Direction]

Optimization direction for each objective.

required
weights list[float] | None

Optional per-objective weights.

None

Returns:

Type Description
NDArray[intp]

Integer array of ranks, shape (n_trials,).

Examples:

>>> scores = np.array([[1.0, 1.0], [2.0, 2.0]])
>>> pareto_rank(scores, [Direction.MINIMIZE] * 2).tolist()
[0, 1]
Source code in src/trade_study/_pareto.py
def pareto_rank(
    scores: NDArray[np.floating[Any]],
    directions: list[Direction],
    weights: list[float] | None = None,
) -> NDArray[np.intp]:
    """Assign Pareto rank to each trial (0 = front, 1 = next layer, ...).

    Args:
        scores: Array of shape (n_trials, n_objectives).
        directions: Optimization direction for each objective.
        weights: Optional per-objective weights.

    Returns:
        Integer array of ranks, shape (n_trials,).

    Examples:
        >>> scores = np.array([[1.0, 1.0], [2.0, 2.0]])
        >>> pareto_rank(scores, [Direction.MINIMIZE] * 2).tolist()
        [0, 1]
    """
    _require_pymoo()
    from pymoo.util.nds.non_dominated_sorting import (
        NonDominatedSorting,
    )

    obj = _normalize_objectives(scores, directions, weights)
    nds = NonDominatedSorting()
    fronts = nds.do(obj)
    ranks = np.empty(len(scores), dtype=np.intp)
    for rank, front in enumerate(fronts):
        ranks[front] = rank
    return ranks

trade_study.hypervolume(front, ref_point, directions=None, weights=None)

Compute hypervolume indicator for a Pareto front.

Parameters:

Name Type Description Default
front NDArray[floating[Any]]

Array of shape (n_points, n_objectives) on the front.

required
ref_point NDArray[floating[Any]]

Reference point worse than all front points in every objective after direction normalization.

required
directions list[Direction] | None

If provided, flips maximize objectives before computing.

None
weights list[float] | None

Optional per-objective weights applied after direction flip.

None

Returns:

Type Description
float

Hypervolume value.

Source code in src/trade_study/_pareto.py
def hypervolume(
    front: NDArray[np.floating[Any]],
    ref_point: NDArray[np.floating[Any]],
    directions: list[Direction] | None = None,
    weights: list[float] | None = None,
) -> float:
    """Compute hypervolume indicator for a Pareto front.

    Args:
        front: Array of shape (n_points, n_objectives) on the front.
        ref_point: Reference point worse than all front points in every
            objective after direction normalization.
        directions: If provided, flips maximize objectives before computing.
        weights: Optional per-objective weights applied after direction flip.

    Returns:
        Hypervolume value.
    """
    _require_pymoo()
    from pymoo.indicators.hv import HV  # type: ignore[import-untyped]

    obj = np.atleast_2d(np.asarray(front, dtype=float)).copy()
    rp = np.atleast_1d(np.asarray(ref_point, dtype=float)).copy()
    if directions is not None:
        for j, d in enumerate(directions):
            if d == Direction.MAXIMIZE:
                obj[:, j] = -obj[:, j]
                rp[j] = -rp[j]
    if weights is not None:
        for j, w in enumerate(weights):
            obj[:, j] *= w
            rp[j] *= w
    if obj.shape[1] == 1:
        # moocore's native 1-D path can segfault (#229); the exact value is a length.
        # Like moocore, only points strictly better than the reference count, so
        # NaN points are ignored.
        better = obj[obj[:, 0] < rp[0], 0]
        return float(rp[0] - better.min()) if better.size else 0.0
    return float(HV(ref_point=rp)(obj))

trade_study.igd_plus(front, reference, directions=None, weights=None)

Compute IGD+ indicator.

Parameters:

Name Type Description Default
front NDArray[floating[Any]]

Obtained Pareto front.

required
reference NDArray[floating[Any]]

Reference Pareto front.

required
directions list[Direction] | None

Optimization directions.

None
weights list[float] | None

Optional per-objective weights applied after direction flip.

None

Returns:

Type Description
float

IGD+ value (lower is better).

Source code in src/trade_study/_pareto.py
def igd_plus(
    front: NDArray[np.floating[Any]],
    reference: NDArray[np.floating[Any]],
    directions: list[Direction] | None = None,
    weights: list[float] | None = None,
) -> float:
    """Compute IGD+ indicator.

    Args:
        front: Obtained Pareto front.
        reference: Reference Pareto front.
        directions: Optimization directions.
        weights: Optional per-objective weights applied after direction flip.

    Returns:
        IGD+ value (lower is better).
    """
    _require_pymoo()
    from pymoo.indicators.igd_plus import IGDPlus  # type: ignore[import-untyped]

    obj = front.copy()
    ref = reference.copy()
    if directions is not None:
        for j, d in enumerate(directions):
            if d == Direction.MAXIMIZE:
                obj[:, j] = -obj[:, j]
                ref[:, j] = -ref[:, j]
    if weights is not None:
        for j, w in enumerate(weights):
            obj[:, j] *= w
            ref[:, j] *= w
    return float(IGDPlus(ref)(obj))