Skip to content

Commit 1dd3e5d

Browse files
authored
Merge pull request #249 from optimas-org/standardization
Attempt to make `optimas` generators compatible with generator standard
2 parents 48c4ce6 + 3dc27c6 commit 1dd3e5d

File tree

14 files changed

+399
-225
lines changed

14 files changed

+399
-225
lines changed

doc/source/examples/ps_grid_sampling.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ where :math:`l_b=0` and :math:`u_b=15`, the grid of sample looks like:
4343

4444
all_trials = []
4545
while True:
46-
trial = gen.ask(1)
46+
trial = gen.ask_trials(1)
4747
if trial:
4848
all_trials.append(trial[0])
4949
else:

doc/source/examples/ps_line_sampling.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ where :math:`x_0` and :math:`x_1` have a default values of :math:`5` and
4646

4747
all_trials = []
4848
while True:
49-
trial = gen.ask(1)
49+
trial = gen.ask_trials(1)
5050
if trial:
5151
all_trials.append(trial[0])
5252
else:

doc/source/examples/ps_random_sampling.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ distribution such as:
4545

4646
all_trials = []
4747
while len(all_trials) <= 100:
48-
trial = gen.ask(1)
48+
trial = gen.ask_trials(1)
4949
if trial:
5050
all_trials.append(trial[0])
5151
else:

