forked from facebook/Ax
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathobjective_p_feasible_frontier.py
More file actions
256 lines (232 loc) · 10.8 KB
/
objective_p_feasible_frontier.py
File metadata and controls
256 lines (232 loc) · 10.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
# pyre-strict
from collections.abc import Sequence
from logging import Logger
from typing import final
from ax.adapter.base import Adapter
from ax.adapter.torch import TorchAdapter
from ax.analysis.analysis import Analysis
from ax.analysis.plotly.plotly_analysis import (
create_plotly_analysis_card,
PlotlyAnalysisCard,
)
from ax.analysis.plotly.scatter import _prepare_figure as _prepare_figure_scatter
from ax.analysis.plotly.utils import get_trial_statuses_with_fallback
from ax.analysis.utils import (
extract_relevant_adapter,
prepare_arm_data,
validate_experiment,
)
from ax.core.arm import Arm
from ax.core.experiment import Experiment
from ax.core.optimization_config import MultiObjectiveOptimizationConfig
from ax.core.trial_status import TrialStatus
from ax.exceptions.core import UnsupportedError
from ax.generation_strategy.generation_strategy import GenerationStrategy
from ax.generators.torch.botorch_modular.generator import BoTorchGenerator
from ax.generators.torch.botorch_modular.multi_acquisition import MultiAcquisition
from ax.utils.common.logger import get_logger
from botorch.acquisition.analytic import LogProbabilityOfFeasibility, PosteriorMean
from pyre_extensions import assert_is_instance, none_throws, override
logger: Logger = get_logger(__name__)
OBJ_PFEAS_CARDGROUP_SUBTITLE = (
"This plot shows <b>newly generated arms</b> with optimal trade-offs between"
" <b>Ax model-estimated effect on the objective (x-axis)</b> and <b>Ax-model"
" estimated probability of satisfying the constraints (y-axis)</b>. This plot"
" is useful for understanding: 1) how tight the constraints are (sometimes the"
" constraints can be configured too conservatively, making it difficult to find"
" an arm that improves the objective(s) while satisfying the constraints),"
" 2) how much headroom there is with the current optimization configuration "
" (objective(s) and constraints). <b>If arms that are likely feasible (y-axis),"
" do not improve your objective enough, revisiting your optimization config and"
" relaxing the constraints may be helpful.</b> This analysis can be computed"
" adhoc in a notebook environment, and will change with modifications to the"
" optimization config, so you can understand the potential impact of optimization"
" config modifications prior to running another iteration. Get in touch with the"
" Ax developers for pointers on including these arms in a trial or running this"
" via a notebook."
)
@final
class ObjectivePFeasibleFrontierPlot(Analysis):
"""
Plotly Scatter plot for the objective vs p(feasible). Each arm is represented by
a single point with 95% confidence intervals if the data is available. Effects are
the predicted effects using a model.
"""
def __init__(
self,
relativize: bool = False,
additional_arms: Sequence[Arm] | None = None,
label: str | None = None,
num_points_to_generate: int = 10,
trial_index: int | None = None,
trial_statuses: Sequence[TrialStatus] | None = None,
) -> None:
"""
Args:
relativize: Whether to relativize the effects of each arm against the status
quo arm. If multiple status quo arms are present, relativize each arm
against the status quo arm from the same trial.
additional_arms: If present, include these arms in the plot in addition to
the arms in the experiment. These arms will be marked as belonging to a
trial with index -1.
label: A label to use in the plot in place of the metric name.
trial_index: If present, only use arms from the trial with the given index.
trial_statuses: If present, only use arms from trials with the given
statuses. By default, exclude STALE, ABANDONED, and FAILED trials.
num_points_to_generate: The number of points to generate on the frontier.
Ideally this should be sufficiently large to provide a frontier with
reasonably good coverage.
"""
self.relativize = relativize
# store original additional_arms so can add newly generated frontier arms to
# additional_arms, but have `compute` remain idempotent.
self._additional_arms: list[Arm] = (
[] if additional_arms is None else [*additional_arms]
)
self.additional_arms: list[Arm] = self._additional_arms
self.label = label
self.num_points_to_generate = num_points_to_generate
self.trial_statuses: list[TrialStatus] | None = (
get_trial_statuses_with_fallback(
trial_statuses=trial_statuses, trial_index=trial_index
)
)
self.trial_index = trial_index
@override
def validate_applicable_state(
self,
experiment: Experiment | None = None,
generation_strategy: GenerationStrategy | None = None,
adapter: Adapter | None = None,
) -> str | None:
"""
ObjectivePFeasibleFrontierPlot requires an Experiment with trials and data, and
is only valid for single objective constrained problems. Additionally, the
supplied adapter must be a TorchAdapter using a BoTorchGenerator.
"""
if (
experiment_invalid_reason := validate_experiment(
experiment=experiment,
require_trials=True,
require_data=True,
)
) is not None:
return experiment_invalid_reason
experiment = none_throws(experiment)
if experiment.optimization_config is None:
return "Optimization_config must be set to compute frontier."
if isinstance(experiment.optimization_config, MultiObjectiveOptimizationConfig):
return "Multi-objective optimization is not supported."
if experiment.optimization_config.objective.is_scalarized_objective:
return "Scalarized objectives are not supported."
if len(experiment.optimization_config.outcome_constraints) == 0:
return (
"Plotting the objective-p(feasible) frontier requires at least one "
"outcome constraint."
)
if any(
len(oc.metric_names) > 1
for oc in experiment.optimization_config.outcome_constraints
):
return "Scalarized outcome constraints are not supported yet."
relevant_adapter = extract_relevant_adapter(
experiment=experiment,
generation_strategy=generation_strategy,
adapter=adapter,
)
if not isinstance(relevant_adapter, TorchAdapter) or not isinstance(
relevant_adapter.generator, BoTorchGenerator
):
return (
"The Objective vs P(feasible) plot cannot be computed using the"
f" current Adapter ({relevant_adapter}) and generator"
f" ({relevant_adapter.generator}). Only TorchAdapters using"
" BoTorchGenerators are supported."
)
@override
def compute(
self,
experiment: Experiment | None = None,
generation_strategy: GenerationStrategy | None = None,
adapter: Adapter | None = None,
) -> PlotlyAnalysisCard:
experiment = none_throws(experiment)
relevant_adapter = extract_relevant_adapter(
experiment=experiment,
generation_strategy=generation_strategy,
adapter=adapter,
)
# Generate arms on the objective p_feasible frontier
# Specify to optimize multiple acquisition functions (via
# MultiAcquisition). This will optimize the PosteriorMean and the
# LogProbabilityOfFeasibility using MOO.
generator = assert_is_instance(relevant_adapter.generator, BoTorchGenerator)
orig_acquisition_class = generator.acquisition_class
orig_acquisition_options = generator.acquisition_options
orig_botorch_acqf_classes_with_options = (
generator._botorch_acqf_classes_with_options
)
generator.acquisition_class = MultiAcquisition
generator.acquisition_options = {}
generator._botorch_acqf_classes_with_options = [
(PosteriorMean, {}),
(LogProbabilityOfFeasibility, {}),
]
frontier_gr = relevant_adapter.gen(n=self.num_points_to_generate)
# in case this generator is used again, restore the original settings
generator.acquisition_class = orig_acquisition_class
generator.acquisition_options = orig_acquisition_options
generator._botorch_acqf_classes_with_options = (
orig_botorch_acqf_classes_with_options
)
arms = [
Arm(name=f"frontier_{i}", parameters=arm.parameters)
for i, arm in enumerate(frontier_gr.arms)
]
# concatenate self._additional_arms and arms, so that `compute`
# remains idempotent
self.additional_arms = self._additional_arms + arms
optimization_config = none_throws(experiment.optimization_config)
df = prepare_arm_data(
experiment=experiment,
metric_names=[*optimization_config.metric_names],
adapter=relevant_adapter,
use_model_predictions=True,
relativize=self.relativize,
additional_arms=self.additional_arms,
trial_index=self.trial_index,
trial_statuses=self.trial_statuses,
)
objective = optimization_config.objective
if objective.is_scalarized_objective:
raise UnsupportedError(
"ObjectivePFeasibleFrontierPlot is not supported for "
"scalarized objectives. The objective is a combination of "
"metrics, not a single metric."
)
objective_name = objective.metric_names[0]
fig = _prepare_figure_scatter(
df=df,
x_metric_name=objective_name,
y_metric_name="p_feasible",
x_metric_label=self.label if self.label is not None else objective_name,
y_metric_label="% Chance of Satisfying the Constraints",
is_relative=self.relativize,
show_pareto_frontier=False,
x_lower_is_better=objective.minimize,
y_lower_is_better=False,
)
return create_plotly_analysis_card(
name="ObjectivePFeasibleFrontierPlot",
title=(
f"Modeled {'Relativized ' if self.relativize else ''} Effect on the"
" Objective vs % Chance of Satisfying the Constraints"
),
subtitle=OBJ_PFEAS_CARDGROUP_SUBTITLE,
df=df,
fig=fig,
)