Skip to content

Multi-Objective Selection

Multi-objective helpers rank finite candidate pools when several objectives matter at once, such as maximizing performance while minimizing cost, toxicity, degradation rate, synthesis difficulty, or experimental burden.

matgpr.multi_objective

ObjectiveSpec dataclass

Definition of one objective in a finite-pool multi-objective workflow.

Parameters:

Name Type Description Default
name str

Short objective label used in utility column names.

required
column str

Numeric dataframe column containing objective values.

required
goal str

Either "maximize" or "minimize".

'maximize'
weight float

Non-negative scalarization weight. At least one objective supplied to a scalarization workflow must have a positive weight.

1.0
Source code in matgpr/multi_objective.py
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
@dataclass(frozen=True)
class ObjectiveSpec:
    """Definition of one objective in a finite-pool multi-objective workflow.

    Parameters
    ----------
    name
        Short objective label used in utility column names.
    column
        Numeric dataframe column containing objective values.
    goal
        Either `"maximize"` or `"minimize"`.
    weight
        Non-negative scalarization weight. At least one objective supplied to a
        scalarization workflow must have a positive weight.
    """

    name: str
    column: str
    goal: str = "maximize"
    weight: float = 1.0

    def __post_init__(self) -> None:
        name = str(self.name).strip()
        column = str(self.column).strip()
        goal = str(self.goal).strip().lower().replace("-", "_")
        weight = float(self.weight)

        if not name:
            raise ValueError("ObjectiveSpec.name must be non-empty")
        if not column:
            raise ValueError("ObjectiveSpec.column must be non-empty")
        if goal not in {"maximize", "minimize"}:
            raise ValueError("ObjectiveSpec.goal must be 'maximize' or 'minimize'")
        if not np.isfinite(weight) or weight < 0.0:
            raise ValueError("ObjectiveSpec.weight must be finite and non-negative")

        object.__setattr__(self, "name", name)
        object.__setattr__(self, "column", column)
        object.__setattr__(self, "goal", goal)
        object.__setattr__(self, "weight", weight)

pareto_front_mask(data, objectives)

Return a boolean mask for nondominated candidate rows.

Objective directions are taken from each ObjectiveSpec, so minimize-type objectives such as cost, toxicity, synthesis difficulty, or degradation rate are handled directly.

Source code in matgpr/multi_objective.py
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
def pareto_front_mask(
    data: pd.DataFrame,
    objectives: ObjectiveSpec | Sequence[ObjectiveSpec],
) -> pd.Series:
    """Return a boolean mask for nondominated candidate rows.

    Objective directions are taken from each `ObjectiveSpec`, so minimize-type
    objectives such as cost, toxicity, synthesis difficulty, or degradation rate
    are handled directly.
    """
    objective_tuple = _as_objective_tuple(objectives)
    values = _objective_values(data, objective_tuple)
    oriented = _to_maximization_space(values, objective_tuple)
    mask = _pareto_mask(oriented)
    return pd.Series(mask, index=data.index, name="matgpr_pareto_front")

select_pareto_front(data, objectives, *, reset_index=True)

Return nondominated rows from a finite candidate table.

Source code in matgpr/multi_objective.py
79
80
81
82
83
84
85
86
87
88
89
90
def select_pareto_front(
    data: pd.DataFrame,
    objectives: ObjectiveSpec | Sequence[ObjectiveSpec],
    *,
    reset_index: bool = True,
) -> pd.DataFrame:
    """Return nondominated rows from a finite candidate table."""
    mask = pareto_front_mask(data, objectives)
    front = data.loc[mask].copy()
    if reset_index:
        return front.reset_index(drop=True)
    return front

scalarize_objectives(data, objectives, *, normalize=True)

Compute a weighted scalar utility score for multiple objectives.

Each objective is first converted so larger utility is better. With normalize=True, every objective is min-max scaled to [0, 1] before the weighted average is computed. This is the recommended default when columns have different physical units.

