Skip to content
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

[WIP] Asktell/variables objectives #1448

Open
wants to merge 31 commits into
base: experimental/jlnav_plus_shuds_asktell
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
2b8e537
initial commit - adding variables/objectives to initializer signature…
jlnav Oct 14, 2024
64c2cd1
Merge branch 'experimental/jlnav_plus_shuds_asktell' into asktell/var…
jlnav Oct 18, 2024
3104240
experiment with UniformSampleDicts using variables/objectiveso
jlnav Oct 18, 2024
3d262fb
i wonder if we can determine lb, ub, and n based on the contents of s…
jlnav Oct 21, 2024
847a617
APOSMM can now accept variables and objectives instead of needing ub,…
jlnav Oct 22, 2024
f45ddbe
cleanup the removed validator; since gen_specs['out'] can be absent
jlnav Oct 22, 2024
26f1d73
cleanup/fixes
jlnav Oct 22, 2024
10e96d8
stop kwargs from replacing entire gen_specs.user; try out vars/objs w…
jlnav Oct 22, 2024
23b1549
Merge branch 'experimental/jlnav_plus_shuds_asktell' into asktell/var…
jlnav Oct 23, 2024
5f777c2
additional experiments with vars/objs, including seeing if we can app…
jlnav Oct 25, 2024
ac5467b
moving logic for determining lb and ub from variables into parent cla…
jlnav Oct 28, 2024
85507a4
init pair of functions for mapping, slot in where they'll be called
jlnav Oct 28, 2024
fc30284
remove a debugging print
jlnav Oct 28, 2024
f9e3cba
small fixes, and first tentative implementation of converter for xs t…
jlnav Nov 1, 2024
14c36fa
perhaps the input conversion will be easier on a numpy array?
jlnav Nov 1, 2024
25299e7
tentatively complete converter for vars/objs -> x/f. but those xs and…
jlnav Nov 1, 2024
f0736fb
some cleanup
jlnav Nov 1, 2024
7fa4d1e
fix continue-condition to occur earlier if we're looking at keys we d…
jlnav Nov 4, 2024
14daf3c
test fixes, plus if our gen naturally returns the requested variables…
jlnav Nov 4, 2024
114c7a4
fix asktell_gen functionality test - including removing wrapper tests…
jlnav Nov 4, 2024
507bc0a
just use UniformSample class
jlnav Nov 4, 2024
18a52c9
remove ask/tell surmise and ask/tell surmise test - they were proof-o…
jlnav Nov 4, 2024
231e6f0
fix import
jlnav Nov 4, 2024
eaebbff
remove the other ask/tell surmise test
jlnav Nov 4, 2024
043feeb
renable persistent_aposmm unit test
jlnav Nov 5, 2024
c380595
preparing to add variables_mapping to LibensembleGenerator parent cla…
jlnav Nov 6, 2024
1e0abd3
Merge branch 'experimental/jlnav_plus_shuds_asktell' into asktell/var…
jlnav Nov 7, 2024
c7ea54b
intermediate work on passing mapping into np_to_list_dicts. need to p…
jlnav Nov 7, 2024
0ee448c
use mapping to construct list_dicts_to_np dtype when provided
jlnav Nov 8, 2024
bb37f4b
additional work on replacing dict keys with xs and fs
jlnav Nov 8, 2024
38b3967
some cleanup of generators.py in anticipation of the changes to the d…
jlnav Nov 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ dist/
.spyproject/

.hypothesis
.pixi
1 change: 0 additions & 1 deletion libensemble/gen_classes/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
from .aposmm import APOSMM # noqa: F401
from .sampling import UniformSample, UniformSampleDicts # noqa: F401
from .surmise import Surmise # noqa: F401
27 changes: 19 additions & 8 deletions libensemble/gen_classes/aposmm.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

from libensemble.generators import LibensembleGenThreadInterfacer
from libensemble.message_numbers import EVAL_GEN_TAG, PERSIS_STOP
from libensemble.tools import add_unique_random_streams


