-
Notifications
You must be signed in to change notification settings - Fork 27
ASK/TELL DEVELOP #1307
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
ASK/TELL DEVELOP #1307
Changes from all commits
c9c4671
601af44
d454b5c
92e22e4
d14f4d2
e27487d
a6feb77
12a133b
de1916a
ee2508e
070fc6f
a969f50
6eb5fe8
1261274
d960b96
3ce0ca2
09cb4a6
601f02c
6733fe5
7466100
751de5e
18e7079
a34d589
5f33724
41c16b7
4860428
ced8992
cbfdf0b
4261ca8
460bbe3
7fdd8a6
69b0584
8c01ca9
4541d8a
345aea3
fe7629e
0d7e1a3
c7d1cb1
94de46f
80df25f
b5d8bcf
f52bf92
5434dfa
0ab048d
7a9a2d8
a68ffb8
5228711
1ef5898
8371d97
092be69
3ebc467
1159e74
138c89e
2922259
c2a2802
5a2eb09
10accde
aa8ad57
6712f1e
1eec392
c4418fb
2b8e537
70dde7b
484304b
b0897d0
cdcb2d8
57bbfb1
dbd19fd
994b652
64c2cd1
3104240
3d262fb
847a617
f45ddbe
26f1d73
10e96d8
0290deb
e01e87b
a1eb450
23b1549
1b4c2c6
5f777c2
a165cdd
ac5467b
85507a4
fc30284
98267e3
74579d5
63ef323
1b1cd59
dcb3486
6828fe0
e443af9
a1937a9
dedef4c
4b812d6
f9e3cba
14c36fa
25299e7
f0736fb
7fa4d1e
14daf3c
114c7a4
507bc0a
18a52c9
231e6f0
eaebbff
043feeb
c66f10b
3d7981b
c380595
fdcfd66
1e0abd3
c7ea54b
c111afd
0ee448c
bb37f4b
38b3967
4b49233
1d213ef
f8c5eaf
dff6bad
c1ec7f6
a5133b9
682daa8
09ebdbc
9f200f0
cf5ac63
f2ef248
2c6a9c4
7224de3
99a7a2c
e8b7052
23e5164
06c14d7
bc1587e
5c2308d
0d146fc
ef906d5
9d07e6c
902b7f0
64b6401
ab09b9f
d66dafb
f4a9691
6fb608e
f926bfa
25bca85
2973b41
5a7160f
b5d66e0
bf4577d
c24730b
8695692
fcb434e
581c9a5
877ecef
54d7b3f
1cd8b45
05fca90
7a96995
c86c571
8379e0f
0ad9dcf
e2263a3
57e8c4b
a952fc1
6ec7528
3ac630a
08ad9df
aca1d50
6ac1faa
56c644e
fec7aa4
0db9f0b
49fda1e
1d6f83b
81267a5
b633f94
a3e8346
4da4298
c0e452c
f9385c2
02b349c
f162e75
c041a6b
23ed512
34f582e
390597f
f80aa9f
80c09d7
7ca34b1
f6341a4
fef5833
673c3eb
65ca79b
1ed05e5
9b1195b
24df60f
715773b
08ead4a
22b69b4
f664e37
99bc450
423f0d6
56e59aa
145e09d
9a6f299
9c0e258
4a52e0b
73bbf69
bf0d79e
b3ce513
2428152
0bcd7c9
bf9ed05
acc4811
2a67724
8c9e313
6b54991
a4ead36
3622219
9b3429b
a2c58fc
012227a
03420b3
682425a
b209901
fd630eb
57a8de9
050c22d
5d31b63
d1d4b76
b05762a
0b8cdec
1e52d99
3c21202
1cb542f
9798b3e
585c521
cf36e85
77efa2a
ed6604d
9cbca1e
ec773d4
5ea9b2b
b14b85d
f8d1833
ad54abd
a383dc0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
|
||
Ask/Tell Generators | ||
=================== | ||
|
||
**BETA - SUBJECT TO CHANGE** | ||
|
||
These generators, implementations, methods, and subclasses are in BETA, and | ||
may change in future releases. | ||
|
||
The Generator interface is expected to roughly correspond with CAMPA's standard: | ||
https://github.com/campa-consortium/gest-api | ||
|
||
libEnsemble is in the process of supporting generator objects that implement the following interface: | ||
|
||
.. automodule:: generators | ||
:members: Generator LibensembleGenerator | ||
:undoc-members: | ||
|
||
.. autoclass:: Generator | ||
:member-order: bysource | ||
:members: |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,3 +12,4 @@ | |
from libensemble import logger | ||
|
||
from .ensemble import Ensemble | ||
from .generators import Generator |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
from .aposmm import APOSMM # noqa: F401 | ||
from .sampling import UniformSample # noqa: F401 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
import copy | ||
from typing import List | ||
|
||
import numpy as np | ||
from gest_api.vocs import VOCS | ||
from numpy import typing as npt | ||
|
||
from libensemble.generators import PersistentGenInterfacer | ||
from libensemble.message_numbers import EVAL_GEN_TAG, PERSIS_STOP | ||
|
||
|
||
class APOSMM(PersistentGenInterfacer): | ||
""" | ||
Standalone object-oriented APOSMM generator | ||
|
||
VOCS variables must include both regular and *_on_cube versions. E.g.,: | ||
vars_std = { | ||
"var1": [-10.0, 10.0], | ||
"var2": [0.0, 100.0], | ||
"var3": [1.0, 50.0], | ||
"var1_on_cube": [0, 1.0], | ||
"var2_on_cube": [0, 1.0], | ||
"var3_on_cube": [0, 1.0] | ||
} | ||
variables_mapping = { | ||
"x": ["var1", "var2", "var3"], | ||
"x_on_cube": ["var1_on_cube", "var2_on_cube", "var3_on_cube"], | ||
} | ||
gen = APOSMM(vocs, variables_mapping=variables_mapping, ...) | ||
""" | ||
|
||
def __init__( | ||
self, | ||
vocs: VOCS, | ||
History: npt.NDArray = [], | ||
persis_info: dict = {}, | ||
gen_specs: dict = {}, | ||
libE_info: dict = {}, | ||
**kwargs, | ||
) -> None: | ||
from libensemble.gen_funcs.persistent_aposmm import aposmm | ||
|
||
self.VOCS = vocs | ||
gen_specs["gen_f"] = aposmm | ||
gen_specs["user"] = {} | ||
super().__init__(vocs, History, persis_info, gen_specs, libE_info, **kwargs) | ||
|
||
# Set bounds using the correct x mapping | ||
x_mapping = self.variables_mapping["x"] | ||
self.gen_specs["user"]["lb"] = np.array([vocs.variables[var].domain[0] for var in x_mapping]) | ||
self.gen_specs["user"]["ub"] = np.array([vocs.variables[var].domain[1] for var in x_mapping]) | ||
|
||
if not gen_specs.get("out"): | ||
x_size = len(self.variables_mapping.get("x", [])) | ||
x_on_cube_size = len(self.variables_mapping.get("x_on_cube", [])) | ||
assert x_size > 0 and x_on_cube_size > 0, "Both x and x_on_cube must be specified in variables_mapping" | ||
assert ( | ||
x_size == x_on_cube_size | ||
), f"x and x_on_cube must have same length but got {x_size} and {x_on_cube_size}" | ||
|
||
gen_specs["out"] = [ | ||
("x", float, x_size), | ||
("x_on_cube", float, x_on_cube_size), | ||
("sim_id", int), | ||
("local_min", bool), | ||
("local_pt", bool), | ||
] | ||
|
||
gen_specs["persis_in"] = ["sim_id", "x", "x_on_cube", "f", "sim_ended"] | ||
if "components" in kwargs or "components" in gen_specs.get("user", {}): | ||
gen_specs["persis_in"].append("fvec") | ||
|
||
# SH - Need to know if this is gen_on_manager or not. | ||
if not self.persis_info.get("nworkers"): | ||
self.persis_info["nworkers"] = kwargs.get("nworkers", gen_specs["user"].get("max_active_runs", 4)) | ||
self.all_local_minima = [] | ||
self._suggest_idx = 0 | ||
self._last_suggest = None | ||
self._ingest_buf = None | ||
self._n_buffd_results = 0 | ||
self._told_initial_sample = False | ||
|
||
def _slot_in_data(self, results): | ||
"""Slot in libE_calc_in and trial data into corresponding array fields. *Initial sample only!!*""" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is what you want: |
||
self._ingest_buf[self._n_buffd_results : self._n_buffd_results + len(results)] = results | ||
|
||
def _enough_initial_sample(self): | ||
return ( | ||
self._n_buffd_results >= int(self.gen_specs["user"]["initial_sample_size"]) | ||
) or self._told_initial_sample | ||
|
||
def _ready_to_suggest_genf(self): | ||
""" | ||
We're presumably ready to be suggested IF: | ||
- When we're working on the initial sample: | ||
- We have no _last_suggest cached | ||
- all points given out have returned AND we've been suggested *at least* as many points as we cached | ||
- When we're done with the initial sample: | ||
- we've been suggested *at least* as many points as we cached | ||
""" | ||
if not self._told_initial_sample and self._last_suggest is not None: | ||
cond = all([i in self._ingest_buf["sim_id"] for i in self._last_suggest["sim_id"]]) | ||
else: | ||
cond = True | ||
return self._last_suggest is None or (cond and (self._suggest_idx >= len(self._last_suggest))) | ||
|
||
def suggest_numpy(self, num_points: int = 0) -> npt.NDArray: | ||
"""Request the next set of points to evaluate, as a NumPy array.""" | ||
if self._ready_to_suggest_genf(): | ||
self._suggest_idx = 0 | ||
self._last_suggest = super().suggest_numpy(num_points) | ||
|
||
if self._last_suggest["local_min"].any(): # filter out local minima rows | ||
min_idxs = self._last_suggest["local_min"] | ||
self.all_local_minima.append(self._last_suggest[min_idxs]) | ||
self._last_suggest = self._last_suggest[~min_idxs] | ||
|
||
if num_points > 0: # we've been suggested for a selection of the last suggest | ||
results = np.copy(self._last_suggest[self._suggest_idx : self._suggest_idx + num_points]) | ||
self._suggest_idx += num_points | ||
|
||
else: | ||
results = np.copy(self._last_suggest) | ||
self._last_suggest = None | ||
|
||
return results | ||
|
||
def ingest_numpy(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: | ||
if (results is None and tag == PERSIS_STOP) or self._told_initial_sample: | ||
super().ingest_numpy(results, tag) | ||
return | ||
|
||
# Initial sample buffering here: | ||
|
||
if self._n_buffd_results == 0: | ||
self._ingest_buf = np.zeros(self.gen_specs["user"]["initial_sample_size"], dtype=results.dtype) | ||
self._ingest_buf["sim_id"] = -1 | ||
|
||
if not self._enough_initial_sample(): | ||
self._slot_in_data(np.copy(results)) | ||
self._n_buffd_results += len(results) | ||
|
||
if self._enough_initial_sample(): | ||
super().ingest_numpy(self._ingest_buf, tag) | ||
self._told_initial_sample = True | ||
self._n_buffd_results = 0 | ||
|
||
def suggest_updates(self) -> List[npt.NDArray]: | ||
"""Request a list of NumPy arrays containing entries that have been identified as minima.""" | ||
minima = copy.deepcopy(self.all_local_minima) | ||
self.all_local_minima = [] | ||
return minima |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
"""Generator class exposing gpCAM functionality""" | ||
|
||
import time | ||
from typing import List | ||
|
||
import numpy as np | ||
from gest_api.vocs import VOCS | ||
from gpcam import GPOptimizer as GP | ||
from numpy import typing as npt | ||
|
||
# While there are class / func duplicates - re-use functions. | ||
from libensemble.gen_funcs.persistent_gpCAM import ( | ||
_calculate_grid_distances, | ||
_eval_var, | ||
_find_eligible_points, | ||
_generate_mesh, | ||
_read_testpoints, | ||
) | ||
from libensemble.generators import LibensembleGenerator | ||
|
||
__all__ = [ | ||
"GP_CAM", | ||
"GP_CAM_Covar", | ||
] | ||
|
||
|
||
# Equivalent to function persistent_gpCAM_ask_tell | ||
class GP_CAM(LibensembleGenerator): | ||
""" | ||
This generation function constructs a global surrogate of `f` values. | ||
|
||
It is a batched method that produces a first batch uniformly random from | ||
(lb, ub). On subequent iterations, it calls an optimization method to | ||
produce the next batch of points. This optimization might be too slow | ||
(relative to the simulation evaluation time) for some use cases. | ||
""" | ||
|
||
def __init__(self, VOCS: VOCS, ask_max_iter: int = 10, random_seed: int = 1, *args, **kwargs): | ||
|
||
super().__init__(VOCS, *args, **kwargs) | ||
self.rng = np.random.default_rng(random_seed) | ||
|
||
self.lb = np.array([VOCS.variables[i].domain[0] for i in VOCS.variables]) | ||
self.ub = np.array([VOCS.variables[i].domain[1] for i in VOCS.variables]) | ||
self.n = len(self.lb) # dimension | ||
self.all_x = np.empty((0, self.n)) | ||
self.all_y = np.empty((0, 1)) | ||
assert isinstance(self.n, int), "Dimension must be an integer" | ||
assert isinstance(self.lb, np.ndarray), "lb must be a numpy array" | ||
assert isinstance(self.ub, np.ndarray), "ub must be a numpy array" | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to decide There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fair enough. My opinion/intuition is a user is more likely to prefer either "classical" gens (e.g. Jeff) or ask/tell gens (e.g. other CAMPA folks). With these gens' interfaces and users being so different, I don't think an arguably simpler rearrangement of the input parameters is too confusing. Similarly to how some people prefer numpy or pandas; they do similar things, but their interfaces being different isn't a point of contention. I'd also lean towards if someone were to initialize some object, like a gen, themselves, they'd prefer their specifications be provided as early and clearly as possible:
vs.
|
||
self.dtype = [("x", float, (self.n))] | ||
|
||
self.my_gp = None | ||
self.noise = 1e-8 # 1e-12 | ||
self.ask_max_iter = ask_max_iter | ||
|
||
def _validate_vocs(self, vocs): | ||
assert len(vocs.variables), "VOCS must contain variables." | ||
assert len(vocs.objectives), "VOCS must contain at least one objective." | ||
|
||
def suggest_numpy(self, n_trials: int) -> npt.NDArray: | ||
if self.all_x.shape[0] == 0: | ||
self.x_new = self.rng.uniform(self.lb, self.ub, (n_trials, self.n)) | ||
else: | ||
start = time.time() | ||
self.x_new = self.my_gp.ask( | ||
input_set=np.column_stack((self.lb, self.ub)), | ||
n=n_trials, | ||
pop_size=n_trials, | ||
acquisition_function="total correlation", | ||
max_iter=self.ask_max_iter, # Larger takes longer. gpCAM default is 20. | ||
)["x"] | ||
print(f"Ask time:{time.time() - start}") | ||
H_o = np.zeros(n_trials, dtype=self.dtype) | ||
H_o["x"] = self.x_new | ||
return H_o | ||
|
||
def ingest_numpy(self, calc_in: npt.NDArray) -> None: | ||
if calc_in is not None: | ||
if "x" in calc_in.dtype.names: # SH should we require x in? | ||
self.x_new = np.atleast_2d(calc_in["x"]) | ||
self.y_new = np.atleast_2d(calc_in["f"]).T | ||
nan_indices = [i for i, fval in enumerate(self.y_new) if np.isnan(fval[0])] | ||
self.x_new = np.delete(self.x_new, nan_indices, axis=0) | ||
self.y_new = np.delete(self.y_new, nan_indices, axis=0) | ||
|
||
self.all_x = np.vstack((self.all_x, self.x_new)) | ||
self.all_y = np.vstack((self.all_y, self.y_new)) | ||
|
||
noise_var = self.noise * np.ones(len(self.all_y)) | ||
if self.my_gp is None: | ||
self.my_gp = GP(self.all_x, self.all_y.flatten(), noise_variances=noise_var) | ||
else: | ||
self.my_gp.tell(self.all_x, self.all_y.flatten(), noise_variances=noise_var) | ||
self.my_gp.train() | ||
|
||
|
||
class GP_CAM_Covar(GP_CAM): | ||
""" | ||
This generation function constructs a global surrogate of `f` values. | ||
|
||
It is a batched method that produces a first batch uniformly random from | ||
(lb, ub) and on following iterations samples the GP posterior covariance | ||
function to find sample points. | ||
""" | ||
|
||
def __init__(self, VOCS, test_points_file: str = None, use_grid: bool = False, *args, **kwargs): | ||
super().__init__(VOCS, *args, **kwargs) | ||
self.test_points = _read_testpoints({"test_points_file": test_points_file}) | ||
self.x_for_var = None | ||
self.var_vals = None | ||
self.use_grid = use_grid | ||
self.persis_info = {} | ||
if self.use_grid: | ||
self.num_points = 10 | ||
self.x_for_var = _generate_mesh(self.lb, self.ub, self.num_points) | ||
self.r_low_init, self.r_high_init = _calculate_grid_distances(self.lb, self.ub, self.num_points) | ||
|
||
def suggest_numpy(self, n_trials: int) -> List[dict]: | ||
if self.all_x.shape[0] == 0: | ||
x_new = self.rng.uniform(self.lb, self.ub, (n_trials, self.n)) | ||
else: | ||
if not self.use_grid: | ||
x_new = self.x_for_var[np.argsort(self.var_vals)[-n_trials:]] | ||
else: | ||
r_high = self.r_high_init | ||
r_low = self.r_low_init | ||
x_new = [] | ||
r_cand = r_high # Let's start with a large radius and stop when we have batchsize points | ||
|
||
sorted_indices = np.argsort(-self.var_vals) | ||
while len(x_new) < n_trials: | ||
x_new = _find_eligible_points(self.x_for_var, sorted_indices, r_cand, n_trials) | ||
if len(x_new) < n_trials: | ||
r_high = r_cand | ||
r_cand = (r_high + r_low) / 2.0 | ||
|
||
self.x_new = x_new | ||
H_o = np.zeros(n_trials, dtype=self.dtype) | ||
H_o["x"] = self.x_new | ||
return H_o | ||
|
||
def ingest_numpy(self, calc_in: npt.NDArray): | ||
if calc_in is not None: | ||
super().ingest_numpy(calc_in) | ||
if not self.use_grid: | ||
n_trials = len(self.y_new) | ||
self.x_for_var = self.rng.uniform(self.lb, self.ub, (10 * n_trials, self.n)) | ||
|
||
self.var_vals = _eval_var( | ||
self.my_gp, self.all_x, self.all_y, self.x_for_var, self.test_points, self.persis_info | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For libEnsemble arguments this is opaque when looking at this routine. Perhaps they should be written out as additional arguments. Then there's the question of whether you keep gen_specs as well.