optimas/core/trial.py

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,12 @@ def __init__(
6363
)
6464
evaluations = [] if evaluations is None else evaluations
6565
self._index = index
66+
self._gen_id = None
6667
self._custom_parameters = (
6768
[] if custom_parameters is None else custom_parameters
6869
)
6970
self._ignored = False
7071
self._ignored_reason = None
71-
7272
# Add custom parameters as trial attributes.
7373
for param in self._custom_parameters:
7474
setattr(self, param.name, None)
@@ -81,6 +81,76 @@ def __init__(
8181
self._mapped_evaluations[ev.parameter.name] = ev
8282
self.mark_as(TrialStatus.CANDIDATE)
8383

84+
def to_dict(self) -> Dict:
85+
"""Convert the trial to a dictionary."""
86+
trial_dict = {
87+
**self.parameters_as_dict(),
88+
**self.objectives_as_dict(),
89+
**self.analyzed_parameters_as_dict(),
90+
**self.custom_parameters_as_dict(),
91+
"_id": self._gen_id,
92+
"_ignored": self._ignored,
93+
"_ignored_reason": self._ignored_reason,
94+
"_status": self._status,
95+
}
96+
97+
return trial_dict
98+
99+
@classmethod
100+
def from_dict(
101+
cls,
102+
trial_dict: Dict,
103+
varying_parameters: List[VaryingParameter],
104+
objectives: List[Objective],
105+
analyzed_parameters: List[Parameter],
106+
custom_parameters: Optional[List[TrialParameter]] = None,
107+
) -> "Trial":
108+
"""Create a trial from a dictionary.
109+
110+
Parameters
111+
----------
112+
trial_dict : dict
113+
Dictionary containing the trial information.
114+
varying_parameters : list of VaryingParameter
115+
The varying parameters of the optimization.
116+
objectives : list of Objective
117+
The optimization objectives.
118+
analyzed_parameters : list of Parameter, optional
119+
Additional parameters to be analyzed during the optimization.
120+
custom_parameters : list of TrialParameter, optional
121+
Additional parameters needed to identify or carry out the trial, and
122+
which will be included in the optimization history.
123+
"""
124+
# Prepare values of the input parameters
125+
parameter_values = [trial_dict[var.name] for var in varying_parameters]
126+
# Prepare evaluations
127+
evaluations = [
128+
Evaluation(parameter=par, value=trial_dict[par.name])
129+
for par in objectives + analyzed_parameters
130+
if par.name in trial_dict
131+
]
132+
# Create the trial object
133+
trial = cls(
134+
varying_parameters=varying_parameters,
135+
objectives=objectives,
136+
analyzed_parameters=analyzed_parameters,
137+
parameter_values=parameter_values,
138+
evaluations=evaluations,
139+
custom_parameters=custom_parameters,
140+
)
141+
if "_id" in trial_dict:
142+
trial._gen_id = trial_dict["_id"]
143+
if "_ignored" in trial_dict:
144+
trial._ignored = trial_dict["_ignored"]
145+
if "_ignored_reason" in trial_dict:
146+
trial._ignored_reason = trial_dict["_ignored_reason"]
147+
if "_status" in trial_dict:
148+
trial._status = trial_dict["_status"]
149+
for custom_param in custom_parameters:
150+
setattr(trial, custom_param.name, trial_dict[custom_param.name])
151+
152+
return trial
153+
84154
@property
85155
def varying_parameters(self) -> List[VaryingParameter]:
86156
"""Get the list of varying parameters."""
@@ -129,6 +199,15 @@ def index(self) -> int:
129199
def index(self, value):
130200
self._index = value
131201

202+
@property
203+
def gen_id(self) -> int:
204+
"""Get the index of the trial."""
205+
return self._gen_id
206+
207+
@gen_id.setter
208+
def gen_id(self, value):
209+
self._gen_id = value
210+
132211
@property
133212
def ignored(self) -> bool:
134213
"""Get whether the trial is ignored by the generator."""
@@ -225,7 +304,8 @@ def objectives_as_dict(self) -> Dict:
225304
params = {}
226305
for obj in self._objectives:
227306
ev = self._mapped_evaluations[obj.name]
228-
params[obj.name] = (ev.value, ev.sem)
307+
if ev is not None:
308+
params[obj.name] = ev.value
229309
return params
230310

231311
def analyzed_parameters_as_dict(self) -> Dict:
@@ -237,5 +317,13 @@ def analyzed_parameters_as_dict(self) -> Dict:
237317
params = {}
238318
for par in self._analyzed_parameters:
239319
ev = self._mapped_evaluations[par.name]
240-
params[par.name] = (ev.value, ev.sem)
320+
if ev is not None:
321+
params[par.name] = ev.value
322+
return params
323+
324+
def custom_parameters_as_dict(self) -> Dict:
325+
"""Get a mapping between names and values of the custom parameters."""
326+
params = {}
327+
for param in self._custom_parameters:
328+
params[param.name] = getattr(self, param.name)
241329
return params

optimas/gen_functions.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def persistent_generator(H, persis_info, gen_specs, libE_info):
2020
"""Generate and launch evaluations with the optimas generators.
2121
2222
This function gets the generator object and uses it to generate new
23-
evaluations via the `ask` method. Once finished, the result of the
23+
evaluations via the `ask_trials` method. Once finished, the result of the
2424
evaluations is communicated back to the generator via the `tell` method.
2525
2626
This is a persistent generator function, i.e., it is called by a dedicated
@@ -68,7 +68,7 @@ def persistent_generator(H, persis_info, gen_specs, libE_info):
6868
# Ask the optimizer to generate `batch_size` new points
6969
# Store this information in the format expected by libE
7070
H_o = np.zeros(number_of_gen_points, dtype=gen_specs["out"])
71-
generated_trials = generator.ask(number_of_gen_points)
71+
generated_trials = generator.ask_trials(number_of_gen_points)
7272
for i, trial in enumerate(generated_trials):
7373
for var, val in zip(
7474
trial.varying_parameters, trial.parameter_values
@@ -107,8 +107,8 @@ def persistent_generator(H, persis_info, gen_specs, libE_info):
107107
y = calc_in[par.name][i]
108108
ev = Evaluation(parameter=par, value=y)
109109
trial.complete_evaluation(ev)
110-
# Register trial with unknown SEM
111-
generator.tell([trial])
110+
generator.tell_trials([trial])
111+
112112
# Set the number of points to generate to that number:
113113
number_of_gen_points = min(n + n_failed_gens, max_evals - n_gens)
114114
n_failed_gens = 0

optimas/generators/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from .grid_sampling import GridSamplingGenerator
2323
from .line_sampling import LineSamplingGenerator
2424
from .random_sampling import RandomSamplingGenerator
25-
25+
from .external import ExternalGenerator
2626

2727
__all__ = [
2828
"AxSingleFidelityGenerator",
@@ -32,4 +32,5 @@
3232
"GridSamplingGenerator",
3333
"LineSamplingGenerator",
3434
"RandomSamplingGenerator",
35+
"ExternalGenerator",
3536
]

optimas/generators/ax/developer/multitask.py

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -196,10 +196,13 @@ def __init__(
196196
model_save_period: Optional[int] = 5,
197197
model_history_dir: Optional[str] = "model_history",
198198
) -> None:
199+
# As trial parameters these get written to history array
200+
# Ax trial_index and arm toegther locate a point
201+
# Multiple points (Optimas trials) can share the same Ax trial_index
199202
custom_trial_parameters = [
200203
TrialParameter("arm_name", "ax_arm_name", dtype="U32"),
201204
TrialParameter("trial_type", "ax_trial_type", dtype="U32"),
202-
TrialParameter("trial_index", "ax_trial_index", dtype=int),
205+
TrialParameter("ax_trial_id", "ax_trial_index", dtype=int),
203206
]
204207
self._check_inputs(varying_parameters, objectives, lofi_task, hifi_task)
205208
super().__init__(
@@ -260,23 +263,38 @@ def _check_inputs(
260263
"to the number of high-fidelity trials"
261264
)
262265

263-
def _ask(self, trials: List[Trial]) -> List[Trial]:
264-
"""Fill in the parameter values of the requested trials."""
265-
for trial in trials:
266+
def suggest(self, num_points: Optional[int]) -> List[dict]:
267+
"""Request the next set of points to evaluate."""
268+
points = []
269+
for _ in range(num_points):
266270
next_trial = self._get_next_trial_arm()
267271
if next_trial is not None:
268272
arm, trial_type, trial_index = next_trial
269-
trial.parameter_values = [
270-
arm.parameters.get(var.name)
273+
point = {
274+
var.name: arm.parameters.get(var.name)
271275
for var in self._varying_parameters
272-
]
273-
trial.trial_type = trial_type
274-
trial.arm_name = arm.name
275-
trial.trial_index = trial_index
276-
return trials
277-
278-
def _tell(self, trials: List[Trial]) -> None:
276+
}
277+
# SH for VOCS standard these will need to be 'variables'
278+
# For now much match the trial parameter names.
279+
point["ax_trial_id"] = trial_index
280+
point["arm_name"] = arm.name
281+
point["trial_type"] = trial_type
282+
points.append(point)
283+
return points
284+
285+
def ingest(self, results: List[dict]) -> None:
279286
"""Incorporate evaluated trials into experiment."""
287+
# reconstruct Optimas trials
288+
trials = []
289+
for result in results:
290+
trial = Trial.from_dict(
291+
trial_dict=result,
292+
varying_parameters=self._varying_parameters,
293+
objectives=self._objectives,
294+
analyzed_parameters=self._analyzed_parameters,
295+
custom_parameters=self._custom_trial_parameters,
296+
)
297+
trials.append(trial)
280298
if self.gen_state == NOT_STARTED:
281299
self._incorporate_external_data(trials)
282300
else:
@@ -285,17 +303,18 @@ def _tell(self, trials: List[Trial]) -> None:
285303
def _incorporate_external_data(self, trials: List[Trial]) -> None:
286304
"""Incorporate external data (e.g., from history) into experiment."""
287305
# Get trial indices.
306+
# SH should have handling if ax_trial_ids are None...
288307
trial_indices = []
289308
for trial in trials:
290-
trial_indices.append(trial.trial_index)
309+
trial_indices.append(trial.ax_trial_id)
291310
trial_indices = np.unique(np.array(trial_indices))
292311

293312
# Group trials by index.
294313
grouped_trials = {}
295314
for index in trial_indices:
296315
grouped_trials[index] = []
297316
for trial in trials:
298-
grouped_trials[trial.trial_index].append(trial)
317+
grouped_trials[trial.ax_trial_id].append(trial)
299318

300319
# Add trials to experiment.
301320
for index in trial_indices:

0 commit comments

Comments
 (0)