class APOSMM(LibensembleGenThreadInterfacer):
Expand All @@ -15,24 +14,36 @@ class APOSMM(LibensembleGenThreadInterfacer):
"""

def __init__(
self, History: npt.NDArray = [], persis_info: dict = {}, gen_specs: dict = {}, libE_info: dict = {}, **kwargs
self,
variables: dict,
objectives: dict,
History: npt.NDArray = [],
persis_info: dict = {},
gen_specs: dict = {},
libE_info: dict = {},
**kwargs
) -> None:
from libensemble.gen_funcs.persistent_aposmm import aposmm

self.variables = variables
self.objectives = objectives

gen_specs["gen_f"] = aposmm

if not gen_specs.get("out"): # gen_specs never especially changes for aposmm even as the problem varies
n = len(kwargs["lb"]) or len(kwargs["ub"])
if not self.variables:
self.n = len(kwargs["lb"]) or len(kwargs["ub"])
else:
self.n = len(self.variables)
gen_specs["out"] = [
("x", float, n),
("x_on_cube", float, n),
("x", float, self.n),
("x_on_cube", float, self.n),
("sim_id", int),
("local_min", bool),
("local_pt", bool),
]
gen_specs["persis_in"] = ["x", "f", "local_pt", "sim_id", "sim_ended", "x_on_cube", "local_min"]
if not persis_info:
persis_info = add_unique_random_streams({}, 2, seed=4321)[1]
super().__init__(History, persis_info, gen_specs, libE_info, **kwargs)
super().__init__(variables, objectives, History, persis_info, gen_specs, libE_info, **kwargs)
if not self.persis_info.get("nworkers"):
self.persis_info["nworkers"] = kwargs.get("nworkers", gen_specs["user"]["max_active_runs"])
self.all_local_minima = []
Expand Down
35 changes: 15 additions & 20 deletions libensemble/gen_classes/sampling.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import numpy as np

from libensemble.generators import Generator, LibensembleGenerator
from libensemble.utils.misc import list_dicts_to_np

__all__ = [
"UniformSample",
Expand Down Expand Up @@ -31,14 +32,16 @@ class UniformSample(SampleBase):
mode by adjusting the allocation function.
"""

def __init__(self, _=[], persis_info={}, gen_specs={}, libE_info=None, **kwargs):
super().__init__(_, persis_info, gen_specs, libE_info, **kwargs)
def __init__(self, variables: dict, objectives: dict, _=[], persis_info={}, gen_specs={}, libE_info=None, **kwargs):
super().__init__(variables, objectives, _, persis_info, gen_specs, libE_info, **kwargs)
self._get_user_params(self.gen_specs["user"])

def ask_numpy(self, n_trials):
H_o = np.zeros(n_trials, dtype=self.gen_specs["out"])
H_o["x"] = self.persis_info["rand_stream"].uniform(self.lb, self.ub, (n_trials, self.n))
return H_o
return list_dicts_to_np(
UniformSampleDicts(
self.variables, self.objectives, self.History, self.persis_info, self.gen_specs, self.libE_info
).ask(n_trials)
)

def tell_numpy(self, calc_in):
pass # random sample so nothing to tell
Expand All @@ -53,31 +56,23 @@ class UniformSampleDicts(Generator):
sampled points the first time it is called. Afterwards, it returns the
number of points given. This can be used in either a batch or asynchronous
mode by adjusting the allocation function.

This currently adheres to the complete standard.
"""

def __init__(self, _, persis_info, gen_specs, libE_info=None, **kwargs):
def __init__(self, variables: dict, objectives: dict, _, persis_info, gen_specs, libE_info=None, **kwargs):
self.variables = variables
self.gen_specs = gen_specs
self.persis_info = persis_info
self._get_user_params(self.gen_specs["user"])

def ask(self, n_trials):
H_o = []
for _ in range(n_trials):
# using same rand number stream
trial = {"x": self.persis_info["rand_stream"].uniform(self.lb, self.ub, self.n)}
trial = {}
for key in self.variables.keys():
trial[key] = self.persis_info["rand_stream"].uniform(self.variables[key][0], self.variables[key][1])
H_o.append(trial)
return H_o

def tell(self, calc_in):
pass # random sample so nothing to tell

# Duplicated for now
def _get_user_params(self, user_specs):
"""Extract user params"""
# b = user_specs["initial_batch_size"]
self.ub = user_specs["ub"]
self.lb = user_specs["lb"]
self.n = len(self.lb) # dimension
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"
60 changes: 0 additions & 60 deletions libensemble/gen_classes/surmise.py

This file was deleted.

90 changes: 62 additions & 28 deletions libensemble/generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
"""


class GeneratorNotStartedException(Exception):
"""Exception raised by a threaded/multiprocessed generator upon being asked without having been started"""


class Generator(ABC):
"""

Expand All @@ -33,9 +37,9 @@ class Generator(ABC):


class MyGenerator(Generator):
def __init__(self, param):
def __init__(self, variables, objectives, param):
self.param = param
self.model = None
self.model = create_model(variables, objectives, self.param)

def ask(self, num_points):
return create_points(num_points, self.param)
Expand All @@ -48,12 +52,15 @@ def final_tell(self, results):
return list(self.model)


my_generator = MyGenerator(my_parameter=100)
variables = {"a": [-1, 1], "b": [-2, 2]}
objectives = {"f": "MINIMIZE"}

my_generator = MyGenerator(variables, objectives, my_parameter=100)
gen_specs = GenSpecs(generator=my_generator, ...)
"""

@abstractmethod
def __init__(self, *args, **kwargs):
def __init__(self, variables: dict[str, List[float]], objectives: dict[str, str], *args, **kwargs):
"""
Initialize the Generator object on the user-side. Constants, class-attributes,
and preparation goes here.
Expand Down Expand Up @@ -95,12 +102,45 @@ class LibensembleGenerator(Generator):
"""

