Skip to content

Commit

Permalink
Merge pull request #1448 from Libensemble/asktell/variables_objectives
Browse files Browse the repository at this point in the history
Asktell/variables objectives
  • Loading branch information
jlnav authored Dec 4, 2024
2 parents ab09b9f + d66dafb commit f4a9691
Show file tree
Hide file tree
Showing 17 changed files with 240 additions and 529 deletions.
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"])

Check warning on line 35 in libensemble/gen_classes/aposmm.py

View check run for this annotation

Codecov / codecov/patch

libensemble/gen_classes/aposmm.py#L35

Added line #L35 was not covered by tests
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

Check warning on line 78 in libensemble/gen_classes/sampling.py

View check run for this annotation

Codecov / codecov/patch

libensemble/gen_classes/sampling.py#L78

Added line #L78 was not covered by tests

# 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.

92 changes: 63 additions & 29 deletions libensemble/generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
"""


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


class Generator(ABC):
"""
Expand All @@ -32,9 +36,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 @@ -47,12 +51,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 @@ -94,12 +101,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

Check warning on line 145 in libensemble/generators.py

View check run for this annotation

Codecov / codecov/patch

libensemble/generators.py#L145

Added line #L145 was not covered by tests
Expand All @@ -121,13 +161,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))

Check warning on line 170 in libensemble/generators.py

View check run for this annotation

Codecov / codecov/patch

libensemble/generators.py#L170

Added line #L170 was not covered by tests


class LibensembleGenThreadInterfacer(LibensembleGenerator):
Expand All @@ -136,36 +176,30 @@ 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()

if self.thread is not None:
return

Check warning on line 197 in libensemble/generators.py

View check run for this annotation

Codecov / codecov/patch

libensemble/generators.py#L197

Added line #L197 was not covered by tests
# SH this contains the thread lock - removing.... wrong comm to pass on anyway.
if hasattr(Executor.executor, "comm"):
del Executor.executor.comm

Check warning on line 200 in libensemble/generators.py

View check run for this annotation

Codecov / codecov/patch

libensemble/generators.py#L200

Added line #L200 was not covered by tests
self.libE_info["executor"] = Executor.executor

# SH - fix comment (thread and process & name object appropriately - task? qcomm?)
# 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 @@ -191,7 +225,7 @@ 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(results, mapping=self.variables_mapping), tag)

def ask_numpy(self, num_points: int = 0) -> npt.NDArray:
"""Request the next set of points to evaluate, as a NumPy array."""
Expand Down
Loading

0 comments on commit f4a9691

Please sign in to comment.