2323)
2424from ax .core .arm import Arm
2525from ax .core .experiment import Experiment
26- from ax .core .objective import MultiObjective , Objective , ScalarizedObjective
26+ from ax .core .objective import Objective
2727from ax .core .observation import Observation , ObservationData , ObservationFeatures
2828from ax .core .optimization_config import (
2929 MultiObjectiveOptimizationConfig ,
@@ -206,6 +206,7 @@ def extract_objective_thresholds(
206206 objective_thresholds : TRefPoint ,
207207 objective : Objective ,
208208 outcomes : list [str ],
209+ experiment : Experiment ,
209210) -> npt .NDArray | None :
210211 """Extracts objective thresholds' values, in the order of `outcomes`.
211212
@@ -221,6 +222,7 @@ def extract_objective_thresholds(
221222 objective_thresholds: Objective thresholds to extract values from.
222223 objective: The corresponding Objective, for validation purposes.
223224 outcomes: n-length list of names of metrics.
225+ experiment: The experiment, used to map metric names to signatures.
224226
225227 Returns:
226228 (n,) array of thresholds
@@ -230,15 +232,19 @@ def extract_objective_thresholds(
230232
231233 objective_threshold_dict = {}
232234 for ot in objective_thresholds :
235+ ot_signature = experiment .get_metric (ot .metric_names [0 ]).signature
233236 if ot .relative :
234237 raise ValueError (
235- f"Objective { ot . metric . signature } has a relative threshold that "
238+ f"Objective { ot_signature } has a relative threshold that "
236239 f"is not supported here."
237240 )
238- objective_threshold_dict [ot . metric . signature ] = ot .bound
241+ objective_threshold_dict [ot_signature ] = ot .bound
239242
240243 # Check that all thresholds correspond to a metric.
241- if set (objective_threshold_dict .keys ()).difference (set (objective .metric_names )):
244+ obj_metric_signatures = [
245+ experiment .get_metric (name ).signature for name in objective .metric_names
246+ ]
247+ if set (objective_threshold_dict .keys ()).difference (set (obj_metric_signatures )):
242248 raise ValueError (
243249 "Some objective thresholds do not have corresponding metrics. "
244250 f"Got { objective_thresholds = } and { objective = } ."
@@ -252,7 +258,9 @@ def extract_objective_thresholds(
252258 return obj_t
253259
254260
255- def extract_objective_weights (objective : Objective , outcomes : list [str ]) -> npt .NDArray :
261+ def extract_objective_weights (
262+ objective : Objective , outcomes : list [str ], experiment : Experiment
263+ ) -> npt .NDArray :
256264 """Extract a weights for objectives.
257265
258266 Weights are for a maximization problem.
@@ -268,29 +276,24 @@ def extract_objective_weights(objective: Objective, outcomes: list[str]) -> npt.
268276
269277 Args:
270278 objective: Objective to extract weights from.
271- outcomes: n-length list of names of metrics.
279+ outcomes: n-length list of metric signatures.
280+ experiment: The experiment, used to map metric names to signatures.
272281
273282 Returns:
274283 n-length array of weights.
275284
276285 """
277286 objective_weights = np .zeros (len (outcomes ))
278- if isinstance (objective , ScalarizedObjective ):
279- s = - 1.0 if objective .minimize else 1.0
280- for obj_metric , obj_weight in objective .metric_weights :
281- objective_weights [outcomes .index (obj_metric .signature )] = obj_weight * s
282- elif isinstance (objective , MultiObjective ):
283- for obj in objective .objectives :
284- s = - 1.0 if obj .minimize else 1.0
285- objective_weights [outcomes .index (obj .metric .signature )] = s
286- else :
287- s = - 1.0 if objective .minimize else 1.0
288- objective_weights [outcomes .index (objective .metric .signature )] = s
287+ # metric_weights returns sign-encoded (name, weight) tuples for all
288+ # objective types (single, scalarized, multi).
289+ for obj_metric_name , obj_weight in objective .metric_weights :
290+ sig = experiment .get_metric (obj_metric_name ).signature
291+ objective_weights [outcomes .index (sig )] = obj_weight
289292 return objective_weights
290293
291294
292295def extract_objective_weight_matrix (
293- objective : Objective , outcomes : list [str ]
296+ objective : Objective , outcomes : list [str ], experiment : Experiment
294297) -> npt .NDArray :
295298 """Extract a 2D weight matrix for objectives.
296299
@@ -304,23 +307,31 @@ def extract_objective_weight_matrix(
304307
305308 Args:
306309 objective: Objective to extract weights from.
307- outcomes: n-length list of names of metrics.
310+ outcomes: n-length list of signatures of metrics.
308311
309312 Returns:
310313 ``(n_objectives, n)`` array of weights.
311314 """
312- if isinstance ( objective , MultiObjective ) :
315+ if objective . is_multi_objective :
313316 rows : list [npt .NDArray ] = []
314- for obj in objective .objectives :
315- rows .append (extract_objective_weights (obj , outcomes ))
317+ for name , weight in objective .metric_weights :
318+ rows .append (
319+ extract_objective_weights (
320+ objective = Objective (expression = f"{ weight } * { name } " ),
321+ outcomes = outcomes ,
322+ experiment = experiment ,
323+ )
324+ )
316325 return np .stack (rows , axis = 0 )
317326 else :
318327 # Single row – covers Objective and ScalarizedObjective
319- return extract_objective_weights (objective , outcomes ).reshape (1 , - 1 )
328+ return extract_objective_weights (objective , outcomes , experiment ).reshape (1 , - 1 )
320329
321330
322331def extract_outcome_constraints (
323- outcome_constraints : list [OutcomeConstraint ], outcomes : list [str ]
332+ outcome_constraints : list [OutcomeConstraint ],
333+ outcomes : list [str ],
334+ experiment : Experiment ,
324335) -> TBounds :
325336 if len (outcome_constraints ) == 0 :
326337 return None
@@ -330,11 +341,11 @@ def extract_outcome_constraints(
330341 for i , c in enumerate (outcome_constraints ):
331342 s = 1 if c .op == ComparisonOp .LEQ else - 1
332343 if isinstance (c , ScalarizedOutcomeConstraint ):
333- for c_metric , c_weight in c .metric_weights :
334- j = outcomes .index (c_metric .signature )
344+ for c_metric_name , c_weight in c .metric_weights :
345+ j = outcomes .index (experiment . get_metric ( c_metric_name ) .signature )
335346 A [i , j ] = s * c_weight
336347 else :
337- j = outcomes .index (c . metric .signature )
348+ j = outcomes .index (experiment . get_metric ( c . metric_names [ 0 ]) .signature )
338349 A [i , j ] = s
339350 b [i , 0 ] = s * c .bound
340351 return (A , b )
@@ -645,16 +656,20 @@ def get_pareto_frontier_and_configs(
645656 )
646657 # Extract weights, constraints, and objective_thresholds
647658 objective_weights = extract_objective_weight_matrix (
648- objective = optimization_config .objective , outcomes = adapter .outcomes
659+ objective = optimization_config .objective ,
660+ outcomes = adapter .outcomes ,
661+ experiment = adapter ._experiment ,
649662 )
650663 outcome_constraints = extract_outcome_constraints (
651664 outcome_constraints = optimization_config .outcome_constraints ,
652665 outcomes = adapter .outcomes ,
666+ experiment = adapter ._experiment ,
653667 )
654668 obj_t = extract_objective_thresholds (
655669 objective_thresholds = optimization_config .objective_thresholds ,
656670 objective = optimization_config .objective ,
657671 outcomes = adapter .outcomes ,
672+ experiment = adapter ._experiment ,
658673 )
659674 if obj_t is not None :
660675 obj_t = array_to_tensor (obj_t )
@@ -1113,6 +1128,7 @@ def observation_features_to_array(
11131128def feasible_hypervolume (
11141129 optimization_config : MultiObjectiveOptimizationConfig ,
11151130 values : dict [str , npt .NDArray ],
1131+ experiment : Experiment ,
11161132) -> npt .NDArray :
11171133 """Compute the feasible hypervolume each iteration.
11181134
@@ -1121,34 +1137,35 @@ def feasible_hypervolume(
11211137 values: Dictionary from metric name to array of value at each
11221138 iteration (each array is `n`-dim). If optimization config contains
11231139 outcome constraints, values for them must be present in `values`.
1140+ experiment: The experiment, used to map metric names to signatures.
11241141
11251142 Returns: Array of feasible hypervolumes.
11261143 """
11271144 # Get objective at each iteration
11281145 obj_threshold_dict = {
1129- ot .metric .signature : ot .bound for ot in optimization_config .objective_thresholds
1146+ experiment .get_metric (ot .metric_names [0 ]).signature : ot .bound
1147+ for ot in optimization_config .objective_thresholds
11301148 }
1131- f_vals = np .hstack (
1132- [
1133- values [m .signature ].reshape (- 1 , 1 )
1134- for m in optimization_config .objective .metrics
1135- ]
1136- )
1137- obj_thresholds = np .array (
1138- [obj_threshold_dict [m .signature ] for m in optimization_config .objective .metrics ]
1139- )
1149+ obj_metric_names = optimization_config .objective .metric_names
1150+ obj_metrics = [experiment .get_metric (name ) for name in obj_metric_names ]
1151+ f_vals = np .hstack ([values [m .signature ].reshape (- 1 , 1 ) for m in obj_metrics ])
1152+ obj_thresholds = np .array ([obj_threshold_dict [m .signature ] for m in obj_metrics ])
11401153 # Set infeasible points to be the objective threshold
11411154 for oc in optimization_config .outcome_constraints :
11421155 if oc .relative :
11431156 raise ValueError (
11441157 "Benchmark aggregation does not support relative constraints"
11451158 )
1146- g = values [oc .metric .signature ]
1159+ oc_sig = experiment .get_metric (oc .metric_names [0 ]).signature
1160+ g = values [oc_sig ]
11471161 feas = g <= oc .bound if oc .op == ComparisonOp .LEQ else g >= oc .bound
11481162 f_vals [~ feas ] = obj_thresholds
11491163
1164+ # Derive objective directions from the objective's metric_weights.
1165+ # Positive weight = maximize, negative weight = minimize.
1166+ obj_weight_dict = dict (optimization_config .objective .metric_weights )
11501167 obj_weights = np .array (
1151- [- 1 if m . lower_is_better else 1 for m in optimization_config . objective . metrics ]
1168+ [1 if obj_weight_dict [ name ] > 0 else - 1 for name in obj_metric_names ]
11521169 )
11531170 obj_thresholds = obj_thresholds * obj_weights
11541171 f_vals = f_vals * obj_weights
0 commit comments