Skip to content

Commit 7f401eb

Browse files
saitcakmakmeta-codesync[bot]
authored andcommitted
Filter by MetricAvailability in get_best_raw_objective_point_with_trial_index
Summary: `get_best_raw_objective_point_with_trial_index` filters to COMPLETED trials but assumes all metrics in the optimization config are present in the data. This can fail with a hard `ValueError` from `_pivot_data_with_feasibility` when a completed trial has incomplete metric data (e.g., due to partial metric fetches, fetch failures, or metrics added mid-experiment). This diff adds `MetricAvailability` filtering after the completed-trials filter to exclude trials with incomplete data before they reach the pivot step. Trials without complete metric data are now silently excluded rather than causing a crash, with a clear error if no trials remain. Differential Revision: D99451732
1 parent 8ac7d52 commit 7f401eb

2 files changed

Lines changed: 54 additions & 25 deletions

File tree

ax/service/tests/test_best_point_utils.py

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,8 @@ def test_best_raw_objective_point(self) -> None:
504504
objective=Objective(metric=get_branin_metric(name="not_branin"))
505505
)
506506
with self.assertRaisesRegex(
507-
ValueError, "Some metrics are not present for all trials and arms"
507+
ValueError,
508+
"no completed trials have complete metric data",
508509
):
509510
get_best_raw_objective_point_with_trial_index(
510511
experiment=exp, optimization_config=opt_conf
@@ -543,40 +544,48 @@ def test_best_raw_objective_point_unsatisfiable(self) -> None:
543544
experiment=exp, optimization_config=opt_conf
544545
)
545546

546-
# adding a new metric that is not present in the data should raise an error,
547-
# even if the other metrics are satisfied
547+
# Adding a constraint on an unobserved metric causes the trial to be
548+
# filtered out by MetricAvailability before reaching feasibility check.
548549
opt_conf.outcome_constraints.pop()
549550
unobserved_metric = get_branin_metric(name="unobserved")
550551
opt_conf.outcome_constraints.append(
551552
OutcomeConstraint(
552553
metric=unobserved_metric, op=ComparisonOp.LEQ, bound=0, relative=False
553554
)
554555
)
555-
# also add a constraint that is always satisfied, as the Branin metric is
556-
# non-negative, and check that only the "unobserved" metric shows up in the
557-
# error message
558-
opt_conf.outcome_constraints.append(
559-
OutcomeConstraint(
560-
metric=get_branin_metric(), op=ComparisonOp.GEQ, bound=0, relative=False
556+
with self.assertRaisesRegex(
557+
ValueError,
558+
"no completed trials have complete metric data",
559+
):
560+
get_best_raw_objective_point_with_trial_index(
561+
experiment=exp, optimization_config=opt_conf
561562
)
562-
)
563563

564-
with self.assertLogs(logger=best_point_logger, level="WARN") as lg:
565-
with self.assertRaisesRegex(
566-
ValueError,
567-
r"No points satisfied all outcome constraints within 95 percent "
568-
r"confidence interval\. The feasibility of 1 arm\(s\) could not be "
569-
r"determined: \['0_0'\]\.",
570-
):
571-
get_best_raw_objective_point_with_trial_index(
572-
experiment=exp, optimization_config=opt_conf
573-
)
574-
self.assertEqual(len(lg.output), 1)
575-
self.assertRegex(
576-
lg.output[0],
577-
r"Arm 0_0 is missing data for one or more constrained metrics: "
578-
r"\{'unobserved'\}\.",
564+
# Using a constrained experiment where all metrics are observed
565+
# (passes MetricAvailability), but constraints are unsatisfiable.
566+
constrained_exp = get_experiment_with_observations(
567+
observations=[[1.0, 2.0]],
568+
constrained=True,
569+
minimize=False,
579570
)
571+
constrained_opt = none_throws(constrained_exp.optimization_config).clone()
572+
# Make constraint unsatisfiable: require m2 >= 9999 (observed m2=2.0).
573+
constrained_opt.outcome_constraints = [
574+
OutcomeConstraint(
575+
metric=Metric(name="m2"),
576+
op=ComparisonOp.GEQ,
577+
bound=9999,
578+
relative=False,
579+
),
580+
]
581+
with self.assertRaisesRegex(
582+
ValueError,
583+
r"No points satisfied all outcome constraints",
584+
):
585+
get_best_raw_objective_point_with_trial_index(
586+
experiment=constrained_exp,
587+
optimization_config=constrained_opt,
588+
)
580589

581590
def test_best_raw_objective_point_unsatisfiable_relative(self) -> None:
582591
exp = get_experiment_with_observations(

ax/service/utils/best_point.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,26 @@ def get_best_raw_objective_point_with_trial_index(
134134
raise ValueError("Cannot identify best point if no trials are completed.")
135135
completed_df = dat.df[dat.df["trial_index"].isin(completed_indices)]
136136

137+
# Filter to trials with complete metric data to avoid errors in
138+
# _pivot_data_with_feasibility when some metrics are missing (e.g., due to
139+
# partial metric fetches, fetch failures, or metrics added mid-experiment).
140+
availability = compute_metric_availability(
141+
experiment=experiment,
142+
trial_indices=sorted(completed_indices),
143+
optimization_config=optimization_config,
144+
)
145+
complete_trials = {
146+
idx
147+
for idx, avail in availability.items()
148+
if avail == MetricAvailability.COMPLETE
149+
}
150+
completed_df = completed_df[completed_df["trial_index"].isin(complete_trials)]
151+
if len(completed_df) == 0:
152+
raise ValueError(
153+
"Cannot identify best point: no completed trials have complete "
154+
"metric data for all metrics in the optimization config."
155+
)
156+
137157
is_feasible = is_row_feasible(
138158
df=completed_df,
139159
optimization_config=optimization_config,

0 commit comments

Comments
 (0)