Skip to content

Commit 53be145

Browse files
mpolson64meta-codesync[bot]
authored andcommitted
Move metric_name_to_signature from Adapter to Objective/OutcomeConstraint (facebook#5122)
Summary: Pull Request resolved: facebook#5122 The `metric_name_to_signature` mapping (user-facing metric name -> canonical signature used in data/modeling) previously lived on `Adapter`, built from `experiment.metrics` at init time. This was architecturally awkward because the mapping is consumed alongside `OptimizationConfig` objects (objectives, constraints) throughout adapter_utils, transforms, and cross-validation, but lived on a separate object. This diff co-locates the mapping with the objects that reference metrics by name: **Core changes (ax/core):** - `Objective` and `OutcomeConstraint` now accept an optional `metric_name_to_signature` kwarg, stored as an instance field - Both classes expose `metric_signatures` and `metric_name_to_signature` properties (with getter and setter) - `metric_weights` on both classes now returns `(signature, weight)` tuples instead of `(name, weight)`, aligning with how downstream consumers use them - `OptimizationConfig` aggregates these into `metric_signatures` and `metric_name_to_signature` properties, with a setter that propagates to children - `clone()` methods preserve the mapping **Adapter changes (ax/adapter):** - Removed `Adapter._metric_name_to_signature` field and property - Removed `metric_name_to_signature` parameter from `adapter_utils.py` functions: `extract_objective_thresholds`, `extract_objective_weights`, `extract_objective_weight_matrix`, `extract_outcome_constraints`, `feasible_hypervolume` - Removed `metric_name_to_signature` parameter from `has_good_opt_config_model_fit` in `cross_validation.py` - Removed `metric_name_to_signature` parameter from `validate_transformed_optimization_config` in `torch.py` - Updated all transforms to use `constraint.metric_signatures` and `objective.metric_signatures` directly instead of looking up through `adapter.metric_name_to_signature` **Orchestration test fix (ax/orchestration):** - Updated `test_get_best_trial` in `test_orchestrator.py` to expect `ValueError` instead of `KeyError` when passing a mismatched `OptimizationConfig`. Previously the dict lookup on `metric_name_to_signature` raised `KeyError`; now `outcomes.index()` raises `ValueError`. **Storage:** No changes needed -- the mapping is not persisted on its own and instead is reconstructed by Experiment initialization. Reviewed By: saitcakmak Differential Revision: D98837790 fbshipit-source-id: b4dbef567a1601f7f23909fac8f8f6383cd11b09
1 parent 227243c commit 53be145

49 files changed

Lines changed: 890 additions & 359 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

ax/adapter/adapter_utils.py

Lines changed: 24 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,6 @@ def extract_objective_thresholds(
210210
objective_thresholds: TRefPoint,
211211
objective: Objective,
212212
outcomes: list[str],
213-
metric_name_to_signature: Mapping[str, str],
214213
) -> npt.NDArray | None:
215214
"""Extracts objective thresholds' values, in the order of objectives.
216215
@@ -231,8 +230,7 @@ def extract_objective_thresholds(
231230
Args:
232231
objective_thresholds: Objective thresholds to extract values from.
233232
objective: The corresponding Objective, for validation purposes.
234-
outcomes: n-length list of names of metrics.
235-
metric_name_to_signature: Mapping from metric names to signatures.
233+
outcomes: n-length list of metric signatures.
236234
237235
Returns:
238236
``(n_objectives,)`` array of maximization-aligned thresholds, or None.
@@ -242,7 +240,7 @@ def extract_objective_thresholds(
242240

243241
objective_threshold_dict = {}
244242
for ot in objective_thresholds:
245-
ot_signature = metric_name_to_signature[ot.metric_names[0]]
243+
ot_signature = ot.metric_signatures[0]
246244
if ot.relative:
247245
raise ValueError(
248246
f"Objective {ot_signature} has a relative threshold that "
@@ -251,9 +249,7 @@ def extract_objective_thresholds(
251249
objective_threshold_dict[ot_signature] = ot.bound
252250

253251
# Check that all thresholds correspond to a metric.
254-
obj_metric_signatures = [
255-
metric_name_to_signature[name] for name in objective.metric_names
256-
]
252+
obj_metric_signatures = objective.metric_signatures
257253
if set(objective_threshold_dict.keys()).difference(set(obj_metric_signatures)):
258254
raise ValueError(
259255
"Some objective thresholds do not have corresponding metrics. "
@@ -273,7 +269,7 @@ def extract_objective_thresholds(
273269
if len(sub_mw) > 1:
274270
continue # Scalarized sub-objective — NaN, will be inferred later.
275271
name, weight = sub_mw[0]
276-
sig = metric_name_to_signature[name]
272+
sig = objective.metric_name_to_signature[name]
277273
if sig in objective_threshold_dict:
278274
sign = 1.0 if weight > 0 else -1.0
279275
obj_t[i] = sign * objective_threshold_dict[sig]
@@ -283,7 +279,6 @@ def extract_objective_thresholds(
283279
def extract_objective_weights(
284280
objective: Objective,
285281
outcomes: list[str],
286-
metric_name_to_signature: Mapping[str, str],
287282
) -> npt.NDArray:
288283
"""Extract a weights for objectives.
289284
@@ -301,25 +296,22 @@ def extract_objective_weights(
301296
Args:
302297
objective: Objective to extract weights from.
303298
outcomes: n-length list of metric signatures.
304-
metric_name_to_signature: Mapping from metric names to signatures.
305299
306300
Returns:
307301
n-length array of weights.
308302
309303
"""
310304
objective_weights = np.zeros(len(outcomes))
311-
# metric_weights returns sign-encoded (name, weight) tuples for all
305+
# metric_weights returns sign-encoded (signature, weight) tuples for all
312306
# objective types (single, scalarized, multi).
313-
for obj_metric_name, obj_weight in objective.metric_weights:
314-
sig = metric_name_to_signature[obj_metric_name]
315-
objective_weights[outcomes.index(sig)] = obj_weight
307+
for obj_metric_sig, obj_weight in objective.metric_weights:
308+
objective_weights[outcomes.index(obj_metric_sig)] = obj_weight
316309
return objective_weights
317310

318311

319312
def extract_objective_weight_matrix(
320313
objective: Objective,
321314
outcomes: list[str],
322-
metric_name_to_signature: Mapping[str, str],
323315
) -> npt.NDArray:
324316
"""Extract a 2D weight matrix for objectives.
325317
@@ -333,20 +325,24 @@ def extract_objective_weight_matrix(
333325
334326
Args:
335327
objective: Objective to extract weights from.
336-
outcomes: n-length list of signatures of metrics.
337-
metric_name_to_signature: Mapping from metric names to signatures.
328+
outcomes: n-length list of metric signatures.
338329
339330
Returns:
340331
``(n_objectives, n)`` array of weights.
341332
"""
342333
if objective.is_multi_objective:
343334
rows: list[npt.NDArray] = []
344-
for name, weight in objective.metric_weights:
335+
obj_names = objective.metric_names
336+
obj_weights = [w for _, w in objective.metric_weights]
337+
name_to_sig = objective.metric_name_to_signature
338+
for name, weight in zip(obj_names, obj_weights):
345339
rows.append(
346340
extract_objective_weights(
347-
objective=Objective(expression=f"{weight} * {name}"),
341+
objective=Objective(
342+
expression=f"{weight} * {name}",
343+
metric_name_to_signature={name: name_to_sig[name]},
344+
),
348345
outcomes=outcomes,
349-
metric_name_to_signature=metric_name_to_signature,
350346
)
351347
)
352348
return np.stack(rows, axis=0)
@@ -355,14 +351,12 @@ def extract_objective_weight_matrix(
355351
return extract_objective_weights(
356352
objective=objective,
357353
outcomes=outcomes,
358-
metric_name_to_signature=metric_name_to_signature,
359354
).reshape(1, -1)
360355

361356

362357
def extract_outcome_constraints(
363358
outcome_constraints: list[OutcomeConstraint],
364359
outcomes: list[str],
365-
metric_name_to_signature: Mapping[str, str],
366360
) -> TBounds:
367361
if len(outcome_constraints) == 0:
368362
return None
@@ -372,11 +366,11 @@ def extract_outcome_constraints(
372366
for i, c in enumerate(outcome_constraints):
373367
s = 1 if c.op == ComparisonOp.LEQ else -1
374368
if isinstance(c, ScalarizedOutcomeConstraint):
375-
for c_metric_name, c_weight in c.metric_weights:
376-
j = outcomes.index(metric_name_to_signature[c_metric_name])
369+
for c_metric_sig, c_weight in c.metric_weights:
370+
j = outcomes.index(c_metric_sig)
377371
A[i, j] = s * c_weight
378372
else:
379-
j = outcomes.index(metric_name_to_signature[c.metric_names[0]])
373+
j = outcomes.index(c.metric_signatures[0])
380374
A[i, j] = s
381375
b[i, 0] = s * c.bound
382376
return (A, b)
@@ -689,18 +683,15 @@ def get_pareto_frontier_and_configs(
689683
objective_weights = extract_objective_weight_matrix(
690684
objective=optimization_config.objective,
691685
outcomes=adapter.outcomes,
692-
metric_name_to_signature=adapter.metric_name_to_signature,
693686
)
694687
outcome_constraints = extract_outcome_constraints(
695688
outcome_constraints=optimization_config.outcome_constraints,
696689
outcomes=adapter.outcomes,
697-
metric_name_to_signature=adapter.metric_name_to_signature,
698690
)
699691
obj_t = extract_objective_thresholds(
700692
objective_thresholds=optimization_config.objective_thresholds,
701693
objective=optimization_config.objective,
702694
outcomes=adapter.outcomes,
703-
metric_name_to_signature=adapter.metric_name_to_signature,
704695
)
705696
if obj_t is not None:
706697
obj_t = array_to_tensor(obj_t)
@@ -1155,26 +1146,23 @@ def observation_features_to_array(
11551146
def feasible_hypervolume(
11561147
optimization_config: MultiObjectiveOptimizationConfig,
11571148
values: dict[str, npt.NDArray],
1158-
metric_name_to_signature: Mapping[str, str],
11591149
) -> npt.NDArray:
11601150
"""Compute the feasible hypervolume each iteration.
11611151
11621152
Args:
11631153
optimization_config: Optimization config.
1164-
values: Dictionary from metric name to array of value at each
1154+
values: Dictionary from metric signature to array of value at each
11651155
iteration (each array is `n`-dim). If optimization config contains
11661156
outcome constraints, values for them must be present in `values`.
1167-
metric_name_to_signature: Mapping from metric names to signatures.
11681157
11691158
Returns: Array of feasible hypervolumes.
11701159
"""
11711160
# Get objective at each iteration
11721161
obj_threshold_dict = {
1173-
metric_name_to_signature[ot.metric_names[0]]: ot.bound
1162+
ot.metric_signatures[0]: ot.bound
11741163
for ot in optimization_config.objective_thresholds
11751164
}
1176-
obj_metric_names = optimization_config.objective.metric_names
1177-
obj_metric_sigs = [metric_name_to_signature[name] for name in obj_metric_names]
1165+
obj_metric_sigs = optimization_config.objective.metric_signatures
11781166
f_vals = np.hstack([values[sig].reshape(-1, 1) for sig in obj_metric_sigs])
11791167
obj_thresholds = np.array([obj_threshold_dict[sig] for sig in obj_metric_sigs])
11801168
# Set infeasible points to be the objective threshold
@@ -1183,7 +1171,7 @@ def feasible_hypervolume(
11831171
raise ValueError(
11841172
"Benchmark aggregation does not support relative constraints"
11851173
)
1186-
oc_sig = metric_name_to_signature[oc.metric_names[0]]
1174+
oc_sig = oc.metric_signatures[0]
11871175
g = values[oc_sig]
11881176
feas = g <= oc.bound if oc.op == ComparisonOp.LEQ else g >= oc.bound
11891177
f_vals[~feas] = obj_thresholds
@@ -1192,7 +1180,7 @@ def feasible_hypervolume(
11921180
# Positive weight = maximize, negative weight = minimize.
11931181
obj_weight_dict = dict(optimization_config.objective.metric_weights)
11941182
obj_weights = np.array(
1195-
[1 if obj_weight_dict[name] > 0 else -1 for name in obj_metric_names]
1183+
[1 if obj_weight_dict[sig] > 0 else -1 for sig in obj_metric_sigs]
11961184
)
11971185
obj_thresholds = obj_thresholds * obj_weights
11981186
f_vals = f_vals * obj_weights

ax/adapter/base.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -191,9 +191,6 @@ def __init__(
191191
)
192192
self._experiment_properties: dict[str, Any] = experiment._properties
193193
self._experiment: Experiment = experiment
194-
self._metric_name_to_signature: dict[str, str] = {
195-
name: metric.signature for name, metric in self._experiment.metrics.items()
196-
}
197194

198195
if self._optimization_config is None:
199196
self._optimization_config = experiment.optimization_config
@@ -563,11 +560,6 @@ def metric_signatures(self) -> set[str]:
563560
"""Metric signatures present in training data."""
564561
return self._metric_signatures
565562

566-
@property
567-
def metric_name_to_signature(self) -> dict[str, str]:
568-
"""Mapping from metric names to their signatures."""
569-
return self._metric_name_to_signature
570-
571563
@property
572564
def model_space(self) -> SearchSpace:
573565
"""SearchSpace used to fit model."""

ax/adapter/cross_validation.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from __future__ import annotations
1010

1111
from collections import defaultdict
12-
from collections.abc import Callable, Iterable, Mapping
12+
from collections.abc import Callable, Iterable
1313
from logging import Logger
1414
from typing import cast, NamedTuple
1515
from warnings import warn
@@ -573,7 +573,6 @@ def assess_model_fit(
573573
def has_good_opt_config_model_fit(
574574
optimization_config: OptimizationConfig,
575575
assess_model_fit_result: AssessModelFitResult,
576-
metric_name_to_signature: Mapping[str, str],
577576
) -> bool:
578577
"""Assess model fit for given diagnostics results across the optimization
579578
config metrics
@@ -586,7 +585,6 @@ def has_good_opt_config_model_fit(
586585
Args:
587586
optimization_config: Objective/Outcome constraint metrics to assess
588587
assess_model_fit_result: Output of assess_model_fit
589-
metric_name_to_signature: Mapping from metric names to signatures.
590588
591589
Returns:
592590
Two dictionaries, one for good metrics, one for bad metrics, each
@@ -596,9 +594,8 @@ def has_good_opt_config_model_fit(
596594
# Bad fit criteria: Any objective metrics are poorly fit
597595
# TODO[]: Incl. outcome constraints in assessment
598596
has_good_opt_config_fit = all(
599-
metric_name_to_signature[name]
600-
in assess_model_fit_result.good_fit_metrics_to_fisher_score
601-
for name in optimization_config.objective.metric_names
597+
sig in assess_model_fit_result.good_fit_metrics_to_fisher_score
598+
for sig in optimization_config.objective.metric_signatures
602599
)
603600
return has_good_opt_config_fit
604601

ax/adapter/discrete.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,17 +166,14 @@ def _gen(
166166
validate_transformed_optimization_config(
167167
optimization_config,
168168
self.outcomes,
169-
metric_name_to_signature=self.metric_name_to_signature,
170169
)
171170
objective_weights = extract_objective_weights(
172171
objective=optimization_config.objective,
173172
outcomes=self.outcomes,
174-
metric_name_to_signature=self.metric_name_to_signature,
175173
)
176174
outcome_constraints = extract_outcome_constraints(
177175
outcome_constraints=optimization_config.outcome_constraints,
178176
outcomes=self.outcomes,
179-
metric_name_to_signature=self.metric_name_to_signature,
180177
)
181178

182179
# Get fixed features

ax/adapter/tests/test_adapter_utils.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,6 @@ def test_feasible_hypervolume(self) -> None:
7878
),
7979
],
8080
)
81-
# For plain Metric objects, signature == name.
82-
metric_name_to_signature = {m.name: m.name for m in [ma, mb, mc]}
8381
feas_hv = feasible_hypervolume(
8482
optimization_config,
8583
values={
@@ -108,7 +106,6 @@ def test_feasible_hypervolume(self) -> None:
108106
]
109107
),
110108
},
111-
metric_name_to_signature=metric_name_to_signature,
112109
)
113110
self.assertEqual(list(feas_hv), [0.0, 0.0, 1.0, 1.0])
114111

@@ -540,15 +537,12 @@ def test_can_map_to_binary(self) -> None:
540537
def test_extract_objective_weight_matrix(self) -> None:
541538
m1, m2, m3 = Metric(name="m1"), Metric(name="m2"), Metric(name="m3")
542539
outcomes = ["m1", "m2", "m3"]
543-
# For plain Metric objects, signature == name.
544-
metric_name_to_signature = {name: name for name in outcomes}
545540

546541
# Single Objective: one row, nonzero only in matching column.
547542
obj = Objective(metric=m1, minimize=False)
548543
result = extract_objective_weight_matrix(
549544
objective=obj,
550545
outcomes=outcomes,
551-
metric_name_to_signature=metric_name_to_signature,
552546
)
553547
np.testing.assert_array_equal(result, [[1.0, 0.0, 0.0]])
554548

@@ -557,7 +551,6 @@ def test_extract_objective_weight_matrix(self) -> None:
557551
result = extract_objective_weight_matrix(
558552
objective=obj_min,
559553
outcomes=outcomes,
560-
metric_name_to_signature=metric_name_to_signature,
561554
)
562555
np.testing.assert_array_equal(result, [[0.0, -1.0, 0.0]])
563556

@@ -566,7 +559,6 @@ def test_extract_objective_weight_matrix(self) -> None:
566559
result = extract_objective_weight_matrix(
567560
objective=scal,
568561
outcomes=outcomes,
569-
metric_name_to_signature=metric_name_to_signature,
570562
)
571563
np.testing.assert_array_almost_equal(result, [[0.3, 0.0, 0.7]])
572564

@@ -580,7 +572,6 @@ def test_extract_objective_weight_matrix(self) -> None:
580572
result = extract_objective_weight_matrix(
581573
objective=multi,
582574
outcomes=outcomes,
583-
metric_name_to_signature=metric_name_to_signature,
584575
)
585576
np.testing.assert_array_equal(result, [[1.0, 0.0, 0.0], [0.0, 0.0, -1.0]])
586577

ax/adapter/tests/test_cross_validation.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,6 @@ def test_has_good_opt_config_model_fit(self) -> None:
480480
has_good_fit = has_good_opt_config_model_fit(
481481
optimization_config=optimization_config,
482482
assess_model_fit_result=assess_model_fit_result,
483-
metric_name_to_signature={"m1": "m1", "m2": "m2"},
484483
)
485484
self.assertFalse(has_good_fit)
486485

@@ -496,7 +495,6 @@ def test_has_good_opt_config_model_fit(self) -> None:
496495
has_good_fit = has_good_opt_config_model_fit(
497496
optimization_config=optimization_config,
498497
assess_model_fit_result=assess_model_fit_result,
499-
metric_name_to_signature={"m1": "m1", "m2": "m2"},
500498
)
501499
self.assertFalse(has_good_fit)
502500

@@ -510,7 +508,6 @@ def test_has_good_opt_config_model_fit(self) -> None:
510508
has_good_fit = has_good_opt_config_model_fit(
511509
optimization_config=optimization_config,
512510
assess_model_fit_result=assess_model_fit_result,
513-
metric_name_to_signature={"m1": "m1", "m2": "m2"},
514511
)
515512
self.assertFalse(has_good_fit)
516513

ax/adapter/tests/test_torch_moo_adapter.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -464,7 +464,10 @@ def test_infer_objective_thresholds(self, _, cuda: bool = False) -> None:
464464
first = sub_exprs[0]
465465
if not first.startswith("-"):
466466
sub_exprs[0] = f"-{first}"
467-
oc.objective = Objective(expression=", ".join(sub_exprs))
467+
oc.objective = Objective(
468+
expression=", ".join(sub_exprs),
469+
metric_name_to_signature={s.lstrip("-"): s.lstrip("-") for s in sub_exprs},
470+
)
468471

469472
for use_partial_thresholds in (False, True):
470473
if use_partial_thresholds:

0 commit comments

Comments
 (0)