Source code in matgpr/multi_objective.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
def scalarize_objectives(
    data: pd.DataFrame,
    objectives: ObjectiveSpec | Sequence[ObjectiveSpec],
    *,
    normalize: bool = True,
) -> pd.Series:
    """Compute a weighted scalar utility score for multiple objectives.

    Each objective is first converted so larger utility is better. With
    `normalize=True`, every objective is min-max scaled to `[0, 1]` before the
    weighted average is computed. This is the recommended default when columns
    have different physical units.
    """
    objective_tuple = _as_objective_tuple(objectives)
    utilities = _objective_utilities(data, objective_tuple, normalize=normalize)
    weights = _objective_weights(objective_tuple)
    score = utilities @ weights
    return pd.Series(score, index=data.index, name="matgpr_multi_objective_score")

rank_multi_objective_candidates(data, objectives, *, top_k=None, normalize=True, score_column='matgpr_multi_objective_score', rank_column='matgpr_multi_objective_rank', pareto_column='matgpr_pareto_front', include_objective_utilities=True, utility_prefix='matgpr_objective_')

Rank finite candidates by weighted multi-objective utility.

The result keeps original candidate columns, adds the scalarized score, marks Pareto-front candidates, and sorts from most to least desirable.

Source code in matgpr/multi_objective.py
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
def rank_multi_objective_candidates(
    data: pd.DataFrame,
    objectives: ObjectiveSpec | Sequence[ObjectiveSpec],
    *,
    top_k: int | None = None,
    normalize: bool = True,
    score_column: str = "matgpr_multi_objective_score",
    rank_column: str = "matgpr_multi_objective_rank",
    pareto_column: str = "matgpr_pareto_front",
    include_objective_utilities: bool = True,
    utility_prefix: str = "matgpr_objective_",
) -> pd.DataFrame:
    """Rank finite candidates by weighted multi-objective utility.

    The result keeps original candidate columns, adds the scalarized score,
    marks Pareto-front candidates, and sorts from most to least desirable.
    """
    if top_k is not None and top_k < 1:
        raise ValueError("top_k must be at least 1 when provided")
    objective_tuple = _as_objective_tuple(objectives)
    score_column = str(score_column).strip()
    rank_column = str(rank_column).strip()
    pareto_column = str(pareto_column).strip()
    utility_prefix = str(utility_prefix).strip()
    _validate_output_column(score_column, objective_tuple, name="score_column")
    _validate_output_column(rank_column, objective_tuple, name="rank_column")
    _validate_output_column(pareto_column, objective_tuple, name="pareto_column")
    _validate_distinct_output_columns(score_column, rank_column, pareto_column)
    if not utility_prefix:
        raise ValueError("utility_prefix must be non-empty")

    result = data.copy()
    utilities = _objective_utilities(result, objective_tuple, normalize=normalize)
    weights = _objective_weights(objective_tuple)
    result[score_column] = utilities @ weights
    result[pareto_column] = pareto_front_mask(result, objective_tuple).to_numpy(dtype=bool)

    if include_objective_utilities:
        for index, objective in enumerate(objective_tuple):
            column = f"{utility_prefix}{_slugify(objective.name)}_utility"
            _validate_output_column(column, objective_tuple, name="utility_column")
            _validate_distinct_output_columns(
                column,
                score_column,
                rank_column,
                pareto_column,
            )
            result[column] = utilities[:, index]

    result["_matgpr_original_order"] = np.arange(result.shape[0], dtype=int)
    result = result.sort_values(
        [score_column, pareto_column, "_matgpr_original_order"],
        ascending=[False, False, True],
    ).drop(columns="_matgpr_original_order")
    result.insert(0, rank_column, np.arange(1, result.shape[0] + 1))
    result = result.reset_index(drop=True)

    if top_k is not None:
        return result.head(top_k).reset_index(drop=True)
    return result