Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 25 additions & 7 deletions ax/api/utils/instantiation/from_string.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
OptimizationConfig,
)
from ax.core.outcome_constraint import _parse_constraint_expression, OutcomeConstraint
from ax.core.types import ComparisonOp
from ax.exceptions.core import UserInputError
from ax.utils.common.sympy import (
extract_metric_names_from_objective_expr,
Expand Down Expand Up @@ -67,16 +68,33 @@ def optimization_config_from_string(
)

if objective.is_multi_objective:
# Convert OutcomeConstraints to ObjectiveThresholds if relevant
objective_metric_names = set(objective.metric_names)
# A single-metric constraint on an objective metric becomes an
# objective threshold only when it bounds the objective from its
# optimization direction (i.e. an upper bound on a minimized
# objective or a lower bound on a maximized one). A constraint that
# bounds against the optimization direction (e.g. ``flops >= 42``
# while minimizing ``flops``) cannot be expressed as a threshold, so
# it is kept as a true outcome constraint -- which MOO supports.
minimize_by_metric_name = {
name: weight < 0
for sub_nw in objective._parsed[1]
for name, weight in sub_nw
}
true_outcome_constraints = []
objective_thresholds: list[OutcomeConstraint] = []
for outcome_constraint in outcome_constraints or []:
if (
len(outcome_constraint.metric_names) == 1
and outcome_constraint.metric_names[0] in objective_metric_names
):
objective_thresholds.append(outcome_constraint)
metric_name = (
outcome_constraint.metric_names[0]
if len(outcome_constraint.metric_names) == 1
else None
)
if metric_name is not None and metric_name in minimize_by_metric_name:
minimize = minimize_by_metric_name[metric_name]
bounded_above = outcome_constraint.op == ComparisonOp.LEQ
if minimize == bounded_above:
objective_thresholds.append(outcome_constraint)
else:
true_outcome_constraints.append(outcome_constraint)
else:
true_outcome_constraints.append(outcome_constraint)

Expand Down
39 changes: 39 additions & 0 deletions ax/api/utils/instantiation/tests/test_from_string.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,45 @@ def test_optimization_config_from_string(self) -> None:
),
)

def test_constraint_against_optimization_direction_on_objective(self) -> None:
# A constraint that bounds a minimized objective from below (against
# its optimization direction) cannot be an objective threshold, so it
# must be kept as a true outcome constraint. The aligned upper bound
# becomes an objective threshold.
config = optimization_config_from_string(
objective_str="-flops, -ne",
outcome_constraint_strs=[
"flops >= 42.50",
"flops <= 94.38",
"ne <= 0.62938",
],
)
self.assertEqual(
config,
MultiObjectiveOptimizationConfig(
objective=Objective(
expression="-flops, -ne",
metric_name_to_signature={"flops": "flops", "ne": "ne"},
),
outcome_constraints=[
OutcomeConstraint(
expression="flops >= 42.50",
metric_name_to_signature={"flops": "flops"},
),
],
objective_thresholds=[
OutcomeConstraint(
expression="flops <= 94.38",
metric_name_to_signature={"flops": "flops"},
),
OutcomeConstraint(
expression="ne <= 0.62938",
metric_name_to_signature={"ne": "ne"},
),
],
),
)

def test_objective_constraint_on_single_objective_raises(self) -> None:
with self.assertRaisesRegex(
UserInputError, "Outcome constraints may not be placed"
Expand Down
Loading