def __init__(
self, History: npt.NDArray = [], persis_info: dict = {}, gen_specs: dict = {}, libE_info: dict = {}, **kwargs
self,
variables: dict,
objectives: dict = {},
History: npt.NDArray = [],
persis_info: dict = {},
gen_specs: dict = {},
libE_info: dict = {},
**kwargs,
):
self.variables = variables
self.objectives = objectives
self.History = History
self.gen_specs = gen_specs
self.libE_info = libE_info

self.variables_mapping = kwargs.get("variables_mapping", {})

self._internal_variable = "x" # need to figure these out dynamically
self._internal_objective = "f"

if self.variables:

self.n = len(self.variables)
# build our own lb and ub
if "lb" not in kwargs and "ub" not in kwargs:
lb = []
ub = []
for i, v in enumerate(self.variables.values()):
if isinstance(v, list) and (isinstance(v[0], int) or isinstance(v[0], float)):
lb.append(v[0])
ub.append(v[1])
kwargs["lb"] = np.array(lb)
kwargs["ub"] = np.array(ub)

if len(kwargs) > 0: # so user can specify gen-specific parameters as kwargs to constructor
self.gen_specs["user"] = kwargs
if not persis_info:
if not self.gen_specs.get("user"):
self.gen_specs["user"] = {}
self.gen_specs["user"].update(kwargs)
if not persis_info.get("rand_stream"):
self.persis_info = add_unique_random_streams({}, 4, seed=4321)[1]
else:
self.persis_info = persis_info
Expand All @@ -122,13 +162,13 @@ def convert_np_types(dict_list):

def ask(self, num_points: Optional[int] = 0) -> List[dict]:
"""Request the next set of points to evaluate."""
return LibensembleGenerator.convert_np_types(np_to_list_dicts(self.ask_numpy(num_points)))
return LibensembleGenerator.convert_np_types(
np_to_list_dicts(self.ask_numpy(num_points), mapping=self.variables_mapping)
)

def tell(self, results: List[dict]) -> None:
"""Send the results of evaluations to the generator."""
self.tell_numpy(list_dicts_to_np(results))
# Note that although we'd prefer to have a complete dtype available, the gen
# doesn't have access to sim_specs["out"] currently.
self.tell_numpy(list_dicts_to_np(results, mapping=self.variables_mapping))


class LibensembleGenThreadInterfacer(LibensembleGenerator):
Expand All @@ -137,19 +177,23 @@ class LibensembleGenThreadInterfacer(LibensembleGenerator):
"""

def __init__(
self, History: npt.NDArray = [], persis_info: dict = {}, gen_specs: dict = {}, libE_info: dict = {}, **kwargs
self,
variables: dict,
objectives: dict = {},
History: npt.NDArray = [],
persis_info: dict = {},
gen_specs: dict = {},
libE_info: dict = {},
**kwargs,
) -> None:
super().__init__(History, persis_info, gen_specs, libE_info, **kwargs)
super().__init__(variables, objectives, History, persis_info, gen_specs, libE_info, **kwargs)
self.gen_f = gen_specs["gen_f"]
self.History = History
self.persis_info = persis_info
self.libE_info = libE_info
self.thread = None

def setup(self) -> None:
"""Must be called once before calling ask/tell. Initializes the background thread."""
# self.inbox = thread_queue.Queue() # sending betweween HERE and gen
# self.outbox = thread_queue.Queue()
self.m = Manager()
self.inbox = self.m.Queue()
self.outbox = self.m.Queue()
Expand All @@ -158,16 +202,6 @@ def setup(self) -> None:
self.libE_info["comm"] = comm # replacing comm so gen sends HERE instead of manager
self.libE_info["executor"] = Executor.executor

# self.thread = QCommThread( # TRY A PROCESS
# self.gen_f,
# None,
# self.History,
# self.persis_info,
# self.gen_specs,
# self.libE_info,
# user_function=True,
# ) # note that self.thread's inbox/outbox are unused by the underlying gen

self.thread = QCommProcess( # TRY A PROCESS
self.gen_f,
None,
Expand All @@ -190,11 +224,11 @@ def _set_sim_ended(self, results: npt.NDArray) -> npt.NDArray:

def tell(self, results: List[dict], tag: int = EVAL_GEN_TAG) -> None:
"""Send the results of evaluations to the generator."""
self.tell_numpy(list_dicts_to_np(results), tag)
self.tell_numpy(list_dicts_to_np(self._objs_and_vars_to_gen_in(results)), tag)

def ask_numpy(self, num_points: int = 0) -> npt.NDArray:
"""Request the next set of points to evaluate, as a NumPy array."""
if not self.thread.running:
if self.thread is None or not self.thread.running:
self.thread.run()
_, ask_full = self.outbox.get()
return ask_full["calc_out"]
Expand Down
Loading