From 862a77641be800c61019b2f296f9a37bd8eb8997 Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 8 Jan 2024 16:54:05 -0600 Subject: [PATCH 001/288] first round of refactoring runners.py, Runner base class for normal in-place launches, but based on the contents of passed-in specs, instantiates the relevant subclass --- libensemble/utils/runners.py | 122 +++++++++++------------------------ libensemble/worker.py | 10 +-- 2 files changed, 44 insertions(+), 88 deletions(-) diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 07897b942..8c35a9064 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -1,76 +1,53 @@ import inspect import logging import logging.handlers -from typing import Callable, Dict, Optional +from typing import Callable, Optional import numpy.typing as npt -from libensemble.message_numbers import EVAL_GEN_TAG, EVAL_SIM_TAG - logger = logging.getLogger(__name__) -class Runners: - """Determines and returns methods for workers to run user functions. - - Currently supported: direct-call and Globus Compute - """ - - def __init__(self, sim_specs: dict, gen_specs: dict) -> None: - self.sim_specs = sim_specs - self.gen_specs = gen_specs - self.sim_f = sim_specs["sim_f"] - self.gen_f = gen_specs.get("gen_f") - self.has_globus_compute_sim = len(sim_specs.get("globus_compute_endpoint", "")) > 0 - self.has_globus_compute_gen = len(gen_specs.get("globus_compute_endpoint", "")) > 0 - - if any([self.has_globus_compute_sim, self.has_globus_compute_gen]): - if self.has_globus_compute_sim: - self.sim_globus_compute_executor = self._get_globus_compute_executor()( - endpoint_id=self.sim_specs["globus_compute_endpoint"] - ) - self.globus_compute_simfid = self.sim_globus_compute_executor.register_function(self.sim_f) - - if self.has_globus_compute_gen: - self.gen_globus_compute_executor = self._get_globus_compute_executor()( - endpoint_id=self.gen_specs["globus_compute_endpoint"] - ) - self.globus_compute_genfid = self.gen_globus_compute_executor.register_function(self.gen_f) +class Runner: + def __new__(cls, specs): + if len(specs.get("globus_compute_endpoint", "")) > 0: + return super(Runner, GlobusComputeRunner).__new__(GlobusComputeRunner) + if specs.get("threaded"): # TODO: undecided interface + return super(Runner, ThreadRunner).__new__(ThreadRunner) + else: + return Runner - def make_runners(self) -> Dict[int, Callable]: - """Creates functions to run a sim or gen. These functions are either - called directly by the worker or submitted to a Globus Compute endpoint.""" + def __init__(self, specs): + self.specs = specs + self.f = specs.get("sim_f") or specs.get("gen_f") - def run_sim(calc_in, Work): - """Determines how to run sim.""" - if self.has_globus_compute_sim: - result = self._globus_compute_result - else: - result = self._normal_result + def _truncate_args(self, calc_in, persis_info, specs, libE_info, user_f): + nparams = len(inspect.signature(user_f).parameters) + args = [calc_in, persis_info, specs, libE_info] + return args[:nparams] - return result(calc_in, Work["persis_info"], self.sim_specs, Work["libE_info"], self.sim_f, Work["tag"]) + def _result( + self, calc_in: npt.NDArray, persis_info: dict, specs: dict, libE_info: dict, user_f: Callable, tag: int + ) -> (npt.NDArray, dict, Optional[int]): + """User function called in-place""" + args = self._truncate_args(calc_in, persis_info, specs, libE_info, user_f) + return user_f(*args) - if self.gen_specs: + def shutdown(self) -> None: + pass - def run_gen(calc_in, Work): - """Determines how to run gen.""" - if self.has_globus_compute_gen: - result = self._globus_compute_result - else: - result = self._normal_result + def run(self, calc_in, Work): + return self._result(calc_in, Work["persis_info"], self.specs, Work["libE_info"], self.f, Work["tag"]) - return result(calc_in, Work["persis_info"], self.gen_specs, Work["libE_info"], self.gen_f, Work["tag"]) - else: - run_gen = [] - - return {EVAL_SIM_TAG: run_sim, EVAL_GEN_TAG: run_gen} +class GlobusComputeRunner(Runner): + def __init__(self, specs): + super().__init__(specs) + self.globus_compute_executor = self._get_globus_compute_executor()(endpoint_id=specs["globus_compute_endpoint"]) + self.globus_compute_fid = self.globus_compute_executor.register_function(self.f) def shutdown(self) -> None: - if self.has_globus_compute_sim: - self.sim_globus_compute_executor.shutdown() - if self.has_globus_compute_gen: - self.gen_globus_compute_executor.shutdown() + self.globus_compute_executor.shutdown() def _get_globus_compute_executor(self): try: @@ -82,42 +59,21 @@ def _get_globus_compute_executor(self): else: return Executor - def _truncate_args(self, calc_in, persis_info, specs, libE_info, user_f): - nparams = len(inspect.signature(user_f).parameters) - args = [calc_in, persis_info, specs, libE_info] - return args[:nparams] - - def _normal_result( - self, calc_in: npt.NDArray, persis_info: dict, specs: dict, libE_info: dict, user_f: Callable, tag: int - ) -> (npt.NDArray, dict, Optional[int]): - """User function called in-place""" - args = self._truncate_args(calc_in, persis_info, specs, libE_info, user_f) - return user_f(*args) - - def _get_func_uuid(self, tag): - if tag == EVAL_SIM_TAG: - return self.globus_compute_simfid - elif tag == EVAL_GEN_TAG: - return self.globus_compute_genfid - - def _get_globus_compute_exctr(self, tag): - if tag == EVAL_SIM_TAG: - return self.sim_globus_compute_executor - elif tag == EVAL_GEN_TAG: - return self.gen_globus_compute_executor - - def _globus_compute_result( + def _result( self, calc_in: npt.NDArray, persis_info: dict, specs: dict, libE_info: dict, user_f: Callable, tag: int ) -> (npt.NDArray, dict, Optional[int]): - """User function submitted to Globus Compute""" from libensemble.worker import Worker libE_info["comm"] = None # 'comm' object not pickle-able Worker._set_executor(0, None) # ditto for executor fargs = self._truncate_args(calc_in, persis_info, specs, libE_info, user_f) - exctr = self._get_globus_compute_exctr(tag) - func_id = self._get_func_uuid(tag) + exctr = self.globus_compute_executor + func_id = self.globus_compute_fid task_fut = exctr.submit_to_registered_function(func_id, fargs) return task_fut.result() + + +class ThreadRunner(Runner): + pass diff --git a/libensemble/worker.py b/libensemble/worker.py index 792c7886b..46ab84db6 100644 --- a/libensemble/worker.py +++ b/libensemble/worker.py @@ -33,7 +33,7 @@ from libensemble.utils.loc_stack import LocationStack from libensemble.utils.misc import extract_H_ranges from libensemble.utils.output_directory import EnsembleDirectory -from libensemble.utils.runners import Runners +from libensemble.utils.runners import Runner from libensemble.utils.timer import Timer logger = logging.getLogger(__name__) @@ -166,10 +166,10 @@ def __init__( self.workerID = workerID self.libE_specs = libE_specs self.stats_fmt = libE_specs.get("stats_fmt", {}) - + self.sim_runner = Runner(sim_specs) + self.gen_runner = Runner(gen_specs) + self.runners = {EVAL_SIM_TAG: self.sim_runner.run, EVAL_GEN_TAG: self.gen_runner.run} self.calc_iter = {EVAL_SIM_TAG: 0, EVAL_GEN_TAG: 0} - self.runners = Runners(sim_specs, gen_specs) - self._run_calc = self.runners.make_runners() Worker._set_executor(self.workerID, self.comm) Worker._set_resources(self.workerID, self.comm) self.EnsembleDirectory = EnsembleDirectory(libE_specs=libE_specs) @@ -258,7 +258,7 @@ def _handle_calc(self, Work: dict, calc_in: npt.NDArray) -> (npt.NDArray, dict, try: logger.debug(f"Starting {enum_desc}: {calc_id}") - calc = self._run_calc[calc_type] + calc = self.runners[calc_type] with timer: if self.EnsembleDirectory.use_calc_dirs(calc_type): loc_stack, calc_dir = self.EnsembleDirectory.prep_calc_dir( From e6874a6657618059c10a1f1a75dc3ba83355c964 Mon Sep 17 00:00:00 2001 From: jlnav Date: Tue, 9 Jan 2024 12:28:21 -0600 Subject: [PATCH 002/288] refactoring classes so class attributes aren't passed around internally. update unit test --- .../tests/unit_tests/test_ufunc_runners.py | 51 +++++++------------ libensemble/utils/runners.py | 42 +++++++-------- libensemble/worker.py | 3 +- 3 files changed, 37 insertions(+), 59 deletions(-) diff --git a/libensemble/tests/unit_tests/test_ufunc_runners.py b/libensemble/tests/unit_tests/test_ufunc_runners.py index 85b986d39..b63360e81 100644 --- a/libensemble/tests/unit_tests/test_ufunc_runners.py +++ b/libensemble/tests/unit_tests/test_ufunc_runners.py @@ -3,9 +3,8 @@ import pytest import libensemble.tests.unit_tests.setup as setup -from libensemble.message_numbers import EVAL_GEN_TAG, EVAL_SIM_TAG from libensemble.tools.fields_keys import libE_fields -from libensemble.utils.runners import Runners +from libensemble.utils.runners import Runner def get_ufunc_args(): @@ -19,7 +18,7 @@ def get_ufunc_args(): sim_ids = np.zeros(1, dtype=int) Work = { - "tag": EVAL_SIM_TAG, + "tag": 1, "persis_info": {}, "libE_info": {"H_rows": sim_ids}, "H_fields": sim_specs["in"], @@ -28,30 +27,15 @@ def get_ufunc_args(): return calc_in, sim_specs, gen_specs -@pytest.mark.extra def test_normal_runners(): calc_in, sim_specs, gen_specs = get_ufunc_args() - runners = Runners(sim_specs, gen_specs) - assert ( - not runners.has_globus_compute_sim and not runners.has_globus_compute_gen + simrunner = Runner(sim_specs) + genrunner = Runner(gen_specs) + assert not hasattr(simrunner, "globus_compute_executor") and not hasattr( + genrunner, "globus_compute_executor" ), "Globus Compute use should not be detected without setting endpoint fields" - ro = runners.make_runners() - assert all( - [i in ro for i in [EVAL_SIM_TAG, EVAL_GEN_TAG]] - ), "Both user function tags should be included in runners dictionary" - - -@pytest.mark.extra -def test_normal_no_gen(): - calc_in, sim_specs, gen_specs = get_ufunc_args() - - runners = Runners(sim_specs, {}) - ro = runners.make_runners() - - assert not ro[2], "generator function shouldn't be provided if not using gen_specs" - @pytest.mark.extra def test_globus_compute_runner_init(): @@ -60,10 +44,10 @@ def test_globus_compute_runner_init(): sim_specs["globus_compute_endpoint"] = "1234" with mock.patch("globus_compute_sdk.Executor"): - runners = Runners(sim_specs, gen_specs) + runner = Runner(sim_specs) - assert ( - runners.sim_globus_compute_executor is not None + assert hasattr( + runner, "globus_compute_executor" ), "Globus ComputeExecutor should have been instantiated when globus_compute_endpoint found in specs" @@ -74,7 +58,7 @@ def test_globus_compute_runner_pass(): sim_specs["globus_compute_endpoint"] = "1234" with mock.patch("globus_compute_sdk.Executor"): - runners = Runners(sim_specs, gen_specs) + runner = Runner(sim_specs) # Creating Mock Globus ComputeExecutor and Globus Compute future object - no exception globus_compute_mock = mock.Mock() @@ -83,12 +67,12 @@ def test_globus_compute_runner_pass(): globus_compute_future.exception.return_value = None globus_compute_future.result.return_value = (True, True) - runners.sim_globus_compute_executor = globus_compute_mock - ro = runners.make_runners() + runner.globus_compute_executor = globus_compute_mock + runners = {1: runner.run} libE_info = {"H_rows": np.array([2, 3, 4]), "workerID": 1, "comm": "fakecomm"} - out, persis_info = ro[1](calc_in, {"libE_info": libE_info, "persis_info": {}, "tag": 1}) + out, persis_info = runners[1](calc_in, {"libE_info": libE_info, "persis_info": {}, "tag": 1}) assert all([out, persis_info]), "Globus Compute runner correctly returned results" @@ -100,7 +84,7 @@ def test_globus_compute_runner_fail(): gen_specs["globus_compute_endpoint"] = "4321" with mock.patch("globus_compute_sdk.Executor"): - runners = Runners(sim_specs, gen_specs) + runner = Runner(gen_specs) # Creating Mock Globus ComputeExecutor and Globus Compute future object - yes exception globus_compute_mock = mock.Mock() @@ -108,19 +92,18 @@ def test_globus_compute_runner_fail(): globus_compute_mock.submit_to_registered_function.return_value = globus_compute_future globus_compute_future.exception.return_value = Exception - runners.gen_globus_compute_executor = globus_compute_mock - ro = runners.make_runners() + runner.globus_compute_executor = globus_compute_mock + runners = {2: runner.run} libE_info = {"H_rows": np.array([2, 3, 4]), "workerID": 1, "comm": "fakecomm"} with pytest.raises(Exception): - out, persis_info = ro[2](calc_in, {"libE_info": libE_info, "persis_info": {}, "tag": 2}) + out, persis_info = runners[2](calc_in, {"libE_info": libE_info, "persis_info": {}, "tag": 2}) pytest.fail("Expected exception") if __name__ == "__main__": test_normal_runners() - test_normal_no_gen() test_globus_compute_runner_init() test_globus_compute_runner_pass() test_globus_compute_runner_fail() diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 8c35a9064..113fcf45b 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -1,7 +1,7 @@ import inspect import logging import logging.handlers -from typing import Callable, Optional +from typing import Optional import numpy.typing as npt @@ -15,29 +15,27 @@ def __new__(cls, specs): if specs.get("threaded"): # TODO: undecided interface return super(Runner, ThreadRunner).__new__(ThreadRunner) else: - return Runner + return super().__new__(Runner) def __init__(self, specs): self.specs = specs self.f = specs.get("sim_f") or specs.get("gen_f") - def _truncate_args(self, calc_in, persis_info, specs, libE_info, user_f): - nparams = len(inspect.signature(user_f).parameters) - args = [calc_in, persis_info, specs, libE_info] + def _truncate_args(self, calc_in: npt.NDArray, persis_info, libE_info): + nparams = len(inspect.signature(self.f).parameters) + args = [calc_in, persis_info, self.specs, libE_info] return args[:nparams] - def _result( - self, calc_in: npt.NDArray, persis_info: dict, specs: dict, libE_info: dict, user_f: Callable, tag: int - ) -> (npt.NDArray, dict, Optional[int]): + def _result(self, calc_in: npt.NDArray, persis_info: dict, libE_info: dict) -> (npt.NDArray, dict, Optional[int]): """User function called in-place""" - args = self._truncate_args(calc_in, persis_info, specs, libE_info, user_f) - return user_f(*args) + args = self._truncate_args(calc_in, persis_info, libE_info) + return self.f(*args) def shutdown(self) -> None: pass - def run(self, calc_in, Work): - return self._result(calc_in, Work["persis_info"], self.specs, Work["libE_info"], self.f, Work["tag"]) + def run(self, calc_in: npt.NDArray, Work: dict) -> (npt.NDArray, dict, Optional[int]): + return self._result(calc_in, Work["persis_info"], Work["libE_info"]) class GlobusComputeRunner(Runner): @@ -46,9 +44,6 @@ def __init__(self, specs): self.globus_compute_executor = self._get_globus_compute_executor()(endpoint_id=specs["globus_compute_endpoint"]) self.globus_compute_fid = self.globus_compute_executor.register_function(self.f) - def shutdown(self) -> None: - self.globus_compute_executor.shutdown() - def _get_globus_compute_executor(self): try: from globus_compute_sdk import Executor @@ -59,21 +54,20 @@ def _get_globus_compute_executor(self): else: return Executor - def _result( - self, calc_in: npt.NDArray, persis_info: dict, specs: dict, libE_info: dict, user_f: Callable, tag: int - ) -> (npt.NDArray, dict, Optional[int]): + def _result(self, calc_in: npt.NDArray, persis_info: dict, libE_info: dict) -> (npt.NDArray, dict, Optional[int]): from libensemble.worker import Worker libE_info["comm"] = None # 'comm' object not pickle-able Worker._set_executor(0, None) # ditto for executor - fargs = self._truncate_args(calc_in, persis_info, specs, libE_info, user_f) - exctr = self.globus_compute_executor - func_id = self.globus_compute_fid - - task_fut = exctr.submit_to_registered_function(func_id, fargs) + fargs = self._truncate_args(calc_in, persis_info, libE_info) + task_fut = self.globus_compute_executor.submit_to_registered_function(self.globus_compute_fid, fargs) return task_fut.result() + def shutdown(self) -> None: + self.globus_compute_executor.shutdown() + class ThreadRunner(Runner): - pass + def __init__(self, specs): + super().__init__(specs) diff --git a/libensemble/worker.py b/libensemble/worker.py index 46ab84db6..ad8bd4530 100644 --- a/libensemble/worker.py +++ b/libensemble/worker.py @@ -413,5 +413,6 @@ def run(self) -> None: else: self.comm.kill_pending() finally: - self.runners.shutdown() + self.gen_runner.shutdown() + self.sim_runner.shutdown() self.EnsembleDirectory.copy_back() From e17eabedf034f0d5005d19be7e96cdccee820d68 Mon Sep 17 00:00:00 2001 From: jlnav Date: Tue, 9 Jan 2024 16:22:31 -0600 Subject: [PATCH 003/288] ThreadRunner uses comms.QCommThread, slightly modified, to launch its user function. corresponding unit test --- libensemble/comms/comms.py | 17 ++++++++++------- .../tests/unit_tests/test_ufunc_runners.py | 18 ++++++++++++++++++ libensemble/utils/runners.py | 11 +++++++++++ 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/libensemble/comms/comms.py b/libensemble/comms/comms.py index 9bf14e98a..30de28ad9 100644 --- a/libensemble/comms/comms.py +++ b/libensemble/comms/comms.py @@ -146,7 +146,7 @@ def mail_flag(self): class QCommLocal(Comm): - def __init__(self, main, nworkers, *args, **kwargs): + def __init__(self, main, *args, **kwargs): self._result = None self._exception = None self._done = False @@ -208,10 +208,13 @@ def result(self, timeout=None): return self._result @staticmethod - def _qcomm_main(comm, main, *args, **kwargs): + def _qcomm_main(comm, main, *fargs, **kwargs): """Main routine -- handles return values and exceptions.""" try: - _result = main(comm, *args, **kwargs) + if not kwargs.get("ufunc"): + _result = main(comm, *fargs, **kwargs) + else: + _result = main(*fargs) comm.send(CommResult(_result)) except Exception as e: comm.send(CommResultErr(str(e), format_exc())) @@ -233,12 +236,12 @@ def __exit__(self, etype, value, traceback): class QCommThread(QCommLocal): """Launch a user function in a thread with an attached QComm.""" - def __init__(self, main, nworkers, *args, **kwargs): + def __init__(self, main, nworkers, *fargs, **kwargs): self.inbox = thread_queue.Queue() self.outbox = thread_queue.Queue() - super().__init__(self, main, nworkers, *args, **kwargs) + super().__init__(self, main, *fargs, **kwargs) comm = QComm(self.inbox, self.outbox, nworkers) - self.handle = Thread(target=QCommThread._qcomm_main, args=(comm, main) + args, kwargs=kwargs) + self.handle = Thread(target=QCommThread._qcomm_main, args=(comm, main) + fargs, kwargs=kwargs) def terminate(self, timeout=None): """Terminate the thread. @@ -260,7 +263,7 @@ class QCommProcess(QCommLocal): def __init__(self, main, nworkers, *args, **kwargs): self.inbox = Queue() self.outbox = Queue() - super().__init__(self, main, nworkers, *args, **kwargs) + super().__init__(self, main, *args, **kwargs) comm = QComm(self.inbox, self.outbox, nworkers) self.handle = Process(target=QCommProcess._qcomm_main, args=(comm, main) + args, kwargs=kwargs) diff --git a/libensemble/tests/unit_tests/test_ufunc_runners.py b/libensemble/tests/unit_tests/test_ufunc_runners.py index b63360e81..1d3cbb4b2 100644 --- a/libensemble/tests/unit_tests/test_ufunc_runners.py +++ b/libensemble/tests/unit_tests/test_ufunc_runners.py @@ -37,6 +37,23 @@ def test_normal_runners(): ), "Globus Compute use should not be detected without setting endpoint fields" +def test_thread_runners(): + calc_in, sim_specs, gen_specs = get_ufunc_args() + + def tupilize(arg1, arg2): + return (arg1, arg2) + + sim_specs["threaded"] = True # TODO: undecided interface + sim_specs["sim_f"] = tupilize + persis_info = {"hello": "threads"} + + simrunner = Runner(sim_specs) + result = simrunner._result(calc_in, persis_info, {}) + assert result == (calc_in, persis_info) + assert hasattr(simrunner, "thread_handle") + simrunner.shutdown() + + @pytest.mark.extra def test_globus_compute_runner_init(): calc_in, sim_specs, gen_specs = get_ufunc_args() @@ -104,6 +121,7 @@ def test_globus_compute_runner_fail(): if __name__ == "__main__": test_normal_runners() + test_thread_runners() test_globus_compute_runner_init() test_globus_compute_runner_pass() test_globus_compute_runner_fail() diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 113fcf45b..e21c87ba5 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -5,6 +5,8 @@ import numpy.typing as npt +from libensemble.comms.comms import QCommThread + logger = logging.getLogger(__name__) @@ -71,3 +73,12 @@ def shutdown(self) -> None: class ThreadRunner(Runner): def __init__(self, specs): super().__init__(specs) + + def _result(self, calc_in: npt.NDArray, persis_info: dict, libE_info: dict) -> (npt.NDArray, dict, Optional[int]): + fargs = self._truncate_args(calc_in, persis_info, libE_info) + self.thread_handle = QCommThread(self.f, None, *fargs, ufunc=True) + self.thread_handle.run() + return self.thread_handle.result() + + def shutdown(self) -> None: + self.thread_handle.terminate() From 83493d027d41049e5967bee9dd05250fe2b9dfc8 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 10 Jan 2024 10:37:08 -0600 Subject: [PATCH 004/288] handful of small changes from experimental/gen_on_manager_inplace --- libensemble/executors/executor.py | 2 +- libensemble/message_numbers.py | 2 ++ libensemble/resources/scheduler.py | 2 +- libensemble/resources/worker_resources.py | 13 ++++--------- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/libensemble/executors/executor.py b/libensemble/executors/executor.py index 35a321767..c04c0760a 100644 --- a/libensemble/executors/executor.py +++ b/libensemble/executors/executor.py @@ -658,7 +658,7 @@ def set_workerID(self, workerid) -> None: """Sets the worker ID for this executor""" self.workerID = workerid - def set_worker_info(self, comm, workerid=None) -> None: + def set_worker_info(self, comm=None, workerid=None) -> None: """Sets info for this executor""" self.workerID = workerid self.comm = comm diff --git a/libensemble/message_numbers.py b/libensemble/message_numbers.py index adfcbc244..6caef0a6e 100644 --- a/libensemble/message_numbers.py +++ b/libensemble/message_numbers.py @@ -41,6 +41,8 @@ # last_calc_status_rst_tag CALC_EXCEPTION = 35 # Reserved: Automatically used if user_f raised an exception +EVAL_FINAL_GEN_TAG = 36 + MAN_KILL_SIGNALS = [MAN_SIGNAL_FINISH, MAN_SIGNAL_KILL] calc_status_strings = { diff --git a/libensemble/resources/scheduler.py b/libensemble/resources/scheduler.py index 04de87e77..386a406bc 100644 --- a/libensemble/resources/scheduler.py +++ b/libensemble/resources/scheduler.py @@ -245,7 +245,7 @@ def get_avail_rsets_by_group(self): for g in groups: self.avail_rsets_by_group[g] = [] for ind, rset in enumerate(rsets): - if not rset["assigned"]: + if rset["assigned"] == -1: # now default is -1. g = rset["group"] self.avail_rsets_by_group[g].append(ind) return self.avail_rsets_by_group diff --git a/libensemble/resources/worker_resources.py b/libensemble/resources/worker_resources.py index 639f27da7..2becaa1df 100644 --- a/libensemble/resources/worker_resources.py +++ b/libensemble/resources/worker_resources.py @@ -50,11 +50,10 @@ def __init__(self, num_workers: int, resources: "GlobalResources") -> None: # n ) self.rsets = np.zeros(self.total_num_rsets, dtype=ResourceManager.man_rset_dtype) - self.rsets["assigned"] = 0 + self.rsets["assigned"] = -1 # Can assign to manager (=0) so make unset value -1 for field in self.all_rsets.dtype.names: self.rsets[field] = self.all_rsets[field] self.num_groups = self.rsets["group"][-1] - self.rsets_free = self.total_num_rsets self.gpu_rsets_free = self.total_num_gpu_rsets self.nongpu_rsets_free = self.total_num_nongpu_rsets @@ -70,7 +69,7 @@ def assign_rsets(self, rset_team, worker_id): if rset_team: rteam = self.rsets["assigned"][rset_team] for i, wid in enumerate(rteam): - if wid == 0: + if wid == -1: self.rsets["assigned"][rset_team[i]] = worker_id self.rsets_free -= 1 if self.rsets["gpus"][rset_team[i]]: @@ -85,13 +84,13 @@ def assign_rsets(self, rset_team, worker_id): def free_rsets(self, worker=None): """Free up assigned resource sets""" if worker is None: - self.rsets["assigned"] = 0 + self.rsets["assigned"] = -1 self.rsets_free = self.total_num_rsets self.gpu_rsets_free = self.total_num_gpu_rsets self.nongpu_rsets_free = self.total_num_nongpu_rsets else: rsets_to_free = np.where(self.rsets["assigned"] == worker)[0] - self.rsets["assigned"][rsets_to_free] = 0 + self.rsets["assigned"][rsets_to_free] = -1 self.rsets_free += len(rsets_to_free) self.gpu_rsets_free += np.count_nonzero(self.rsets["gpus"][rsets_to_free]) self.nongpu_rsets_free += np.count_nonzero(~self.rsets["gpus"][rsets_to_free]) @@ -200,7 +199,6 @@ def __init__(self, num_workers, resources, workerID): self.gen_nprocs = None self.gen_ngpus = None self.platform_info = resources.platform_info - self.tiles_per_gpu = resources.tiles_per_gpu # User convenience functions ---------------------------------------------- @@ -218,9 +216,6 @@ def get_slots_as_string(self, multiplier=1, delimiter=",", limit=None): slot_list = [j for i in self.slots_on_node for j in range(i * n, (i + 1) * n)] if limit is not None: slot_list = slot_list[:limit] - if self.tiles_per_gpu > 1: - ntiles = self.tiles_per_gpu - slot_list = [f"{i // ntiles}.{i % ntiles}" for i in slot_list] slots = delimiter.join(map(str, slot_list)) return slots From 6ad870c7591b6f639768fe6d4b85f0d542ef24c3 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 10 Jan 2024 15:44:51 -0600 Subject: [PATCH 005/288] first incredibly long and ugly concatenation of "pipeline" and "state" management routines from manager.py into pipelines.py --- libensemble/utils/pipelines.py | 382 +++++++++++++++++++++++++++++++++ 1 file changed, 382 insertions(+) create mode 100644 libensemble/utils/pipelines.py diff --git a/libensemble/utils/pipelines.py b/libensemble/utils/pipelines.py new file mode 100644 index 000000000..558c9c962 --- /dev/null +++ b/libensemble/utils/pipelines.py @@ -0,0 +1,382 @@ +import logging +import time +from dataclasses import dataclass + +import numpy as np +import numpy.typing as npt +from numpy.lib.recfunctions import repack_fields + +from libensemble.comms.comms import CommFinishedException +from libensemble.message_numbers import ( + EVAL_GEN_TAG, + EVAL_SIM_TAG, + FINISHED_PERSISTENT_GEN_TAG, + FINISHED_PERSISTENT_SIM_TAG, + MAN_SIGNAL_FINISH, + MAN_SIGNAL_KILL, + PERSIS_STOP, + STOP_TAG, + calc_status_strings, + calc_type_strings, +) +from libensemble.resources.resources import Resources +from libensemble.tools.tools import _PERSIS_RETURN_WARNING +from libensemble.utils.misc import extract_H_ranges +from libensemble.worker import WorkerErrMsg + +logger = logging.getLogger(__name__) + +_WALLCLOCK_MSG_ALL_RETURNED = """ +Termination due to wallclock_max has occurred. +All completed work has been returned. +Posting kill messages for all workers. +""" + +_WALLCLOCK_MSG_ACTIVE = """ +Termination due to wallclock_max has occurred. +Some issued work has not been returned. +Posting kill messages for all workers. +""" + + +class WorkerException(Exception): + """Exception raised on abort signal from worker""" + + +class _WorkPipeline: + def __init__(self, libE_specs, sim_specs, gen_specs): + self.libE_specs = libE_specs + self.sim_specs = sim_specs + self.gen_specs = gen_specs + + +class WorkerToManager(_WorkPipeline): + def __init__(self, libE_specs, sim_specs, gen_specs): + super().__init__(libE_specs, sim_specs, gen_specs) + + +class Worker: + """Wrapper class for Worker array and worker comms""" + + def __init__(self, W: npt.NDArray, wid: int, wcomms: list = []): + self.__dict__["_W"] = W + self.__dict__["_wid"] = wid - 1 + self.__dict__["_wcomms"] = wcomms + + def __setattr__(self, field, value): + self._W[self._wid][field] = value + + def __getattr__(self, field): + return self._W[self._wid][field] + + def update_state_on_alloc(self, Work: dict): + self.active = Work["tag"] + if "libE_info" in Work: + if "persistent" in Work["libE_info"]: + self.persis_state = Work["tag"] + if Work["libE_info"].get("active_recv", False): + self.active_recv = Work["tag"] + else: + assert "active_recv" not in Work["libE_info"], "active_recv worker must also be persistent" + + def update_persistent_state(self): + self.persis_state = 0 + if self.active_recv: + self.active = 0 + self.active_recv = 0 + + def send(self, tag, data): + self._wcomms[self._wid].send(tag, data) + + def mail_flag(self): + return self._wcomms[self._wid].mail_flag() + + def recv(self): + return self._wcomms[self._wid].recv() + + +class _ManagerPipeline(_WorkPipeline): + def __init__(self, libE_specs, sim_specs, gen_specs, W, hist, wcomms): + super().__init__(libE_specs, sim_specs, gen_specs) + self.W = W + self.hist = hist + self.wcomms = wcomms + + def _update_state_on_alloc(self, Work: dict, w: int): + """Updates a workers' active/idle status following an allocation order""" + worker = Worker(self.W, w) + worker.update_state_on_alloc(Work) + + work_rows = Work["libE_info"]["H_rows"] + if Work["tag"] == EVAL_SIM_TAG: + self.hist.update_history_x_out(work_rows, w, self.kill_canceled_sims) + elif Work["tag"] == EVAL_GEN_TAG: + self.hist.update_history_to_gen(work_rows) + + def _kill_workers(self) -> None: + """Kills the workers""" + for w in self.W["worker_id"]: + self.wcomms[w - 1].send(STOP_TAG, MAN_SIGNAL_FINISH) + + +class ManagerFromWorker(_ManagerPipeline): + def __init__(self, libE_specs, sim_specs, gen_specs, W, hist, wcomms): + super().__init__(libE_specs, sim_specs, gen_specs, W, hist) + self.WorkerExc = False + + def _handle_msg_from_worker(self, persis_info: dict, w: int) -> None: + """Handles a message from worker w""" + try: + msg = self.wcomms[w - 1].recv() + tag, D_recv = msg + except CommFinishedException: + logger.debug(f"Finalizing message from Worker {w}") + return + if isinstance(D_recv, WorkerErrMsg): + self.W[w - 1]["active"] = 0 + logger.debug(f"Manager received exception from worker {w}") + if not self.WorkerExc: + self.WorkerExc = True + self._kill_workers() + raise WorkerException(f"Received error message from worker {w}", D_recv.msg, D_recv.exc) + elif isinstance(D_recv, logging.LogRecord): + logger.debug(f"Manager received a log message from worker {w}") + logging.getLogger(D_recv.name).handle(D_recv) + else: + logger.debug(f"Manager received data message from worker {w}") + self._update_state_on_worker_msg(persis_info, D_recv, w) + + def _update_state_on_worker_msg(self, persis_info: dict, D_recv: dict, w: int) -> None: + """Updates history and worker info on worker message""" + calc_type = D_recv["calc_type"] + calc_status = D_recv["calc_status"] + ManagerFromWorker._check_received_calc(D_recv) + + worker = Worker(self.W, w) + + keep_state = D_recv["libE_info"].get("keep_state", False) + if w not in self.persis_pending and not worker.active_recv and not keep_state: + worker.active = 0 + + if calc_status in [FINISHED_PERSISTENT_SIM_TAG, FINISHED_PERSISTENT_GEN_TAG]: + final_data = D_recv.get("calc_out", None) + if isinstance(final_data, np.ndarray): + if calc_status is FINISHED_PERSISTENT_GEN_TAG and self.libE_specs.get("use_persis_return_gen", False): + self.hist.update_history_x_in(w, final_data, self.W[w - 1]["gen_started_time"]) + elif calc_status is FINISHED_PERSISTENT_SIM_TAG and self.libE_specs.get("use_persis_return_sim", False): + self.hist.update_history_f(D_recv, self.kill_canceled_sims) + else: + logger.info(_PERSIS_RETURN_WARNING) + worker.update_persistent_state() + if w in self.persis_pending: + self.persis_pending.remove(w) + worker.active = 0 + self._freeup_resources(w) + else: + if calc_type == EVAL_SIM_TAG: + self.hist.update_history_f(D_recv, self.kill_canceled_sims) + if calc_type == EVAL_GEN_TAG: + self.hist.update_history_x_in(w, D_recv["calc_out"], worker.gen_started_time) + assert ( + len(D_recv["calc_out"]) or np.any(self.W["active"]) or worker.persis_state + ), "Gen must return work when is is the only thing active and not persistent." + if "libE_info" in D_recv and "persistent" in D_recv["libE_info"]: + # Now a waiting, persistent worker + worker.persis_state = calc_type + else: + self._freeup_resources(w) + + def _receive_from_workers(self, persis_info: dict) -> dict: + """Receives calculation output from workers. Loops over all + active workers and probes to see if worker is ready to + communicate. If any output is received, all other workers are + looped back over. + """ + time.sleep(0.0001) # Critical for multiprocessing performance + new_stuff = True + while new_stuff: + new_stuff = False + for w in self.W["worker_id"]: + if self.wcomms[w - 1].mail_flag(): + new_stuff = True + self._handle_msg_from_worker(persis_info, w) + + self._init_every_k_save() + return persis_info + + def _final_receive_and_kill(self, persis_info: dict) -> (dict, int, int): + """ + Tries to receive from any active workers. + + If time expires before all active workers have been received from, a + nonblocking receive is posted (though the manager will not receive this + data) and a kill signal is sent. + """ + + # Send a handshake signal to each persistent worker. + if any(self.W["persis_state"]): + for w in self.W["worker_id"][self.W["persis_state"] > 0]: + logger.debug(f"Manager sending PERSIS_STOP to worker {w}") + if self.libE_specs.get("final_gen_send", False): + rows_to_send = np.where(self.hist.H["sim_ended"] & ~self.hist.H["gen_informed"])[0] + work = { + "H_fields": self.gen_specs["persis_in"], + "persis_info": persis_info[w], + "tag": PERSIS_STOP, + "libE_info": {"persistent": True, "H_rows": rows_to_send}, + } + self._check_work_order(work, w, force=True) + self._send_work_order(work, w) + self.hist.update_history_to_gen(rows_to_send) + else: + self.wcomms[w - 1].send(PERSIS_STOP, MAN_SIGNAL_KILL) + if not self.W[w - 1]["active"]: + # Re-activate if necessary + self.W[w - 1]["active"] = self.W[w - 1]["persis_state"] + self.persis_pending.append(w) + + exit_flag = 0 + while (any(self.W["active"]) or any(self.W["persis_state"])) and exit_flag == 0: + persis_info = self._receive_from_workers(persis_info) + if self.term_test(logged=False) == 2: + # Elapsed Wallclock has expired + if not any(self.W["persis_state"]): + if any(self.W["active"]): + logger.manager_warning(_WALLCLOCK_MSG_ACTIVE) + else: + logger.manager_warning(_WALLCLOCK_MSG_ALL_RETURNED) + exit_flag = 2 + if self.WorkerExc: + exit_flag = 1 + + self._init_every_k_save(complete=self.libE_specs["save_H_on_completion"]) + self._kill_workers() + return persis_info, exit_flag, self.elapsed() + + @staticmethod + def _check_received_calc(D_recv: dict) -> None: + """Checks the type and status fields on a receive calculation""" + calc_type = D_recv["calc_type"] + calc_status = D_recv["calc_status"] + assert calc_type in [ + EVAL_SIM_TAG, + EVAL_GEN_TAG, + ], f"Aborting, Unknown calculation type received. Received type: {calc_type}" + + assert calc_status in list(calc_status_strings.keys()) + [PERSIS_STOP] or isinstance( + calc_status, str + ), f"Aborting: Unknown calculation status received. Received status: {calc_status}" + + +@dataclass +class Work: + wid: int + H_fields: list + persis_info: dict + tag: int + libE_info: dict + + +class ManagerToWorker(_ManagerPipeline): + def __init__(self, libE_specs, sim_specs, gen_specs, W, wcomms): + super().__init__(libE_specs, sim_specs, gen_specs, W) + self.wcomms = wcomms + + def _kill_cancelled_sims(self) -> None: + """Send kill signals to any sims marked as cancel_requested""" + + if self.kill_canceled_sims: + inds_to_check = np.arange(self.hist.last_ended + 1, self.hist.last_started + 1) + + kill_sim = ( + self.hist.H["sim_started"][inds_to_check] + & self.hist.H["cancel_requested"][inds_to_check] + & ~self.hist.H["sim_ended"][inds_to_check] + & ~self.hist.H["kill_sent"][inds_to_check] + ) + kill_sim_rows = inds_to_check[kill_sim] + + # Note that a return is still expected when running sims are killed + if np.any(kill_sim): + logger.debug(f"Manager sending kill signals to H indices {kill_sim_rows}") + kill_ids = self.hist.H["sim_id"][kill_sim_rows] + kill_on_workers = self.hist.H["sim_worker"][kill_sim_rows] + for w in kill_on_workers: + self.wcomms[w - 1].send(STOP_TAG, MAN_SIGNAL_KILL) + self.hist.H["kill_sent"][kill_ids] = True + + @staticmethod + def _set_resources(Work: dict, w: int) -> None: + """Check rsets given in Work match rsets assigned in resources. + + If rsets are not assigned, then assign using default mapping + """ + resource_manager = Resources.resources.resource_manager + rset_req = Work["libE_info"].get("rset_team") + + if rset_req is None: + rset_team = [] + default_rset = resource_manager.index_list[w - 1] + if default_rset is not None: + rset_team.append(default_rset) + Work["libE_info"]["rset_team"] = rset_team + + resource_manager.assign_rsets(Work["libE_info"]["rset_team"], w) + + def _send_work_order(self, Work: dict, w: int) -> None: + """Sends an allocation function order to a worker""" + logger.debug(f"Manager sending work unit to worker {w}") + + if Resources.resources: + self._set_resources(Work, w) + + self.wcomms[w - 1].send(Work["tag"], Work) + + if Work["tag"] == EVAL_GEN_TAG: + self.W[w - 1]["gen_started_time"] = time.time() + + work_rows = Work["libE_info"]["H_rows"] + work_name = calc_type_strings[Work["tag"]] + logger.debug(f"Manager sending {work_name} work to worker {w}. Rows {extract_H_ranges(Work) or None}") + if len(work_rows): + new_dtype = [(name, self.hist.H.dtype.fields[name][0]) for name in Work["H_fields"]] + H_to_be_sent = np.empty(len(work_rows), dtype=new_dtype) + for i, row in enumerate(work_rows): + H_to_be_sent[i] = repack_fields(self.hist.H[Work["H_fields"]][row]) + self.wcomms[w - 1].send(0, H_to_be_sent) + + def _check_work_order(self, Work: dict, w: int, force: bool = False) -> None: + """Checks validity of an allocation function order""" + assert w != 0, "Can't send to worker 0; this is the manager." + if self.W[w - 1]["active_recv"]: + assert "active_recv" in Work["libE_info"], ( + "Messages to a worker in active_recv mode should have active_recv" + f"set to True in libE_info. Work['libE_info'] is {Work['libE_info']}" + ) + else: + if not force: + assert self.W[w - 1]["active"] == 0, ( + "Allocation function requested work be sent to worker %d, an already active worker." % w + ) + work_rows = Work["libE_info"]["H_rows"] + if len(work_rows): + work_fields = set(Work["H_fields"]) + + assert len(work_fields), ( + f"Allocation function requested rows={work_rows} be sent to worker={w}, " + "but requested no fields to be sent." + ) + hist_fields = self.hist.H.dtype.names + diff_fields = list(work_fields.difference(hist_fields)) + + assert not diff_fields, f"Allocation function requested invalid fields {diff_fields} be sent to worker={w}." + + def _freeup_resources(self, w: int) -> None: + """Free up resources assigned to the worker""" + if self.resources: + self.resources.resource_manager.free_rsets(w) + + +class ManagerInplace(_ManagerPipeline): + def __init__(self, libE_specs, sim_specs, gen_specs): + super().__init__(libE_specs, sim_specs, gen_specs) From d14b0aae4e55375c1dc9694bd6a9dfa530ee3828 Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 11 Jan 2024 13:57:53 -0600 Subject: [PATCH 006/288] progress --- libensemble/utils/pipelines.py | 36 ++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/libensemble/utils/pipelines.py b/libensemble/utils/pipelines.py index 558c9c962..694710527 100644 --- a/libensemble/utils/pipelines.py +++ b/libensemble/utils/pipelines.py @@ -85,6 +85,9 @@ def update_persistent_state(self): self.active = 0 self.active_recv = 0 + def set_work(self, Work): + self.__dict__["_Work"] = Work + def send(self, tag, data): self._wcomms[self._wid].send(tag, data) @@ -126,14 +129,15 @@ def __init__(self, libE_specs, sim_specs, gen_specs, W, hist, wcomms): def _handle_msg_from_worker(self, persis_info: dict, w: int) -> None: """Handles a message from worker w""" + worker = Worker(self.W, w) try: - msg = self.wcomms[w - 1].recv() + msg = worker.recv() tag, D_recv = msg except CommFinishedException: logger.debug(f"Finalizing message from Worker {w}") return if isinstance(D_recv, WorkerErrMsg): - self.W[w - 1]["active"] = 0 + worker.active = 0 logger.debug(f"Manager received exception from worker {w}") if not self.WorkerExc: self.WorkerExc = True @@ -162,7 +166,7 @@ def _update_state_on_worker_msg(self, persis_info: dict, D_recv: dict, w: int) - final_data = D_recv.get("calc_out", None) if isinstance(final_data, np.ndarray): if calc_status is FINISHED_PERSISTENT_GEN_TAG and self.libE_specs.get("use_persis_return_gen", False): - self.hist.update_history_x_in(w, final_data, self.W[w - 1]["gen_started_time"]) + self.hist.update_history_x_in(w, final_data, worker.gen_started_time) elif calc_status is FINISHED_PERSISTENT_SIM_TAG and self.libE_specs.get("use_persis_return_sim", False): self.hist.update_history_f(D_recv, self.kill_canceled_sims) else: @@ -216,6 +220,7 @@ def _final_receive_and_kill(self, persis_info: dict) -> (dict, int, int): # Send a handshake signal to each persistent worker. if any(self.W["persis_state"]): for w in self.W["worker_id"][self.W["persis_state"] > 0]: + worker = Worker(self.W, w) logger.debug(f"Manager sending PERSIS_STOP to worker {w}") if self.libE_specs.get("final_gen_send", False): rows_to_send = np.where(self.hist.H["sim_ended"] & ~self.hist.H["gen_informed"])[0] @@ -225,14 +230,14 @@ def _final_receive_and_kill(self, persis_info: dict) -> (dict, int, int): "tag": PERSIS_STOP, "libE_info": {"persistent": True, "H_rows": rows_to_send}, } - self._check_work_order(work, w, force=True) + # self._check_work_order(work, w, force=True) # this work is hardcoded, not from an alloc_f. trust! self._send_work_order(work, w) self.hist.update_history_to_gen(rows_to_send) else: - self.wcomms[w - 1].send(PERSIS_STOP, MAN_SIGNAL_KILL) - if not self.W[w - 1]["active"]: + worker.send(PERSIS_STOP, MAN_SIGNAL_KILL) + if not worker.active: # Re-activate if necessary - self.W[w - 1]["active"] = self.W[w - 1]["persis_state"] + worker.active = worker.persis_state self.persis_pending.append(w) exit_flag = 0 @@ -327,13 +332,15 @@ def _send_work_order(self, Work: dict, w: int) -> None: """Sends an allocation function order to a worker""" logger.debug(f"Manager sending work unit to worker {w}") + worker = Worker(self.W, w) + if Resources.resources: self._set_resources(Work, w) - self.wcomms[w - 1].send(Work["tag"], Work) + worker.send(Work["tag"], Work) if Work["tag"] == EVAL_GEN_TAG: - self.W[w - 1]["gen_started_time"] = time.time() + worker.gen_started_time = time.time() work_rows = Work["libE_info"]["H_rows"] work_name = calc_type_strings[Work["tag"]] @@ -343,19 +350,22 @@ def _send_work_order(self, Work: dict, w: int) -> None: H_to_be_sent = np.empty(len(work_rows), dtype=new_dtype) for i, row in enumerate(work_rows): H_to_be_sent[i] = repack_fields(self.hist.H[Work["H_fields"]][row]) - self.wcomms[w - 1].send(0, H_to_be_sent) + worker.send(0, H_to_be_sent) def _check_work_order(self, Work: dict, w: int, force: bool = False) -> None: """Checks validity of an allocation function order""" - assert w != 0, "Can't send to worker 0; this is the manager." - if self.W[w - 1]["active_recv"]: + # assert w != 0, "Can't send to worker 0; this is the manager." + + worker = Worker(self.W, w) + + if worker.active_recv: assert "active_recv" in Work["libE_info"], ( "Messages to a worker in active_recv mode should have active_recv" f"set to True in libE_info. Work['libE_info'] is {Work['libE_info']}" ) else: if not force: - assert self.W[w - 1]["active"] == 0, ( + assert worker.active == 0, ( "Allocation function requested work be sent to worker %d, an already active worker." % w ) work_rows = Work["libE_info"]["H_rows"] From ab32e3fa22b60c635637e5ce7d6d8438c6b8dbf2 Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 11 Jan 2024 17:46:07 -0600 Subject: [PATCH 007/288] bugfixes, first "working" refactor of manager can run 1d_sampling using utils.pipelines --- libensemble/manager.py | 25 ++++---- libensemble/utils/pipelines.py | 101 ++++++++++++--------------------- 2 files changed, 51 insertions(+), 75 deletions(-) diff --git a/libensemble/manager.py b/libensemble/manager.py index cce7682f8..25e82ada1 100644 --- a/libensemble/manager.py +++ b/libensemble/manager.py @@ -36,6 +36,7 @@ from libensemble.tools.tools import _PERSIS_RETURN_WARNING, _USER_CALC_DIR_WARNING from libensemble.utils.misc import extract_H_ranges from libensemble.utils.output_directory import EnsembleDirectory +from libensemble.utils.pipelines import ManagerFromWorker, ManagerToWorker from libensemble.utils.timer import Timer from libensemble.worker import WorkerErrMsg @@ -108,9 +109,6 @@ def manager_main( pr = cProfile.Profile() pr.enable() - if "in" not in gen_specs: - gen_specs["in"] = [] - # Send dtypes to workers dtypes = { EVAL_SIM_TAG: repack_fields(hist.H[sim_specs["in"]]).dtype, @@ -642,11 +640,15 @@ def run(self, persis_info: dict) -> (dict, int, int): logger.info(f"Manager initiated on node {socket.gethostname()}") logger.info(f"Manager exit_criteria: {self.exit_criteria}") + self.ToWorker = ManagerToWorker(self) + self.FromWorker = ManagerFromWorker(self) + # Continue receiving and giving until termination test is satisfied try: while not self.term_test(): - self._kill_cancelled_sims() - persis_info = self._receive_from_workers(persis_info) + self.ToWorker._kill_cancelled_sims() + persis_info = self.FromWorker._receive_from_workers(persis_info) + self._init_every_k_save() Work, persis_info, flag = self._alloc_work(self.hist.trim_H(), persis_info) if flag: break @@ -654,21 +656,22 @@ def run(self, persis_info: dict) -> (dict, int, int): for w in Work: if self._sim_max_given(): break - self._check_work_order(Work[w], w) - self._send_work_order(Work[w], w) - self._update_state_on_alloc(Work[w], w) + self.ToWorker._check_work_order(Work[w], w) + self.ToWorker._send_work_order(Work[w], w) + self.ToWorker._update_state_on_alloc(Work[w], w) assert self.term_test() or any( self.W["active"] != 0 ), "alloc_f did not return any work, although all workers are idle." - except WorkerException as e: + except WorkerException as e: # catches all error messages from worker report_worker_exc(e) raise LoggedException(e.args[0], e.args[1]) from None - except Exception as e: + except Exception as e: # should only catch bugs within manager, or AssertionErrors logger.error(traceback.format_exc()) raise LoggedException(e.args) from None finally: # Return persis_info, exit_flag, elapsed time - result = self._final_receive_and_kill(persis_info) + result = self.FromWorker._final_receive_and_kill(persis_info) + self._init_every_k_save(complete=self.libE_specs["save_H_on_completion"]) sys.stdout.flush() sys.stderr.flush() return result diff --git a/libensemble/utils/pipelines.py b/libensemble/utils/pipelines.py index 694710527..a50d85a82 100644 --- a/libensemble/utils/pipelines.py +++ b/libensemble/utils/pipelines.py @@ -1,6 +1,5 @@ import logging import time -from dataclasses import dataclass import numpy as np import numpy.typing as npt @@ -16,7 +15,6 @@ MAN_SIGNAL_KILL, PERSIS_STOP, STOP_TAG, - calc_status_strings, calc_type_strings, ) from libensemble.resources.resources import Resources @@ -60,24 +58,23 @@ class Worker: def __init__(self, W: npt.NDArray, wid: int, wcomms: list = []): self.__dict__["_W"] = W - self.__dict__["_wid"] = wid - 1 + self.__dict__["_wididx"] = wid - 1 self.__dict__["_wcomms"] = wcomms def __setattr__(self, field, value): - self._W[self._wid][field] = value + self._W[self._wididx][field] = value def __getattr__(self, field): - return self._W[self._wid][field] + return self._W[self._wididx][field] def update_state_on_alloc(self, Work: dict): self.active = Work["tag"] - if "libE_info" in Work: - if "persistent" in Work["libE_info"]: - self.persis_state = Work["tag"] - if Work["libE_info"].get("active_recv", False): - self.active_recv = Work["tag"] - else: - assert "active_recv" not in Work["libE_info"], "active_recv worker must also be persistent" + if "persistent" in Work["libE_info"]: + self.persis_state = Work["tag"] + if Work["libE_info"].get("active_recv", False): + self.active_recv = Work["tag"] + else: + assert "active_recv" not in Work["libE_info"], "active_recv worker must also be persistent" def update_persistent_state(self): self.persis_state = 0 @@ -89,25 +86,27 @@ def set_work(self, Work): self.__dict__["_Work"] = Work def send(self, tag, data): - self._wcomms[self._wid].send(tag, data) + self._wcomms[self._wididx].send(tag, data) def mail_flag(self): - return self._wcomms[self._wid].mail_flag() + return self._wcomms[self._wididx].mail_flag() def recv(self): - return self._wcomms[self._wid].recv() + return self._wcomms[self._wididx].recv() class _ManagerPipeline(_WorkPipeline): - def __init__(self, libE_specs, sim_specs, gen_specs, W, hist, wcomms): - super().__init__(libE_specs, sim_specs, gen_specs) - self.W = W - self.hist = hist - self.wcomms = wcomms + def __init__(self, Manager): + super().__init__(Manager.libE_specs, Manager.sim_specs, Manager.gen_specs) + self.W = Manager.W + self.hist = Manager.hist + self.wcomms = Manager.wcomms + self.kill_canceled_sims = Manager.kill_canceled_sims + self.persis_pending = Manager.persis_pending def _update_state_on_alloc(self, Work: dict, w: int): """Updates a workers' active/idle status following an allocation order""" - worker = Worker(self.W, w) + worker = Worker(self.W, w, self.wcomms) worker.update_state_on_alloc(Work) work_rows = Work["libE_info"]["H_rows"] @@ -123,16 +122,19 @@ def _kill_workers(self) -> None: class ManagerFromWorker(_ManagerPipeline): - def __init__(self, libE_specs, sim_specs, gen_specs, W, hist, wcomms): - super().__init__(libE_specs, sim_specs, gen_specs, W, hist) + def __init__(self, Manager): + super().__init__(Manager) self.WorkerExc = False + self.resources = Manager.resources + self.term_test = Manager.term_test + self.elapsed = Manager.elapsed def _handle_msg_from_worker(self, persis_info: dict, w: int) -> None: """Handles a message from worker w""" - worker = Worker(self.W, w) + worker = Worker(self.W, w, self.wcomms) try: msg = worker.recv() - tag, D_recv = msg + _, D_recv = msg except CommFinishedException: logger.debug(f"Finalizing message from Worker {w}") return @@ -154,9 +156,8 @@ def _update_state_on_worker_msg(self, persis_info: dict, D_recv: dict, w: int) - """Updates history and worker info on worker message""" calc_type = D_recv["calc_type"] calc_status = D_recv["calc_status"] - ManagerFromWorker._check_received_calc(D_recv) - worker = Worker(self.W, w) + worker = Worker(self.W, w, self.wcomms) keep_state = D_recv["libE_info"].get("keep_state", False) if w not in self.persis_pending and not worker.active_recv and not keep_state: @@ -205,7 +206,6 @@ def _receive_from_workers(self, persis_info: dict) -> dict: new_stuff = True self._handle_msg_from_worker(persis_info, w) - self._init_every_k_save() return persis_info def _final_receive_and_kill(self, persis_info: dict) -> (dict, int, int): @@ -220,7 +220,7 @@ def _final_receive_and_kill(self, persis_info: dict) -> (dict, int, int): # Send a handshake signal to each persistent worker. if any(self.W["persis_state"]): for w in self.W["worker_id"][self.W["persis_state"] > 0]: - worker = Worker(self.W, w) + worker = Worker(self.W, w, self.wcomms) logger.debug(f"Manager sending PERSIS_STOP to worker {w}") if self.libE_specs.get("final_gen_send", False): rows_to_send = np.where(self.hist.H["sim_ended"] & ~self.hist.H["gen_informed"])[0] @@ -230,7 +230,6 @@ def _final_receive_and_kill(self, persis_info: dict) -> (dict, int, int): "tag": PERSIS_STOP, "libE_info": {"persistent": True, "H_rows": rows_to_send}, } - # self._check_work_order(work, w, force=True) # this work is hardcoded, not from an alloc_f. trust! self._send_work_order(work, w) self.hist.update_history_to_gen(rows_to_send) else: @@ -254,38 +253,18 @@ def _final_receive_and_kill(self, persis_info: dict) -> (dict, int, int): if self.WorkerExc: exit_flag = 1 - self._init_every_k_save(complete=self.libE_specs["save_H_on_completion"]) self._kill_workers() return persis_info, exit_flag, self.elapsed() - @staticmethod - def _check_received_calc(D_recv: dict) -> None: - """Checks the type and status fields on a receive calculation""" - calc_type = D_recv["calc_type"] - calc_status = D_recv["calc_status"] - assert calc_type in [ - EVAL_SIM_TAG, - EVAL_GEN_TAG, - ], f"Aborting, Unknown calculation type received. Received type: {calc_type}" - - assert calc_status in list(calc_status_strings.keys()) + [PERSIS_STOP] or isinstance( - calc_status, str - ), f"Aborting: Unknown calculation status received. Received status: {calc_status}" - - -@dataclass -class Work: - wid: int - H_fields: list - persis_info: dict - tag: int - libE_info: dict + def _freeup_resources(self, w: int) -> None: + """Free up resources assigned to the worker""" + if self.resources: + self.resources.resource_manager.free_rsets(w) class ManagerToWorker(_ManagerPipeline): - def __init__(self, libE_specs, sim_specs, gen_specs, W, wcomms): - super().__init__(libE_specs, sim_specs, gen_specs, W) - self.wcomms = wcomms + def __init__(self, Manager): + super().__init__(Manager) def _kill_cancelled_sims(self) -> None: """Send kill signals to any sims marked as cancel_requested""" @@ -332,7 +311,7 @@ def _send_work_order(self, Work: dict, w: int) -> None: """Sends an allocation function order to a worker""" logger.debug(f"Manager sending work unit to worker {w}") - worker = Worker(self.W, w) + worker = Worker(self.W, w, self.wcomms) if Resources.resources: self._set_resources(Work, w) @@ -354,9 +333,8 @@ def _send_work_order(self, Work: dict, w: int) -> None: def _check_work_order(self, Work: dict, w: int, force: bool = False) -> None: """Checks validity of an allocation function order""" - # assert w != 0, "Can't send to worker 0; this is the manager." - worker = Worker(self.W, w) + worker = Worker(self.W, w, self.wcomms) if worker.active_recv: assert "active_recv" in Work["libE_info"], ( @@ -381,11 +359,6 @@ def _check_work_order(self, Work: dict, w: int, force: bool = False) -> None: assert not diff_fields, f"Allocation function requested invalid fields {diff_fields} be sent to worker={w}." - def _freeup_resources(self, w: int) -> None: - """Free up resources assigned to the worker""" - if self.resources: - self.resources.resource_manager.free_rsets(w) - class ManagerInplace(_ManagerPipeline): def __init__(self, libE_specs, sim_specs, gen_specs): From 68d8855b8fce28f60705b80246398e5c19dae055 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 12 Jan 2024 17:52:12 -0600 Subject: [PATCH 008/288] removing now-redundant content from manager, trying to see if we can start a temporary, local Worker for handling work --- libensemble/comms/comms.py | 1 + libensemble/manager.py | 284 ++------------------------------- libensemble/utils/pipelines.py | 41 +++-- libensemble/worker.py | 7 +- 4 files changed, 51 insertions(+), 282 deletions(-) diff --git a/libensemble/comms/comms.py b/libensemble/comms/comms.py index 30de28ad9..70458dd98 100644 --- a/libensemble/comms/comms.py +++ b/libensemble/comms/comms.py @@ -150,6 +150,7 @@ def __init__(self, main, *args, **kwargs): self._result = None self._exception = None self._done = False + self._ufunc = kwargs.get("ufunc", False) def _is_result_msg(self, msg): """Return true if message indicates final result (and set result/except).""" diff --git a/libensemble/manager.py b/libensemble/manager.py index 25e82ada1..a822de005 100644 --- a/libensemble/manager.py +++ b/libensemble/manager.py @@ -10,35 +10,22 @@ import platform import socket import sys -import time import traceback +from queue import SimpleQueue from typing import Any, Union import numpy as np import numpy.typing as npt from numpy.lib.recfunctions import repack_fields -from libensemble.comms.comms import CommFinishedException -from libensemble.message_numbers import ( - EVAL_GEN_TAG, - EVAL_SIM_TAG, - FINISHED_PERSISTENT_GEN_TAG, - FINISHED_PERSISTENT_SIM_TAG, - MAN_SIGNAL_FINISH, - MAN_SIGNAL_KILL, - PERSIS_STOP, - STOP_TAG, - calc_status_strings, - calc_type_strings, -) +from libensemble.comms.comms import QComm +from libensemble.message_numbers import EVAL_GEN_TAG, EVAL_SIM_TAG, PERSIS_STOP, calc_status_strings from libensemble.resources.resources import Resources from libensemble.tools.fields_keys import protected_libE_fields -from libensemble.tools.tools import _PERSIS_RETURN_WARNING, _USER_CALC_DIR_WARNING -from libensemble.utils.misc import extract_H_ranges +from libensemble.tools.tools import _USER_CALC_DIR_WARNING from libensemble.utils.output_directory import EnsembleDirectory from libensemble.utils.pipelines import ManagerFromWorker, ManagerToWorker from libensemble.utils.timer import Timer -from libensemble.worker import WorkerErrMsg logger = logging.getLogger(__name__) # For debug messages - uncomment @@ -122,6 +109,8 @@ def manager_main( for wcomm in wcomms: wcomm.send(0, libE_specs.get("workflow_dir_path")) + libE_specs["_dtypes"] = dtypes + # Set up and run manager mgr = Manager(hist, libE_specs, alloc_specs, sim_specs, gen_specs, exit_criteria, wcomms) result = mgr.run(persis_info) @@ -198,8 +187,8 @@ def __init__( self.gen_num_procs = libE_specs.get("gen_num_procs", 0) self.gen_num_gpus = libE_specs.get("gen_num_gpus", 0) - self.W = np.zeros(len(self.wcomms), dtype=Manager.worker_dtype) - self.W["worker_id"] = np.arange(len(self.wcomms)) + 1 + self.W = np.zeros(len(self.wcomms) + 1, dtype=Manager.worker_dtype) + self.W["worker_id"] = np.arange(len(self.wcomms) + 1) self.term_tests = [ (2, "wallclock_max", self.term_test_wallclock), (1, "sim_max", self.term_test_sim_max), @@ -207,6 +196,11 @@ def __init__( (1, "stop_val", self.term_test_stop_val), ] + self.self_inbox = SimpleQueue() + self.self_outbox = SimpleQueue() + + self.wcomms = [QComm(self.self_inbox, self.self_outbox, len(self.W))] + self.wcomms + temp_EnsembleDirectory = EnsembleDirectory(libE_specs=libE_specs) self.resources = Resources.resources self.scheduler_opts = self.libE_specs.get("scheduler_opts", {}) @@ -259,13 +253,6 @@ def term_test(self, logged: bool = True) -> Union[bool, int]: return retval return 0 - # --- Low-level communication routines - - def _kill_workers(self) -> None: - """Kills the workers""" - for w in self.W["worker_id"]: - self.wcomms[w - 1].send(STOP_TAG, MAN_SIGNAL_FINISH) - # --- Checkpointing logic def _get_date_start_str(self) -> str: @@ -314,95 +301,6 @@ def _init_every_k_save(self, complete=False) -> None: if self.libE_specs.get("save_every_k_gens"): self._save_every_k_gens(complete) - # --- Handle outgoing messages to workers (work orders from alloc) - - def _check_work_order(self, Work: dict, w: int, force: bool = False) -> None: - """Checks validity of an allocation function order""" - assert w != 0, "Can't send to worker 0; this is the manager." - if self.W[w - 1]["active_recv"]: - assert "active_recv" in Work["libE_info"], ( - "Messages to a worker in active_recv mode should have active_recv" - f"set to True in libE_info. Work['libE_info'] is {Work['libE_info']}" - ) - else: - if not force: - assert self.W[w - 1]["active"] == 0, ( - "Allocation function requested work be sent to worker %d, an already active worker." % w - ) - work_rows = Work["libE_info"]["H_rows"] - if len(work_rows): - work_fields = set(Work["H_fields"]) - - assert len(work_fields), ( - f"Allocation function requested rows={work_rows} be sent to worker={w}, " - "but requested no fields to be sent." - ) - hist_fields = self.hist.H.dtype.names - diff_fields = list(work_fields.difference(hist_fields)) - - assert not diff_fields, f"Allocation function requested invalid fields {diff_fields} be sent to worker={w}." - - def _set_resources(self, Work: dict, w: int) -> None: - """Check rsets given in Work match rsets assigned in resources. - - If rsets are not assigned, then assign using default mapping - """ - resource_manager = self.resources.resource_manager - rset_req = Work["libE_info"].get("rset_team") - - if rset_req is None: - rset_team = [] - default_rset = resource_manager.index_list[w - 1] - if default_rset is not None: - rset_team.append(default_rset) - Work["libE_info"]["rset_team"] = rset_team - - resource_manager.assign_rsets(Work["libE_info"]["rset_team"], w) - - def _freeup_resources(self, w: int) -> None: - """Free up resources assigned to the worker""" - if self.resources: - self.resources.resource_manager.free_rsets(w) - - def _send_work_order(self, Work: dict, w: int) -> None: - """Sends an allocation function order to a worker""" - logger.debug(f"Manager sending work unit to worker {w}") - - if self.resources: - self._set_resources(Work, w) - - self.wcomms[w - 1].send(Work["tag"], Work) - - if Work["tag"] == EVAL_GEN_TAG: - self.W[w - 1]["gen_started_time"] = time.time() - - work_rows = Work["libE_info"]["H_rows"] - work_name = calc_type_strings[Work["tag"]] - logger.debug(f"Manager sending {work_name} work to worker {w}. Rows {extract_H_ranges(Work) or None}") - if len(work_rows): - new_dtype = [(name, self.hist.H.dtype.fields[name][0]) for name in Work["H_fields"]] - H_to_be_sent = np.empty(len(work_rows), dtype=new_dtype) - for i, row in enumerate(work_rows): - H_to_be_sent[i] = repack_fields(self.hist.H[Work["H_fields"]][row]) - self.wcomms[w - 1].send(0, H_to_be_sent) - - def _update_state_on_alloc(self, Work: dict, w: int): - """Updates a workers' active/idle status following an allocation order""" - self.W[w - 1]["active"] = Work["tag"] - if "libE_info" in Work: - if "persistent" in Work["libE_info"]: - self.W[w - 1]["persis_state"] = Work["tag"] - if Work["libE_info"].get("active_recv", False): - self.W[w - 1]["active_recv"] = Work["tag"] - else: - assert "active_recv" not in Work["libE_info"], "active_recv worker must also be persistent" - - work_rows = Work["libE_info"]["H_rows"] - if Work["tag"] == EVAL_SIM_TAG: - self.hist.update_history_x_out(work_rows, w, self.kill_canceled_sims) - elif Work["tag"] == EVAL_GEN_TAG: - self.hist.update_history_to_gen(work_rows) - # --- Handle incoming messages from workers @staticmethod @@ -419,164 +317,8 @@ def _check_received_calc(D_recv: dict) -> None: calc_status, str ), f"Aborting: Unknown calculation status received. Received status: {calc_status}" - def _receive_from_workers(self, persis_info: dict) -> dict: - """Receives calculation output from workers. Loops over all - active workers and probes to see if worker is ready to - communticate. If any output is received, all other workers are - looped back over. - """ - time.sleep(0.0001) # Critical for multiprocessing performance - new_stuff = True - while new_stuff: - new_stuff = False - for w in self.W["worker_id"]: - if self.wcomms[w - 1].mail_flag(): - new_stuff = True - self._handle_msg_from_worker(persis_info, w) - - self._init_every_k_save() - return persis_info - - def _update_state_on_worker_msg(self, persis_info: dict, D_recv: dict, w: int) -> None: - """Updates history and worker info on worker message""" - calc_type = D_recv["calc_type"] - calc_status = D_recv["calc_status"] - Manager._check_received_calc(D_recv) - - keep_state = D_recv["libE_info"].get("keep_state", False) - if w not in self.persis_pending and not self.W[w - 1]["active_recv"] and not keep_state: - self.W[w - 1]["active"] = 0 - - if calc_status in [FINISHED_PERSISTENT_SIM_TAG, FINISHED_PERSISTENT_GEN_TAG]: - final_data = D_recv.get("calc_out", None) - if isinstance(final_data, np.ndarray): - if calc_status is FINISHED_PERSISTENT_GEN_TAG and self.libE_specs.get("use_persis_return_gen", False): - self.hist.update_history_x_in(w, final_data, self.W[w - 1]["gen_started_time"]) - elif calc_status is FINISHED_PERSISTENT_SIM_TAG and self.libE_specs.get("use_persis_return_sim", False): - self.hist.update_history_f(D_recv, self.kill_canceled_sims) - else: - logger.info(_PERSIS_RETURN_WARNING) - self.W[w - 1]["persis_state"] = 0 - if self.W[w - 1]["active_recv"]: - self.W[w - 1]["active"] = 0 - self.W[w - 1]["active_recv"] = 0 - if w in self.persis_pending: - self.persis_pending.remove(w) - self.W[w - 1]["active"] = 0 - self._freeup_resources(w) - else: - if calc_type == EVAL_SIM_TAG: - self.hist.update_history_f(D_recv, self.kill_canceled_sims) - if calc_type == EVAL_GEN_TAG: - self.hist.update_history_x_in(w, D_recv["calc_out"], self.W[w - 1]["gen_started_time"]) - assert ( - len(D_recv["calc_out"]) or np.any(self.W["active"]) or self.W[w - 1]["persis_state"] - ), "Gen must return work when is is the only thing active and not persistent." - if "libE_info" in D_recv and "persistent" in D_recv["libE_info"]: - # Now a waiting, persistent worker - self.W[w - 1]["persis_state"] = calc_type - else: - self._freeup_resources(w) - - if D_recv.get("persis_info"): - persis_info[w].update(D_recv["persis_info"]) - - def _handle_msg_from_worker(self, persis_info: dict, w: int) -> None: - """Handles a message from worker w""" - try: - msg = self.wcomms[w - 1].recv() - tag, D_recv = msg - except CommFinishedException: - logger.debug(f"Finalizing message from Worker {w}") - return - if isinstance(D_recv, WorkerErrMsg): - self.W[w - 1]["active"] = 0 - logger.debug(f"Manager received exception from worker {w}") - if not self.WorkerExc: - self.WorkerExc = True - self._kill_workers() - raise WorkerException(f"Received error message from worker {w}", D_recv.msg, D_recv.exc) - elif isinstance(D_recv, logging.LogRecord): - logger.debug(f"Manager received a log message from worker {w}") - logging.getLogger(D_recv.name).handle(D_recv) - else: - logger.debug(f"Manager received data message from worker {w}") - self._update_state_on_worker_msg(persis_info, D_recv, w) - - def _kill_cancelled_sims(self) -> None: - """Send kill signals to any sims marked as cancel_requested""" - - if self.kill_canceled_sims: - inds_to_check = np.arange(self.hist.last_ended + 1, self.hist.last_started + 1) - - kill_sim = ( - self.hist.H["sim_started"][inds_to_check] - & self.hist.H["cancel_requested"][inds_to_check] - & ~self.hist.H["sim_ended"][inds_to_check] - & ~self.hist.H["kill_sent"][inds_to_check] - ) - kill_sim_rows = inds_to_check[kill_sim] - - # Note that a return is still expected when running sims are killed - if np.any(kill_sim): - logger.debug(f"Manager sending kill signals to H indices {kill_sim_rows}") - kill_ids = self.hist.H["sim_id"][kill_sim_rows] - kill_on_workers = self.hist.H["sim_worker"][kill_sim_rows] - for w in kill_on_workers: - self.wcomms[w - 1].send(STOP_TAG, MAN_SIGNAL_KILL) - self.hist.H["kill_sent"][kill_ids] = True - # --- Handle termination - def _final_receive_and_kill(self, persis_info: dict) -> (dict, int, int): - """ - Tries to receive from any active workers. - - If time expires before all active workers have been received from, a - nonblocking receive is posted (though the manager will not receive this - data) and a kill signal is sent. - """ - - # Send a handshake signal to each persistent worker. - if any(self.W["persis_state"]): - for w in self.W["worker_id"][self.W["persis_state"] > 0]: - logger.debug(f"Manager sending PERSIS_STOP to worker {w}") - if self.libE_specs.get("final_gen_send", False): - rows_to_send = np.where(self.hist.H["sim_ended"] & ~self.hist.H["gen_informed"])[0] - work = { - "H_fields": self.gen_specs["persis_in"], - "persis_info": persis_info[w], - "tag": PERSIS_STOP, - "libE_info": {"persistent": True, "H_rows": rows_to_send}, - } - self._check_work_order(work, w, force=True) - self._send_work_order(work, w) - self.hist.update_history_to_gen(rows_to_send) - else: - self.wcomms[w - 1].send(PERSIS_STOP, MAN_SIGNAL_KILL) - if not self.W[w - 1]["active"]: - # Re-activate if necessary - self.W[w - 1]["active"] = self.W[w - 1]["persis_state"] - self.persis_pending.append(w) - - exit_flag = 0 - while (any(self.W["active"]) or any(self.W["persis_state"])) and exit_flag == 0: - persis_info = self._receive_from_workers(persis_info) - if self.term_test(logged=False) == 2: - # Elapsed Wallclock has expired - if not any(self.W["persis_state"]): - if any(self.W["active"]): - logger.manager_warning(_WALLCLOCK_MSG_ACTIVE) - else: - logger.manager_warning(_WALLCLOCK_MSG_ALL_RETURNED) - exit_flag = 2 - if self.WorkerExc: - exit_flag = 1 - - self._init_every_k_save(complete=self.libE_specs["save_H_on_completion"]) - self._kill_workers() - return persis_info, exit_flag, self.elapsed() - def _sim_max_given(self) -> bool: if "sim_max" in self.exit_criteria: return self.hist.sim_started_count >= self.exit_criteria["sim_max"] + self.hist.sim_started_offset diff --git a/libensemble/utils/pipelines.py b/libensemble/utils/pipelines.py index a50d85a82..0c81cbd03 100644 --- a/libensemble/utils/pipelines.py +++ b/libensemble/utils/pipelines.py @@ -20,6 +20,7 @@ from libensemble.resources.resources import Resources from libensemble.tools.tools import _PERSIS_RETURN_WARNING from libensemble.utils.misc import extract_H_ranges +from libensemble.worker import Worker as LocalWorker from libensemble.worker import WorkerErrMsg logger = logging.getLogger(__name__) @@ -53,12 +54,23 @@ def __init__(self, libE_specs, sim_specs, gen_specs): super().__init__(libE_specs, sim_specs, gen_specs) +class WorkerFromManager(_WorkPipeline): + def __init__(self, libE_specs, sim_specs, gen_specs): + super().__init__(libE_specs, sim_specs, gen_specs) + + class Worker: """Wrapper class for Worker array and worker comms""" + def __new__(cls, W: npt.NDArray, wid: int, wcomms: list = []): + if wid == 0: + return super(Worker, ManagerWorker).__new__(ManagerWorker) + else: + return super().__new__(Worker) + def __init__(self, W: npt.NDArray, wid: int, wcomms: list = []): self.__dict__["_W"] = W - self.__dict__["_wididx"] = wid - 1 + self.__dict__["_wididx"] = wid self.__dict__["_wcomms"] = wcomms def __setattr__(self, field, value): @@ -82,9 +94,6 @@ def update_persistent_state(self): self.active = 0 self.active_recv = 0 - def set_work(self, Work): - self.__dict__["_Work"] = Work - def send(self, tag, data): self._wcomms[self._wididx].send(tag, data) @@ -95,6 +104,20 @@ def recv(self): return self._wcomms[self._wididx].recv() +class ManagerWorker(Worker): + """Manager invisibly sends work to itself, then performs work""" + + def __init__(self, W: npt.NDArray, wid: int, wcomms: list = []): + super().__init__(W, wid, wcomms) + + def run_gen_work(self, pipeline): + comm = self.__dict__["_wcomms"][0] + local_worker = LocalWorker( + comm, pipeline.libE_specs["_dtypes"], 0, pipeline.sim_specs, pipeline.gen_specs, pipeline.libE_specs + ) + local_worker.run(iterations=1) + + class _ManagerPipeline(_WorkPipeline): def __init__(self, Manager): super().__init__(Manager.libE_specs, Manager.sim_specs, Manager.gen_specs) @@ -202,7 +225,7 @@ def _receive_from_workers(self, persis_info: dict) -> dict: while new_stuff: new_stuff = False for w in self.W["worker_id"]: - if self.wcomms[w - 1].mail_flag(): + if self.wcomms[w].mail_flag(): new_stuff = True self._handle_msg_from_worker(persis_info, w) @@ -331,6 +354,9 @@ def _send_work_order(self, Work: dict, w: int) -> None: H_to_be_sent[i] = repack_fields(self.hist.H[Work["H_fields"]][row]) worker.send(0, H_to_be_sent) + if Work["tag"] == EVAL_GEN_TAG and w == 0: + worker.run_gen_work(self) + def _check_work_order(self, Work: dict, w: int, force: bool = False) -> None: """Checks validity of an allocation function order""" @@ -358,8 +384,3 @@ def _check_work_order(self, Work: dict, w: int, force: bool = False) -> None: diff_fields = list(work_fields.difference(hist_fields)) assert not diff_fields, f"Allocation function requested invalid fields {diff_fields} be sent to worker={w}." - - -class ManagerInplace(_ManagerPipeline): - def __init__(self, libE_specs, sim_specs, gen_specs): - super().__init__(libE_specs, sim_specs, gen_specs) diff --git a/libensemble/worker.py b/libensemble/worker.py index ad8bd4530..c13567750 100644 --- a/libensemble/worker.py +++ b/libensemble/worker.py @@ -374,11 +374,13 @@ def _handle(self, Work: dict) -> dict: "calc_type": calc_type, } - def run(self) -> None: + def run(self, iterations=0) -> None: """Runs the main worker loop.""" try: logger.info(f"Worker {self.workerID} initiated on node {socket.gethostname()}") + current_iterations = 0 + for worker_iter in count(start=1): logger.debug(f"Iteration {worker_iter}") @@ -407,6 +409,9 @@ def run(self) -> None: if response is None: break self.comm.send(0, response) + current_iterations += 1 + if iterations > 0 and (current_iterations >= iterations): + break except Exception as e: self.comm.send(0, WorkerErrMsg(" ".join(format_exc_msg(type(e), e)).strip(), format_exc())) From 33ea282e4c07d017d29bf0dc5a573f2179bf48b2 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 17 Jan 2024 11:11:41 -0600 Subject: [PATCH 009/288] restore version of manager from develop. specify iterations for worker. --- libensemble/manager.py | 309 +++++++++++++++++++++++++++++++++++++---- libensemble/worker.py | 5 +- 2 files changed, 285 insertions(+), 29 deletions(-) diff --git a/libensemble/manager.py b/libensemble/manager.py index a822de005..cce7682f8 100644 --- a/libensemble/manager.py +++ b/libensemble/manager.py @@ -10,22 +10,34 @@ import platform import socket import sys +import time import traceback -from queue import SimpleQueue from typing import Any, Union import numpy as np import numpy.typing as npt from numpy.lib.recfunctions import repack_fields -from libensemble.comms.comms import QComm -from libensemble.message_numbers import EVAL_GEN_TAG, EVAL_SIM_TAG, PERSIS_STOP, calc_status_strings +from libensemble.comms.comms import CommFinishedException +from libensemble.message_numbers import ( + EVAL_GEN_TAG, + EVAL_SIM_TAG, + FINISHED_PERSISTENT_GEN_TAG, + FINISHED_PERSISTENT_SIM_TAG, + MAN_SIGNAL_FINISH, + MAN_SIGNAL_KILL, + PERSIS_STOP, + STOP_TAG, + calc_status_strings, + calc_type_strings, +) from libensemble.resources.resources import Resources from libensemble.tools.fields_keys import protected_libE_fields -from libensemble.tools.tools import _USER_CALC_DIR_WARNING +from libensemble.tools.tools import _PERSIS_RETURN_WARNING, _USER_CALC_DIR_WARNING +from libensemble.utils.misc import extract_H_ranges from libensemble.utils.output_directory import EnsembleDirectory -from libensemble.utils.pipelines import ManagerFromWorker, ManagerToWorker from libensemble.utils.timer import Timer +from libensemble.worker import WorkerErrMsg logger = logging.getLogger(__name__) # For debug messages - uncomment @@ -96,6 +108,9 @@ def manager_main( pr = cProfile.Profile() pr.enable() + if "in" not in gen_specs: + gen_specs["in"] = [] + # Send dtypes to workers dtypes = { EVAL_SIM_TAG: repack_fields(hist.H[sim_specs["in"]]).dtype, @@ -109,8 +124,6 @@ def manager_main( for wcomm in wcomms: wcomm.send(0, libE_specs.get("workflow_dir_path")) - libE_specs["_dtypes"] = dtypes - # Set up and run manager mgr = Manager(hist, libE_specs, alloc_specs, sim_specs, gen_specs, exit_criteria, wcomms) result = mgr.run(persis_info) @@ -187,8 +200,8 @@ def __init__( self.gen_num_procs = libE_specs.get("gen_num_procs", 0) self.gen_num_gpus = libE_specs.get("gen_num_gpus", 0) - self.W = np.zeros(len(self.wcomms) + 1, dtype=Manager.worker_dtype) - self.W["worker_id"] = np.arange(len(self.wcomms) + 1) + self.W = np.zeros(len(self.wcomms), dtype=Manager.worker_dtype) + self.W["worker_id"] = np.arange(len(self.wcomms)) + 1 self.term_tests = [ (2, "wallclock_max", self.term_test_wallclock), (1, "sim_max", self.term_test_sim_max), @@ -196,11 +209,6 @@ def __init__( (1, "stop_val", self.term_test_stop_val), ] - self.self_inbox = SimpleQueue() - self.self_outbox = SimpleQueue() - - self.wcomms = [QComm(self.self_inbox, self.self_outbox, len(self.W))] + self.wcomms - temp_EnsembleDirectory = EnsembleDirectory(libE_specs=libE_specs) self.resources = Resources.resources self.scheduler_opts = self.libE_specs.get("scheduler_opts", {}) @@ -253,6 +261,13 @@ def term_test(self, logged: bool = True) -> Union[bool, int]: return retval return 0 + # --- Low-level communication routines + + def _kill_workers(self) -> None: + """Kills the workers""" + for w in self.W["worker_id"]: + self.wcomms[w - 1].send(STOP_TAG, MAN_SIGNAL_FINISH) + # --- Checkpointing logic def _get_date_start_str(self) -> str: @@ -301,6 +316,95 @@ def _init_every_k_save(self, complete=False) -> None: if self.libE_specs.get("save_every_k_gens"): self._save_every_k_gens(complete) + # --- Handle outgoing messages to workers (work orders from alloc) + + def _check_work_order(self, Work: dict, w: int, force: bool = False) -> None: + """Checks validity of an allocation function order""" + assert w != 0, "Can't send to worker 0; this is the manager." + if self.W[w - 1]["active_recv"]: + assert "active_recv" in Work["libE_info"], ( + "Messages to a worker in active_recv mode should have active_recv" + f"set to True in libE_info. Work['libE_info'] is {Work['libE_info']}" + ) + else: + if not force: + assert self.W[w - 1]["active"] == 0, ( + "Allocation function requested work be sent to worker %d, an already active worker." % w + ) + work_rows = Work["libE_info"]["H_rows"] + if len(work_rows): + work_fields = set(Work["H_fields"]) + + assert len(work_fields), ( + f"Allocation function requested rows={work_rows} be sent to worker={w}, " + "but requested no fields to be sent." + ) + hist_fields = self.hist.H.dtype.names + diff_fields = list(work_fields.difference(hist_fields)) + + assert not diff_fields, f"Allocation function requested invalid fields {diff_fields} be sent to worker={w}." + + def _set_resources(self, Work: dict, w: int) -> None: + """Check rsets given in Work match rsets assigned in resources. + + If rsets are not assigned, then assign using default mapping + """ + resource_manager = self.resources.resource_manager + rset_req = Work["libE_info"].get("rset_team") + + if rset_req is None: + rset_team = [] + default_rset = resource_manager.index_list[w - 1] + if default_rset is not None: + rset_team.append(default_rset) + Work["libE_info"]["rset_team"] = rset_team + + resource_manager.assign_rsets(Work["libE_info"]["rset_team"], w) + + def _freeup_resources(self, w: int) -> None: + """Free up resources assigned to the worker""" + if self.resources: + self.resources.resource_manager.free_rsets(w) + + def _send_work_order(self, Work: dict, w: int) -> None: + """Sends an allocation function order to a worker""" + logger.debug(f"Manager sending work unit to worker {w}") + + if self.resources: + self._set_resources(Work, w) + + self.wcomms[w - 1].send(Work["tag"], Work) + + if Work["tag"] == EVAL_GEN_TAG: + self.W[w - 1]["gen_started_time"] = time.time() + + work_rows = Work["libE_info"]["H_rows"] + work_name = calc_type_strings[Work["tag"]] + logger.debug(f"Manager sending {work_name} work to worker {w}. Rows {extract_H_ranges(Work) or None}") + if len(work_rows): + new_dtype = [(name, self.hist.H.dtype.fields[name][0]) for name in Work["H_fields"]] + H_to_be_sent = np.empty(len(work_rows), dtype=new_dtype) + for i, row in enumerate(work_rows): + H_to_be_sent[i] = repack_fields(self.hist.H[Work["H_fields"]][row]) + self.wcomms[w - 1].send(0, H_to_be_sent) + + def _update_state_on_alloc(self, Work: dict, w: int): + """Updates a workers' active/idle status following an allocation order""" + self.W[w - 1]["active"] = Work["tag"] + if "libE_info" in Work: + if "persistent" in Work["libE_info"]: + self.W[w - 1]["persis_state"] = Work["tag"] + if Work["libE_info"].get("active_recv", False): + self.W[w - 1]["active_recv"] = Work["tag"] + else: + assert "active_recv" not in Work["libE_info"], "active_recv worker must also be persistent" + + work_rows = Work["libE_info"]["H_rows"] + if Work["tag"] == EVAL_SIM_TAG: + self.hist.update_history_x_out(work_rows, w, self.kill_canceled_sims) + elif Work["tag"] == EVAL_GEN_TAG: + self.hist.update_history_to_gen(work_rows) + # --- Handle incoming messages from workers @staticmethod @@ -317,8 +421,164 @@ def _check_received_calc(D_recv: dict) -> None: calc_status, str ), f"Aborting: Unknown calculation status received. Received status: {calc_status}" + def _receive_from_workers(self, persis_info: dict) -> dict: + """Receives calculation output from workers. Loops over all + active workers and probes to see if worker is ready to + communticate. If any output is received, all other workers are + looped back over. + """ + time.sleep(0.0001) # Critical for multiprocessing performance + new_stuff = True + while new_stuff: + new_stuff = False + for w in self.W["worker_id"]: + if self.wcomms[w - 1].mail_flag(): + new_stuff = True + self._handle_msg_from_worker(persis_info, w) + + self._init_every_k_save() + return persis_info + + def _update_state_on_worker_msg(self, persis_info: dict, D_recv: dict, w: int) -> None: + """Updates history and worker info on worker message""" + calc_type = D_recv["calc_type"] + calc_status = D_recv["calc_status"] + Manager._check_received_calc(D_recv) + + keep_state = D_recv["libE_info"].get("keep_state", False) + if w not in self.persis_pending and not self.W[w - 1]["active_recv"] and not keep_state: + self.W[w - 1]["active"] = 0 + + if calc_status in [FINISHED_PERSISTENT_SIM_TAG, FINISHED_PERSISTENT_GEN_TAG]: + final_data = D_recv.get("calc_out", None) + if isinstance(final_data, np.ndarray): + if calc_status is FINISHED_PERSISTENT_GEN_TAG and self.libE_specs.get("use_persis_return_gen", False): + self.hist.update_history_x_in(w, final_data, self.W[w - 1]["gen_started_time"]) + elif calc_status is FINISHED_PERSISTENT_SIM_TAG and self.libE_specs.get("use_persis_return_sim", False): + self.hist.update_history_f(D_recv, self.kill_canceled_sims) + else: + logger.info(_PERSIS_RETURN_WARNING) + self.W[w - 1]["persis_state"] = 0 + if self.W[w - 1]["active_recv"]: + self.W[w - 1]["active"] = 0 + self.W[w - 1]["active_recv"] = 0 + if w in self.persis_pending: + self.persis_pending.remove(w) + self.W[w - 1]["active"] = 0 + self._freeup_resources(w) + else: + if calc_type == EVAL_SIM_TAG: + self.hist.update_history_f(D_recv, self.kill_canceled_sims) + if calc_type == EVAL_GEN_TAG: + self.hist.update_history_x_in(w, D_recv["calc_out"], self.W[w - 1]["gen_started_time"]) + assert ( + len(D_recv["calc_out"]) or np.any(self.W["active"]) or self.W[w - 1]["persis_state"] + ), "Gen must return work when is is the only thing active and not persistent." + if "libE_info" in D_recv and "persistent" in D_recv["libE_info"]: + # Now a waiting, persistent worker + self.W[w - 1]["persis_state"] = calc_type + else: + self._freeup_resources(w) + + if D_recv.get("persis_info"): + persis_info[w].update(D_recv["persis_info"]) + + def _handle_msg_from_worker(self, persis_info: dict, w: int) -> None: + """Handles a message from worker w""" + try: + msg = self.wcomms[w - 1].recv() + tag, D_recv = msg + except CommFinishedException: + logger.debug(f"Finalizing message from Worker {w}") + return + if isinstance(D_recv, WorkerErrMsg): + self.W[w - 1]["active"] = 0 + logger.debug(f"Manager received exception from worker {w}") + if not self.WorkerExc: + self.WorkerExc = True + self._kill_workers() + raise WorkerException(f"Received error message from worker {w}", D_recv.msg, D_recv.exc) + elif isinstance(D_recv, logging.LogRecord): + logger.debug(f"Manager received a log message from worker {w}") + logging.getLogger(D_recv.name).handle(D_recv) + else: + logger.debug(f"Manager received data message from worker {w}") + self._update_state_on_worker_msg(persis_info, D_recv, w) + + def _kill_cancelled_sims(self) -> None: + """Send kill signals to any sims marked as cancel_requested""" + + if self.kill_canceled_sims: + inds_to_check = np.arange(self.hist.last_ended + 1, self.hist.last_started + 1) + + kill_sim = ( + self.hist.H["sim_started"][inds_to_check] + & self.hist.H["cancel_requested"][inds_to_check] + & ~self.hist.H["sim_ended"][inds_to_check] + & ~self.hist.H["kill_sent"][inds_to_check] + ) + kill_sim_rows = inds_to_check[kill_sim] + + # Note that a return is still expected when running sims are killed + if np.any(kill_sim): + logger.debug(f"Manager sending kill signals to H indices {kill_sim_rows}") + kill_ids = self.hist.H["sim_id"][kill_sim_rows] + kill_on_workers = self.hist.H["sim_worker"][kill_sim_rows] + for w in kill_on_workers: + self.wcomms[w - 1].send(STOP_TAG, MAN_SIGNAL_KILL) + self.hist.H["kill_sent"][kill_ids] = True + # --- Handle termination + def _final_receive_and_kill(self, persis_info: dict) -> (dict, int, int): + """ + Tries to receive from any active workers. + + If time expires before all active workers have been received from, a + nonblocking receive is posted (though the manager will not receive this + data) and a kill signal is sent. + """ + + # Send a handshake signal to each persistent worker. + if any(self.W["persis_state"]): + for w in self.W["worker_id"][self.W["persis_state"] > 0]: + logger.debug(f"Manager sending PERSIS_STOP to worker {w}") + if self.libE_specs.get("final_gen_send", False): + rows_to_send = np.where(self.hist.H["sim_ended"] & ~self.hist.H["gen_informed"])[0] + work = { + "H_fields": self.gen_specs["persis_in"], + "persis_info": persis_info[w], + "tag": PERSIS_STOP, + "libE_info": {"persistent": True, "H_rows": rows_to_send}, + } + self._check_work_order(work, w, force=True) + self._send_work_order(work, w) + self.hist.update_history_to_gen(rows_to_send) + else: + self.wcomms[w - 1].send(PERSIS_STOP, MAN_SIGNAL_KILL) + if not self.W[w - 1]["active"]: + # Re-activate if necessary + self.W[w - 1]["active"] = self.W[w - 1]["persis_state"] + self.persis_pending.append(w) + + exit_flag = 0 + while (any(self.W["active"]) or any(self.W["persis_state"])) and exit_flag == 0: + persis_info = self._receive_from_workers(persis_info) + if self.term_test(logged=False) == 2: + # Elapsed Wallclock has expired + if not any(self.W["persis_state"]): + if any(self.W["active"]): + logger.manager_warning(_WALLCLOCK_MSG_ACTIVE) + else: + logger.manager_warning(_WALLCLOCK_MSG_ALL_RETURNED) + exit_flag = 2 + if self.WorkerExc: + exit_flag = 1 + + self._init_every_k_save(complete=self.libE_specs["save_H_on_completion"]) + self._kill_workers() + return persis_info, exit_flag, self.elapsed() + def _sim_max_given(self) -> bool: if "sim_max" in self.exit_criteria: return self.hist.sim_started_count >= self.exit_criteria["sim_max"] + self.hist.sim_started_offset @@ -382,15 +642,11 @@ def run(self, persis_info: dict) -> (dict, int, int): logger.info(f"Manager initiated on node {socket.gethostname()}") logger.info(f"Manager exit_criteria: {self.exit_criteria}") - self.ToWorker = ManagerToWorker(self) - self.FromWorker = ManagerFromWorker(self) - # Continue receiving and giving until termination test is satisfied try: while not self.term_test(): - self.ToWorker._kill_cancelled_sims() - persis_info = self.FromWorker._receive_from_workers(persis_info) - self._init_every_k_save() + self._kill_cancelled_sims() + persis_info = self._receive_from_workers(persis_info) Work, persis_info, flag = self._alloc_work(self.hist.trim_H(), persis_info) if flag: break @@ -398,22 +654,21 @@ def run(self, persis_info: dict) -> (dict, int, int): for w in Work: if self._sim_max_given(): break - self.ToWorker._check_work_order(Work[w], w) - self.ToWorker._send_work_order(Work[w], w) - self.ToWorker._update_state_on_alloc(Work[w], w) + self._check_work_order(Work[w], w) + self._send_work_order(Work[w], w) + self._update_state_on_alloc(Work[w], w) assert self.term_test() or any( self.W["active"] != 0 ), "alloc_f did not return any work, although all workers are idle." - except WorkerException as e: # catches all error messages from worker + except WorkerException as e: report_worker_exc(e) raise LoggedException(e.args[0], e.args[1]) from None - except Exception as e: # should only catch bugs within manager, or AssertionErrors + except Exception as e: logger.error(traceback.format_exc()) raise LoggedException(e.args) from None finally: # Return persis_info, exit_flag, elapsed time - result = self.FromWorker._final_receive_and_kill(persis_info) - self._init_every_k_save(complete=self.libE_specs["save_H_on_completion"]) + result = self._final_receive_and_kill(persis_info) sys.stdout.flush() sys.stderr.flush() return result diff --git a/libensemble/worker.py b/libensemble/worker.py index c13567750..96d2de8bf 100644 --- a/libensemble/worker.py +++ b/libensemble/worker.py @@ -51,6 +51,7 @@ def worker_main( log_comm: bool = True, resources: Resources = None, executor: Executor = None, + iterations: int = 0, ) -> None: # noqa: F821 """Evaluates calculations given to it by the manager. @@ -96,7 +97,7 @@ def worker_main( if libE_specs.get("use_workflow_dir"): _, libE_specs["workflow_dir_path"] = comm.recv() - workerID = workerID or comm.rank + workerID = workerID or getattr(comm, "rank", 0) # Initialize logging on comms if log_comm: @@ -108,7 +109,7 @@ def worker_main( # Set up and run worker worker = Worker(comm, dtypes, workerID, sim_specs, gen_specs, libE_specs) with LS.loc("workflow"): - worker.run() + worker.run(iterations) if libE_specs.get("profile"): pr.disable() From 843df3972da97c5b9071ae75c1f9384771948e07 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 17 Jan 2024 11:12:14 -0600 Subject: [PATCH 010/288] remove pipelines.py. will start simpler --- libensemble/utils/pipelines.py | 386 --------------------------------- 1 file changed, 386 deletions(-) delete mode 100644 libensemble/utils/pipelines.py diff --git a/libensemble/utils/pipelines.py b/libensemble/utils/pipelines.py deleted file mode 100644 index 0c81cbd03..000000000 --- a/libensemble/utils/pipelines.py +++ /dev/null @@ -1,386 +0,0 @@ -import logging -import time - -import numpy as np -import numpy.typing as npt -from numpy.lib.recfunctions import repack_fields - -from libensemble.comms.comms import CommFinishedException -from libensemble.message_numbers import ( - EVAL_GEN_TAG, - EVAL_SIM_TAG, - FINISHED_PERSISTENT_GEN_TAG, - FINISHED_PERSISTENT_SIM_TAG, - MAN_SIGNAL_FINISH, - MAN_SIGNAL_KILL, - PERSIS_STOP, - STOP_TAG, - calc_type_strings, -) -from libensemble.resources.resources import Resources -from libensemble.tools.tools import _PERSIS_RETURN_WARNING -from libensemble.utils.misc import extract_H_ranges -from libensemble.worker import Worker as LocalWorker -from libensemble.worker import WorkerErrMsg - -logger = logging.getLogger(__name__) - -_WALLCLOCK_MSG_ALL_RETURNED = """ -Termination due to wallclock_max has occurred. -All completed work has been returned. -Posting kill messages for all workers. -""" - -_WALLCLOCK_MSG_ACTIVE = """ -Termination due to wallclock_max has occurred. -Some issued work has not been returned. -Posting kill messages for all workers. -""" - - -class WorkerException(Exception): - """Exception raised on abort signal from worker""" - - -class _WorkPipeline: - def __init__(self, libE_specs, sim_specs, gen_specs): - self.libE_specs = libE_specs - self.sim_specs = sim_specs - self.gen_specs = gen_specs - - -class WorkerToManager(_WorkPipeline): - def __init__(self, libE_specs, sim_specs, gen_specs): - super().__init__(libE_specs, sim_specs, gen_specs) - - -class WorkerFromManager(_WorkPipeline): - def __init__(self, libE_specs, sim_specs, gen_specs): - super().__init__(libE_specs, sim_specs, gen_specs) - - -class Worker: - """Wrapper class for Worker array and worker comms""" - - def __new__(cls, W: npt.NDArray, wid: int, wcomms: list = []): - if wid == 0: - return super(Worker, ManagerWorker).__new__(ManagerWorker) - else: - return super().__new__(Worker) - - def __init__(self, W: npt.NDArray, wid: int, wcomms: list = []): - self.__dict__["_W"] = W - self.__dict__["_wididx"] = wid - self.__dict__["_wcomms"] = wcomms - - def __setattr__(self, field, value): - self._W[self._wididx][field] = value - - def __getattr__(self, field): - return self._W[self._wididx][field] - - def update_state_on_alloc(self, Work: dict): - self.active = Work["tag"] - if "persistent" in Work["libE_info"]: - self.persis_state = Work["tag"] - if Work["libE_info"].get("active_recv", False): - self.active_recv = Work["tag"] - else: - assert "active_recv" not in Work["libE_info"], "active_recv worker must also be persistent" - - def update_persistent_state(self): - self.persis_state = 0 - if self.active_recv: - self.active = 0 - self.active_recv = 0 - - def send(self, tag, data): - self._wcomms[self._wididx].send(tag, data) - - def mail_flag(self): - return self._wcomms[self._wididx].mail_flag() - - def recv(self): - return self._wcomms[self._wididx].recv() - - -class ManagerWorker(Worker): - """Manager invisibly sends work to itself, then performs work""" - - def __init__(self, W: npt.NDArray, wid: int, wcomms: list = []): - super().__init__(W, wid, wcomms) - - def run_gen_work(self, pipeline): - comm = self.__dict__["_wcomms"][0] - local_worker = LocalWorker( - comm, pipeline.libE_specs["_dtypes"], 0, pipeline.sim_specs, pipeline.gen_specs, pipeline.libE_specs - ) - local_worker.run(iterations=1) - - -class _ManagerPipeline(_WorkPipeline): - def __init__(self, Manager): - super().__init__(Manager.libE_specs, Manager.sim_specs, Manager.gen_specs) - self.W = Manager.W - self.hist = Manager.hist - self.wcomms = Manager.wcomms - self.kill_canceled_sims = Manager.kill_canceled_sims - self.persis_pending = Manager.persis_pending - - def _update_state_on_alloc(self, Work: dict, w: int): - """Updates a workers' active/idle status following an allocation order""" - worker = Worker(self.W, w, self.wcomms) - worker.update_state_on_alloc(Work) - - work_rows = Work["libE_info"]["H_rows"] - if Work["tag"] == EVAL_SIM_TAG: - self.hist.update_history_x_out(work_rows, w, self.kill_canceled_sims) - elif Work["tag"] == EVAL_GEN_TAG: - self.hist.update_history_to_gen(work_rows) - - def _kill_workers(self) -> None: - """Kills the workers""" - for w in self.W["worker_id"]: - self.wcomms[w - 1].send(STOP_TAG, MAN_SIGNAL_FINISH) - - -class ManagerFromWorker(_ManagerPipeline): - def __init__(self, Manager): - super().__init__(Manager) - self.WorkerExc = False - self.resources = Manager.resources - self.term_test = Manager.term_test - self.elapsed = Manager.elapsed - - def _handle_msg_from_worker(self, persis_info: dict, w: int) -> None: - """Handles a message from worker w""" - worker = Worker(self.W, w, self.wcomms) - try: - msg = worker.recv() - _, D_recv = msg - except CommFinishedException: - logger.debug(f"Finalizing message from Worker {w}") - return - if isinstance(D_recv, WorkerErrMsg): - worker.active = 0 - logger.debug(f"Manager received exception from worker {w}") - if not self.WorkerExc: - self.WorkerExc = True - self._kill_workers() - raise WorkerException(f"Received error message from worker {w}", D_recv.msg, D_recv.exc) - elif isinstance(D_recv, logging.LogRecord): - logger.debug(f"Manager received a log message from worker {w}") - logging.getLogger(D_recv.name).handle(D_recv) - else: - logger.debug(f"Manager received data message from worker {w}") - self._update_state_on_worker_msg(persis_info, D_recv, w) - - def _update_state_on_worker_msg(self, persis_info: dict, D_recv: dict, w: int) -> None: - """Updates history and worker info on worker message""" - calc_type = D_recv["calc_type"] - calc_status = D_recv["calc_status"] - - worker = Worker(self.W, w, self.wcomms) - - keep_state = D_recv["libE_info"].get("keep_state", False) - if w not in self.persis_pending and not worker.active_recv and not keep_state: - worker.active = 0 - - if calc_status in [FINISHED_PERSISTENT_SIM_TAG, FINISHED_PERSISTENT_GEN_TAG]: - final_data = D_recv.get("calc_out", None) - if isinstance(final_data, np.ndarray): - if calc_status is FINISHED_PERSISTENT_GEN_TAG and self.libE_specs.get("use_persis_return_gen", False): - self.hist.update_history_x_in(w, final_data, worker.gen_started_time) - elif calc_status is FINISHED_PERSISTENT_SIM_TAG and self.libE_specs.get("use_persis_return_sim", False): - self.hist.update_history_f(D_recv, self.kill_canceled_sims) - else: - logger.info(_PERSIS_RETURN_WARNING) - worker.update_persistent_state() - if w in self.persis_pending: - self.persis_pending.remove(w) - worker.active = 0 - self._freeup_resources(w) - else: - if calc_type == EVAL_SIM_TAG: - self.hist.update_history_f(D_recv, self.kill_canceled_sims) - if calc_type == EVAL_GEN_TAG: - self.hist.update_history_x_in(w, D_recv["calc_out"], worker.gen_started_time) - assert ( - len(D_recv["calc_out"]) or np.any(self.W["active"]) or worker.persis_state - ), "Gen must return work when is is the only thing active and not persistent." - if "libE_info" in D_recv and "persistent" in D_recv["libE_info"]: - # Now a waiting, persistent worker - worker.persis_state = calc_type - else: - self._freeup_resources(w) - - def _receive_from_workers(self, persis_info: dict) -> dict: - """Receives calculation output from workers. Loops over all - active workers and probes to see if worker is ready to - communicate. If any output is received, all other workers are - looped back over. - """ - time.sleep(0.0001) # Critical for multiprocessing performance - new_stuff = True - while new_stuff: - new_stuff = False - for w in self.W["worker_id"]: - if self.wcomms[w].mail_flag(): - new_stuff = True - self._handle_msg_from_worker(persis_info, w) - - return persis_info - - def _final_receive_and_kill(self, persis_info: dict) -> (dict, int, int): - """ - Tries to receive from any active workers. - - If time expires before all active workers have been received from, a - nonblocking receive is posted (though the manager will not receive this - data) and a kill signal is sent. - """ - - # Send a handshake signal to each persistent worker. - if any(self.W["persis_state"]): - for w in self.W["worker_id"][self.W["persis_state"] > 0]: - worker = Worker(self.W, w, self.wcomms) - logger.debug(f"Manager sending PERSIS_STOP to worker {w}") - if self.libE_specs.get("final_gen_send", False): - rows_to_send = np.where(self.hist.H["sim_ended"] & ~self.hist.H["gen_informed"])[0] - work = { - "H_fields": self.gen_specs["persis_in"], - "persis_info": persis_info[w], - "tag": PERSIS_STOP, - "libE_info": {"persistent": True, "H_rows": rows_to_send}, - } - self._send_work_order(work, w) - self.hist.update_history_to_gen(rows_to_send) - else: - worker.send(PERSIS_STOP, MAN_SIGNAL_KILL) - if not worker.active: - # Re-activate if necessary - worker.active = worker.persis_state - self.persis_pending.append(w) - - exit_flag = 0 - while (any(self.W["active"]) or any(self.W["persis_state"])) and exit_flag == 0: - persis_info = self._receive_from_workers(persis_info) - if self.term_test(logged=False) == 2: - # Elapsed Wallclock has expired - if not any(self.W["persis_state"]): - if any(self.W["active"]): - logger.manager_warning(_WALLCLOCK_MSG_ACTIVE) - else: - logger.manager_warning(_WALLCLOCK_MSG_ALL_RETURNED) - exit_flag = 2 - if self.WorkerExc: - exit_flag = 1 - - self._kill_workers() - return persis_info, exit_flag, self.elapsed() - - def _freeup_resources(self, w: int) -> None: - """Free up resources assigned to the worker""" - if self.resources: - self.resources.resource_manager.free_rsets(w) - - -class ManagerToWorker(_ManagerPipeline): - def __init__(self, Manager): - super().__init__(Manager) - - def _kill_cancelled_sims(self) -> None: - """Send kill signals to any sims marked as cancel_requested""" - - if self.kill_canceled_sims: - inds_to_check = np.arange(self.hist.last_ended + 1, self.hist.last_started + 1) - - kill_sim = ( - self.hist.H["sim_started"][inds_to_check] - & self.hist.H["cancel_requested"][inds_to_check] - & ~self.hist.H["sim_ended"][inds_to_check] - & ~self.hist.H["kill_sent"][inds_to_check] - ) - kill_sim_rows = inds_to_check[kill_sim] - - # Note that a return is still expected when running sims are killed - if np.any(kill_sim): - logger.debug(f"Manager sending kill signals to H indices {kill_sim_rows}") - kill_ids = self.hist.H["sim_id"][kill_sim_rows] - kill_on_workers = self.hist.H["sim_worker"][kill_sim_rows] - for w in kill_on_workers: - self.wcomms[w - 1].send(STOP_TAG, MAN_SIGNAL_KILL) - self.hist.H["kill_sent"][kill_ids] = True - - @staticmethod - def _set_resources(Work: dict, w: int) -> None: - """Check rsets given in Work match rsets assigned in resources. - - If rsets are not assigned, then assign using default mapping - """ - resource_manager = Resources.resources.resource_manager - rset_req = Work["libE_info"].get("rset_team") - - if rset_req is None: - rset_team = [] - default_rset = resource_manager.index_list[w - 1] - if default_rset is not None: - rset_team.append(default_rset) - Work["libE_info"]["rset_team"] = rset_team - - resource_manager.assign_rsets(Work["libE_info"]["rset_team"], w) - - def _send_work_order(self, Work: dict, w: int) -> None: - """Sends an allocation function order to a worker""" - logger.debug(f"Manager sending work unit to worker {w}") - - worker = Worker(self.W, w, self.wcomms) - - if Resources.resources: - self._set_resources(Work, w) - - worker.send(Work["tag"], Work) - - if Work["tag"] == EVAL_GEN_TAG: - worker.gen_started_time = time.time() - - work_rows = Work["libE_info"]["H_rows"] - work_name = calc_type_strings[Work["tag"]] - logger.debug(f"Manager sending {work_name} work to worker {w}. Rows {extract_H_ranges(Work) or None}") - if len(work_rows): - new_dtype = [(name, self.hist.H.dtype.fields[name][0]) for name in Work["H_fields"]] - H_to_be_sent = np.empty(len(work_rows), dtype=new_dtype) - for i, row in enumerate(work_rows): - H_to_be_sent[i] = repack_fields(self.hist.H[Work["H_fields"]][row]) - worker.send(0, H_to_be_sent) - - if Work["tag"] == EVAL_GEN_TAG and w == 0: - worker.run_gen_work(self) - - def _check_work_order(self, Work: dict, w: int, force: bool = False) -> None: - """Checks validity of an allocation function order""" - - worker = Worker(self.W, w, self.wcomms) - - if worker.active_recv: - assert "active_recv" in Work["libE_info"], ( - "Messages to a worker in active_recv mode should have active_recv" - f"set to True in libE_info. Work['libE_info'] is {Work['libE_info']}" - ) - else: - if not force: - assert worker.active == 0, ( - "Allocation function requested work be sent to worker %d, an already active worker." % w - ) - work_rows = Work["libE_info"]["H_rows"] - if len(work_rows): - work_fields = set(Work["H_fields"]) - - assert len(work_fields), ( - f"Allocation function requested rows={work_rows} be sent to worker={w}, " - "but requested no fields to be sent." - ) - hist_fields = self.hist.H.dtype.names - diff_fields = list(work_fields.difference(hist_fields)) - - assert not diff_fields, f"Allocation function requested invalid fields {diff_fields} be sent to worker={w}." From 3aeab06b4a6054810cdaf5d39bb6ff7cc0f30895 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 17 Jan 2024 11:40:15 -0600 Subject: [PATCH 011/288] undoing "iterations" change in worker, seeing if we can simply submit gen work to local worker thread --- libensemble/manager.py | 45 ++++++++++++++++++++++++++++++++++++++---- libensemble/worker.py | 10 ++-------- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/libensemble/manager.py b/libensemble/manager.py index cce7682f8..d1f7a2d83 100644 --- a/libensemble/manager.py +++ b/libensemble/manager.py @@ -18,7 +18,8 @@ import numpy.typing as npt from numpy.lib.recfunctions import repack_fields -from libensemble.comms.comms import CommFinishedException +from libensemble.comms.comms import CommFinishedException, QCommThread +from libensemble.executors.executor import Executor from libensemble.message_numbers import ( EVAL_GEN_TAG, EVAL_SIM_TAG, @@ -37,7 +38,7 @@ from libensemble.utils.misc import extract_H_ranges from libensemble.utils.output_directory import EnsembleDirectory from libensemble.utils.timer import Timer -from libensemble.worker import WorkerErrMsg +from libensemble.worker import WorkerErrMsg, worker_main logger = logging.getLogger(__name__) # For debug messages - uncomment @@ -209,6 +210,29 @@ def __init__( (1, "stop_val", self.term_test_stop_val), ] + self.local_worker_comm = None + self.libE_specs["gen_man"] = True + + dtypes = { + EVAL_SIM_TAG: repack_fields(hist.H[sim_specs["in"]]).dtype, + EVAL_GEN_TAG: repack_fields(hist.H[gen_specs["in"]]).dtype, + } + + if self.libE_specs.get("gen_man", False): + self.local_worker_comm = QCommThread( + worker_main, + len(self.wcomms), + sim_specs, + gen_specs, + libE_specs, + 0, + False, + Resources.resources, + Executor.executor, + ) + self.local_worker_comm.run() + self.local_worker_comm.send(0, dtypes) + temp_EnsembleDirectory = EnsembleDirectory(libE_specs=libE_specs) self.resources = Resources.resources self.scheduler_opts = self.libE_specs.get("scheduler_opts", {}) @@ -265,6 +289,8 @@ def term_test(self, logged: bool = True) -> Union[bool, int]: def _kill_workers(self) -> None: """Kills the workers""" + if self.local_worker_comm: + self.local_worker_comm.send(STOP_TAG, MAN_SIGNAL_FINISH) for w in self.W["worker_id"]: self.wcomms[w - 1].send(STOP_TAG, MAN_SIGNAL_FINISH) @@ -373,7 +399,10 @@ def _send_work_order(self, Work: dict, w: int) -> None: if self.resources: self._set_resources(Work, w) - self.wcomms[w - 1].send(Work["tag"], Work) + if Work["tag"] == EVAL_GEN_TAG and self.libE_specs.get("gen_man", False): + self.local_worker_comm.send(Work["tag"], Work) + else: + self.wcomms[w - 1].send(Work["tag"], Work) if Work["tag"] == EVAL_GEN_TAG: self.W[w - 1]["gen_started_time"] = time.time() @@ -386,7 +415,11 @@ def _send_work_order(self, Work: dict, w: int) -> None: H_to_be_sent = np.empty(len(work_rows), dtype=new_dtype) for i, row in enumerate(work_rows): H_to_be_sent[i] = repack_fields(self.hist.H[Work["H_fields"]][row]) - self.wcomms[w - 1].send(0, H_to_be_sent) + + if Work["tag"] == EVAL_GEN_TAG and self.libE_specs.get("gen_man", False): + self.local_worker_comm.send(0, H_to_be_sent) + else: + self.wcomms[w - 1].send(0, H_to_be_sent) def _update_state_on_alloc(self, Work: dict, w: int): """Updates a workers' active/idle status following an allocation order""" @@ -525,6 +558,8 @@ def _kill_cancelled_sims(self) -> None: kill_ids = self.hist.H["sim_id"][kill_sim_rows] kill_on_workers = self.hist.H["sim_worker"][kill_sim_rows] for w in kill_on_workers: + if self.local_worker_comm: + self.local_worker_comm.send(STOP_TAG, MAN_SIGNAL_KILL) self.wcomms[w - 1].send(STOP_TAG, MAN_SIGNAL_KILL) self.hist.H["kill_sent"][kill_ids] = True @@ -555,6 +590,8 @@ def _final_receive_and_kill(self, persis_info: dict) -> (dict, int, int): self._send_work_order(work, w) self.hist.update_history_to_gen(rows_to_send) else: + if self.local_worker_comm: + self.local_worker_comm.send(PERSIS_STOP, MAN_SIGNAL_KILL) self.wcomms[w - 1].send(PERSIS_STOP, MAN_SIGNAL_KILL) if not self.W[w - 1]["active"]: # Re-activate if necessary diff --git a/libensemble/worker.py b/libensemble/worker.py index 96d2de8bf..9c18c18d6 100644 --- a/libensemble/worker.py +++ b/libensemble/worker.py @@ -51,7 +51,6 @@ def worker_main( log_comm: bool = True, resources: Resources = None, executor: Executor = None, - iterations: int = 0, ) -> None: # noqa: F821 """Evaluates calculations given to it by the manager. @@ -109,7 +108,7 @@ def worker_main( # Set up and run worker worker = Worker(comm, dtypes, workerID, sim_specs, gen_specs, libE_specs) with LS.loc("workflow"): - worker.run(iterations) + worker.run() if libE_specs.get("profile"): pr.disable() @@ -375,13 +374,11 @@ def _handle(self, Work: dict) -> dict: "calc_type": calc_type, } - def run(self, iterations=0) -> None: + def run(self) -> None: """Runs the main worker loop.""" try: logger.info(f"Worker {self.workerID} initiated on node {socket.gethostname()}") - current_iterations = 0 - for worker_iter in count(start=1): logger.debug(f"Iteration {worker_iter}") @@ -410,9 +407,6 @@ def run(self, iterations=0) -> None: if response is None: break self.comm.send(0, response) - current_iterations += 1 - if iterations > 0 and (current_iterations >= iterations): - break except Exception as e: self.comm.send(0, WorkerErrMsg(" ".join(format_exc_msg(type(e), e)).strip(), format_exc())) From b083a2158d4ffef1c30075294999b7e5aeacc679 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 17 Jan 2024 14:21:34 -0600 Subject: [PATCH 012/288] add attempted update_state_on_local_gen_msg and handle_msg_from_local_gen, add in Worker wrapper class to manager, but not used yet --- libensemble/manager.py | 83 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/libensemble/manager.py b/libensemble/manager.py index d1f7a2d83..18f818ff1 100644 --- a/libensemble/manager.py +++ b/libensemble/manager.py @@ -155,6 +155,51 @@ def filter_nans(array: npt.NDArray) -> npt.NDArray: """ +class _Worker: + """Wrapper class for Worker array and worker comms""" + + # def __new__(cls, W: npt.NDArray, wid: int, wcomms: list = []): + # if wid == 0: + # return super(Worker, ManagerWorker).__new__(ManagerWorker) + # else: + # return super().__new__(Worker) + + # def __init__(self, W: npt.NDArray, wid: int, wcomms: list = []): + # self.__dict__["_W"] = W + # self.__dict__["_wididx"] = wid + # self.__dict__["_wcomms"] = wcomms + + # def __setattr__(self, field, value): + # self._W[self._wididx][field] = value + + # def __getattr__(self, field): + # return self._W[self._wididx][field] + + # def update_state_on_alloc(self, Work: dict): + # self.active = Work["tag"] + # if "persistent" in Work["libE_info"]: + # self.persis_state = Work["tag"] + # if Work["libE_info"].get("active_recv", False): + # self.active_recv = Work["tag"] + # else: + # assert "active_recv" not in Work["libE_info"], "active_recv worker must also be persistent" + + # def update_persistent_state(self): + # self.persis_state = 0 + # if self.active_recv: + # self.active = 0 + # self.active_recv = 0 + + # def send(self, tag, data): + # self._wcomms[self._wididx].send(tag, data) + + # def mail_flag(self): + # return self._wcomms[self._wididx].mail_flag() + + # def recv(self): + # return self._wcomms[self._wididx].recv() + + class Manager: """Manager class for libensemble.""" @@ -454,6 +499,40 @@ def _check_received_calc(D_recv: dict) -> None: calc_status, str ), f"Aborting: Unknown calculation status received. Received status: {calc_status}" + def _update_state_on_local_gen_msg(self, persis_info, D_recv): + calc_type = D_recv["calc_type"] + # calc_status = D_recv["calc_status"] + Manager._check_received_calc(D_recv) + + # keep_state = D_recv["libE_info"].get("keep_state", False) + + if calc_type == EVAL_GEN_TAG: + self.hist.update_history_x_in(0, D_recv["calc_out"], 999) + + if D_recv.get("persis_info"): + persis_info[0].update(D_recv["persis_info"]) + + def _handle_msg_from_local_gen(self, persis_info: dict) -> None: + """Handles a message from worker w""" + try: + msg = self.local_worker_comm.recv() + tag, D_recv = msg + except CommFinishedException: + logger.debug("Finalizing message from Worker 0") + return + if isinstance(D_recv, WorkerErrMsg): + logger.debug("Manager received exception from worker 0") + if not self.WorkerExc: + self.WorkerExc = True + self._kill_workers() + raise WorkerException("Received error message from worker 0", D_recv.msg, D_recv.exc) + elif isinstance(D_recv, logging.LogRecord): + logger.debug("Manager received a log message from worker 0") + logging.getLogger(D_recv.name).handle(D_recv) + else: + logger.debug("Manager received data message from worker 0") + self._update_state_on_local_gen_msg(persis_info, D_recv) + def _receive_from_workers(self, persis_info: dict) -> dict: """Receives calculation output from workers. Loops over all active workers and probes to see if worker is ready to @@ -464,6 +543,9 @@ def _receive_from_workers(self, persis_info: dict) -> dict: new_stuff = True while new_stuff: new_stuff = False + if self.local_worker_comm.mail_flag(): + new_stuff = True + self._handle_msg_from_local_gen(persis_info) for w in self.W["worker_id"]: if self.wcomms[w - 1].mail_flag(): new_stuff = True @@ -638,6 +720,7 @@ def _get_alloc_libE_info(self) -> dict: "use_resource_sets": self.use_resource_sets, "gen_num_procs": self.gen_num_procs, "gen_num_gpus": self.gen_num_gpus, + "gen_on_man": self.libE_specs.get("gen_man", False), } def _alloc_work(self, H: npt.NDArray, persis_info: dict) -> dict: From 231e2b725220948a3a2a0cd138f3f6b286bcd77a Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 17 Jan 2024 15:36:58 -0600 Subject: [PATCH 013/288] use _Worker class to correctly index into W and wcomms. add initial option to libE_specs --- docs/data_structures/libE_specs.rst | 5 +- libensemble/manager.py | 201 +++++++++++----------------- libensemble/specs.py | 5 +- 3 files changed, 84 insertions(+), 127 deletions(-) diff --git a/docs/data_structures/libE_specs.rst b/docs/data_structures/libE_specs.rst index d471cf968..15646b1c3 100644 --- a/docs/data_structures/libE_specs.rst +++ b/docs/data_structures/libE_specs.rst @@ -28,7 +28,10 @@ libEnsemble is primarily customized by setting options within a ``LibeSpecs`` cl Manager/Worker communications mode: ``'mpi'``, ``'local'``, or ``'tcp'``. **nworkers** [int]: - Number of worker processes in ``"local"`` or ``"tcp"``. + Number of worker processes in ``"local"``, ``"threads"``, or ``"tcp"``. + + **manager_runs_additional_worker** [int] = False + Manager process can launch an additional threaded worker **mpi_comm** [MPI communicator] = ``MPI.COMM_WORLD``: libEnsemble MPI communicator. diff --git a/libensemble/manager.py b/libensemble/manager.py index 18f818ff1..2fedd5336 100644 --- a/libensemble/manager.py +++ b/libensemble/manager.py @@ -158,46 +158,43 @@ def filter_nans(array: npt.NDArray) -> npt.NDArray: class _Worker: """Wrapper class for Worker array and worker comms""" - # def __new__(cls, W: npt.NDArray, wid: int, wcomms: list = []): - # if wid == 0: - # return super(Worker, ManagerWorker).__new__(ManagerWorker) - # else: - # return super().__new__(Worker) - - # def __init__(self, W: npt.NDArray, wid: int, wcomms: list = []): - # self.__dict__["_W"] = W - # self.__dict__["_wididx"] = wid - # self.__dict__["_wcomms"] = wcomms + def __init__(self, W: npt.NDArray, wid: int, wcomms: list = []): + self.__dict__["_W"] = W + if 0 in W["worker_id"]: # Contains "0" for manager. Otherwise first entry is Worker 1 + self.__dict__["_wididx"] = wid + else: + self.__dict__["_wididx"] = wid - 1 + self.__dict__["_wcomms"] = wcomms - # def __setattr__(self, field, value): - # self._W[self._wididx][field] = value + def __setattr__(self, field, value): + self._W[self._wididx][field] = value - # def __getattr__(self, field): - # return self._W[self._wididx][field] + def __getattr__(self, field): + return self._W[self._wididx][field] - # def update_state_on_alloc(self, Work: dict): - # self.active = Work["tag"] - # if "persistent" in Work["libE_info"]: - # self.persis_state = Work["tag"] - # if Work["libE_info"].get("active_recv", False): - # self.active_recv = Work["tag"] - # else: - # assert "active_recv" not in Work["libE_info"], "active_recv worker must also be persistent" + def update_state_on_alloc(self, Work: dict): + self.active = Work["tag"] + if "persistent" in Work["libE_info"]: + self.persis_state = Work["tag"] + if Work["libE_info"].get("active_recv", False): + self.active_recv = Work["tag"] + else: + assert "active_recv" not in Work["libE_info"], "active_recv worker must also be persistent" - # def update_persistent_state(self): - # self.persis_state = 0 - # if self.active_recv: - # self.active = 0 - # self.active_recv = 0 + def update_persistent_state(self): + self.persis_state = 0 + if self.active_recv: + self.active = 0 + self.active_recv = 0 - # def send(self, tag, data): - # self._wcomms[self._wididx].send(tag, data) + def send(self, tag, data): + self._wcomms[self._wididx].send(tag, data) - # def mail_flag(self): - # return self._wcomms[self._wididx].mail_flag() + def mail_flag(self): + return self._wcomms[self._wididx].mail_flag() - # def recv(self): - # return self._wcomms[self._wididx].recv() + def recv(self): + return self._wcomms[self._wididx].recv() class Manager: @@ -255,16 +252,16 @@ def __init__( (1, "stop_val", self.term_test_stop_val), ] - self.local_worker_comm = None - self.libE_specs["gen_man"] = True + if self.libE_specs.get("manager_runs_additional_worker", False): - dtypes = { - EVAL_SIM_TAG: repack_fields(hist.H[sim_specs["in"]]).dtype, - EVAL_GEN_TAG: repack_fields(hist.H[gen_specs["in"]]).dtype, - } + dtypes = { + EVAL_SIM_TAG: repack_fields(hist.H[sim_specs["in"]]).dtype, + EVAL_GEN_TAG: repack_fields(hist.H[gen_specs["in"]]).dtype, + } - if self.libE_specs.get("gen_man", False): - self.local_worker_comm = QCommThread( + self.W = np.zeros(len(self.wcomms) + 1, dtype=Manager.worker_dtype) + self.W["worker_id"] = np.arange(len(self.wcomms) + 1) + local_worker_comm = QCommThread( worker_main, len(self.wcomms), sim_specs, @@ -275,8 +272,9 @@ def __init__( Resources.resources, Executor.executor, ) - self.local_worker_comm.run() - self.local_worker_comm.send(0, dtypes) + self.wcomms = [local_worker_comm] + self.wcomms + local_worker_comm.run() + local_worker_comm.send(0, dtypes) temp_EnsembleDirectory = EnsembleDirectory(libE_specs=libE_specs) self.resources = Resources.resources @@ -334,10 +332,9 @@ def term_test(self, logged: bool = True) -> Union[bool, int]: def _kill_workers(self) -> None: """Kills the workers""" - if self.local_worker_comm: - self.local_worker_comm.send(STOP_TAG, MAN_SIGNAL_FINISH) for w in self.W["worker_id"]: - self.wcomms[w - 1].send(STOP_TAG, MAN_SIGNAL_FINISH) + worker = _Worker(self.W, w, self.wcomms) + worker.send(STOP_TAG, MAN_SIGNAL_FINISH) # --- Checkpointing logic @@ -391,15 +388,16 @@ def _init_every_k_save(self, complete=False) -> None: def _check_work_order(self, Work: dict, w: int, force: bool = False) -> None: """Checks validity of an allocation function order""" - assert w != 0, "Can't send to worker 0; this is the manager." - if self.W[w - 1]["active_recv"]: + # assert w != 0, "Can't send to worker 0; this is the manager." + worker = _Worker(self.W, w, self.wcomms) + if worker.active_recv: assert "active_recv" in Work["libE_info"], ( "Messages to a worker in active_recv mode should have active_recv" f"set to True in libE_info. Work['libE_info'] is {Work['libE_info']}" ) else: if not force: - assert self.W[w - 1]["active"] == 0, ( + assert worker.active == 0, ( "Allocation function requested work be sent to worker %d, an already active worker." % w ) work_rows = Work["libE_info"]["H_rows"] @@ -441,16 +439,15 @@ def _send_work_order(self, Work: dict, w: int) -> None: """Sends an allocation function order to a worker""" logger.debug(f"Manager sending work unit to worker {w}") + worker = _Worker(self.W, w, self.wcomms) + if self.resources: self._set_resources(Work, w) - if Work["tag"] == EVAL_GEN_TAG and self.libE_specs.get("gen_man", False): - self.local_worker_comm.send(Work["tag"], Work) - else: - self.wcomms[w - 1].send(Work["tag"], Work) + worker.send(Work["tag"], Work) if Work["tag"] == EVAL_GEN_TAG: - self.W[w - 1]["gen_started_time"] = time.time() + worker.gen_started_time = time.time() work_rows = Work["libE_info"]["H_rows"] work_name = calc_type_strings[Work["tag"]] @@ -461,21 +458,13 @@ def _send_work_order(self, Work: dict, w: int) -> None: for i, row in enumerate(work_rows): H_to_be_sent[i] = repack_fields(self.hist.H[Work["H_fields"]][row]) - if Work["tag"] == EVAL_GEN_TAG and self.libE_specs.get("gen_man", False): - self.local_worker_comm.send(0, H_to_be_sent) - else: - self.wcomms[w - 1].send(0, H_to_be_sent) + worker.send(0, H_to_be_sent) def _update_state_on_alloc(self, Work: dict, w: int): """Updates a workers' active/idle status following an allocation order""" - self.W[w - 1]["active"] = Work["tag"] - if "libE_info" in Work: - if "persistent" in Work["libE_info"]: - self.W[w - 1]["persis_state"] = Work["tag"] - if Work["libE_info"].get("active_recv", False): - self.W[w - 1]["active_recv"] = Work["tag"] - else: - assert "active_recv" not in Work["libE_info"], "active_recv worker must also be persistent" + + worker = _Worker(self.W, w, self.wcomms) + worker.update_state_on_alloc(Work) work_rows = Work["libE_info"]["H_rows"] if Work["tag"] == EVAL_SIM_TAG: @@ -499,40 +488,6 @@ def _check_received_calc(D_recv: dict) -> None: calc_status, str ), f"Aborting: Unknown calculation status received. Received status: {calc_status}" - def _update_state_on_local_gen_msg(self, persis_info, D_recv): - calc_type = D_recv["calc_type"] - # calc_status = D_recv["calc_status"] - Manager._check_received_calc(D_recv) - - # keep_state = D_recv["libE_info"].get("keep_state", False) - - if calc_type == EVAL_GEN_TAG: - self.hist.update_history_x_in(0, D_recv["calc_out"], 999) - - if D_recv.get("persis_info"): - persis_info[0].update(D_recv["persis_info"]) - - def _handle_msg_from_local_gen(self, persis_info: dict) -> None: - """Handles a message from worker w""" - try: - msg = self.local_worker_comm.recv() - tag, D_recv = msg - except CommFinishedException: - logger.debug("Finalizing message from Worker 0") - return - if isinstance(D_recv, WorkerErrMsg): - logger.debug("Manager received exception from worker 0") - if not self.WorkerExc: - self.WorkerExc = True - self._kill_workers() - raise WorkerException("Received error message from worker 0", D_recv.msg, D_recv.exc) - elif isinstance(D_recv, logging.LogRecord): - logger.debug("Manager received a log message from worker 0") - logging.getLogger(D_recv.name).handle(D_recv) - else: - logger.debug("Manager received data message from worker 0") - self._update_state_on_local_gen_msg(persis_info, D_recv) - def _receive_from_workers(self, persis_info: dict) -> dict: """Receives calculation output from workers. Loops over all active workers and probes to see if worker is ready to @@ -543,11 +498,9 @@ def _receive_from_workers(self, persis_info: dict) -> dict: new_stuff = True while new_stuff: new_stuff = False - if self.local_worker_comm.mail_flag(): - new_stuff = True - self._handle_msg_from_local_gen(persis_info) for w in self.W["worker_id"]: - if self.wcomms[w - 1].mail_flag(): + worker = _Worker(self.W, w, self.wcomms) + if worker.mail_flag(): new_stuff = True self._handle_msg_from_worker(persis_info, w) @@ -560,38 +513,37 @@ def _update_state_on_worker_msg(self, persis_info: dict, D_recv: dict, w: int) - calc_status = D_recv["calc_status"] Manager._check_received_calc(D_recv) + worker = _Worker(self.W, w, self.wcomms) + keep_state = D_recv["libE_info"].get("keep_state", False) - if w not in self.persis_pending and not self.W[w - 1]["active_recv"] and not keep_state: - self.W[w - 1]["active"] = 0 + if w not in self.persis_pending and not worker.active_recv and not keep_state: + worker.active = 0 if calc_status in [FINISHED_PERSISTENT_SIM_TAG, FINISHED_PERSISTENT_GEN_TAG]: final_data = D_recv.get("calc_out", None) if isinstance(final_data, np.ndarray): if calc_status is FINISHED_PERSISTENT_GEN_TAG and self.libE_specs.get("use_persis_return_gen", False): - self.hist.update_history_x_in(w, final_data, self.W[w - 1]["gen_started_time"]) + self.hist.update_history_x_in(w, final_data, worker.gen_started_time) elif calc_status is FINISHED_PERSISTENT_SIM_TAG and self.libE_specs.get("use_persis_return_sim", False): self.hist.update_history_f(D_recv, self.kill_canceled_sims) else: logger.info(_PERSIS_RETURN_WARNING) - self.W[w - 1]["persis_state"] = 0 - if self.W[w - 1]["active_recv"]: - self.W[w - 1]["active"] = 0 - self.W[w - 1]["active_recv"] = 0 + worker.update_persistent_state() if w in self.persis_pending: self.persis_pending.remove(w) - self.W[w - 1]["active"] = 0 + worker.active = 0 self._freeup_resources(w) else: if calc_type == EVAL_SIM_TAG: self.hist.update_history_f(D_recv, self.kill_canceled_sims) if calc_type == EVAL_GEN_TAG: - self.hist.update_history_x_in(w, D_recv["calc_out"], self.W[w - 1]["gen_started_time"]) + self.hist.update_history_x_in(w, D_recv["calc_out"], worker.gen_started_time) assert ( - len(D_recv["calc_out"]) or np.any(self.W["active"]) or self.W[w - 1]["persis_state"] + len(D_recv["calc_out"]) or np.any(self.W["active"]) or worker.persis_state ), "Gen must return work when is is the only thing active and not persistent." if "libE_info" in D_recv and "persistent" in D_recv["libE_info"]: # Now a waiting, persistent worker - self.W[w - 1]["persis_state"] = calc_type + worker.persis_state = calc_type else: self._freeup_resources(w) @@ -600,14 +552,15 @@ def _update_state_on_worker_msg(self, persis_info: dict, D_recv: dict, w: int) - def _handle_msg_from_worker(self, persis_info: dict, w: int) -> None: """Handles a message from worker w""" + worker = _Worker(self.W, w, self.wcomms) try: - msg = self.wcomms[w - 1].recv() + msg = worker.recv() tag, D_recv = msg except CommFinishedException: logger.debug(f"Finalizing message from Worker {w}") return if isinstance(D_recv, WorkerErrMsg): - self.W[w - 1]["active"] = 0 + worker.active = 0 logger.debug(f"Manager received exception from worker {w}") if not self.WorkerExc: self.WorkerExc = True @@ -640,9 +593,8 @@ def _kill_cancelled_sims(self) -> None: kill_ids = self.hist.H["sim_id"][kill_sim_rows] kill_on_workers = self.hist.H["sim_worker"][kill_sim_rows] for w in kill_on_workers: - if self.local_worker_comm: - self.local_worker_comm.send(STOP_TAG, MAN_SIGNAL_KILL) - self.wcomms[w - 1].send(STOP_TAG, MAN_SIGNAL_KILL) + worker = _Worker(self.W, w, self.wcomms) + worker.send(STOP_TAG, MAN_SIGNAL_KILL) self.hist.H["kill_sent"][kill_ids] = True # --- Handle termination @@ -659,6 +611,7 @@ def _final_receive_and_kill(self, persis_info: dict) -> (dict, int, int): # Send a handshake signal to each persistent worker. if any(self.W["persis_state"]): for w in self.W["worker_id"][self.W["persis_state"] > 0]: + worker = _Worker(self.W, w, self.wcomms) logger.debug(f"Manager sending PERSIS_STOP to worker {w}") if self.libE_specs.get("final_gen_send", False): rows_to_send = np.where(self.hist.H["sim_ended"] & ~self.hist.H["gen_informed"])[0] @@ -672,12 +625,10 @@ def _final_receive_and_kill(self, persis_info: dict) -> (dict, int, int): self._send_work_order(work, w) self.hist.update_history_to_gen(rows_to_send) else: - if self.local_worker_comm: - self.local_worker_comm.send(PERSIS_STOP, MAN_SIGNAL_KILL) - self.wcomms[w - 1].send(PERSIS_STOP, MAN_SIGNAL_KILL) - if not self.W[w - 1]["active"]: + worker.send(PERSIS_STOP, MAN_SIGNAL_KILL) + if not worker.active: # Re-activate if necessary - self.W[w - 1]["active"] = self.W[w - 1]["persis_state"] + worker.active = worker.persis_state self.persis_pending.append(w) exit_flag = 0 diff --git a/libensemble/specs.py b/libensemble/specs.py index f7b7b3ea5..4678b01d4 100644 --- a/libensemble/specs.py +++ b/libensemble/specs.py @@ -160,7 +160,10 @@ class LibeSpecs(BaseModel): """ Manager/Worker communications mode. ``'mpi'``, ``'local'``, ``'threads'``, or ``'tcp'`` """ nworkers: Optional[int] = 0 - """ Number of worker processes in ``"local"`` or ``"tcp"``.""" + """ Number of worker processes in ``"local"``, ``"threads"``, or ``"tcp"``.""" + + manager_runs_additional_worker: Optional[int] = False + """ Manager process can launch an additional threaded worker """ mpi_comm: Optional[Any] = None """ libEnsemble MPI communicator. Default: ``MPI.COMM_WORLD``""" From d251363158114b97e306307bd322ec6bba1b16bd Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 17 Jan 2024 15:48:11 -0600 Subject: [PATCH 014/288] add "threaded" tentative option to sim/gen_specs --- libensemble/message_numbers.py | 2 -- libensemble/specs.py | 10 ++++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/libensemble/message_numbers.py b/libensemble/message_numbers.py index 6caef0a6e..adfcbc244 100644 --- a/libensemble/message_numbers.py +++ b/libensemble/message_numbers.py @@ -41,8 +41,6 @@ # last_calc_status_rst_tag CALC_EXCEPTION = 35 # Reserved: Automatically used if user_f raised an exception -EVAL_FINAL_GEN_TAG = 36 - MAN_KILL_SIGNALS = [MAN_SIGNAL_FINISH, MAN_SIGNAL_KILL] calc_status_strings = { diff --git a/libensemble/specs.py b/libensemble/specs.py index 4678b01d4..13824bbc1 100644 --- a/libensemble/specs.py +++ b/libensemble/specs.py @@ -55,6 +55,11 @@ class SimSpecs(BaseModel): calling them locally. """ + threaded: Optional[bool] = False + """ + Instruct Worker process to launch user function to a thread. + """ + user: Optional[dict] = {} """ A user-data dictionary to place bounds, constants, settings, or other parameters for customizing @@ -100,6 +105,11 @@ class GenSpecs(BaseModel): calling them locally. """ + threaded: Optional[bool] = False + """ + Instruct Worker process to launch user function to a thread. + """ + user: Optional[dict] = {} """ A user-data dictionary to place bounds, constants, settings, or other parameters for From 368bf937c4136a05d13dfdb5e36bf7fe3f3ebc96 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 17 Jan 2024 15:56:40 -0600 Subject: [PATCH 015/288] fix ThreadRunner shutdown when that worker didn't launch a thread --- libensemble/utils/runners.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index e21c87ba5..0ea9ce1e7 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -73,6 +73,7 @@ def shutdown(self) -> None: class ThreadRunner(Runner): def __init__(self, specs): super().__init__(specs) + self.thread_handle = None def _result(self, calc_in: npt.NDArray, persis_info: dict, libE_info: dict) -> (npt.NDArray, dict, Optional[int]): fargs = self._truncate_args(calc_in, persis_info, libE_info) @@ -81,4 +82,5 @@ def _result(self, calc_in: npt.NDArray, persis_info: dict, libE_info: dict) -> ( return self.thread_handle.result() def shutdown(self) -> None: - self.thread_handle.terminate() + if self.thread_handle is not None: + self.thread_handle.terminate() From 744620d381e7b4881d8ac2fe83d28eb7e5f1717a Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 18 Jan 2024 10:08:48 -0600 Subject: [PATCH 016/288] adds test-case to functionality tests, fixes alloc_f libE_info usable entry --- libensemble/manager.py | 2 +- .../functionality_tests/test_persistent_uniform_sampling.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/libensemble/manager.py b/libensemble/manager.py index 2fedd5336..e9a42f74d 100644 --- a/libensemble/manager.py +++ b/libensemble/manager.py @@ -671,7 +671,7 @@ def _get_alloc_libE_info(self) -> dict: "use_resource_sets": self.use_resource_sets, "gen_num_procs": self.gen_num_procs, "gen_num_gpus": self.gen_num_gpus, - "gen_on_man": self.libE_specs.get("gen_man", False), + "manager_additional_worker": self.libE_specs.get("manager_runs_additional_worker", False), } def _alloc_work(self, H: npt.NDArray, persis_info: dict) -> dict: diff --git a/libensemble/tests/functionality_tests/test_persistent_uniform_sampling.py b/libensemble/tests/functionality_tests/test_persistent_uniform_sampling.py index bd381f3ae..e343ff991 100644 --- a/libensemble/tests/functionality_tests/test_persistent_uniform_sampling.py +++ b/libensemble/tests/functionality_tests/test_persistent_uniform_sampling.py @@ -62,7 +62,7 @@ libE_specs["kill_canceled_sims"] = False - for run in range(3): + for run in range(4): persis_info = add_unique_random_streams({}, nworkers + 1) for i in persis_info: persis_info[i]["get_grad"] = True @@ -86,6 +86,8 @@ sim_specs["out"] = [("f_i", float), ("gradf_i", float, 2 * m)] sim_specs["in"] = ["x", "obj_component"] # sim_specs["out"] = [("f", float), ("grad", float, n)] + elif run == 3: + libE_specs["manager_runs_additional_worker"] = True # Perform the run H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) From cd6f0db09dc5b5e66f8e3d4e0bff383f9828e98f Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 18 Jan 2024 12:37:03 -0600 Subject: [PATCH 017/288] make resources reflect develop? --- libensemble/resources/scheduler.py | 2 +- libensemble/resources/worker_resources.py | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/libensemble/resources/scheduler.py b/libensemble/resources/scheduler.py index 386a406bc..04de87e77 100644 --- a/libensemble/resources/scheduler.py +++ b/libensemble/resources/scheduler.py @@ -245,7 +245,7 @@ def get_avail_rsets_by_group(self): for g in groups: self.avail_rsets_by_group[g] = [] for ind, rset in enumerate(rsets): - if rset["assigned"] == -1: # now default is -1. + if not rset["assigned"]: g = rset["group"] self.avail_rsets_by_group[g].append(ind) return self.avail_rsets_by_group diff --git a/libensemble/resources/worker_resources.py b/libensemble/resources/worker_resources.py index 2becaa1df..639f27da7 100644 --- a/libensemble/resources/worker_resources.py +++ b/libensemble/resources/worker_resources.py @@ -50,10 +50,11 @@ def __init__(self, num_workers: int, resources: "GlobalResources") -> None: # n ) self.rsets = np.zeros(self.total_num_rsets, dtype=ResourceManager.man_rset_dtype) - self.rsets["assigned"] = -1 # Can assign to manager (=0) so make unset value -1 + self.rsets["assigned"] = 0 for field in self.all_rsets.dtype.names: self.rsets[field] = self.all_rsets[field] self.num_groups = self.rsets["group"][-1] + self.rsets_free = self.total_num_rsets self.gpu_rsets_free = self.total_num_gpu_rsets self.nongpu_rsets_free = self.total_num_nongpu_rsets @@ -69,7 +70,7 @@ def assign_rsets(self, rset_team, worker_id): if rset_team: rteam = self.rsets["assigned"][rset_team] for i, wid in enumerate(rteam): - if wid == -1: + if wid == 0: self.rsets["assigned"][rset_team[i]] = worker_id self.rsets_free -= 1 if self.rsets["gpus"][rset_team[i]]: @@ -84,13 +85,13 @@ def assign_rsets(self, rset_team, worker_id): def free_rsets(self, worker=None): """Free up assigned resource sets""" if worker is None: - self.rsets["assigned"] = -1 + self.rsets["assigned"] = 0 self.rsets_free = self.total_num_rsets self.gpu_rsets_free = self.total_num_gpu_rsets self.nongpu_rsets_free = self.total_num_nongpu_rsets else: rsets_to_free = np.where(self.rsets["assigned"] == worker)[0] - self.rsets["assigned"][rsets_to_free] = -1 + self.rsets["assigned"][rsets_to_free] = 0 self.rsets_free += len(rsets_to_free) self.gpu_rsets_free += np.count_nonzero(self.rsets["gpus"][rsets_to_free]) self.nongpu_rsets_free += np.count_nonzero(~self.rsets["gpus"][rsets_to_free]) @@ -199,6 +200,7 @@ def __init__(self, num_workers, resources, workerID): self.gen_nprocs = None self.gen_ngpus = None self.platform_info = resources.platform_info + self.tiles_per_gpu = resources.tiles_per_gpu # User convenience functions ---------------------------------------------- @@ -216,6 +218,9 @@ def get_slots_as_string(self, multiplier=1, delimiter=",", limit=None): slot_list = [j for i in self.slots_on_node for j in range(i * n, (i + 1) * n)] if limit is not None: slot_list = slot_list[:limit] + if self.tiles_per_gpu > 1: + ntiles = self.tiles_per_gpu + slot_list = [f"{i // ntiles}.{i % ntiles}" for i in slot_list] slots = delimiter.join(map(str, slot_list)) return slots From 884d61b7174626ab91e05e0040c37371a61bcee5 Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 22 Jan 2024 13:19:40 -0600 Subject: [PATCH 018/288] remove old symlink --- examples/calling_scripts/tutorial_calling.py | 1 - 1 file changed, 1 deletion(-) delete mode 120000 examples/calling_scripts/tutorial_calling.py diff --git a/examples/calling_scripts/tutorial_calling.py b/examples/calling_scripts/tutorial_calling.py deleted file mode 120000 index f54fe1ad7..000000000 --- a/examples/calling_scripts/tutorial_calling.py +++ /dev/null @@ -1 +0,0 @@ -../tutorials/simple_sine/tutorial_calling.py \ No newline at end of file From dfb0fbbcf176e20182093fc0544232e9cb1cdcad Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 22 Jan 2024 14:07:48 -0600 Subject: [PATCH 019/288] print evaluated lines in check_libe_stats for now --- libensemble/tests/functionality_tests/check_libE_stats.py | 1 + 1 file changed, 1 insertion(+) diff --git a/libensemble/tests/functionality_tests/check_libE_stats.py b/libensemble/tests/functionality_tests/check_libE_stats.py index 8e4e9c0cc..8260c25c0 100644 --- a/libensemble/tests/functionality_tests/check_libE_stats.py +++ b/libensemble/tests/functionality_tests/check_libE_stats.py @@ -39,6 +39,7 @@ def check_start_end_times(start="Start:", end="End:", everyline=True): with open(infile) as f: total_cnt = 0 for line in f: + print(line) s_cnt = 0 e_cnt = 0 lst = line.split() From ec236ed15d7e302c69edbdb96df970f2d26468bf Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 22 Jan 2024 14:49:26 -0600 Subject: [PATCH 020/288] only want to perform this specific datetime check on indexes 5 and 6 of a split stats line if the line is a Manager: starting or Manager: exiting line --- libensemble/tests/functionality_tests/check_libE_stats.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libensemble/tests/functionality_tests/check_libE_stats.py b/libensemble/tests/functionality_tests/check_libE_stats.py index 8260c25c0..424c07d8b 100644 --- a/libensemble/tests/functionality_tests/check_libE_stats.py +++ b/libensemble/tests/functionality_tests/check_libE_stats.py @@ -39,11 +39,10 @@ def check_start_end_times(start="Start:", end="End:", everyline=True): with open(infile) as f: total_cnt = 0 for line in f: - print(line) s_cnt = 0 e_cnt = 0 lst = line.split() - if lst[0] == "Manager": + if line.startswith("Manager : Starting") or line.startswith("Manager : Exiting"): check_datetime(lst[5], lst[6]) continue for i, val in enumerate(lst): From f06148a2d5dee26edf44ba1e1ac65e9b0f7753db Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 24 Jan 2024 11:54:42 -0600 Subject: [PATCH 021/288] a much simpler indexing solution from shuds --- libensemble/manager.py | 118 ++++++++++++++++------------------------- 1 file changed, 45 insertions(+), 73 deletions(-) diff --git a/libensemble/manager.py b/libensemble/manager.py index e9a42f74d..3d0b926dc 100644 --- a/libensemble/manager.py +++ b/libensemble/manager.py @@ -155,46 +155,16 @@ def filter_nans(array: npt.NDArray) -> npt.NDArray: """ -class _Worker: - """Wrapper class for Worker array and worker comms""" - - def __init__(self, W: npt.NDArray, wid: int, wcomms: list = []): - self.__dict__["_W"] = W - if 0 in W["worker_id"]: # Contains "0" for manager. Otherwise first entry is Worker 1 - self.__dict__["_wididx"] = wid - else: - self.__dict__["_wididx"] = wid - 1 - self.__dict__["_wcomms"] = wcomms - - def __setattr__(self, field, value): - self._W[self._wididx][field] = value - - def __getattr__(self, field): - return self._W[self._wididx][field] - - def update_state_on_alloc(self, Work: dict): - self.active = Work["tag"] - if "persistent" in Work["libE_info"]: - self.persis_state = Work["tag"] - if Work["libE_info"].get("active_recv", False): - self.active_recv = Work["tag"] +class _WorkerIndexer: + def __init__(self, iterable: list, additional_worker=False): + self.iterable = iterable + self.additional_worker = additional_worker + + def __getitem__(self, key): + if self.additional_worker or isinstance(key, str): + return self.iterable[key] else: - assert "active_recv" not in Work["libE_info"], "active_recv worker must also be persistent" - - def update_persistent_state(self): - self.persis_state = 0 - if self.active_recv: - self.active = 0 - self.active_recv = 0 - - def send(self, tag, data): - self._wcomms[self._wididx].send(tag, data) - - def mail_flag(self): - return self._wcomms[self._wididx].mail_flag() - - def recv(self): - return self._wcomms[self._wididx].recv() + return self.iterable[key - 1] class Manager: @@ -253,6 +223,7 @@ def __init__( ] if self.libE_specs.get("manager_runs_additional_worker", False): + # We start an additional Worker 0 on a thread. dtypes = { EVAL_SIM_TAG: repack_fields(hist.H[sim_specs["in"]]).dtype, @@ -276,13 +247,16 @@ def __init__( local_worker_comm.run() local_worker_comm.send(0, dtypes) + self.W = _WorkerIndexer(self.W, self.libE_specs.get("manager_runs_additional_worker", False)) + self.wcomms = _WorkerIndexer(self.wcomms, self.libE_specs.get("manager_runs_additional_worker", False)) + temp_EnsembleDirectory = EnsembleDirectory(libE_specs=libE_specs) self.resources = Resources.resources self.scheduler_opts = self.libE_specs.get("scheduler_opts", {}) if self.resources is not None: gresource = self.resources.glob_resources self.scheduler_opts = gresource.update_scheduler_opts(self.scheduler_opts) - for wrk in self.W: + for wrk in self.W.iterable: if wrk["worker_id"] in gresource.zero_resource_workers: wrk["zero_resource_worker"] = True @@ -333,8 +307,7 @@ def term_test(self, logged: bool = True) -> Union[bool, int]: def _kill_workers(self) -> None: """Kills the workers""" for w in self.W["worker_id"]: - worker = _Worker(self.W, w, self.wcomms) - worker.send(STOP_TAG, MAN_SIGNAL_FINISH) + self.wcomms[w].send(STOP_TAG, MAN_SIGNAL_FINISH) # --- Checkpointing logic @@ -389,15 +362,14 @@ def _init_every_k_save(self, complete=False) -> None: def _check_work_order(self, Work: dict, w: int, force: bool = False) -> None: """Checks validity of an allocation function order""" # assert w != 0, "Can't send to worker 0; this is the manager." - worker = _Worker(self.W, w, self.wcomms) - if worker.active_recv: + if self.W[w]["active_recv"]: assert "active_recv" in Work["libE_info"], ( "Messages to a worker in active_recv mode should have active_recv" f"set to True in libE_info. Work['libE_info'] is {Work['libE_info']}" ) else: if not force: - assert worker.active == 0, ( + assert self.W[w]["active"] == 0, ( "Allocation function requested work be sent to worker %d, an already active worker." % w ) work_rows = Work["libE_info"]["H_rows"] @@ -439,15 +411,13 @@ def _send_work_order(self, Work: dict, w: int) -> None: """Sends an allocation function order to a worker""" logger.debug(f"Manager sending work unit to worker {w}") - worker = _Worker(self.W, w, self.wcomms) - if self.resources: self._set_resources(Work, w) - worker.send(Work["tag"], Work) + self.wcomms[w].send(Work["tag"], Work) if Work["tag"] == EVAL_GEN_TAG: - worker.gen_started_time = time.time() + self.W[w]["gen_started_time"] = time.time() work_rows = Work["libE_info"]["H_rows"] work_name = calc_type_strings[Work["tag"]] @@ -458,13 +428,18 @@ def _send_work_order(self, Work: dict, w: int) -> None: for i, row in enumerate(work_rows): H_to_be_sent[i] = repack_fields(self.hist.H[Work["H_fields"]][row]) - worker.send(0, H_to_be_sent) + self.wcomms[w].send(0, H_to_be_sent) def _update_state_on_alloc(self, Work: dict, w: int): """Updates a workers' active/idle status following an allocation order""" - worker = _Worker(self.W, w, self.wcomms) - worker.update_state_on_alloc(Work) + self.W[w]["active"] = Work["tag"] + if "persistent" in Work["libE_info"]: + self.W[w]["persis_state"] = Work["tag"] + if Work["libE_info"].get("active_recv", False): + self.W[w]["active_recv"] = Work["tag"] + else: + assert "active_recv" not in Work["libE_info"], "active_recv worker must also be persistent" work_rows = Work["libE_info"]["H_rows"] if Work["tag"] == EVAL_SIM_TAG: @@ -499,8 +474,7 @@ def _receive_from_workers(self, persis_info: dict) -> dict: while new_stuff: new_stuff = False for w in self.W["worker_id"]: - worker = _Worker(self.W, w, self.wcomms) - if worker.mail_flag(): + if self.wcomms[w].mail_flag(): new_stuff = True self._handle_msg_from_worker(persis_info, w) @@ -513,37 +487,38 @@ def _update_state_on_worker_msg(self, persis_info: dict, D_recv: dict, w: int) - calc_status = D_recv["calc_status"] Manager._check_received_calc(D_recv) - worker = _Worker(self.W, w, self.wcomms) - keep_state = D_recv["libE_info"].get("keep_state", False) - if w not in self.persis_pending and not worker.active_recv and not keep_state: - worker.active = 0 + if w not in self.persis_pending and not self.W[w]["active_recv"] and not keep_state: + self.W[w]["active"] = 0 if calc_status in [FINISHED_PERSISTENT_SIM_TAG, FINISHED_PERSISTENT_GEN_TAG]: final_data = D_recv.get("calc_out", None) if isinstance(final_data, np.ndarray): if calc_status is FINISHED_PERSISTENT_GEN_TAG and self.libE_specs.get("use_persis_return_gen", False): - self.hist.update_history_x_in(w, final_data, worker.gen_started_time) + self.hist.update_history_x_in(w, final_data, self.W[w]["gen_started_time"]) elif calc_status is FINISHED_PERSISTENT_SIM_TAG and self.libE_specs.get("use_persis_return_sim", False): self.hist.update_history_f(D_recv, self.kill_canceled_sims) else: logger.info(_PERSIS_RETURN_WARNING) - worker.update_persistent_state() + self.W[w]["persis_state"] = 0 + if self.W[w]["active_recv"]: + self.W[w]["active"] = 0 + self.W[w]["active_recv"] = 0 if w in self.persis_pending: self.persis_pending.remove(w) - worker.active = 0 + self.W[w]["active"] = 0 self._freeup_resources(w) else: if calc_type == EVAL_SIM_TAG: self.hist.update_history_f(D_recv, self.kill_canceled_sims) if calc_type == EVAL_GEN_TAG: - self.hist.update_history_x_in(w, D_recv["calc_out"], worker.gen_started_time) + self.hist.update_history_x_in(w, D_recv["calc_out"], self.W[w]["gen_started_time"]) assert ( - len(D_recv["calc_out"]) or np.any(self.W["active"]) or worker.persis_state + len(D_recv["calc_out"]) or np.any(self.W["active"]) or self.W[w]["persis_state"] ), "Gen must return work when is is the only thing active and not persistent." if "libE_info" in D_recv and "persistent" in D_recv["libE_info"]: # Now a waiting, persistent worker - worker.persis_state = calc_type + self.W[w]["persis_state"] = calc_type else: self._freeup_resources(w) @@ -552,15 +527,14 @@ def _update_state_on_worker_msg(self, persis_info: dict, D_recv: dict, w: int) - def _handle_msg_from_worker(self, persis_info: dict, w: int) -> None: """Handles a message from worker w""" - worker = _Worker(self.W, w, self.wcomms) try: - msg = worker.recv() + msg = self.wcomms[w].recv() tag, D_recv = msg except CommFinishedException: logger.debug(f"Finalizing message from Worker {w}") return if isinstance(D_recv, WorkerErrMsg): - worker.active = 0 + self.W[w]["active"] = 0 logger.debug(f"Manager received exception from worker {w}") if not self.WorkerExc: self.WorkerExc = True @@ -593,8 +567,7 @@ def _kill_cancelled_sims(self) -> None: kill_ids = self.hist.H["sim_id"][kill_sim_rows] kill_on_workers = self.hist.H["sim_worker"][kill_sim_rows] for w in kill_on_workers: - worker = _Worker(self.W, w, self.wcomms) - worker.send(STOP_TAG, MAN_SIGNAL_KILL) + self.wcomms[w].send(STOP_TAG, MAN_SIGNAL_KILL) self.hist.H["kill_sent"][kill_ids] = True # --- Handle termination @@ -611,7 +584,6 @@ def _final_receive_and_kill(self, persis_info: dict) -> (dict, int, int): # Send a handshake signal to each persistent worker. if any(self.W["persis_state"]): for w in self.W["worker_id"][self.W["persis_state"] > 0]: - worker = _Worker(self.W, w, self.wcomms) logger.debug(f"Manager sending PERSIS_STOP to worker {w}") if self.libE_specs.get("final_gen_send", False): rows_to_send = np.where(self.hist.H["sim_ended"] & ~self.hist.H["gen_informed"])[0] @@ -625,10 +597,10 @@ def _final_receive_and_kill(self, persis_info: dict) -> (dict, int, int): self._send_work_order(work, w) self.hist.update_history_to_gen(rows_to_send) else: - worker.send(PERSIS_STOP, MAN_SIGNAL_KILL) - if not worker.active: + self.wcomms[w].send(PERSIS_STOP, MAN_SIGNAL_KILL) + if not self.W[w]["active"]: # Re-activate if necessary - worker.active = worker.persis_state + self.W[w]["active"] = self.W[w]["persis_state"] self.persis_pending.append(w) exit_flag = 0 From d584152e6ffb1b441d89f8b6b676d6e76d365ea9 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 24 Jan 2024 12:03:42 -0600 Subject: [PATCH 022/288] add comment for why using self.W.iterable in "for wrk in self.W.iterable" --- libensemble/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libensemble/manager.py b/libensemble/manager.py index 3d0b926dc..068d60d60 100644 --- a/libensemble/manager.py +++ b/libensemble/manager.py @@ -256,7 +256,7 @@ def __init__( if self.resources is not None: gresource = self.resources.glob_resources self.scheduler_opts = gresource.update_scheduler_opts(self.scheduler_opts) - for wrk in self.W.iterable: + for wrk in self.W.iterable: # "for wrk in self.W" produces a key of 0 when not applicable if wrk["worker_id"] in gresource.zero_resource_workers: wrk["zero_resource_worker"] = True From 592c8c4d5f819b66582da7d5c7ce49cccd06e42b Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 24 Jan 2024 12:23:11 -0600 Subject: [PATCH 023/288] add __len__ and __iter__ to indexer --- libensemble/manager.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/libensemble/manager.py b/libensemble/manager.py index 068d60d60..ae543d38a 100644 --- a/libensemble/manager.py +++ b/libensemble/manager.py @@ -166,6 +166,12 @@ def __getitem__(self, key): else: return self.iterable[key - 1] + def __len__(self): + return len(self.iterable) + + def __iter__(self): + return iter(self.iterable) + class Manager: """Manager class for libensemble.""" @@ -256,7 +262,7 @@ def __init__( if self.resources is not None: gresource = self.resources.glob_resources self.scheduler_opts = gresource.update_scheduler_opts(self.scheduler_opts) - for wrk in self.W.iterable: # "for wrk in self.W" produces a key of 0 when not applicable + for wrk in self.W: if wrk["worker_id"] in gresource.zero_resource_workers: wrk["zero_resource_worker"] = True From 59ca40a94f769c04830423d13edc455afa822bda Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 24 Jan 2024 12:32:28 -0600 Subject: [PATCH 024/288] add __setitem__ --- libensemble/manager.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libensemble/manager.py b/libensemble/manager.py index ae543d38a..8a28ce235 100644 --- a/libensemble/manager.py +++ b/libensemble/manager.py @@ -166,6 +166,9 @@ def __getitem__(self, key): else: return self.iterable[key - 1] + def __setitem__(self, key, value): + self.iterable[key] = value + def __len__(self): return len(self.iterable) From d8a3a4208ef0609040a84c5a6c4b4f8eb95a2250 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 24 Jan 2024 13:32:47 -0600 Subject: [PATCH 025/288] adjust alloc_support to not use w - 1 indexing --- libensemble/tools/alloc_support.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libensemble/tools/alloc_support.py b/libensemble/tools/alloc_support.py index ed1148411..a544477e7 100644 --- a/libensemble/tools/alloc_support.py +++ b/libensemble/tools/alloc_support.py @@ -201,7 +201,7 @@ def _update_rset_team(self, libE_info, wid, H=None, H_rows=None): """Add rset_team to libE_info.""" if self.manage_resources and not libE_info.get("rset_team"): num_rsets_req = 0 - if self.W[wid - 1]["persis_state"]: + if self.W[wid]["persis_state"]: # Even if empty list, non-None rset_team stops manager giving default resources libE_info["rset_team"] = [] return @@ -272,7 +272,7 @@ def gen_work(self, wid, H_fields, H_rows, persis_info, **libE_info): """ self._update_rset_team(libE_info, wid) - if not self.W[wid - 1]["persis_state"]: + if not self.W[wid]["persis_state"]: AllocSupport.gen_counter += 1 # Count total gens libE_info["gen_count"] = AllocSupport.gen_counter From 1839ff2952d6734b46a16755a16fa17818cbe826 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 24 Jan 2024 15:16:59 -0600 Subject: [PATCH 026/288] just pass in the iterable for now. resource changes coming in another branch --- libensemble/manager.py | 2 +- libensemble/tools/alloc_support.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libensemble/manager.py b/libensemble/manager.py index 8a28ce235..7c77b0c27 100644 --- a/libensemble/manager.py +++ b/libensemble/manager.py @@ -665,7 +665,7 @@ def _alloc_work(self, H: npt.NDArray, persis_info: dict) -> dict: alloc_f = self.alloc_specs["alloc_f"] output = alloc_f( - self.W, + self.W.iterable, H, self.sim_specs, self.gen_specs, diff --git a/libensemble/tools/alloc_support.py b/libensemble/tools/alloc_support.py index a544477e7..ed1148411 100644 --- a/libensemble/tools/alloc_support.py +++ b/libensemble/tools/alloc_support.py @@ -201,7 +201,7 @@ def _update_rset_team(self, libE_info, wid, H=None, H_rows=None): """Add rset_team to libE_info.""" if self.manage_resources and not libE_info.get("rset_team"): num_rsets_req = 0 - if self.W[wid]["persis_state"]: + if self.W[wid - 1]["persis_state"]: # Even if empty list, non-None rset_team stops manager giving default resources libE_info["rset_team"] = [] return @@ -272,7 +272,7 @@ def gen_work(self, wid, H_fields, H_rows, persis_info, **libE_info): """ self._update_rset_team(libE_info, wid) - if not self.W[wid]["persis_state"]: + if not self.W[wid - 1]["persis_state"]: AllocSupport.gen_counter += 1 # Count total gens libE_info["gen_count"] = AllocSupport.gen_counter From e177a1800a15aed61982ad3a6a7e253df64c4c2d Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 7 Feb 2024 15:46:48 -0600 Subject: [PATCH 027/288] initial commit. we can naively format a non-adaptive, non-persistent gen in *this* way. --- libensemble/specs.py | 2 +- .../test_1d_asktell_gen.py | 87 +++++++++++++++++++ libensemble/utils/runners.py | 10 +++ libensemble/utils/validators.py | 4 +- libensemble/worker.py | 5 +- 5 files changed, 103 insertions(+), 5 deletions(-) create mode 100644 libensemble/tests/functionality_tests/test_1d_asktell_gen.py diff --git a/libensemble/specs.py b/libensemble/specs.py index e796aee46..91071e4f8 100644 --- a/libensemble/specs.py +++ b/libensemble/specs.py @@ -72,7 +72,7 @@ class GenSpecs(BaseModel): Specifications for configuring a Generator Function. """ - gen_f: Optional[Callable] = None + gen_f: Optional[Any] = None """ Python function matching the ``gen_f`` interface. Produces parameters for evaluation by a simulator function, and makes decisions based on simulator function output. diff --git a/libensemble/tests/functionality_tests/test_1d_asktell_gen.py b/libensemble/tests/functionality_tests/test_1d_asktell_gen.py new file mode 100644 index 000000000..de611b3c5 --- /dev/null +++ b/libensemble/tests/functionality_tests/test_1d_asktell_gen.py @@ -0,0 +1,87 @@ +""" +Runs libEnsemble with Latin hypercube sampling on a simple 1D problem + +Execute via one of the following commands (e.g. 3 workers): + mpiexec -np 4 python test_1d_sampling.py + python test_1d_sampling.py --nworkers 3 --comms local + python test_1d_sampling.py --nworkers 3 --comms tcp + +The number of concurrent evaluations of the objective function will be 4-1=3. +""" + +# Do not change these lines - they are parsed by run-tests.sh +# TESTSUITE_COMMS: mpi local +# TESTSUITE_NPROCS: 2 4 + +import numpy as np + +from libensemble.gen_funcs.sampling import latin_hypercube_sample as gen_f +from libensemble.gen_funcs.sampling import lhs_sample + +# Import libEnsemble items for this test +from libensemble.libE import libE +from libensemble.tools import add_unique_random_streams, parse_args + + +def sim_f(In): + Out = np.zeros(1, dtype=[("f", float)]) + Out["f"] = np.linalg.norm(In) + return Out + + +class LHSGenerator: + def __init__(self, persis_info, gen_specs): + self.persis_info = persis_info + self.gen_specs = gen_specs + + def ask(self): + ub = self.gen_specs["user"]["ub"] + lb = self.gen_specs["user"]["lb"] + + n = len(lb) + b = self.gen_specs["user"]["gen_batch_size"] + + H_o = np.zeros(b, dtype=self.gen_specs["out"]) + + A = lhs_sample(n, b, self.persis_info["rand_stream"]) + + H_o["x"] = A * (ub - lb) + lb + + return H_o + + def tell(self): + pass + + +if __name__ == "__main__": + nworkers, is_manager, libE_specs, _ = parse_args() + + sim_specs = { + "sim_f": sim_f, + "in": ["x"], + "out": [("f", float)], + } + + gen_specs = { + "gen_f": gen_f, + "out": [("x", float, (1,))], + "user": { + "gen_batch_size": 500, + "lb": np.array([-3]), + "ub": np.array([3]), + }, + } + + persis_info = add_unique_random_streams({}, nworkers + 1, seed=1234) + + my_gen = LHSGenerator(persis_info[1], gen_specs) + gen_specs["gen_f"] = my_gen + + exit_criteria = {"gen_max": 501} + + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) + + if is_manager: + assert len(H) >= 501 + print("\nlibEnsemble with random sampling has generated enough points") + print(H[:20]) diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 0ea9ce1e7..9f185ca6d 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -16,6 +16,8 @@ def __new__(cls, specs): return super(Runner, GlobusComputeRunner).__new__(GlobusComputeRunner) if specs.get("threaded"): # TODO: undecided interface return super(Runner, ThreadRunner).__new__(ThreadRunner) + if hasattr(specs.get("gen_f", None), "ask"): + return super(Runner, AskTellGenRunner).__new__(AskTellGenRunner) else: return super().__new__(Runner) @@ -84,3 +86,11 @@ def _result(self, calc_in: npt.NDArray, persis_info: dict, libE_info: dict) -> ( def shutdown(self) -> None: if self.thread_handle is not None: self.thread_handle.terminate() + + +class AskTellGenRunner(Runner): + def __init__(self, specs): + super().__init__(specs) + + def _result(self, calc_in: npt.NDArray, persis_info: dict, libE_info: dict) -> (npt.NDArray, dict, Optional[int]): + return self.f.ask() diff --git a/libensemble/utils/validators.py b/libensemble/utils/validators.py index e18465724..3c75279d3 100644 --- a/libensemble/utils/validators.py +++ b/libensemble/utils/validators.py @@ -132,7 +132,7 @@ def check_provided_ufuncs(cls, values): if values.get("alloc_specs").alloc_f.__name__ != "give_pregenerated_sim_work": gen_specs = values.get("gen_specs") assert hasattr(gen_specs, "gen_f"), "Generator function not provided to GenSpecs." - assert isinstance(gen_specs.gen_f, Callable), "Generator function is not callable." + # assert isinstance(gen_specs.gen_f, Callable), "Generator function is not callable." return values @@ -221,7 +221,7 @@ def check_provided_ufuncs(self): if self.alloc_specs.alloc_f.__name__ != "give_pregenerated_sim_work": assert hasattr(self.gen_specs, "gen_f"), "Generator function not provided to GenSpecs." - assert isinstance(self.gen_specs.gen_f, Callable), "Generator function is not callable." + # assert isinstance(self.gen_specs.gen_f, Callable), "Generator function is not callable." return self diff --git a/libensemble/worker.py b/libensemble/worker.py index f1fc2a4e2..d5269f040 100644 --- a/libensemble/worker.py +++ b/libensemble/worker.py @@ -257,6 +257,7 @@ def _handle_calc(self, Work: dict, calc_in: npt.NDArray) -> (npt.NDArray, dict, try: logger.debug(f"Starting {enum_desc}: {calc_id}") + out = None calc = self.runners[calc_type] with timer: if self.EnsembleDirectory.use_calc_dirs(calc_type): @@ -280,8 +281,8 @@ def _handle_calc(self, Work: dict, calc_in: npt.NDArray) -> (npt.NDArray, dict, if tag in [STOP_TAG, PERSIS_STOP] and message is MAN_SIGNAL_FINISH: calc_status = MAN_SIGNAL_FINISH - if out: - if len(out) >= 3: # Out, persis_info, calc_status + if out is not None: + if not isinstance(out, np.ndarray) and len(out) >= 3: # Out, persis_info, calc_status calc_status = out[2] return out elif len(out) == 2: # Out, persis_info OR Out, calc_status From c78296affabc2abb98a692c345e32371a63fd9e9 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 7 Feb 2024 17:19:06 -0600 Subject: [PATCH 028/288] implement persistent_uniform_sampling as class. Determine method for starting gen; if libE_info indicates persistent, then start Persistent ask/tell loop --- .../test_1d_asktell_gen.py | 75 ++++++++++++++++--- libensemble/utils/runners.py | 15 ++++ 2 files changed, 81 insertions(+), 9 deletions(-) diff --git a/libensemble/tests/functionality_tests/test_1d_asktell_gen.py b/libensemble/tests/functionality_tests/test_1d_asktell_gen.py index de611b3c5..11633e01e 100644 --- a/libensemble/tests/functionality_tests/test_1d_asktell_gen.py +++ b/libensemble/tests/functionality_tests/test_1d_asktell_gen.py @@ -15,11 +15,15 @@ import numpy as np +from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f +from libensemble.gen_funcs.persistent_sampling import _get_user_params from libensemble.gen_funcs.sampling import latin_hypercube_sample as gen_f from libensemble.gen_funcs.sampling import lhs_sample # Import libEnsemble items for this test from libensemble.libE import libE +from libensemble.message_numbers import FINISHED_PERSISTENT_GEN_TAG +from libensemble.sim_funcs.rosenbrock import rosenbrock_eval as sim_f2 from libensemble.tools import add_unique_random_streams, parse_args @@ -49,8 +53,29 @@ def ask(self): return H_o - def tell(self): - pass + +class PersistentUniform: + def __init__(self, persis_info, gen_specs): + self.persis_info = persis_info + self.gen_specs = gen_specs + self.b, self.n, self.lb, self.ub = _get_user_params(gen_specs["user"]) + + def ask(self): + H_o = np.zeros(self.b, dtype=self.gen_specs["out"]) + H_o["x"] = self.persis_info["rand_stream"].uniform(self.lb, self.ub, (self.b, self.n)) + if "obj_component" in H_o.dtype.fields: + H_o["obj_component"] = self.persis_info["rand_stream"].integers( + low=0, high=self.gen_specs["user"]["num_components"], size=self.b + ) + self.last_H = H_o + return H_o + + def tell(self, H_in): + if hasattr(H_in, "__len__"): + self.b = len(H_in) + + def finalize(self): + return self.last_H, self.persis_info, FINISHED_PERSISTENT_GEN_TAG if __name__ == "__main__": @@ -62,7 +87,7 @@ def tell(self): "out": [("f", float)], } - gen_specs = { + gen_specs_normal = { "gen_f": gen_f, "out": [("x", float, (1,))], "user": { @@ -74,14 +99,46 @@ def tell(self): persis_info = add_unique_random_streams({}, nworkers + 1, seed=1234) - my_gen = LHSGenerator(persis_info[1], gen_specs) - gen_specs["gen_f"] = my_gen + gen_one = LHSGenerator(persis_info[1], gen_specs_normal) + gen_specs_normal["gen_f"] = gen_one + + exit_criteria = {"gen_max": 201} + + H, persis_info, flag = libE(sim_specs, gen_specs_normal, exit_criteria, persis_info, libE_specs=libE_specs) + + if is_manager: + assert len(H) >= 201 + print("\nlibEnsemble with NORMAL random sampling has generated enough points") + print(H[:20]) + + sim_specs = { + "sim_f": sim_f2, + "in": ["x"], + "out": [("f", float), ("grad", float, 2)], + } + + gen_specs_persistent = { + "persis_in": ["x", "f", "grad", "sim_id"], + "out": [("x", float, (2,))], + "user": { + "initial_batch_size": 20, + "lb": np.array([-3, -2]), + "ub": np.array([3, 2]), + }, + } + + persis_info = add_unique_random_streams({}, nworkers + 1, seed=1234) + + gen_two = PersistentUniform(persis_info[1], gen_specs_persistent) + gen_specs_persistent["gen_f"] = gen_two - exit_criteria = {"gen_max": 501} + alloc_specs = {"alloc_f": alloc_f} - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) + H, persis_info, flag = libE( + sim_specs, gen_specs_persistent, exit_criteria, persis_info, alloc_specs, libE_specs=libE_specs + ) if is_manager: - assert len(H) >= 501 - print("\nlibEnsemble with random sampling has generated enough points") + assert len(H) >= 201 + print("\nlibEnsemble with PERSISTENT random sampling has generated enough points") print(H[:20]) diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 9f185ca6d..4ef948240 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -6,6 +6,8 @@ import numpy.typing as npt from libensemble.comms.comms import QCommThread +from libensemble.message_numbers import EVAL_GEN_TAG, PERSIS_STOP, STOP_TAG +from libensemble.tools.persistent_support import PersistentSupport logger = logging.getLogger(__name__) @@ -92,5 +94,18 @@ class AskTellGenRunner(Runner): def __init__(self, specs): super().__init__(specs) + def _persistent_result( + self, calc_in: npt.NDArray, persis_info: dict, libE_info: dict + ) -> (npt.NDArray, dict, Optional[int]): + self.ps = PersistentSupport(libE_info, EVAL_GEN_TAG) + tag = None + while tag not in [STOP_TAG, PERSIS_STOP]: + H_out = self.f.ask() + tag, _, H_in = self.ps.send_recv(H_out) + self.f.tell(H_in) + return self.f.finalize() + def _result(self, calc_in: npt.NDArray, persis_info: dict, libE_info: dict) -> (npt.NDArray, dict, Optional[int]): + if libE_info.get("persistent"): + return self._persistent_result(calc_in, persis_info, libE_info) return self.f.ask() From 197eaca2e893e6bb8056b93876ada8f1c390235d Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 8 Feb 2024 16:03:31 -0600 Subject: [PATCH 029/288] the ugliest block i've code I've ever written; first round/attempt of splitting surmise into ask/tell sections. dramatic reorganization still needed... --- .../persistent_surmise_calib_class.py | 246 ++++++++++++++++++ .../test_1d_asktell_gen.py | 2 +- libensemble/utils/runners.py | 2 +- 3 files changed, 248 insertions(+), 2 deletions(-) create mode 100644 libensemble/gen_funcs/persistent_surmise_calib_class.py diff --git a/libensemble/gen_funcs/persistent_surmise_calib_class.py b/libensemble/gen_funcs/persistent_surmise_calib_class.py new file mode 100644 index 000000000..159eefb23 --- /dev/null +++ b/libensemble/gen_funcs/persistent_surmise_calib_class.py @@ -0,0 +1,246 @@ +""" +This module contains a simple calibration example using the Surmise package. +""" + +import numpy as np +from surmise.calibration import calibrator +from surmise.emulation import emulator + +from libensemble.gen_funcs.surmise_calib_support import ( + gen_observations, + gen_thetas, + gen_true_theta, + gen_xs, + select_next_theta, + thetaprior, +) +from libensemble.message_numbers import FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG + + +def build_emulator(theta, x, fevals): + """Build the emulator.""" + print(x.shape, theta.shape, fevals.shape) + emu = emulator( + x, + theta, + fevals, + method="PCGPwM", + options={ + "xrmnan": "all", + "thetarmnan": "never", + "return_grad": True, + }, + ) + emu.fit() + return emu + + +def select_condition(pending, n_remaining_theta=5): + n_x = pending.shape[0] + return False if np.sum(pending) > n_remaining_theta * n_x else True + + +def rebuild_condition(pending, prev_pending, n_theta=5): # needs changes + n_x = pending.shape[0] + if np.sum(prev_pending) - np.sum(pending) >= n_x * n_theta or np.sum(pending) == 0: + return True + else: + return False + + +def create_arrays(calc_in, n_thetas, n_x): + """Create 2D (point * rows) arrays fevals, pending and complete""" + fevals = np.reshape(calc_in["f"], (n_x, n_thetas)) + pending = np.full(fevals.shape, False) + prev_pending = pending.copy() + complete = np.full(fevals.shape, True) + + return fevals, pending, prev_pending, complete + + +def pad_arrays(n_x, thetanew, theta, fevals, pending, prev_pending, complete): + """Extend arrays to appropriate sizes.""" + n_thetanew = len(thetanew) + + theta = np.vstack((theta, thetanew)) + fevals = np.hstack((fevals, np.full((n_x, n_thetanew), np.nan))) + pending = np.hstack((pending, np.full((n_x, n_thetanew), True))) + prev_pending = np.hstack((prev_pending, np.full((n_x, n_thetanew), True))) + complete = np.hstack((complete, np.full((n_x, n_thetanew), False))) + + # print('after:', fevals.shape, theta.shape, pending.shape, complete.shape) + return theta, fevals, pending, prev_pending, complete + + +def update_arrays(fevals, pending, complete, calc_in, obs_offset, n_x): + """Unpack from calc_in into 2D (point * rows) fevals""" + sim_id = calc_in["sim_id"] + c, r = divmod(sim_id - obs_offset, n_x) # r, c are arrays if sim_id is an array + + fevals[r, c] = calc_in["f"] + pending[r, c] = False + complete[r, c] = True + return + + +def cancel_columns_get_H(obs_offset, c, n_x, pending): + """Cancel columns""" + sim_ids_to_cancel = [] + columns = np.unique(c) + for c in columns: + col_offset = c * n_x + for i in range(n_x): + sim_id_cancel = obs_offset + col_offset + i + if pending[i, c]: + sim_ids_to_cancel.append(sim_id_cancel) + pending[i, c] = 0 + + H_o = np.zeros(len(sim_ids_to_cancel), dtype=[("sim_id", int), ("cancel_requested", bool)]) + H_o["sim_id"] = sim_ids_to_cancel + H_o["cancel_requested"] = True + return H_o + + +def assign_priority(n_x, n_thetas): + """Assign priorities to points.""" + # Arbitrary priorities + priority = np.arange(n_x * n_thetas) + np.random.shuffle(priority) + return priority + + +def load_H(H, xs, thetas, offset=0, set_priorities=False): + """Fill inputs into H0. + + There will be num_points x num_thetas entries + """ + n_thetas = len(thetas) + for i, x in enumerate(xs): + start = (i + offset) * n_thetas + H["x"][start : start + n_thetas] = x + H["thetas"][start : start + n_thetas] = thetas + + if set_priorities: + n_x = len(xs) + H["priority"] = assign_priority(n_x, n_thetas) + + +def gen_truevals(x, gen_specs): + """Generate true values using libE.""" + n_x = len(x) + H_o = np.zeros((1) * n_x, dtype=gen_specs["out"]) + + # Generate true theta and load into H + true_theta = gen_true_theta() + H_o["x"][0:n_x] = x + H_o["thetas"][0:n_x] = true_theta + return H_o + + +class SurmiseCalibrator: + def __init__(self, persis_info, gen_specs): + self.gen_specs = gen_specs + self.rand_stream = persis_info["rand_stream"] + self.n_thetas = gen_specs["user"]["n_init_thetas"] + self.n_x = gen_specs["user"]["num_x_vals"] # Num of x points + self.step_add_theta = gen_specs["user"]["step_add_theta"] # No. of thetas to generate per step + self.n_explore_theta = gen_specs["user"]["n_explore_theta"] # No. of thetas to explore + self.obsvar_const = gen_specs["user"]["obsvar"] # Constant for generator + self.priorloc = gen_specs["user"]["priorloc"] + self.priorscale = gen_specs["user"]["priorscale"] + self.initial_ask = True + self.initial_tell = True + self.fevals = None + self.prev_pending = None + + def ask(self, initial_batch=False, cancellation=False): + if self.initial_ask: + self.prior = thetaprior(self.priorloc, self.priorscale) + self.x = gen_xs(self.n_x, self.rand_stream) + H_o = gen_truevals(self.x, self.gen_specs) + self.obs_offset = len(H_o) + self.initial_ask = False + + elif initial_batch: + H_o = np.zeros(self.n_x * (self.n_thetas), dtype=self.gen_specs["out"]) + self.theta = gen_thetas(self.prior, self.n_thetas) + load_H(H_o, self.x, self.theta, set_priorities=True) + + else: + if select_condition(self.pending): + new_theta, info = select_next_theta( + self.step_add_theta, self.cal, self.emu, self.pending, self.n_explore_theta + ) + + # Add space for new thetas + self.theta, fevals, pending, self.prev_pending, self.complete = pad_arrays( + self.n_x, new_theta, self.theta, self.fevals, self.pending, self.prev_pending, self.complete + ) + # n_thetas = step_add_theta + H_o = np.zeros(self.n_x * (len(new_theta)), dtype=self.gen_specs["out"]) + load_H(H_o, self.x, new_theta, set_priorities=True) + + c_obviate = info["obviatesugg"] + if len(c_obviate) > 0: + print(f"columns sent for cancel is: {c_obviate}", flush=True) + H_o = cancel_columns_get_H(self.obs_offset, c_obviate, self.n_x, pending) + pending[:, c_obviate] = False + + return H_o + + def tell(self, calc_in, tag): + if self.initial_tell: + returned_fevals = np.reshape(calc_in["f"], (1, self.n_x)) + true_fevals = returned_fevals + obs, obsvar = gen_observations(true_fevals, self.obsvar_const, self.rand_stream) + self.initial_tell = False + self.ask(initial_batch=True) + + else: + if self.fevals is None: # initial batch + self.fevals, self.pending, prev_pending, self.complete = create_arrays(calc_in, self.n_thetas, self.n_x) + self.emu = build_emulator(self.theta, self.x, self.fevals) + # Refer to surmise package for additional options + self.cal = calibrator(self.emu, obs, self.x, self.prior, obsvar, method="directbayes") + + print("quantiles:", np.round(np.quantile(self.cal.theta.rnd(10000), (0.01, 0.99), axis=0), 3)) + update_model = False + else: + # Update fevals from calc_in + update_arrays(self.fevals, self.pending, self.complete, calc_in, self.obs_offset, self.n_x) + update_model = rebuild_condition(self.pending, self.prev_pending) + if not update_model: + if tag in [STOP_TAG, PERSIS_STOP]: + return + + if update_model: + print( + "Percentage Cancelled: %0.2f ( %d / %d)" + % ( + 100 * np.round(np.mean(1 - self.pending - self.complete), 4), + np.sum(1 - self.pending - self.complete), + np.prod(self.pending.shape), + ) + ) + print( + "Percentage Pending: %0.2f ( %d / %d)" + % (100 * np.round(np.mean(self.pending), 4), np.sum(self.pending), np.prod(self.pending.shape)) + ) + print( + "Percentage Complete: %0.2f ( %d / %d)" + % (100 * np.round(np.mean(self.complete), 4), np.sum(self.complete), np.prod(self.pending.shape)) + ) + + self.emu.update(theta=self.theta, f=self.fevals) + self.cal.fit() + + samples = self.cal.theta.rnd(2500) + print(np.mean(np.sum((samples - np.array([0.5] * 4)) ** 2, 1))) + print(np.round(np.quantile(self.cal.theta.rnd(10000), (0.01, 0.99), axis=0), 3)) + + self.step_add_theta += 2 + self.prev_pending = self.pending.copy() + update_model = False + + def finalize(self): + return None, self.persis_info, FINISHED_PERSISTENT_GEN_TAG diff --git a/libensemble/tests/functionality_tests/test_1d_asktell_gen.py b/libensemble/tests/functionality_tests/test_1d_asktell_gen.py index 11633e01e..1b6cd2f56 100644 --- a/libensemble/tests/functionality_tests/test_1d_asktell_gen.py +++ b/libensemble/tests/functionality_tests/test_1d_asktell_gen.py @@ -70,7 +70,7 @@ def ask(self): self.last_H = H_o return H_o - def tell(self, H_in): + def tell(self, H_in, *args): if hasattr(H_in, "__len__"): self.b = len(H_in) diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 4ef948240..b1cfda821 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -102,7 +102,7 @@ def _persistent_result( while tag not in [STOP_TAG, PERSIS_STOP]: H_out = self.f.ask() tag, _, H_in = self.ps.send_recv(H_out) - self.f.tell(H_in) + self.f.tell(H_in, tag) return self.f.finalize() def _result(self, calc_in: npt.NDArray, persis_info: dict, libE_info: dict) -> (npt.NDArray, dict, Optional[int]): From ad525bb9e8a042b497b466a67d6a59b4163a8b17 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 23 Feb 2024 14:42:18 -0600 Subject: [PATCH 030/288] add tentative gen_on_manager option, separate additional_worker_launch into function --- docs/data_structures/libE_specs.rst | 8 +++-- libensemble/manager.py | 47 +++++++++++++++-------------- libensemble/specs.py | 9 ++++-- 3 files changed, 38 insertions(+), 26 deletions(-) diff --git a/docs/data_structures/libE_specs.rst b/docs/data_structures/libE_specs.rst index 15646b1c3..6d5dd879e 100644 --- a/docs/data_structures/libE_specs.rst +++ b/docs/data_structures/libE_specs.rst @@ -30,8 +30,12 @@ libEnsemble is primarily customized by setting options within a ``LibeSpecs`` cl **nworkers** [int]: Number of worker processes in ``"local"``, ``"threads"``, or ``"tcp"``. - **manager_runs_additional_worker** [int] = False - Manager process can launch an additional threaded worker + **manager_runs_additional_worker** [bool] = False + Manager process launches an additional threaded Worker 0. + This worker can access/modify user objects by reference. + + **gen_on_manager** Optional[bool] = False + Enable ``manager_runs_additional_worker`` and reserve that worker for a single generator. **mpi_comm** [MPI communicator] = ``MPI.COMM_WORLD``: libEnsemble MPI communicator. diff --git a/libensemble/manager.py b/libensemble/manager.py index 6faff43f5..f944ce54c 100644 --- a/libensemble/manager.py +++ b/libensemble/manager.py @@ -187,6 +187,29 @@ class Manager: ("zero_resource_worker", bool), ] + def _run_additional_worker(self, hist, sim_specs, gen_specs, libE_specs): + dtypes = { + EVAL_SIM_TAG: repack_fields(hist.H[sim_specs["in"]]).dtype, + EVAL_GEN_TAG: repack_fields(hist.H[gen_specs["in"]]).dtype, + } + + self.W = np.zeros(len(self.wcomms) + 1, dtype=Manager.worker_dtype) + self.W["worker_id"] = np.arange(len(self.wcomms) + 1) + local_worker_comm = QCommThread( + worker_main, + len(self.wcomms), + sim_specs, + gen_specs, + libE_specs, + 0, + False, + Resources.resources, + Executor.executor, + ) + self.wcomms = [local_worker_comm] + self.wcomms + local_worker_comm.run() + local_worker_comm.send(0, dtypes) + def __init__( self, hist, @@ -232,28 +255,7 @@ def __init__( if self.libE_specs.get("manager_runs_additional_worker", False): # We start an additional Worker 0 on a thread. - - dtypes = { - EVAL_SIM_TAG: repack_fields(hist.H[sim_specs["in"]]).dtype, - EVAL_GEN_TAG: repack_fields(hist.H[gen_specs["in"]]).dtype, - } - - self.W = np.zeros(len(self.wcomms) + 1, dtype=Manager.worker_dtype) - self.W["worker_id"] = np.arange(len(self.wcomms) + 1) - local_worker_comm = QCommThread( - worker_main, - len(self.wcomms), - sim_specs, - gen_specs, - libE_specs, - 0, - False, - Resources.resources, - Executor.executor, - ) - self.wcomms = [local_worker_comm] + self.wcomms - local_worker_comm.run() - local_worker_comm.send(0, dtypes) + self._run_additional_worker(hist, sim_specs, gen_specs, libE_specs) self.W = _WorkerIndexer(self.W, self.libE_specs.get("manager_runs_additional_worker", False)) self.wcomms = _WorkerIndexer(self.wcomms, self.libE_specs.get("manager_runs_additional_worker", False)) @@ -637,6 +639,7 @@ def _get_alloc_libE_info(self) -> dict: "gen_num_procs": self.gen_num_procs, "gen_num_gpus": self.gen_num_gpus, "manager_additional_worker": self.libE_specs.get("manager_runs_additional_worker", False), + "gen_on_manager": self.libE_specs.get("gen_on_manager", False), } def _alloc_work(self, H: npt.NDArray, persis_info: dict) -> dict: diff --git a/libensemble/specs.py b/libensemble/specs.py index e796aee46..5c7990867 100644 --- a/libensemble/specs.py +++ b/libensemble/specs.py @@ -172,8 +172,13 @@ class LibeSpecs(BaseModel): nworkers: Optional[int] = 0 """ Number of worker processes in ``"local"``, ``"threads"``, or ``"tcp"``.""" - manager_runs_additional_worker: Optional[int] = False - """ Manager process can launch an additional threaded worker """ + manager_runs_additional_worker: Optional[bool] = False + """ Manager process launches an additional threaded Worker 0. + This worker can access/modify user objects by reference. + """ + + gen_on_manager: Optional[bool] = False + """ Enable ``manager_runs_additional_worker`` and reserve that worker for a single generator. """ mpi_comm: Optional[Any] = None """ libEnsemble MPI communicator. Default: ``MPI.COMM_WORLD``""" From fe64869b659f0a844d07f3305517d2c698f21ddb Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 26 Feb 2024 11:07:24 -0600 Subject: [PATCH 031/288] various refactors based on PR suggestions, then manager-refactors based on tracking worker_type as EVAL_SIM/EVAL_GEN_TAG, and active/persistent/active_recv as bools --- .../alloc_funcs/start_only_persistent.py | 5 +- libensemble/comms/comms.py | 15 ++-- libensemble/manager.py | 69 ++++++++++--------- libensemble/tools/alloc_support.py | 48 +++++++++---- libensemble/utils/runners.py | 8 +-- 5 files changed, 83 insertions(+), 62 deletions(-) diff --git a/libensemble/alloc_funcs/start_only_persistent.py b/libensemble/alloc_funcs/start_only_persistent.py index ee9d4105f..17784be35 100644 --- a/libensemble/alloc_funcs/start_only_persistent.py +++ b/libensemble/alloc_funcs/start_only_persistent.py @@ -1,6 +1,6 @@ import numpy as np -from libensemble.message_numbers import EVAL_GEN_TAG, EVAL_SIM_TAG +from libensemble.message_numbers import EVAL_SIM_TAG from libensemble.tools.alloc_support import AllocSupport, InsufficientFreeResources @@ -51,7 +51,6 @@ def only_persistent_gens(W, H, sim_specs, gen_specs, alloc_specs, persis_info, l if libE_info["sim_max_given"] or not libE_info["any_idle_workers"]: return {}, persis_info - # Initialize alloc_specs["user"] as user. user = alloc_specs.get("user", {}) manage_resources = libE_info["use_resource_sets"] @@ -71,7 +70,7 @@ def only_persistent_gens(W, H, sim_specs, gen_specs, alloc_specs, persis_info, l return Work, persis_info, 1 # Give evaluated results back to a running persistent gen - for wid in support.avail_worker_ids(persistent=EVAL_GEN_TAG, active_recv=active_recv_gen): + for wid in support.avail_gen_worker_ids(persistent=True, active_recv=active_recv_gen): gen_inds = H["gen_worker"] == wid returned_but_not_given = np.logical_and.reduce((H["sim_ended"], ~H["gen_informed"], gen_inds)) if np.any(returned_but_not_given): diff --git a/libensemble/comms/comms.py b/libensemble/comms/comms.py index 70458dd98..bebca9344 100644 --- a/libensemble/comms/comms.py +++ b/libensemble/comms/comms.py @@ -150,7 +150,6 @@ def __init__(self, main, *args, **kwargs): self._result = None self._exception = None self._done = False - self._ufunc = kwargs.get("ufunc", False) def _is_result_msg(self, msg): """Return true if message indicates final result (and set result/except).""" @@ -209,13 +208,13 @@ def result(self, timeout=None): return self._result @staticmethod - def _qcomm_main(comm, main, *fargs, **kwargs): + def _qcomm_main(comm, main, *args, **kwargs): """Main routine -- handles return values and exceptions.""" try: - if not kwargs.get("ufunc"): - _result = main(comm, *fargs, **kwargs) + if not kwargs.get("user_function"): + _result = main(comm, *args, **kwargs) else: - _result = main(*fargs) + _result = main(*args) comm.send(CommResult(_result)) except Exception as e: comm.send(CommResultErr(str(e), format_exc())) @@ -237,12 +236,12 @@ def __exit__(self, etype, value, traceback): class QCommThread(QCommLocal): """Launch a user function in a thread with an attached QComm.""" - def __init__(self, main, nworkers, *fargs, **kwargs): + def __init__(self, main, nworkers, *args, **kwargs): self.inbox = thread_queue.Queue() self.outbox = thread_queue.Queue() - super().__init__(self, main, *fargs, **kwargs) + super().__init__(self, main, *args, **kwargs) comm = QComm(self.inbox, self.outbox, nworkers) - self.handle = Thread(target=QCommThread._qcomm_main, args=(comm, main) + fargs, kwargs=kwargs) + self.handle = Thread(target=QCommThread._qcomm_main, args=(comm, main) + args, kwargs=kwargs) def terminate(self, timeout=None): """Terminate the thread. diff --git a/libensemble/manager.py b/libensemble/manager.py index f944ce54c..bd7a6d4ea 100644 --- a/libensemble/manager.py +++ b/libensemble/manager.py @@ -180,9 +180,10 @@ class Manager: worker_dtype = [ ("worker_id", int), - ("active", int), - ("persis_state", int), - ("active_recv", int), + ("worker_type", int), + ("active", bool), + ("persistent", bool), + ("active_recv", bool), ("gen_started_time", float), ("zero_resource_worker", bool), ] @@ -192,9 +193,6 @@ def _run_additional_worker(self, hist, sim_specs, gen_specs, libE_specs): EVAL_SIM_TAG: repack_fields(hist.H[sim_specs["in"]]).dtype, EVAL_GEN_TAG: repack_fields(hist.H[gen_specs["in"]]).dtype, } - - self.W = np.zeros(len(self.wcomms) + 1, dtype=Manager.worker_dtype) - self.W["worker_id"] = np.arange(len(self.wcomms) + 1) local_worker_comm = QCommThread( worker_main, len(self.wcomms), @@ -206,9 +204,9 @@ def _run_additional_worker(self, hist, sim_specs, gen_specs, libE_specs): Resources.resources, Executor.executor, ) - self.wcomms = [local_worker_comm] + self.wcomms local_worker_comm.run() local_worker_comm.send(0, dtypes) + return local_worker_comm def __init__( self, @@ -244,8 +242,6 @@ def __init__( self.gen_num_procs = libE_specs.get("gen_num_procs", 0) self.gen_num_gpus = libE_specs.get("gen_num_gpus", 0) - self.W = np.zeros(len(self.wcomms), dtype=Manager.worker_dtype) - self.W["worker_id"] = np.arange(len(self.wcomms)) + 1 self.term_tests = [ (2, "wallclock_max", self.term_test_wallclock), (1, "sim_max", self.term_test_sim_max), @@ -253,12 +249,18 @@ def __init__( (1, "stop_val", self.term_test_stop_val), ] - if self.libE_specs.get("manager_runs_additional_worker", False): - # We start an additional Worker 0 on a thread. - self._run_additional_worker(hist, sim_specs, gen_specs, libE_specs) + additional_worker = self.libE_specs.get("manager_runs_additional_worker", False) + + self.W = np.zeros(len(self.wcomms) + additional_worker, dtype=Manager.worker_dtype) + if additional_worker: + self.W["worker_id"] = np.arange(len(self.wcomms) + 1) # [0, 1, 2, ...] + local_worker_comm = self._run_additional_worker(hist, sim_specs, gen_specs, libE_specs) + self.wcomms = [local_worker_comm] + self.wcomms + else: + self.W["worker_id"] = np.arange(len(self.wcomms)) + 1 # [1, 2, 3, ...] - self.W = _WorkerIndexer(self.W, self.libE_specs.get("manager_runs_additional_worker", False)) - self.wcomms = _WorkerIndexer(self.wcomms, self.libE_specs.get("manager_runs_additional_worker", False)) + self.W = _WorkerIndexer(self.W, additional_worker) + self.wcomms = _WorkerIndexer(self.wcomms, additional_worker) temp_EnsembleDirectory = EnsembleDirectory(libE_specs=libE_specs) self.resources = Resources.resources @@ -379,7 +381,7 @@ def _check_work_order(self, Work: dict, w: int, force: bool = False) -> None: ) else: if not force: - assert self.W[w]["active"] == 0, ( + assert not self.W[w]["active"], ( "Allocation function requested work be sent to worker %d, an already active worker." % w ) work_rows = Work["libE_info"]["H_rows"] @@ -443,11 +445,12 @@ def _send_work_order(self, Work: dict, w: int) -> None: def _update_state_on_alloc(self, Work: dict, w: int): """Updates a workers' active/idle status following an allocation order""" - self.W[w]["active"] = Work["tag"] + self.W[w]["active"] = True + self.W[w]["worker_type"] = Work["tag"] if "persistent" in Work["libE_info"]: - self.W[w]["persis_state"] = Work["tag"] + self.W[w]["persistent"] = True if Work["libE_info"].get("active_recv", False): - self.W[w]["active_recv"] = Work["tag"] + self.W[w]["active_recv"] = True else: assert "active_recv" not in Work["libE_info"], "active_recv worker must also be persistent" @@ -484,7 +487,7 @@ def _update_state_on_worker_msg(self, persis_info: dict, D_recv: dict, w: int) - keep_state = D_recv["libE_info"].get("keep_state", False) if w not in self.persis_pending and not self.W[w]["active_recv"] and not keep_state: - self.W[w]["active"] = 0 + self.W[w]["active"] = False if calc_status in [FINISHED_PERSISTENT_SIM_TAG, FINISHED_PERSISTENT_GEN_TAG]: final_data = D_recv.get("calc_out", None) @@ -495,13 +498,13 @@ def _update_state_on_worker_msg(self, persis_info: dict, D_recv: dict, w: int) - self.hist.update_history_f(D_recv, self.kill_canceled_sims) else: logger.info(_PERSIS_RETURN_WARNING) - self.W[w]["persis_state"] = 0 + self.W[w]["persistent"] = False if self.W[w]["active_recv"]: - self.W[w]["active"] = 0 - self.W[w]["active_recv"] = 0 + self.W[w]["active"] = False + self.W[w]["active_recv"] = False if w in self.persis_pending: self.persis_pending.remove(w) - self.W[w]["active"] = 0 + self.W[w]["active"] = False self._freeup_resources(w) else: if calc_type == EVAL_SIM_TAG: @@ -509,11 +512,11 @@ def _update_state_on_worker_msg(self, persis_info: dict, D_recv: dict, w: int) - if calc_type == EVAL_GEN_TAG: self.hist.update_history_x_in(w, D_recv["calc_out"], self.W[w]["gen_started_time"]) assert ( - len(D_recv["calc_out"]) or np.any(self.W["active"]) or self.W[w]["persis_state"] + len(D_recv["calc_out"]) or np.any(self.W["active"]) or self.W[w]["persistent"] ), "Gen must return work when is is the only thing active and not persistent." if "libE_info" in D_recv and "persistent" in D_recv["libE_info"]: # Now a waiting, persistent worker - self.W[w]["persis_state"] = calc_type + self.W[w]["persistent"] = True else: self._freeup_resources(w) @@ -529,7 +532,7 @@ def _handle_msg_from_worker(self, persis_info: dict, w: int) -> None: logger.debug(f"Finalizing message from Worker {w}") return if isinstance(D_recv, WorkerErrMsg): - self.W[w]["active"] = 0 + self.W[w]["active"] = False logger.debug(f"Manager received exception from worker {w}") if not self.WorkerExc: self.WorkerExc = True @@ -577,8 +580,8 @@ def _final_receive_and_kill(self, persis_info: dict) -> (dict, int, int): """ # Send a handshake signal to each persistent worker. - if any(self.W["persis_state"]): - for w in self.W["worker_id"][self.W["persis_state"] > 0]: + if any(self.W["persistent"]): + for w in self.W["worker_id"][self.W["persistent"]]: logger.debug(f"Manager sending PERSIS_STOP to worker {w}") if self.libE_specs.get("final_gen_send", False): rows_to_send = np.where(self.hist.H["sim_ended"] & ~self.hist.H["gen_informed"])[0] @@ -595,15 +598,15 @@ def _final_receive_and_kill(self, persis_info: dict) -> (dict, int, int): self.wcomms[w].send(PERSIS_STOP, MAN_SIGNAL_KILL) if not self.W[w]["active"]: # Re-activate if necessary - self.W[w]["active"] = self.W[w]["persis_state"] + self.W[w]["active"] = self.W[w]["persistent"] self.persis_pending.append(w) exit_flag = 0 - while (any(self.W["active"]) or any(self.W["persis_state"])) and exit_flag == 0: + while (any(self.W["active"]) or any(self.W["persistent"])) and exit_flag == 0: persis_info = self._receive_from_workers(persis_info) if self.term_test(logged=False) == 2: # Elapsed Wallclock has expired - if not any(self.W["persis_state"]): + if not any(self.W["persistent"]): if any(self.W["active"]): logger.manager_warning(_WALLCLOCK_MSG_ACTIVE) else: @@ -626,7 +629,7 @@ def _get_alloc_libE_info(self) -> dict: """Selected statistics useful for alloc_f""" return { - "any_idle_workers": any(self.W["active"] == 0), + "any_idle_workers": any(~self.W["active"]), "exit_criteria": self.exit_criteria, "elapsed_time": self.elapsed(), "gen_informed_count": self.hist.gen_informed_count, @@ -697,7 +700,7 @@ def run(self, persis_info: dict) -> (dict, int, int): self._send_work_order(Work[w], w) self._update_state_on_alloc(Work[w], w) assert self.term_test() or any( - self.W["active"] != 0 + self.W["active"] ), "alloc_f did not return any work, although all workers are idle." except WorkerException as e: report_worker_exc(e) diff --git a/libensemble/tools/alloc_support.py b/libensemble/tools/alloc_support.py index d1d8ac802..21d46b1b0 100644 --- a/libensemble/tools/alloc_support.py +++ b/libensemble/tools/alloc_support.py @@ -87,29 +87,25 @@ def assign_resources(self, rsets_req, use_gpus=None, user_params=[]): rset_team = self.sched.assign_resources(rsets_req, use_gpus, user_params) return rset_team - def avail_worker_ids(self, persistent=None, active_recv=False, zero_resource_workers=None): + def avail_worker_ids(self, persistent=False, active_recv=False, zero_resource_workers=None, worker_type=None): """Returns available workers as a list of IDs, filtered by the given options. :param persistent: (Optional) Int. Only return workers with given ``persis_state`` (1=sim, 2=gen). :param active_recv: (Optional) Boolean. Only return workers with given active_recv state. :param zero_resource_workers: (Optional) Boolean. Only return workers that require no resources. + :param worker_type: (Optional) Int. Only return workers with given ``worker_type`` (1=sim, 2=gen). :returns: List of worker IDs. If there are no zero resource workers defined, then the ``zero_resource_workers`` argument will be ignored. """ - def fltr(wrk, field, option): - """Filter by condition if supplied""" - if option is None: - return True - return wrk[field] == option - # For abbrev. def fltr_persis(): - if persistent is None: + if persistent: + return wrk["persistent"] + else: return True - return wrk["persis_state"] == persistent def fltr_zrw(): # If none exist or you did not ask for zrw then return True @@ -123,6 +119,12 @@ def fltr_recving(): else: return not wrk["active"] + def fltr_worker_type(): + if worker_type: + return wrk["worker_type"] == worker_type + else: + return True + if active_recv and not persistent: raise AllocException("Cannot ask for non-persistent active receive workers") @@ -130,13 +132,31 @@ def fltr_recving(): no_zrw = not any(self.W["zero_resource_worker"]) wrks = [] for wrk in self.W: - if fltr_recving() and fltr_persis() and fltr_zrw(): + if fltr_recving() and fltr_persis() and fltr_zrw() and fltr_worker_type(): wrks.append(wrk["worker_id"]) return wrks + def avail_gen_worker_ids(self, persistent=False, active_recv=False, zero_resource_workers=None): + """Returns available generator workers as a list of IDs.""" + return self.avail_worker_ids( + persistent=persistent, + active_recv=active_recv, + zero_resource_workers=zero_resource_workers, + worker_type=EVAL_GEN_TAG, + ) + + def avail_sim_worker_ids(self, persistent=False, active_recv=False, zero_resource_workers=None): + """Returns available generator workers as a list of IDs.""" + return self.avail_worker_ids( + persistent=persistent, + active_recv=active_recv, + zero_resource_workers=zero_resource_workers, + worker_type=EVAL_SIM_TAG, + ) + def count_gens(self): """Returns the number of active generators.""" - return sum(self.W["active"] == EVAL_GEN_TAG) + return sum(self.W["active"] & self.W["worker_type"] == EVAL_GEN_TAG) def test_any_gen(self): """Returns ``True`` if a generator worker is active.""" @@ -144,7 +164,7 @@ def test_any_gen(self): def count_persis_gens(self): """Return the number of active persistent generators.""" - return sum(self.W["persis_state"] == EVAL_GEN_TAG) + return sum(self.W["persistent"] == EVAL_GEN_TAG) def _req_resources_sim(self, libE_info, user_params, H, H_rows): """Determine required resources for a sim work unit""" @@ -201,7 +221,7 @@ def _update_rset_team(self, libE_info, wid, H=None, H_rows=None): """Add rset_team to libE_info.""" if self.manage_resources and not libE_info.get("rset_team"): num_rsets_req = 0 - if self.W[wid - 1]["persis_state"]: + if self.W[wid - 1]["persistent"]: # Even if empty list, non-None rset_team stops manager giving default resources libE_info["rset_team"] = [] return @@ -272,7 +292,7 @@ def gen_work(self, wid, H_fields, H_rows, persis_info, **libE_info): """ self._update_rset_team(libE_info, wid) - if not self.W[wid - 1]["persis_state"]: + if not self.W[wid - 1]["persistent"]: AllocSupport.gen_counter += 1 # Count total gens libE_info["gen_count"] = AllocSupport.gen_counter diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 0ea9ce1e7..629c733b1 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -62,8 +62,8 @@ def _result(self, calc_in: npt.NDArray, persis_info: dict, libE_info: dict) -> ( libE_info["comm"] = None # 'comm' object not pickle-able Worker._set_executor(0, None) # ditto for executor - fargs = self._truncate_args(calc_in, persis_info, libE_info) - task_fut = self.globus_compute_executor.submit_to_registered_function(self.globus_compute_fid, fargs) + args = self._truncate_args(calc_in, persis_info, libE_info) + task_fut = self.globus_compute_executor.submit_to_registered_function(self.globus_compute_fid, args) return task_fut.result() def shutdown(self) -> None: @@ -76,8 +76,8 @@ def __init__(self, specs): self.thread_handle = None def _result(self, calc_in: npt.NDArray, persis_info: dict, libE_info: dict) -> (npt.NDArray, dict, Optional[int]): - fargs = self._truncate_args(calc_in, persis_info, libE_info) - self.thread_handle = QCommThread(self.f, None, *fargs, ufunc=True) + args = self._truncate_args(calc_in, persis_info, libE_info) + self.thread_handle = QCommThread(self.f, None, *args, user_function=True) self.thread_handle.run() return self.thread_handle.result() From dcf6db76e728b45b602795259cfb536399552c23 Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 26 Feb 2024 12:24:17 -0600 Subject: [PATCH 032/288] fix persistent filter, update avail/running gens counters --- libensemble/tools/alloc_support.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/libensemble/tools/alloc_support.py b/libensemble/tools/alloc_support.py index 21d46b1b0..5f223df52 100644 --- a/libensemble/tools/alloc_support.py +++ b/libensemble/tools/alloc_support.py @@ -102,10 +102,7 @@ def avail_worker_ids(self, persistent=False, active_recv=False, zero_resource_wo # For abbrev. def fltr_persis(): - if persistent: - return wrk["persistent"] - else: - return True + return wrk["persistent"] == persistent def fltr_zrw(): # If none exist or you did not ask for zrw then return True @@ -160,11 +157,11 @@ def count_gens(self): def test_any_gen(self): """Returns ``True`` if a generator worker is active.""" - return any(self.W["active"] == EVAL_GEN_TAG) + return any(self.W["active"] & self.W["worker_type"] == EVAL_GEN_TAG) def count_persis_gens(self): """Return the number of active persistent generators.""" - return sum(self.W["persistent"] == EVAL_GEN_TAG) + return sum((self.W["persistent"]) & (self.W["worker_type"] == EVAL_GEN_TAG)) def _req_resources_sim(self, libE_info, user_params, H, H_rows): """Determine required resources for a sim work unit""" From ba059004ae27640640c7771f109aa808f66bbf0a Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 26 Feb 2024 13:52:02 -0600 Subject: [PATCH 033/288] update unit test, bugfix --- .../test_allocation_funcs_and_support.py | 40 ++++++++----------- libensemble/tools/alloc_support.py | 4 +- 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/libensemble/tests/unit_tests/test_allocation_funcs_and_support.py b/libensemble/tests/unit_tests/test_allocation_funcs_and_support.py index 631c0a60b..8f5959ce9 100644 --- a/libensemble/tests/unit_tests/test_allocation_funcs_and_support.py +++ b/libensemble/tests/unit_tests/test_allocation_funcs_and_support.py @@ -17,12 +17,13 @@ H0 = [] W = np.array( - [(1, 0, 0, 0, False), (2, 0, 0, 0, False), (3, 0, 0, 0, False), (4, 0, 0, 0, False)], + [(1, 0, 0, 0, 0, False), (2, 0, 0, 0, 0, False), (3, 0, 0, 0, 0, False), (4, 0, 0, 0, 0, False)], dtype=[ ("worker_id", " Date: Mon, 26 Feb 2024 13:58:33 -0600 Subject: [PATCH 034/288] update persistent allocs, but also add backwards-compatibility check in avail_worker_ids --- libensemble/alloc_funcs/inverse_bayes_allocf.py | 3 +-- libensemble/alloc_funcs/persistent_aposmm_alloc.py | 3 +-- libensemble/alloc_funcs/start_fd_persistent.py | 3 +-- libensemble/alloc_funcs/start_persistent_local_opt_gens.py | 2 +- libensemble/tools/alloc_support.py | 3 +++ 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/libensemble/alloc_funcs/inverse_bayes_allocf.py b/libensemble/alloc_funcs/inverse_bayes_allocf.py index 56a3f6e79..dcc1e13d7 100644 --- a/libensemble/alloc_funcs/inverse_bayes_allocf.py +++ b/libensemble/alloc_funcs/inverse_bayes_allocf.py @@ -1,6 +1,5 @@ import numpy as np -from libensemble.message_numbers import EVAL_GEN_TAG from libensemble.tools.alloc_support import AllocSupport, InsufficientFreeResources @@ -25,7 +24,7 @@ def only_persistent_gens_for_inverse_bayes(W, H, sim_specs, gen_specs, alloc_spe # If wid is idle, but in persistent mode, and generated work has all returned # give output back to wid. Otherwise, give nothing to wid - for wid in support.avail_worker_ids(persistent=EVAL_GEN_TAG): + for wid in support.avail_gen_worker_ids(persistent=True): # if > 1 persistent generator, assign the correct work to it inds_generated_by_wid = H["gen_worker"] == wid if support.all_sim_ended(H, inds_generated_by_wid): diff --git a/libensemble/alloc_funcs/persistent_aposmm_alloc.py b/libensemble/alloc_funcs/persistent_aposmm_alloc.py index 8327d3975..47b584309 100644 --- a/libensemble/alloc_funcs/persistent_aposmm_alloc.py +++ b/libensemble/alloc_funcs/persistent_aposmm_alloc.py @@ -1,6 +1,5 @@ import numpy as np -from libensemble.message_numbers import EVAL_GEN_TAG from libensemble.tools.alloc_support import AllocSupport, InsufficientFreeResources @@ -40,7 +39,7 @@ def persistent_aposmm_alloc(W, H, sim_specs, gen_specs, alloc_specs, persis_info return Work, persis_info, 1 # If any persistent worker's calculated values have returned, give them back. - for wid in support.avail_worker_ids(persistent=EVAL_GEN_TAG): + for wid in support.avail_gen_worker_ids(persistent=True): if persis_info.get("sample_done") or sum(H["sim_ended"]) >= init_sample_size + persis_info["samples_in_H0"]: # Don't return if the initial sample is not complete persis_info["sample_done"] = True diff --git a/libensemble/alloc_funcs/start_fd_persistent.py b/libensemble/alloc_funcs/start_fd_persistent.py index 0c2e939d3..33af61765 100644 --- a/libensemble/alloc_funcs/start_fd_persistent.py +++ b/libensemble/alloc_funcs/start_fd_persistent.py @@ -1,6 +1,5 @@ import numpy as np -from libensemble.message_numbers import EVAL_GEN_TAG from libensemble.tools.alloc_support import AllocSupport, InsufficientFreeResources @@ -30,7 +29,7 @@ def finite_diff_alloc(W, H, sim_specs, gen_specs, alloc_specs, persis_info, libE # If wid is in persistent mode, and all of its calculated values have # returned, give them back to wid. Otherwise, give nothing to wid - for wid in support.avail_worker_ids(persistent=EVAL_GEN_TAG): + for wid in support.avail_gen_worker_ids(persistent=True): # What (x_ind, f_ind) pairs have all of the evaluation of all n_ind # values complete. inds_not_sent_back = ~H["gen_informed"] diff --git a/libensemble/alloc_funcs/start_persistent_local_opt_gens.py b/libensemble/alloc_funcs/start_persistent_local_opt_gens.py index 12ad45100..ac01db407 100644 --- a/libensemble/alloc_funcs/start_persistent_local_opt_gens.py +++ b/libensemble/alloc_funcs/start_persistent_local_opt_gens.py @@ -46,7 +46,7 @@ def start_persistent_local_opt_gens(W, H, sim_specs, gen_specs, alloc_specs, per # If wid is idle, but in persistent mode, and its calculated values have # returned, give them back to i. Otherwise, give nothing to wid - for wid in support.avail_worker_ids(persistent=EVAL_GEN_TAG): + for wid in support.avail_gen_worker_ids(persistent=True): gen_inds = H["gen_worker"] == wid if support.all_sim_ended(H, gen_inds): last_time_pos = np.argmax(H["sim_started_time"][gen_inds]) diff --git a/libensemble/tools/alloc_support.py b/libensemble/tools/alloc_support.py index d5e4a7125..7e1871fe9 100644 --- a/libensemble/tools/alloc_support.py +++ b/libensemble/tools/alloc_support.py @@ -100,6 +100,9 @@ def avail_worker_ids(self, persistent=False, active_recv=False, zero_resource_wo be ignored. """ + if persistent == EVAL_GEN_TAG: # backwards compatibility + return self.avail_gen_worker_ids(persistent, active_recv, zero_resource_workers) + # For abbrev. def fltr_persis(): return wrk["persistent"] == persistent From 3d06b1c3d896d5c4db5542d769c0d4e405f690c5 Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 26 Feb 2024 14:16:53 -0600 Subject: [PATCH 035/288] fix persistent sim test --- libensemble/alloc_funcs/start_only_persistent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libensemble/alloc_funcs/start_only_persistent.py b/libensemble/alloc_funcs/start_only_persistent.py index 17784be35..870973dc4 100644 --- a/libensemble/alloc_funcs/start_only_persistent.py +++ b/libensemble/alloc_funcs/start_only_persistent.py @@ -92,7 +92,7 @@ def only_persistent_gens(W, H, sim_specs, gen_specs, alloc_specs, persis_info, l if user.get("alt_type"): avail_workers = list( set(support.avail_worker_ids(persistent=False, zero_resource_workers=False)) - | set(support.avail_worker_ids(persistent=EVAL_SIM_TAG, zero_resource_workers=False)) + | set(support.avail_worker_ids(persistent=True, zero_resource_workers=False, worker_type=EVAL_SIM_TAG)) ) for wid in avail_workers: if not np.any(points_to_evaluate): From 9165d7df49c6a2a004dfd62ca079b96b91cb15da Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 26 Feb 2024 15:35:40 -0600 Subject: [PATCH 036/288] move _WorkerIndexer into libensemble.utils, also use within PersistentSupport --- libensemble/manager.py | 23 +---------------------- libensemble/tools/alloc_support.py | 8 ++++---- libensemble/utils/misc.py | 21 +++++++++++++++++++++ 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/libensemble/manager.py b/libensemble/manager.py index bd7a6d4ea..888958608 100644 --- a/libensemble/manager.py +++ b/libensemble/manager.py @@ -34,7 +34,7 @@ from libensemble.resources.resources import Resources from libensemble.tools.fields_keys import protected_libE_fields from libensemble.tools.tools import _PERSIS_RETURN_WARNING, _USER_CALC_DIR_WARNING -from libensemble.utils.misc import extract_H_ranges +from libensemble.utils.misc import _WorkerIndexer, extract_H_ranges from libensemble.utils.output_directory import EnsembleDirectory from libensemble.utils.timer import Timer from libensemble.worker import WorkerErrMsg, worker_main @@ -154,27 +154,6 @@ def filter_nans(array: npt.NDArray) -> npt.NDArray: """ -class _WorkerIndexer: - def __init__(self, iterable: list, additional_worker=False): - self.iterable = iterable - self.additional_worker = additional_worker - - def __getitem__(self, key): - if self.additional_worker or isinstance(key, str): - return self.iterable[key] - else: - return self.iterable[key - 1] - - def __setitem__(self, key, value): - self.iterable[key] = value - - def __len__(self): - return len(self.iterable) - - def __iter__(self): - return iter(self.iterable) - - class Manager: """Manager class for libensemble.""" diff --git a/libensemble/tools/alloc_support.py b/libensemble/tools/alloc_support.py index 7e1871fe9..b8d9e98ce 100644 --- a/libensemble/tools/alloc_support.py +++ b/libensemble/tools/alloc_support.py @@ -5,7 +5,7 @@ from libensemble.message_numbers import EVAL_GEN_TAG, EVAL_SIM_TAG from libensemble.resources.resources import Resources from libensemble.resources.scheduler import InsufficientFreeResources, InsufficientResourcesError, ResourceScheduler -from libensemble.utils.misc import extract_H_ranges +from libensemble.utils.misc import _WorkerIndexer, extract_H_ranges logger = logging.getLogger(__name__) # For debug messages - uncomment @@ -47,7 +47,7 @@ def __init__( :param user_resources: (Optional) A user supplied ``resources`` object. :param user_scheduler: (Optional) A user supplied ``user_scheduler`` object. """ - self.W = W + self.W = _WorkerIndexer(W, libE_info.get("manager_runs_additional_worker", False)) self.persis_info = persis_info self.manage_resources = manage_resources self.resources = user_resources or Resources.resources @@ -221,7 +221,7 @@ def _update_rset_team(self, libE_info, wid, H=None, H_rows=None): """Add rset_team to libE_info.""" if self.manage_resources and not libE_info.get("rset_team"): num_rsets_req = 0 - if self.W[wid - 1]["persistent"]: + if self.W[wid]["persistent"]: # Even if empty list, non-None rset_team stops manager giving default resources libE_info["rset_team"] = [] return @@ -292,7 +292,7 @@ def gen_work(self, wid, H_fields, H_rows, persis_info, **libE_info): """ self._update_rset_team(libE_info, wid) - if not self.W[wid - 1]["persistent"]: + if not self.W[wid]["persistent"]: AllocSupport.gen_counter += 1 # Count total gens libE_info["gen_count"] = AllocSupport.gen_counter diff --git a/libensemble/utils/misc.py b/libensemble/utils/misc.py index 76e4ccaf2..ca67095ac 100644 --- a/libensemble/utils/misc.py +++ b/libensemble/utils/misc.py @@ -33,6 +33,27 @@ def extract_H_ranges(Work: dict) -> str: return "_".join(ranges) +class _WorkerIndexer: + def __init__(self, iterable: list, additional_worker=False): + self.iterable = iterable + self.additional_worker = additional_worker + + def __getitem__(self, key): + if self.additional_worker or isinstance(key, str): + return self.iterable[key] + else: + return self.iterable[key - 1] + + def __setitem__(self, key, value): + self.iterable[key] = value + + def __len__(self): + return len(self.iterable) + + def __iter__(self): + return iter(self.iterable) + + def specs_dump(specs, **kwargs): if pydanticV1: return specs.dict(**kwargs) From f7ba2057f2ade7f09bf59a6abf7ced1814699e6a Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 26 Feb 2024 16:49:51 -0600 Subject: [PATCH 037/288] manager also needs to send workflow_dir location to worker 0 --- libensemble/manager.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libensemble/manager.py b/libensemble/manager.py index 888958608..ab430decb 100644 --- a/libensemble/manager.py +++ b/libensemble/manager.py @@ -185,6 +185,8 @@ def _run_additional_worker(self, hist, sim_specs, gen_specs, libE_specs): ) local_worker_comm.run() local_worker_comm.send(0, dtypes) + if libE_specs.get("use_workflow_dir"): + local_worker_comm.send(0, libE_specs.get("workflow_dir_path")) return local_worker_comm def __init__( From 376e4506755d9b4d266975feb45412f7f6a3959f Mon Sep 17 00:00:00 2001 From: jlnav Date: Tue, 27 Feb 2024 08:56:01 -0600 Subject: [PATCH 038/288] missed an alloc --- libensemble/alloc_funcs/start_persistent_local_opt_gens.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libensemble/alloc_funcs/start_persistent_local_opt_gens.py b/libensemble/alloc_funcs/start_persistent_local_opt_gens.py index ac01db407..1a16ea817 100644 --- a/libensemble/alloc_funcs/start_persistent_local_opt_gens.py +++ b/libensemble/alloc_funcs/start_persistent_local_opt_gens.py @@ -90,7 +90,9 @@ def start_persistent_local_opt_gens(W, H, sim_specs, gen_specs, alloc_specs, per break points_to_evaluate[sim_ids_to_send] = False - elif gen_count == 0 and not np.any(np.logical_and(W["active"] == EVAL_GEN_TAG, W["persis_state"] == 0)): + elif gen_count == 0 and not np.any( + np.logical_and((W["active"]), (W["persistent"] is False), (W["worker_type"] == EVAL_GEN_TAG)) + ): # Finally, generate points since there is nothing else to do (no resource sets req.) Work[wid] = support.gen_work(wid, gen_specs.get("in", []), [], persis_info[wid], rset_team=[]) gen_count += 1 From 63750588ac1a0921b7242258b0021732a1b53476 Mon Sep 17 00:00:00 2001 From: jlnav Date: Tue, 27 Feb 2024 12:20:14 -0600 Subject: [PATCH 039/288] make alloc_f's libE_info additional worker option match libE_specs --- libensemble/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libensemble/manager.py b/libensemble/manager.py index ab430decb..5f8604f11 100644 --- a/libensemble/manager.py +++ b/libensemble/manager.py @@ -622,7 +622,7 @@ def _get_alloc_libE_info(self) -> dict: "use_resource_sets": self.use_resource_sets, "gen_num_procs": self.gen_num_procs, "gen_num_gpus": self.gen_num_gpus, - "manager_additional_worker": self.libE_specs.get("manager_runs_additional_worker", False), + "manager_runs_additional_worker": self.libE_specs.get("manager_runs_additional_worker", False), "gen_on_manager": self.libE_specs.get("gen_on_manager", False), } From c07a5659b081961f9756d73a52fb239e4940438f Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 28 Feb 2024 09:18:19 -0600 Subject: [PATCH 040/288] removes manager_runs_additional_worker in favor of gen_on_manager. pass in wrapped self.W to allocs --- docs/data_structures/libE_specs.rst | 7 ++----- libensemble/manager.py | 14 +++++++------- libensemble/specs.py | 9 +++------ .../test_persistent_uniform_sampling.py | 2 +- libensemble/tools/alloc_support.py | 4 ++-- 5 files changed, 15 insertions(+), 21 deletions(-) diff --git a/docs/data_structures/libE_specs.rst b/docs/data_structures/libE_specs.rst index 6d5dd879e..b2bb74d58 100644 --- a/docs/data_structures/libE_specs.rst +++ b/docs/data_structures/libE_specs.rst @@ -30,12 +30,9 @@ libEnsemble is primarily customized by setting options within a ``LibeSpecs`` cl **nworkers** [int]: Number of worker processes in ``"local"``, ``"threads"``, or ``"tcp"``. - **manager_runs_additional_worker** [bool] = False - Manager process launches an additional threaded Worker 0. - This worker can access/modify user objects by reference. - **gen_on_manager** Optional[bool] = False - Enable ``manager_runs_additional_worker`` and reserve that worker for a single generator. + Instructs Manager process to run generator functions. + This generator function can access/modify user objects by reference. **mpi_comm** [MPI communicator] = ``MPI.COMM_WORLD``: libEnsemble MPI communicator. diff --git a/libensemble/manager.py b/libensemble/manager.py index 5f8604f11..5d0dbf156 100644 --- a/libensemble/manager.py +++ b/libensemble/manager.py @@ -230,18 +230,19 @@ def __init__( (1, "stop_val", self.term_test_stop_val), ] - additional_worker = self.libE_specs.get("manager_runs_additional_worker", False) + gen_on_manager = self.libE_specs.get("gen_on_manager", False) - self.W = np.zeros(len(self.wcomms) + additional_worker, dtype=Manager.worker_dtype) - if additional_worker: + self.W = np.zeros(len(self.wcomms) + gen_on_manager, dtype=Manager.worker_dtype) + if gen_on_manager: self.W["worker_id"] = np.arange(len(self.wcomms) + 1) # [0, 1, 2, ...] + self.W[0]["worker_type"] = EVAL_GEN_TAG local_worker_comm = self._run_additional_worker(hist, sim_specs, gen_specs, libE_specs) self.wcomms = [local_worker_comm] + self.wcomms else: self.W["worker_id"] = np.arange(len(self.wcomms)) + 1 # [1, 2, 3, ...] - self.W = _WorkerIndexer(self.W, additional_worker) - self.wcomms = _WorkerIndexer(self.wcomms, additional_worker) + self.W = _WorkerIndexer(self.W, gen_on_manager) + self.wcomms = _WorkerIndexer(self.wcomms, gen_on_manager) temp_EnsembleDirectory = EnsembleDirectory(libE_specs=libE_specs) self.resources = Resources.resources @@ -622,7 +623,6 @@ def _get_alloc_libE_info(self) -> dict: "use_resource_sets": self.use_resource_sets, "gen_num_procs": self.gen_num_procs, "gen_num_gpus": self.gen_num_gpus, - "manager_runs_additional_worker": self.libE_specs.get("manager_runs_additional_worker", False), "gen_on_manager": self.libE_specs.get("gen_on_manager", False), } @@ -636,7 +636,7 @@ def _alloc_work(self, H: npt.NDArray, persis_info: dict) -> dict: alloc_f = self.alloc_specs["alloc_f"] output = alloc_f( - self.W.iterable, + self.W, H, self.sim_specs, self.gen_specs, diff --git a/libensemble/specs.py b/libensemble/specs.py index 5c7990867..0073c6cd6 100644 --- a/libensemble/specs.py +++ b/libensemble/specs.py @@ -172,13 +172,10 @@ class LibeSpecs(BaseModel): nworkers: Optional[int] = 0 """ Number of worker processes in ``"local"``, ``"threads"``, or ``"tcp"``.""" - manager_runs_additional_worker: Optional[bool] = False - """ Manager process launches an additional threaded Worker 0. - This worker can access/modify user objects by reference. - """ - gen_on_manager: Optional[bool] = False - """ Enable ``manager_runs_additional_worker`` and reserve that worker for a single generator. """ + """ Instructs Manager process to run generator functions. + This generator function can access/modify user objects by reference. + """ mpi_comm: Optional[Any] = None """ libEnsemble MPI communicator. Default: ``MPI.COMM_WORLD``""" diff --git a/libensemble/tests/functionality_tests/test_persistent_uniform_sampling.py b/libensemble/tests/functionality_tests/test_persistent_uniform_sampling.py index e343ff991..5470b814d 100644 --- a/libensemble/tests/functionality_tests/test_persistent_uniform_sampling.py +++ b/libensemble/tests/functionality_tests/test_persistent_uniform_sampling.py @@ -87,7 +87,7 @@ sim_specs["in"] = ["x", "obj_component"] # sim_specs["out"] = [("f", float), ("grad", float, n)] elif run == 3: - libE_specs["manager_runs_additional_worker"] = True + libE_specs["gen_on_manager"] = True # Perform the run H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) diff --git a/libensemble/tools/alloc_support.py b/libensemble/tools/alloc_support.py index b8d9e98ce..3cda02079 100644 --- a/libensemble/tools/alloc_support.py +++ b/libensemble/tools/alloc_support.py @@ -5,7 +5,7 @@ from libensemble.message_numbers import EVAL_GEN_TAG, EVAL_SIM_TAG from libensemble.resources.resources import Resources from libensemble.resources.scheduler import InsufficientFreeResources, InsufficientResourcesError, ResourceScheduler -from libensemble.utils.misc import _WorkerIndexer, extract_H_ranges +from libensemble.utils.misc import extract_H_ranges logger = logging.getLogger(__name__) # For debug messages - uncomment @@ -47,7 +47,7 @@ def __init__( :param user_resources: (Optional) A user supplied ``resources`` object. :param user_scheduler: (Optional) A user supplied ``user_scheduler`` object. """ - self.W = _WorkerIndexer(W, libE_info.get("manager_runs_additional_worker", False)) + self.W = W self.persis_info = persis_info self.manage_resources = manage_resources self.resources = user_resources or Resources.resources From c46802e20d5b2dffdb2440874afa15ee0e34d6aa Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 28 Feb 2024 10:19:38 -0600 Subject: [PATCH 041/288] turning W["active"] back to an int --- libensemble/manager.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/libensemble/manager.py b/libensemble/manager.py index 5d0dbf156..c1fad1af5 100644 --- a/libensemble/manager.py +++ b/libensemble/manager.py @@ -160,7 +160,7 @@ class Manager: worker_dtype = [ ("worker_id", int), ("worker_type", int), - ("active", bool), + ("active", int), ("persistent", bool), ("active_recv", bool), ("gen_started_time", float), @@ -427,7 +427,7 @@ def _send_work_order(self, Work: dict, w: int) -> None: def _update_state_on_alloc(self, Work: dict, w: int): """Updates a workers' active/idle status following an allocation order""" - self.W[w]["active"] = True + self.W[w]["active"] = Work["tag"] self.W[w]["worker_type"] = Work["tag"] if "persistent" in Work["libE_info"]: self.W[w]["persistent"] = True @@ -469,7 +469,7 @@ def _update_state_on_worker_msg(self, persis_info: dict, D_recv: dict, w: int) - keep_state = D_recv["libE_info"].get("keep_state", False) if w not in self.persis_pending and not self.W[w]["active_recv"] and not keep_state: - self.W[w]["active"] = False + self.W[w]["active"] = 0 if calc_status in [FINISHED_PERSISTENT_SIM_TAG, FINISHED_PERSISTENT_GEN_TAG]: final_data = D_recv.get("calc_out", None) @@ -482,11 +482,11 @@ def _update_state_on_worker_msg(self, persis_info: dict, D_recv: dict, w: int) - logger.info(_PERSIS_RETURN_WARNING) self.W[w]["persistent"] = False if self.W[w]["active_recv"]: - self.W[w]["active"] = False + self.W[w]["active"] = 0 self.W[w]["active_recv"] = False if w in self.persis_pending: self.persis_pending.remove(w) - self.W[w]["active"] = False + self.W[w]["active"] = 0 self._freeup_resources(w) else: if calc_type == EVAL_SIM_TAG: @@ -514,7 +514,7 @@ def _handle_msg_from_worker(self, persis_info: dict, w: int) -> None: logger.debug(f"Finalizing message from Worker {w}") return if isinstance(D_recv, WorkerErrMsg): - self.W[w]["active"] = False + self.W[w]["active"] = 0 logger.debug(f"Manager received exception from worker {w}") if not self.WorkerExc: self.WorkerExc = True @@ -580,7 +580,7 @@ def _final_receive_and_kill(self, persis_info: dict) -> (dict, int, int): self.wcomms[w].send(PERSIS_STOP, MAN_SIGNAL_KILL) if not self.W[w]["active"]: # Re-activate if necessary - self.W[w]["active"] = self.W[w]["persistent"] + self.W[w]["active"] = self.W[w]["worker_type"] if self.W[w]["persistent"] else 0 self.persis_pending.append(w) exit_flag = 0 From 2ee94665845ca3874f282ae44acf908488a7a138 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 28 Feb 2024 11:41:20 -0600 Subject: [PATCH 042/288] experimenting with gen_on_manager with give_pregenerated_work - worker 0 shouldn't be given gen work --- libensemble/alloc_funcs/give_pregenerated_work.py | 2 +- .../tests/regression_tests/test_evaluate_mixed_sample.py | 1 + .../tests/unit_tests/test_allocation_funcs_and_support.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/libensemble/alloc_funcs/give_pregenerated_work.py b/libensemble/alloc_funcs/give_pregenerated_work.py index 1d6edb160..060046d27 100644 --- a/libensemble/alloc_funcs/give_pregenerated_work.py +++ b/libensemble/alloc_funcs/give_pregenerated_work.py @@ -23,7 +23,7 @@ def give_pregenerated_sim_work(W, H, sim_specs, gen_specs, alloc_specs, persis_i if persis_info["next_to_give"] >= len(H): return Work, persis_info, 1 - for i in support.avail_worker_ids(): + for i in support.avail_sim_worker_ids(): persis_info = support.skip_canceled_points(H, persis_info) # Give sim work diff --git a/libensemble/tests/regression_tests/test_evaluate_mixed_sample.py b/libensemble/tests/regression_tests/test_evaluate_mixed_sample.py index 38998baa7..1574e8d57 100644 --- a/libensemble/tests/regression_tests/test_evaluate_mixed_sample.py +++ b/libensemble/tests/regression_tests/test_evaluate_mixed_sample.py @@ -44,6 +44,7 @@ H0["sim_ended"][:500] = True sampling = Ensemble(parse_args=True) + sampling.libE_specs.gen_on_manager = True sampling.H0 = H0 sampling.sim_specs = SimSpecs(sim_f=sim_f, inputs=["x"], out=[("f", float)]) sampling.alloc_specs = AllocSpecs(alloc_f=alloc_f) diff --git a/libensemble/tests/unit_tests/test_allocation_funcs_and_support.py b/libensemble/tests/unit_tests/test_allocation_funcs_and_support.py index 8f5959ce9..d04f3fb88 100644 --- a/libensemble/tests/unit_tests/test_allocation_funcs_and_support.py +++ b/libensemble/tests/unit_tests/test_allocation_funcs_and_support.py @@ -21,7 +21,7 @@ dtype=[ ("worker_id", " Date: Wed, 28 Feb 2024 13:01:22 -0600 Subject: [PATCH 043/288] I think for sim workers, the only requirement is that they're not gen workers --- libensemble/alloc_funcs/start_only_persistent.py | 2 +- libensemble/tools/alloc_support.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/libensemble/alloc_funcs/start_only_persistent.py b/libensemble/alloc_funcs/start_only_persistent.py index 870973dc4..35dee7752 100644 --- a/libensemble/alloc_funcs/start_only_persistent.py +++ b/libensemble/alloc_funcs/start_only_persistent.py @@ -88,7 +88,7 @@ def only_persistent_gens(W, H, sim_specs, gen_specs, alloc_specs, persis_info, l # Now the give_sim_work_first part points_to_evaluate = ~H["sim_started"] & ~H["cancel_requested"] - avail_workers = support.avail_worker_ids(persistent=False, zero_resource_workers=False) + avail_workers = support.avail_sim_worker_ids(persistent=False, zero_resource_workers=False) if user.get("alt_type"): avail_workers = list( set(support.avail_worker_ids(persistent=False, zero_resource_workers=False)) diff --git a/libensemble/tools/alloc_support.py b/libensemble/tools/alloc_support.py index 3cda02079..d93ab9814 100644 --- a/libensemble/tools/alloc_support.py +++ b/libensemble/tools/alloc_support.py @@ -120,8 +120,10 @@ def fltr_recving(): return not wrk["active"] def fltr_worker_type(): - if worker_type: - return wrk["worker_type"] == worker_type + if worker_type == EVAL_SIM_TAG: + return wrk["worker_type"] != EVAL_GEN_TAG # only workers not given gen work *yet* + elif worker_type == EVAL_GEN_TAG: + return wrk["worker_type"] == EVAL_GEN_TAG # explicitly want gen_workers else: return True @@ -146,7 +148,7 @@ def avail_gen_worker_ids(self, persistent=False, active_recv=False, zero_resourc ) def avail_sim_worker_ids(self, persistent=False, active_recv=False, zero_resource_workers=None): - """Returns available generator workers as a list of IDs.""" + """Returns available non-generator workers as a list of IDs.""" return self.avail_worker_ids( persistent=persistent, active_recv=active_recv, From 09d030c866b83b193b58da80bf53a6fed22fa328 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 28 Feb 2024 14:09:32 -0600 Subject: [PATCH 044/288] fixing alloc unit test based on passing wrapped W into alloc --- .../unit_tests/test_allocation_funcs_and_support.py | 12 +++++++----- libensemble/tools/alloc_support.py | 6 +++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/libensemble/tests/unit_tests/test_allocation_funcs_and_support.py b/libensemble/tests/unit_tests/test_allocation_funcs_and_support.py index d04f3fb88..38e3ecee7 100644 --- a/libensemble/tests/unit_tests/test_allocation_funcs_and_support.py +++ b/libensemble/tests/unit_tests/test_allocation_funcs_and_support.py @@ -11,6 +11,7 @@ from libensemble.tools import add_unique_random_streams from libensemble.tools.alloc_support import AllocException, AllocSupport from libensemble.tools.fields_keys import libE_fields +from libensemble.utils.misc import _WorkerIndexer al = {"alloc_f": give_sim_work_first} libE_specs = {"comms": "local", "nworkers": 4} @@ -58,7 +59,7 @@ def test_decide_work_and_resources(): libE_info = {"sim_max_given": False, "any_idle_workers": True, "use_resource_sets": False} # Don't give out work when all workers are active - W["active"] = True + W["active"] = 1 Work, persis_info = al["alloc_f"](W, hist.H, sim_specs, gen_specs, al, {}, libE_info) assert len(Work) == 0 @@ -131,8 +132,8 @@ def test_als_worker_ids(): def test_als_evaluate_gens(): W_gens = W.copy() - W_gens["active"] = np.array([True, 0, True, 0]) - W_gens["worker_type"] = np.array([2, 0, 2, 0]) + W_gens["active"] = np.array([EVAL_GEN_TAG, 0, EVAL_GEN_TAG, 0]) + W_gens["worker_type"] = np.array([EVAL_GEN_TAG, 0, EVAL_GEN_TAG, 0]) als = AllocSupport(W_gens, True) assert als.count_gens() == 2, "count_gens() didn't return correct number of active generators" @@ -166,7 +167,8 @@ def test_als_sim_work(): W_ps = W.copy() W_ps["persistent"] = np.array([True, 0, 0, 0]) - als = AllocSupport(W_ps, True) + W_ps["zero_resource_worker"] = np.array([True, 0, 0, 0]) + als = AllocSupport(_WorkerIndexer(W_ps, False), True) Work = {} Work[1] = als.sim_work(1, H, ["x"], np.array([0, 1, 2, 3, 4]), persis_info[1], persistent=True) @@ -203,7 +205,7 @@ def test_als_gen_work(): W_ps = W.copy() W_ps["persistent"] = np.array([True, 0, 0, 0]) - als = AllocSupport(W_ps, True) + als = AllocSupport(_WorkerIndexer(W_ps, False), True) Work = {} Work[1] = als.gen_work(1, ["sim_id"], range(0, 5), persis_info[1], persistent=True) diff --git a/libensemble/tools/alloc_support.py b/libensemble/tools/alloc_support.py index d93ab9814..12216259a 100644 --- a/libensemble/tools/alloc_support.py +++ b/libensemble/tools/alloc_support.py @@ -117,7 +117,7 @@ def fltr_recving(): if active_recv: return wrk["active_recv"] else: - return not wrk["active"] + return wrk["active"] == 0 def fltr_worker_type(): if worker_type == EVAL_SIM_TAG: @@ -158,11 +158,11 @@ def avail_sim_worker_ids(self, persistent=False, active_recv=False, zero_resourc def count_gens(self): """Returns the number of active generators.""" - return sum(self.W["active"] & (self.W["worker_type"] == EVAL_GEN_TAG)) + return sum((self.W["active"] == EVAL_GEN_TAG) & (self.W["worker_type"] == EVAL_GEN_TAG)) def test_any_gen(self): """Returns ``True`` if a generator worker is active.""" - return any(self.W["active"] & (self.W["worker_type"] == EVAL_GEN_TAG)) + return any((self.W["active"] == EVAL_GEN_TAG) & (self.W["worker_type"] == EVAL_GEN_TAG)) def count_persis_gens(self): """Return the number of active persistent generators.""" From 2f631e095a62b38d26c2dd7e69656967079ebfd0 Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 29 Feb 2024 15:52:19 -0600 Subject: [PATCH 045/288] refactoring Worker array fields to more closely match develop. worker_type:int is now gen_worker:bool. revert allocs --- .../alloc_funcs/give_pregenerated_work.py | 2 +- .../alloc_funcs/inverse_bayes_allocf.py | 3 +- .../alloc_funcs/persistent_aposmm_alloc.py | 3 +- .../alloc_funcs/start_fd_persistent.py | 3 +- .../alloc_funcs/start_only_persistent.py | 6 +-- .../start_persistent_local_opt_gens.py | 6 +-- libensemble/manager.py | 29 ++++++----- .../test_allocation_funcs_and_support.py | 21 +++++--- libensemble/tools/alloc_support.py | 49 ++++++------------- 9 files changed, 53 insertions(+), 69 deletions(-) diff --git a/libensemble/alloc_funcs/give_pregenerated_work.py b/libensemble/alloc_funcs/give_pregenerated_work.py index 060046d27..1d6edb160 100644 --- a/libensemble/alloc_funcs/give_pregenerated_work.py +++ b/libensemble/alloc_funcs/give_pregenerated_work.py @@ -23,7 +23,7 @@ def give_pregenerated_sim_work(W, H, sim_specs, gen_specs, alloc_specs, persis_i if persis_info["next_to_give"] >= len(H): return Work, persis_info, 1 - for i in support.avail_sim_worker_ids(): + for i in support.avail_worker_ids(): persis_info = support.skip_canceled_points(H, persis_info) # Give sim work diff --git a/libensemble/alloc_funcs/inverse_bayes_allocf.py b/libensemble/alloc_funcs/inverse_bayes_allocf.py index dcc1e13d7..56a3f6e79 100644 --- a/libensemble/alloc_funcs/inverse_bayes_allocf.py +++ b/libensemble/alloc_funcs/inverse_bayes_allocf.py @@ -1,5 +1,6 @@ import numpy as np +from libensemble.message_numbers import EVAL_GEN_TAG from libensemble.tools.alloc_support import AllocSupport, InsufficientFreeResources @@ -24,7 +25,7 @@ def only_persistent_gens_for_inverse_bayes(W, H, sim_specs, gen_specs, alloc_spe # If wid is idle, but in persistent mode, and generated work has all returned # give output back to wid. Otherwise, give nothing to wid - for wid in support.avail_gen_worker_ids(persistent=True): + for wid in support.avail_worker_ids(persistent=EVAL_GEN_TAG): # if > 1 persistent generator, assign the correct work to it inds_generated_by_wid = H["gen_worker"] == wid if support.all_sim_ended(H, inds_generated_by_wid): diff --git a/libensemble/alloc_funcs/persistent_aposmm_alloc.py b/libensemble/alloc_funcs/persistent_aposmm_alloc.py index 47b584309..8327d3975 100644 --- a/libensemble/alloc_funcs/persistent_aposmm_alloc.py +++ b/libensemble/alloc_funcs/persistent_aposmm_alloc.py @@ -1,5 +1,6 @@ import numpy as np +from libensemble.message_numbers import EVAL_GEN_TAG from libensemble.tools.alloc_support import AllocSupport, InsufficientFreeResources @@ -39,7 +40,7 @@ def persistent_aposmm_alloc(W, H, sim_specs, gen_specs, alloc_specs, persis_info return Work, persis_info, 1 # If any persistent worker's calculated values have returned, give them back. - for wid in support.avail_gen_worker_ids(persistent=True): + for wid in support.avail_worker_ids(persistent=EVAL_GEN_TAG): if persis_info.get("sample_done") or sum(H["sim_ended"]) >= init_sample_size + persis_info["samples_in_H0"]: # Don't return if the initial sample is not complete persis_info["sample_done"] = True diff --git a/libensemble/alloc_funcs/start_fd_persistent.py b/libensemble/alloc_funcs/start_fd_persistent.py index 33af61765..0c2e939d3 100644 --- a/libensemble/alloc_funcs/start_fd_persistent.py +++ b/libensemble/alloc_funcs/start_fd_persistent.py @@ -1,5 +1,6 @@ import numpy as np +from libensemble.message_numbers import EVAL_GEN_TAG from libensemble.tools.alloc_support import AllocSupport, InsufficientFreeResources @@ -29,7 +30,7 @@ def finite_diff_alloc(W, H, sim_specs, gen_specs, alloc_specs, persis_info, libE # If wid is in persistent mode, and all of its calculated values have # returned, give them back to wid. Otherwise, give nothing to wid - for wid in support.avail_gen_worker_ids(persistent=True): + for wid in support.avail_worker_ids(persistent=EVAL_GEN_TAG): # What (x_ind, f_ind) pairs have all of the evaluation of all n_ind # values complete. inds_not_sent_back = ~H["gen_informed"] diff --git a/libensemble/alloc_funcs/start_only_persistent.py b/libensemble/alloc_funcs/start_only_persistent.py index 35dee7752..6176a71ea 100644 --- a/libensemble/alloc_funcs/start_only_persistent.py +++ b/libensemble/alloc_funcs/start_only_persistent.py @@ -1,6 +1,6 @@ import numpy as np -from libensemble.message_numbers import EVAL_SIM_TAG +from libensemble.message_numbers import EVAL_GEN_TAG, EVAL_SIM_TAG from libensemble.tools.alloc_support import AllocSupport, InsufficientFreeResources @@ -70,7 +70,7 @@ def only_persistent_gens(W, H, sim_specs, gen_specs, alloc_specs, persis_info, l return Work, persis_info, 1 # Give evaluated results back to a running persistent gen - for wid in support.avail_gen_worker_ids(persistent=True, active_recv=active_recv_gen): + for wid in support.avail_worker_ids(persistent=EVAL_GEN_TAG, active_recv=active_recv_gen): gen_inds = H["gen_worker"] == wid returned_but_not_given = np.logical_and.reduce((H["sim_ended"], ~H["gen_informed"], gen_inds)) if np.any(returned_but_not_given): @@ -92,7 +92,7 @@ def only_persistent_gens(W, H, sim_specs, gen_specs, alloc_specs, persis_info, l if user.get("alt_type"): avail_workers = list( set(support.avail_worker_ids(persistent=False, zero_resource_workers=False)) - | set(support.avail_worker_ids(persistent=True, zero_resource_workers=False, worker_type=EVAL_SIM_TAG)) + | set(support.avail_worker_ids(persistent=EVAL_SIM_TAG, zero_resource_workers=False)) ) for wid in avail_workers: if not np.any(points_to_evaluate): diff --git a/libensemble/alloc_funcs/start_persistent_local_opt_gens.py b/libensemble/alloc_funcs/start_persistent_local_opt_gens.py index 1a16ea817..255663c0b 100644 --- a/libensemble/alloc_funcs/start_persistent_local_opt_gens.py +++ b/libensemble/alloc_funcs/start_persistent_local_opt_gens.py @@ -46,7 +46,7 @@ def start_persistent_local_opt_gens(W, H, sim_specs, gen_specs, alloc_specs, per # If wid is idle, but in persistent mode, and its calculated values have # returned, give them back to i. Otherwise, give nothing to wid - for wid in support.avail_gen_worker_ids(persistent=True): + for wid in support.avail_worker_ids(persistent=EVAL_GEN_TAG): gen_inds = H["gen_worker"] == wid if support.all_sim_ended(H, gen_inds): last_time_pos = np.argmax(H["sim_started_time"][gen_inds]) @@ -90,9 +90,7 @@ def start_persistent_local_opt_gens(W, H, sim_specs, gen_specs, alloc_specs, per break points_to_evaluate[sim_ids_to_send] = False - elif gen_count == 0 and not np.any( - np.logical_and((W["active"]), (W["persistent"] is False), (W["worker_type"] == EVAL_GEN_TAG)) - ): + elif gen_count == 0 and not np.any(np.logical_and((W["active"] == EVAL_GEN_TAG), (W["persis_state"] == 0))): # Finally, generate points since there is nothing else to do (no resource sets req.) Work[wid] = support.gen_work(wid, gen_specs.get("in", []), [], persis_info[wid], rset_team=[]) gen_count += 1 diff --git a/libensemble/manager.py b/libensemble/manager.py index c1fad1af5..d228d089f 100644 --- a/libensemble/manager.py +++ b/libensemble/manager.py @@ -159,9 +159,9 @@ class Manager: worker_dtype = [ ("worker_id", int), - ("worker_type", int), + ("gen_worker", bool), ("active", int), - ("persistent", bool), + ("persis_state", int), ("active_recv", bool), ("gen_started_time", float), ("zero_resource_worker", bool), @@ -235,7 +235,7 @@ def __init__( self.W = np.zeros(len(self.wcomms) + gen_on_manager, dtype=Manager.worker_dtype) if gen_on_manager: self.W["worker_id"] = np.arange(len(self.wcomms) + 1) # [0, 1, 2, ...] - self.W[0]["worker_type"] = EVAL_GEN_TAG + self.W[0]["gen_worker"] = True local_worker_comm = self._run_additional_worker(hist, sim_specs, gen_specs, libE_specs) self.wcomms = [local_worker_comm] + self.wcomms else: @@ -428,9 +428,8 @@ def _update_state_on_alloc(self, Work: dict, w: int): """Updates a workers' active/idle status following an allocation order""" self.W[w]["active"] = Work["tag"] - self.W[w]["worker_type"] = Work["tag"] if "persistent" in Work["libE_info"]: - self.W[w]["persistent"] = True + self.W[w]["persis_state"] = Work["tag"] if Work["libE_info"].get("active_recv", False): self.W[w]["active_recv"] = True else: @@ -480,7 +479,7 @@ def _update_state_on_worker_msg(self, persis_info: dict, D_recv: dict, w: int) - self.hist.update_history_f(D_recv, self.kill_canceled_sims) else: logger.info(_PERSIS_RETURN_WARNING) - self.W[w]["persistent"] = False + self.W[w]["persis_state"] = 0 if self.W[w]["active_recv"]: self.W[w]["active"] = 0 self.W[w]["active_recv"] = False @@ -494,11 +493,11 @@ def _update_state_on_worker_msg(self, persis_info: dict, D_recv: dict, w: int) - if calc_type == EVAL_GEN_TAG: self.hist.update_history_x_in(w, D_recv["calc_out"], self.W[w]["gen_started_time"]) assert ( - len(D_recv["calc_out"]) or np.any(self.W["active"]) or self.W[w]["persistent"] + len(D_recv["calc_out"]) or np.any(self.W["active"]) or self.W[w]["persis_state"] ), "Gen must return work when is is the only thing active and not persistent." if "libE_info" in D_recv and "persistent" in D_recv["libE_info"]: # Now a waiting, persistent worker - self.W[w]["persistent"] = True + self.W[w]["persis_state"] = D_recv["calc_type"] else: self._freeup_resources(w) @@ -562,8 +561,8 @@ def _final_receive_and_kill(self, persis_info: dict) -> (dict, int, int): """ # Send a handshake signal to each persistent worker. - if any(self.W["persistent"]): - for w in self.W["worker_id"][self.W["persistent"]]: + if any(self.W["persis_state"]): + for w in self.W["worker_id"][self.W["persis_state"] > 0]: logger.debug(f"Manager sending PERSIS_STOP to worker {w}") if self.libE_specs.get("final_gen_send", False): rows_to_send = np.where(self.hist.H["sim_ended"] & ~self.hist.H["gen_informed"])[0] @@ -580,15 +579,15 @@ def _final_receive_and_kill(self, persis_info: dict) -> (dict, int, int): self.wcomms[w].send(PERSIS_STOP, MAN_SIGNAL_KILL) if not self.W[w]["active"]: # Re-activate if necessary - self.W[w]["active"] = self.W[w]["worker_type"] if self.W[w]["persistent"] else 0 + self.W[w]["active"] = self.W[w]["persis_state"] self.persis_pending.append(w) exit_flag = 0 - while (any(self.W["active"]) or any(self.W["persistent"])) and exit_flag == 0: + while (any(self.W["active"]) or any(self.W["persis_state"])) and exit_flag == 0: persis_info = self._receive_from_workers(persis_info) if self.term_test(logged=False) == 2: # Elapsed Wallclock has expired - if not any(self.W["persistent"]): + if not any(self.W["persis_state"]): if any(self.W["active"]): logger.manager_warning(_WALLCLOCK_MSG_ACTIVE) else: @@ -611,7 +610,7 @@ def _get_alloc_libE_info(self) -> dict: """Selected statistics useful for alloc_f""" return { - "any_idle_workers": any(~self.W["active"]), + "any_idle_workers": any(self.W["active"] == 0), "exit_criteria": self.exit_criteria, "elapsed_time": self.elapsed(), "gen_informed_count": self.hist.gen_informed_count, @@ -681,7 +680,7 @@ def run(self, persis_info: dict) -> (dict, int, int): self._send_work_order(Work[w], w) self._update_state_on_alloc(Work[w], w) assert self.term_test() or any( - self.W["active"] + self.W["active"] != 0 ), "alloc_f did not return any work, although all workers are idle." except WorkerException as e: report_worker_exc(e) diff --git a/libensemble/tests/unit_tests/test_allocation_funcs_and_support.py b/libensemble/tests/unit_tests/test_allocation_funcs_and_support.py index 38e3ecee7..41a9aad83 100644 --- a/libensemble/tests/unit_tests/test_allocation_funcs_and_support.py +++ b/libensemble/tests/unit_tests/test_allocation_funcs_and_support.py @@ -18,12 +18,17 @@ H0 = [] W = np.array( - [(1, 0, 0, 0, 0, False), (2, 0, 0, 0, 0, False), (3, 0, 0, 0, 0, False), (4, 0, 0, 0, 0, False)], + [ + (1, False, 0, 0, False, False), + (2, False, 0, 0, False, False), + (3, False, 0, 0, False, False), + (4, False, 0, 0, False, False), + ], dtype=[ ("worker_id", " Date: Fri, 1 Mar 2024 09:42:17 -0600 Subject: [PATCH 046/288] fix tests --- .../tests/unit_tests/test_allocation_funcs_and_support.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/libensemble/tests/unit_tests/test_allocation_funcs_and_support.py b/libensemble/tests/unit_tests/test_allocation_funcs_and_support.py index 41a9aad83..6d056b1e0 100644 --- a/libensemble/tests/unit_tests/test_allocation_funcs_and_support.py +++ b/libensemble/tests/unit_tests/test_allocation_funcs_and_support.py @@ -108,7 +108,7 @@ def test_als_worker_ids(): W_ps = W.copy() W_ps["persis_state"] = np.array([EVAL_GEN_TAG, 0, 0, 0]) als = AllocSupport(W_ps, True) - assert als.avail_worker_ids(persistent=True) == [ + assert als.avail_worker_ids(persistent=EVAL_GEN_TAG) == [ 1 ], "avail_worker_ids() didn't return expected persistent worker list." @@ -116,7 +116,7 @@ def test_als_worker_ids(): W_ar["active_recv"] = np.array([True, 0, 0, 0]) W_ar["persis_state"] = np.array([EVAL_GEN_TAG, 0, 0, 0]) als = AllocSupport(W_ar, True) - assert als.avail_worker_ids(persistent=True, active_recv=True) == [ + assert als.avail_worker_ids(persistent=EVAL_GEN_TAG, active_recv=True) == [ 1 ], "avail_worker_ids() didn't return expected persistent worker list." @@ -138,7 +138,6 @@ def test_als_worker_ids(): def test_als_evaluate_gens(): W_gens = W.copy() W_gens["active"] = np.array([EVAL_GEN_TAG, 0, EVAL_GEN_TAG, 0]) - W_gens["worker_type"] = np.array([EVAL_GEN_TAG, 0, EVAL_GEN_TAG, 0]) als = AllocSupport(W_gens, True) assert als.count_gens() == 2, "count_gens() didn't return correct number of active generators" From 550ca1fdc60756d6b5ccb7e679a5fc7abf9cc583 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 1 Mar 2024 10:13:03 -0600 Subject: [PATCH 047/288] missed a revert in alloc --- libensemble/alloc_funcs/start_only_persistent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libensemble/alloc_funcs/start_only_persistent.py b/libensemble/alloc_funcs/start_only_persistent.py index 6176a71ea..4eaf8fa1c 100644 --- a/libensemble/alloc_funcs/start_only_persistent.py +++ b/libensemble/alloc_funcs/start_only_persistent.py @@ -88,7 +88,7 @@ def only_persistent_gens(W, H, sim_specs, gen_specs, alloc_specs, persis_info, l # Now the give_sim_work_first part points_to_evaluate = ~H["sim_started"] & ~H["cancel_requested"] - avail_workers = support.avail_sim_worker_ids(persistent=False, zero_resource_workers=False) + avail_workers = support.avail_worker_ids(persistent=False, zero_resource_workers=False) if user.get("alt_type"): avail_workers = list( set(support.avail_worker_ids(persistent=False, zero_resource_workers=False)) From e7591b6e2a8dfdda4438a8b1a0573c1a795da6d5 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 1 Mar 2024 10:20:02 -0600 Subject: [PATCH 048/288] undo inconsequential tiny changes to allocs --- libensemble/alloc_funcs/start_only_persistent.py | 1 + libensemble/alloc_funcs/start_persistent_local_opt_gens.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/libensemble/alloc_funcs/start_only_persistent.py b/libensemble/alloc_funcs/start_only_persistent.py index 4eaf8fa1c..ee9d4105f 100644 --- a/libensemble/alloc_funcs/start_only_persistent.py +++ b/libensemble/alloc_funcs/start_only_persistent.py @@ -51,6 +51,7 @@ def only_persistent_gens(W, H, sim_specs, gen_specs, alloc_specs, persis_info, l if libE_info["sim_max_given"] or not libE_info["any_idle_workers"]: return {}, persis_info + # Initialize alloc_specs["user"] as user. user = alloc_specs.get("user", {}) manage_resources = libE_info["use_resource_sets"] diff --git a/libensemble/alloc_funcs/start_persistent_local_opt_gens.py b/libensemble/alloc_funcs/start_persistent_local_opt_gens.py index 255663c0b..12ad45100 100644 --- a/libensemble/alloc_funcs/start_persistent_local_opt_gens.py +++ b/libensemble/alloc_funcs/start_persistent_local_opt_gens.py @@ -90,7 +90,7 @@ def start_persistent_local_opt_gens(W, H, sim_specs, gen_specs, alloc_specs, per break points_to_evaluate[sim_ids_to_send] = False - elif gen_count == 0 and not np.any(np.logical_and((W["active"] == EVAL_GEN_TAG), (W["persis_state"] == 0))): + elif gen_count == 0 and not np.any(np.logical_and(W["active"] == EVAL_GEN_TAG, W["persis_state"] == 0)): # Finally, generate points since there is nothing else to do (no resource sets req.) Work[wid] = support.gen_work(wid, gen_specs.get("in", []), [], persis_info[wid], rset_team=[]) gen_count += 1 From 68b991aa1c30c6281527734b1bc87805bf600ebb Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 1 Mar 2024 11:16:18 -0600 Subject: [PATCH 049/288] run each of the test_GPU_gen_resources tests also with the gen running on manager --- .../test_GPU_gen_resources.py | 58 +++++++++++-------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/libensemble/tests/functionality_tests/test_GPU_gen_resources.py b/libensemble/tests/functionality_tests/test_GPU_gen_resources.py index 6e692dfa2..a0ef24e15 100644 --- a/libensemble/tests/functionality_tests/test_GPU_gen_resources.py +++ b/libensemble/tests/functionality_tests/test_GPU_gen_resources.py @@ -42,6 +42,12 @@ from libensemble.sim_funcs.var_resources import gpu_variable_resources_from_gen as sim_f from libensemble.tools import add_unique_random_streams, parse_args +# TODO: multiple libE calls with gen-on-manager currently not supported with spawn on macOS +if sys.platform == "darwin": + from multiprocessing import set_start_method + + set_start_method("fork", force=True) + # from libensemble import logger # logger.set_level("DEBUG") # For testing the test @@ -100,30 +106,32 @@ libE_specs["resource_info"] = {"cores_on_node": (nworkers * 2, nworkers * 4), "gpus_on_node": nworkers} base_libE_specs = libE_specs.copy() - for run in range(5): - # reset - libE_specs = base_libE_specs.copy() - persis_info = add_unique_random_streams({}, nworkers + 1) - - if run == 0: - libE_specs["gen_num_procs"] = 2 - elif run == 1: - libE_specs["gen_num_gpus"] = 1 - elif run == 2: - persis_info["gen_num_gpus"] = 1 - elif run == 3: - # Two GPUs per resource set - libE_specs["resource_info"]["gpus_on_node"] = nworkers * 2 - persis_info["gen_num_gpus"] = 1 - elif run == 4: - # Two GPUs requested for gen - persis_info["gen_num_procs"] = 2 - persis_info["gen_num_gpus"] = 2 - gen_specs["user"]["max_procs"] = max(nworkers - 2, 1) - - # Perform the run - H, persis_info, flag = libE( - sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs, alloc_specs=alloc_specs - ) + for gen_on_manager in [False, True]: + for run in range(5): + # reset + libE_specs = base_libE_specs.copy() + libE_specs["gen_on_manager"] = gen_on_manager + persis_info = add_unique_random_streams({}, nworkers + 1) + + if run == 0: + libE_specs["gen_num_procs"] = 2 + elif run == 1: + libE_specs["gen_num_gpus"] = 1 + elif run == 2: + persis_info["gen_num_gpus"] = 1 + elif run == 3: + # Two GPUs per resource set + libE_specs["resource_info"]["gpus_on_node"] = nworkers * 2 + persis_info["gen_num_gpus"] = 1 + elif run == 4: + # Two GPUs requested for gen + persis_info["gen_num_procs"] = 2 + persis_info["gen_num_gpus"] = 2 + gen_specs["user"]["max_procs"] = max(nworkers - 2, 1) + + # Perform the run + H, persis_info, flag = libE( + sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs, alloc_specs=alloc_specs + ) # All asserts are in gen and sim funcs From f2a75ca5018043936c0db31a25e35b2fa21aea48 Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 4 Mar 2024 11:08:34 -0600 Subject: [PATCH 050/288] removes iffy effort to convert surmise --- .../persistent_surmise_calib_class.py | 246 ------------------ 1 file changed, 246 deletions(-) delete mode 100644 libensemble/gen_funcs/persistent_surmise_calib_class.py diff --git a/libensemble/gen_funcs/persistent_surmise_calib_class.py b/libensemble/gen_funcs/persistent_surmise_calib_class.py deleted file mode 100644 index 159eefb23..000000000 --- a/libensemble/gen_funcs/persistent_surmise_calib_class.py +++ /dev/null @@ -1,246 +0,0 @@ -""" -This module contains a simple calibration example using the Surmise package. -""" - -import numpy as np -from surmise.calibration import calibrator -from surmise.emulation import emulator - -from libensemble.gen_funcs.surmise_calib_support import ( - gen_observations, - gen_thetas, - gen_true_theta, - gen_xs, - select_next_theta, - thetaprior, -) -from libensemble.message_numbers import FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG - - -def build_emulator(theta, x, fevals): - """Build the emulator.""" - print(x.shape, theta.shape, fevals.shape) - emu = emulator( - x, - theta, - fevals, - method="PCGPwM", - options={ - "xrmnan": "all", - "thetarmnan": "never", - "return_grad": True, - }, - ) - emu.fit() - return emu - - -def select_condition(pending, n_remaining_theta=5): - n_x = pending.shape[0] - return False if np.sum(pending) > n_remaining_theta * n_x else True - - -def rebuild_condition(pending, prev_pending, n_theta=5): # needs changes - n_x = pending.shape[0] - if np.sum(prev_pending) - np.sum(pending) >= n_x * n_theta or np.sum(pending) == 0: - return True - else: - return False - - -def create_arrays(calc_in, n_thetas, n_x): - """Create 2D (point * rows) arrays fevals, pending and complete""" - fevals = np.reshape(calc_in["f"], (n_x, n_thetas)) - pending = np.full(fevals.shape, False) - prev_pending = pending.copy() - complete = np.full(fevals.shape, True) - - return fevals, pending, prev_pending, complete - - -def pad_arrays(n_x, thetanew, theta, fevals, pending, prev_pending, complete): - """Extend arrays to appropriate sizes.""" - n_thetanew = len(thetanew) - - theta = np.vstack((theta, thetanew)) - fevals = np.hstack((fevals, np.full((n_x, n_thetanew), np.nan))) - pending = np.hstack((pending, np.full((n_x, n_thetanew), True))) - prev_pending = np.hstack((prev_pending, np.full((n_x, n_thetanew), True))) - complete = np.hstack((complete, np.full((n_x, n_thetanew), False))) - - # print('after:', fevals.shape, theta.shape, pending.shape, complete.shape) - return theta, fevals, pending, prev_pending, complete - - -def update_arrays(fevals, pending, complete, calc_in, obs_offset, n_x): - """Unpack from calc_in into 2D (point * rows) fevals""" - sim_id = calc_in["sim_id"] - c, r = divmod(sim_id - obs_offset, n_x) # r, c are arrays if sim_id is an array - - fevals[r, c] = calc_in["f"] - pending[r, c] = False - complete[r, c] = True - return - - -def cancel_columns_get_H(obs_offset, c, n_x, pending): - """Cancel columns""" - sim_ids_to_cancel = [] - columns = np.unique(c) - for c in columns: - col_offset = c * n_x - for i in range(n_x): - sim_id_cancel = obs_offset + col_offset + i - if pending[i, c]: - sim_ids_to_cancel.append(sim_id_cancel) - pending[i, c] = 0 - - H_o = np.zeros(len(sim_ids_to_cancel), dtype=[("sim_id", int), ("cancel_requested", bool)]) - H_o["sim_id"] = sim_ids_to_cancel - H_o["cancel_requested"] = True - return H_o - - -def assign_priority(n_x, n_thetas): - """Assign priorities to points.""" - # Arbitrary priorities - priority = np.arange(n_x * n_thetas) - np.random.shuffle(priority) - return priority - - -def load_H(H, xs, thetas, offset=0, set_priorities=False): - """Fill inputs into H0. - - There will be num_points x num_thetas entries - """ - n_thetas = len(thetas) - for i, x in enumerate(xs): - start = (i + offset) * n_thetas - H["x"][start : start + n_thetas] = x - H["thetas"][start : start + n_thetas] = thetas - - if set_priorities: - n_x = len(xs) - H["priority"] = assign_priority(n_x, n_thetas) - - -def gen_truevals(x, gen_specs): - """Generate true values using libE.""" - n_x = len(x) - H_o = np.zeros((1) * n_x, dtype=gen_specs["out"]) - - # Generate true theta and load into H - true_theta = gen_true_theta() - H_o["x"][0:n_x] = x - H_o["thetas"][0:n_x] = true_theta - return H_o - - -class SurmiseCalibrator: - def __init__(self, persis_info, gen_specs): - self.gen_specs = gen_specs - self.rand_stream = persis_info["rand_stream"] - self.n_thetas = gen_specs["user"]["n_init_thetas"] - self.n_x = gen_specs["user"]["num_x_vals"] # Num of x points - self.step_add_theta = gen_specs["user"]["step_add_theta"] # No. of thetas to generate per step - self.n_explore_theta = gen_specs["user"]["n_explore_theta"] # No. of thetas to explore - self.obsvar_const = gen_specs["user"]["obsvar"] # Constant for generator - self.priorloc = gen_specs["user"]["priorloc"] - self.priorscale = gen_specs["user"]["priorscale"] - self.initial_ask = True - self.initial_tell = True - self.fevals = None - self.prev_pending = None - - def ask(self, initial_batch=False, cancellation=False): - if self.initial_ask: - self.prior = thetaprior(self.priorloc, self.priorscale) - self.x = gen_xs(self.n_x, self.rand_stream) - H_o = gen_truevals(self.x, self.gen_specs) - self.obs_offset = len(H_o) - self.initial_ask = False - - elif initial_batch: - H_o = np.zeros(self.n_x * (self.n_thetas), dtype=self.gen_specs["out"]) - self.theta = gen_thetas(self.prior, self.n_thetas) - load_H(H_o, self.x, self.theta, set_priorities=True) - - else: - if select_condition(self.pending): - new_theta, info = select_next_theta( - self.step_add_theta, self.cal, self.emu, self.pending, self.n_explore_theta - ) - - # Add space for new thetas - self.theta, fevals, pending, self.prev_pending, self.complete = pad_arrays( - self.n_x, new_theta, self.theta, self.fevals, self.pending, self.prev_pending, self.complete - ) - # n_thetas = step_add_theta - H_o = np.zeros(self.n_x * (len(new_theta)), dtype=self.gen_specs["out"]) - load_H(H_o, self.x, new_theta, set_priorities=True) - - c_obviate = info["obviatesugg"] - if len(c_obviate) > 0: - print(f"columns sent for cancel is: {c_obviate}", flush=True) - H_o = cancel_columns_get_H(self.obs_offset, c_obviate, self.n_x, pending) - pending[:, c_obviate] = False - - return H_o - - def tell(self, calc_in, tag): - if self.initial_tell: - returned_fevals = np.reshape(calc_in["f"], (1, self.n_x)) - true_fevals = returned_fevals - obs, obsvar = gen_observations(true_fevals, self.obsvar_const, self.rand_stream) - self.initial_tell = False - self.ask(initial_batch=True) - - else: - if self.fevals is None: # initial batch - self.fevals, self.pending, prev_pending, self.complete = create_arrays(calc_in, self.n_thetas, self.n_x) - self.emu = build_emulator(self.theta, self.x, self.fevals) - # Refer to surmise package for additional options - self.cal = calibrator(self.emu, obs, self.x, self.prior, obsvar, method="directbayes") - - print("quantiles:", np.round(np.quantile(self.cal.theta.rnd(10000), (0.01, 0.99), axis=0), 3)) - update_model = False - else: - # Update fevals from calc_in - update_arrays(self.fevals, self.pending, self.complete, calc_in, self.obs_offset, self.n_x) - update_model = rebuild_condition(self.pending, self.prev_pending) - if not update_model: - if tag in [STOP_TAG, PERSIS_STOP]: - return - - if update_model: - print( - "Percentage Cancelled: %0.2f ( %d / %d)" - % ( - 100 * np.round(np.mean(1 - self.pending - self.complete), 4), - np.sum(1 - self.pending - self.complete), - np.prod(self.pending.shape), - ) - ) - print( - "Percentage Pending: %0.2f ( %d / %d)" - % (100 * np.round(np.mean(self.pending), 4), np.sum(self.pending), np.prod(self.pending.shape)) - ) - print( - "Percentage Complete: %0.2f ( %d / %d)" - % (100 * np.round(np.mean(self.complete), 4), np.sum(self.complete), np.prod(self.pending.shape)) - ) - - self.emu.update(theta=self.theta, f=self.fevals) - self.cal.fit() - - samples = self.cal.theta.rnd(2500) - print(np.mean(np.sum((samples - np.array([0.5] * 4)) ** 2, 1))) - print(np.round(np.quantile(self.cal.theta.rnd(10000), (0.01, 0.99), axis=0), 3)) - - self.step_add_theta += 2 - self.prev_pending = self.pending.copy() - update_model = False - - def finalize(self): - return None, self.persis_info, FINISHED_PERSISTENT_GEN_TAG From 9c1cb1162225175409c23254f4781e86c8eb7b98 Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 4 Mar 2024 16:00:10 -0600 Subject: [PATCH 051/288] initial framework for AskTellGenerator abc. needs more docs --- libensemble/generators.py | 54 +++++++++++++++++++ .../test_1d_asktell_gen.py | 1 + 2 files changed, 55 insertions(+) create mode 100644 libensemble/generators.py diff --git a/libensemble/generators.py b/libensemble/generators.py new file mode 100644 index 000000000..50707c540 --- /dev/null +++ b/libensemble/generators.py @@ -0,0 +1,54 @@ +from abc import ABC, abstractmethod +from typing import Optional + +import numpy.typing as npt + + +class AskTellGenerator(ABC): + """ + Pattern of operations: + 0. User initialize the generator in their script, provides object to libEnsemble + 1. Initial ask for points + 2. Send initial points to libEnsemble for evaluation + while not instructed to cleanup: + 3. Tell results to generator + 4. Ask for subsequent points + 5. Send points to libEnsemble for evaluation. Get results and any cleanup instruction. + 6. Perform final_tell to generator, retrieve final results if any. + """ + + @abstractmethod + def __init__(self, *args, **kwargs): + """ + Initialize the Generator object. Constants and class-attributes go here. + This will be called only once. + + .. code-block:: python + + my_generator = MyGenerator(my_parameter, batch_size=10) + """ + pass + + @abstractmethod + def initial_ask(self, *args, **kwargs) -> npt.NDArray: + """ + The initial set of generated points is often produced differently than subsequent sets. + This is a separate method to simplify the common pattern of noting internally if a + specific ask was the first. This will be called only once. + """ + pass + + @abstractmethod + def ask(self, *args, **kwargs) -> npt.NDArray: + """ """ + pass + + @abstractmethod + def tell(self, Input: npt.NDArray, *args, **kwargs) -> None: + """ """ + pass + + @abstractmethod + def final_tell(self, Input: npt.NDArray, *args, **kwargs) -> Optional[npt.NDArray]: + """ """ + pass diff --git a/libensemble/tests/functionality_tests/test_1d_asktell_gen.py b/libensemble/tests/functionality_tests/test_1d_asktell_gen.py index 1b6cd2f56..4bc030654 100644 --- a/libensemble/tests/functionality_tests/test_1d_asktell_gen.py +++ b/libensemble/tests/functionality_tests/test_1d_asktell_gen.py @@ -80,6 +80,7 @@ def finalize(self): if __name__ == "__main__": nworkers, is_manager, libE_specs, _ = parse_args() + libE_specs["gen_on_manager"] = True sim_specs = { "sim_f": sim_f, From 3d1f5031bf2012243220b4f187f7b36f10fdfcb6 Mon Sep 17 00:00:00 2001 From: jlnav Date: Tue, 5 Mar 2024 11:35:20 -0600 Subject: [PATCH 052/288] more docs --- libensemble/generators.py | 71 ++++++++++++++++++++++++++++++--------- 1 file changed, 55 insertions(+), 16 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 50707c540..7e5dd67c2 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -1,27 +1,57 @@ from abc import ABC, abstractmethod -from typing import Optional +from typing import Iterable, Optional -import numpy.typing as npt - -class AskTellGenerator(ABC): +class Generator(ABC): """ + + Tentative generator interface for use with libEnsemble. Such an interface should be broadly + compatible with other workflow packages. + + .. code-block:: python + + from libensemble import Ensemble + from libensemble.generators import Generator + + + class MyGenerator(Generator): + def __init__(self, param): + self.param = param + self.model = None + + def initial_ask(self, num_points): + return create_initial_points(num_points, self.param) + + def ask(self, num_points): + return create_points(num_points, self.param) + + def tell(self, results): + self.model = update_model(results, self.model) + + def final_tell(self, results): + self.tell(results) + return list(self.model) + + + my_generator = MyGenerator(my_parameter=100) + my_ensemble = Ensemble(generator=my_generator) + Pattern of operations: 0. User initialize the generator in their script, provides object to libEnsemble 1. Initial ask for points - 2. Send initial points to libEnsemble for evaluation + 2. Send initial points to workflow for evaluation while not instructed to cleanup: 3. Tell results to generator - 4. Ask for subsequent points - 5. Send points to libEnsemble for evaluation. Get results and any cleanup instruction. + 4. Ask generator for subsequent points + 5. Send points to workflow for evaluation. Get results and any cleanup instruction. 6. Perform final_tell to generator, retrieve final results if any. + """ @abstractmethod def __init__(self, *args, **kwargs): """ - Initialize the Generator object. Constants and class-attributes go here. - This will be called only once. + Initialize the Generator object on the user-side. Constants and class-attributes go here. .. code-block:: python @@ -30,7 +60,7 @@ def __init__(self, *args, **kwargs): pass @abstractmethod - def initial_ask(self, *args, **kwargs) -> npt.NDArray: + def initial_ask(self, num_points: int) -> Iterable: """ The initial set of generated points is often produced differently than subsequent sets. This is a separate method to simplify the common pattern of noting internally if a @@ -39,16 +69,25 @@ def initial_ask(self, *args, **kwargs) -> npt.NDArray: pass @abstractmethod - def ask(self, *args, **kwargs) -> npt.NDArray: - """ """ + def ask(self, num_points: int) -> Iterable: + """ + Request the next set of points to evaluate. + """ pass @abstractmethod - def tell(self, Input: npt.NDArray, *args, **kwargs) -> None: - """ """ + def tell(self, results: Iterable) -> None: + """ + Send the results of evaluations to the generator. + """ pass @abstractmethod - def final_tell(self, Input: npt.NDArray, *args, **kwargs) -> Optional[npt.NDArray]: - """ """ + def final_tell(self, results: Iterable) -> Optional[Iterable]: + """ + Send the last set of results to the generator, instruct it to cleanup, and + optionally retrieve an updated final state of evaluations. This is a separate + method to simplify the common pattern of noting internally if a + specific tell is the last. This will be called only once. + """ pass From c433ecb397b2c3c5c76f37beb6a371fe067473f6 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 6 Mar 2024 10:42:57 -0600 Subject: [PATCH 053/288] simply gen_workers parameter description for avail_worker_ids --- libensemble/tools/alloc_support.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libensemble/tools/alloc_support.py b/libensemble/tools/alloc_support.py index e514bad02..9b25a267d 100644 --- a/libensemble/tools/alloc_support.py +++ b/libensemble/tools/alloc_support.py @@ -93,7 +93,7 @@ def avail_worker_ids(self, persistent=None, active_recv=False, zero_resource_wor :param persistent: (Optional) Int. Only return workers with given ``persis_state`` (1=sim, 2=gen). :param active_recv: (Optional) Boolean. Only return workers with given active_recv state. :param zero_resource_workers: (Optional) Boolean. Only return workers that require no resources. - :param gen_workers: (Optional) Boolean. If True, return gen-only workers and manager's ID. + :param gen_workers: (Optional) Boolean. If True, return gen-only workers. :returns: List of worker IDs. If there are no zero resource workers defined, then the ``zero_resource_workers`` argument will From 01727e8dbfccfd1af57558b0f06aa0168335d861 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 6 Mar 2024 15:28:53 -0600 Subject: [PATCH 054/288] refactor test_1d_asktell_gen classes to subclass from libensemble.Generator, asks receive num_points (which defaults to n idle sim workers). batch_size and initial_batch_size attributes of a Generator subclass are also honored --- libensemble/__init__.py | 1 + libensemble/generators.py | 19 +++--- .../test_1d_asktell_gen.py | 64 +++++++++---------- libensemble/tools/alloc_support.py | 5 ++ libensemble/utils/runners.py | 20 +++--- 5 files changed, 55 insertions(+), 54 deletions(-) diff --git a/libensemble/__init__.py b/libensemble/__init__.py index 605336821..8df3af207 100644 --- a/libensemble/__init__.py +++ b/libensemble/__init__.py @@ -12,3 +12,4 @@ from libensemble import logger from .ensemble import Ensemble +from .generators import Generator diff --git a/libensemble/generators.py b/libensemble/generators.py index 7e5dd67c2..d62d17210 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -5,8 +5,8 @@ class Generator(ABC): """ - Tentative generator interface for use with libEnsemble. Such an interface should be broadly - compatible with other workflow packages. + Tentative generator interface for use with libEnsemble, and generic enough to be + broadly compatible with other workflow packages. .. code-block:: python @@ -37,8 +37,8 @@ def final_tell(self, results): my_ensemble = Ensemble(generator=my_generator) Pattern of operations: - 0. User initialize the generator in their script, provides object to libEnsemble - 1. Initial ask for points + 0. User initialize the generator class in their script, provides object to workflow/libEnsemble + 1. Initial ask for points from the generator 2. Send initial points to workflow for evaluation while not instructed to cleanup: 3. Tell results to generator @@ -51,7 +51,8 @@ def final_tell(self, results): @abstractmethod def __init__(self, *args, **kwargs): """ - Initialize the Generator object on the user-side. Constants and class-attributes go here. + Initialize the Generator object on the user-side. Constants, class-attributes, + and preparation goes here. .. code-block:: python @@ -59,12 +60,12 @@ def __init__(self, *args, **kwargs): """ pass - @abstractmethod - def initial_ask(self, num_points: int) -> Iterable: + def initial_ask(self, num_points: int, previous_results: Optional[Iterable]) -> Iterable: """ The initial set of generated points is often produced differently than subsequent sets. This is a separate method to simplify the common pattern of noting internally if a - specific ask was the first. This will be called only once. + specific ask was the first. Previous results can be provided to build a foundation + for the initial sample. This will be called only once. """ pass @@ -75,14 +76,12 @@ def ask(self, num_points: int) -> Iterable: """ pass - @abstractmethod def tell(self, results: Iterable) -> None: """ Send the results of evaluations to the generator. """ pass - @abstractmethod def final_tell(self, results: Iterable) -> Optional[Iterable]: """ Send the last set of results to the generator, instruct it to cleanup, and diff --git a/libensemble/tests/functionality_tests/test_1d_asktell_gen.py b/libensemble/tests/functionality_tests/test_1d_asktell_gen.py index 4bc030654..efd515939 100644 --- a/libensemble/tests/functionality_tests/test_1d_asktell_gen.py +++ b/libensemble/tests/functionality_tests/test_1d_asktell_gen.py @@ -15,14 +15,13 @@ import numpy as np +# Import libEnsemble items for this test +from libensemble import Generator from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f from libensemble.gen_funcs.persistent_sampling import _get_user_params from libensemble.gen_funcs.sampling import latin_hypercube_sample as gen_f from libensemble.gen_funcs.sampling import lhs_sample - -# Import libEnsemble items for this test from libensemble.libE import libE -from libensemble.message_numbers import FINISHED_PERSISTENT_GEN_TAG from libensemble.sim_funcs.rosenbrock import rosenbrock_eval as sim_f2 from libensemble.tools import add_unique_random_streams, parse_args @@ -33,49 +32,44 @@ def sim_f(In): return Out -class LHSGenerator: - def __init__(self, persis_info, gen_specs): - self.persis_info = persis_info - self.gen_specs = gen_specs - - def ask(self): - ub = self.gen_specs["user"]["ub"] - lb = self.gen_specs["user"]["lb"] - - n = len(lb) - b = self.gen_specs["user"]["gen_batch_size"] - - H_o = np.zeros(b, dtype=self.gen_specs["out"]) - - A = lhs_sample(n, b, self.persis_info["rand_stream"]) - - H_o["x"] = A * (ub - lb) + lb +class LHS(Generator): + def __init__(self, rand_stream, ub, lb, b, dtype): + self.rand_stream = rand_stream + self.ub = ub + self.lb = lb + self.batch_size = b + self.dtype = dtype + def ask(self, *args): + n = len(self.lb) + H_o = np.zeros(self.batch_size, dtype=self.dtype) + A = lhs_sample(n, self.batch_size, self.rand_stream) + H_o["x"] = A * (self.ub - self.lb) + self.lb return H_o -class PersistentUniform: +class PersistentUniform(Generator): def __init__(self, persis_info, gen_specs): self.persis_info = persis_info self.gen_specs = gen_specs - self.b, self.n, self.lb, self.ub = _get_user_params(gen_specs["user"]) - - def ask(self): - H_o = np.zeros(self.b, dtype=self.gen_specs["out"]) - H_o["x"] = self.persis_info["rand_stream"].uniform(self.lb, self.ub, (self.b, self.n)) - if "obj_component" in H_o.dtype.fields: - H_o["obj_component"] = self.persis_info["rand_stream"].integers( - low=0, high=self.gen_specs["user"]["num_components"], size=self.b - ) + _, self.n, self.lb, self.ub = _get_user_params(gen_specs["user"]) + + def initial_ask(self, num_points, *args): + return self.ask(num_points) + + def ask(self, num_points): + H_o = np.zeros(num_points, dtype=self.gen_specs["out"]) + H_o["x"] = self.persis_info["rand_stream"].uniform(self.lb, self.ub, (num_points, self.n)) self.last_H = H_o return H_o - def tell(self, H_in, *args): + def tell(self, H_in): if hasattr(H_in, "__len__"): - self.b = len(H_in) + self.batch_size = len(H_in) - def finalize(self): - return self.last_H, self.persis_info, FINISHED_PERSISTENT_GEN_TAG + def final_tell(self, H_in): + self.tell(H_in) + return self.last_H if __name__ == "__main__": @@ -100,7 +94,7 @@ def finalize(self): persis_info = add_unique_random_streams({}, nworkers + 1, seed=1234) - gen_one = LHSGenerator(persis_info[1], gen_specs_normal) + gen_one = LHS(persis_info[1]["rand_stream"], np.array([3]), np.array([-3]), 500, gen_specs_normal["out"]) gen_specs_normal["gen_f"] = gen_one exit_criteria = {"gen_max": 201} diff --git a/libensemble/tools/alloc_support.py b/libensemble/tools/alloc_support.py index 9b25a267d..72e2bd04a 100644 --- a/libensemble/tools/alloc_support.py +++ b/libensemble/tools/alloc_support.py @@ -279,6 +279,11 @@ def gen_work(self, wid, H_fields, H_rows, persis_info, **libE_info): H_fields = AllocSupport._check_H_fields(H_fields) libE_info["H_rows"] = AllocSupport._check_H_rows(H_rows) + libE_info["batch_size"] = len( + self.avail_worker_ids( + gen_workers=False, + ) + ) work = { "H_fields": H_fields, diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 90ed8cbc7..25e0d7392 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -6,7 +6,7 @@ import numpy.typing as npt from libensemble.comms.comms import QCommThread -from libensemble.message_numbers import EVAL_GEN_TAG, PERSIS_STOP, STOP_TAG +from libensemble.message_numbers import EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG from libensemble.tools.persistent_support import PersistentSupport logger = logging.getLogger(__name__) @@ -94,18 +94,20 @@ class AskTellGenRunner(Runner): def __init__(self, specs): super().__init__(specs) - def _persistent_result( - self, calc_in: npt.NDArray, persis_info: dict, libE_info: dict - ) -> (npt.NDArray, dict, Optional[int]): + def _persistent_result(self, calc_in, persis_info, libE_info): self.ps = PersistentSupport(libE_info, EVAL_GEN_TAG) tag = None + initial_batch = getattr(self.f, "initial_batch_size", 0) or libE_info["batch_size"] + H_out = self.f.initial_ask(initial_batch, calc_in) + tag, Work, H_in = self.ps.send_recv(H_out) while tag not in [STOP_TAG, PERSIS_STOP]: - H_out = self.f.ask() - tag, _, H_in = self.ps.send_recv(H_out) - self.f.tell(H_in, tag) - return self.f.finalize() + batch_size = getattr(self.f, "batch_size", 0) or Work["libE_info"]["batch_size"] + self.f.tell(H_in) + H_out = self.f.ask(batch_size) + tag, Work, H_in = self.ps.send_recv(H_out) + return self.f.final_tell(H_in), FINISHED_PERSISTENT_GEN_TAG def _result(self, calc_in: npt.NDArray, persis_info: dict, libE_info: dict) -> (npt.NDArray, dict, Optional[int]): if libE_info.get("persistent"): return self._persistent_result(calc_in, persis_info, libE_info) - return self.f.ask() + return self.f.ask(getattr(self.f, "batch_size", 0) or libE_info["batch_size"]) From 520fe2212cb1b2a3e6fb9399e8b6e132b0d22dee Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 7 Mar 2024 15:34:26 -0600 Subject: [PATCH 055/288] tiny adjusts --- libensemble/generators.py | 8 ++++---- libensemble/utils/validators.py | 2 -- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index d62d17210..6a3b01ec5 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -19,8 +19,8 @@ def __init__(self, param): self.param = param self.model = None - def initial_ask(self, num_points): - return create_initial_points(num_points, self.param) + def initial_ask(self, num_points, yesterdays_points): + return create_initial_points(num_points, self.param, yesterdays_points) def ask(self, num_points): return create_points(num_points, self.param) @@ -37,14 +37,14 @@ def final_tell(self, results): my_ensemble = Ensemble(generator=my_generator) Pattern of operations: - 0. User initialize the generator class in their script, provides object to workflow/libEnsemble + 0. User initializes the generator class in their script, provides object to workflow/libEnsemble 1. Initial ask for points from the generator 2. Send initial points to workflow for evaluation while not instructed to cleanup: 3. Tell results to generator 4. Ask generator for subsequent points 5. Send points to workflow for evaluation. Get results and any cleanup instruction. - 6. Perform final_tell to generator, retrieve final results if any. + 6. Perform final_tell to generator, retrieve any final results/points if any. """ diff --git a/libensemble/utils/validators.py b/libensemble/utils/validators.py index 11f2cf4c1..80477f7e9 100644 --- a/libensemble/utils/validators.py +++ b/libensemble/utils/validators.py @@ -132,7 +132,6 @@ def check_provided_ufuncs(cls, values): if values.get("alloc_specs").alloc_f.__name__ != "give_pregenerated_sim_work": gen_specs = values.get("gen_specs") assert hasattr(gen_specs, "gen_f"), "Generator function not provided to GenSpecs." - # assert isinstance(gen_specs.gen_f, Callable), "Generator function is not callable." return values @@ -221,7 +220,6 @@ def check_provided_ufuncs(self): if self.alloc_specs.alloc_f.__name__ != "give_pregenerated_sim_work": assert hasattr(self.gen_specs, "gen_f"), "Generator function not provided to GenSpecs." - # assert isinstance(self.gen_specs.gen_f, Callable), "Generator function is not callable." return self From d48dcf09b17b0484e3f0b21512afb29b5550e816 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 8 Mar 2024 09:50:10 -0600 Subject: [PATCH 056/288] abstract methods dont need passes --- libensemble/generators.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 6a3b01ec5..9bcc465dd 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -58,7 +58,6 @@ def __init__(self, *args, **kwargs): my_generator = MyGenerator(my_parameter, batch_size=10) """ - pass def initial_ask(self, num_points: int, previous_results: Optional[Iterable]) -> Iterable: """ @@ -67,20 +66,17 @@ def initial_ask(self, num_points: int, previous_results: Optional[Iterable]) -> specific ask was the first. Previous results can be provided to build a foundation for the initial sample. This will be called only once. """ - pass @abstractmethod def ask(self, num_points: int) -> Iterable: """ Request the next set of points to evaluate. """ - pass def tell(self, results: Iterable) -> None: """ Send the results of evaluations to the generator. """ - pass def final_tell(self, results: Iterable) -> Optional[Iterable]: """ @@ -89,4 +85,3 @@ def final_tell(self, results: Iterable) -> Optional[Iterable]: method to simplify the common pattern of noting internally if a specific tell is the last. This will be called only once. """ - pass From e78056b0acbc5572385e1618aa38ae928e7eb4d3 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 8 Mar 2024 13:33:08 -0600 Subject: [PATCH 057/288] debugging consecutive libE calls with gen_on_manager --- libensemble/comms/comms.py | 30 +++++++++---------- libensemble/comms/logs.py | 1 + libensemble/manager.py | 4 +++ .../test_GPU_gen_resources.py | 6 ---- 4 files changed, 20 insertions(+), 21 deletions(-) diff --git a/libensemble/comms/comms.py b/libensemble/comms/comms.py index bebca9344..51042c463 100644 --- a/libensemble/comms/comms.py +++ b/libensemble/comms/comms.py @@ -207,19 +207,6 @@ def result(self, timeout=None): raise RemoteException(self._exception.msg, self._exception.exc) return self._result - @staticmethod - def _qcomm_main(comm, main, *args, **kwargs): - """Main routine -- handles return values and exceptions.""" - try: - if not kwargs.get("user_function"): - _result = main(comm, *args, **kwargs) - else: - _result = main(*args) - comm.send(CommResult(_result)) - except Exception as e: - comm.send(CommResultErr(str(e), format_exc())) - raise e - @property def running(self): """Check if the thread/process is running.""" @@ -233,6 +220,19 @@ def __exit__(self, etype, value, traceback): self.handle.join() +def _qcomm_main(comm, main, *args, **kwargs): + """Main routine -- handles return values and exceptions.""" + try: + if not kwargs.get("user_function"): + _result = main(comm, *args, **kwargs) + else: + _result = main(*args) + comm.send(CommResult(_result)) + except Exception as e: + comm.send(CommResultErr(str(e), format_exc())) + raise e + + class QCommThread(QCommLocal): """Launch a user function in a thread with an attached QComm.""" @@ -241,7 +241,7 @@ def __init__(self, main, nworkers, *args, **kwargs): self.outbox = thread_queue.Queue() super().__init__(self, main, *args, **kwargs) comm = QComm(self.inbox, self.outbox, nworkers) - self.handle = Thread(target=QCommThread._qcomm_main, args=(comm, main) + args, kwargs=kwargs) + self.handle = Thread(target=_qcomm_main, args=(comm, main) + args, kwargs=kwargs) def terminate(self, timeout=None): """Terminate the thread. @@ -265,7 +265,7 @@ def __init__(self, main, nworkers, *args, **kwargs): self.outbox = Queue() super().__init__(self, main, *args, **kwargs) comm = QComm(self.inbox, self.outbox, nworkers) - self.handle = Process(target=QCommProcess._qcomm_main, args=(comm, main) + args, kwargs=kwargs) + self.handle = Process(target=_qcomm_main, args=(comm, main) + args, kwargs=kwargs) def terminate(self, timeout=None): """Terminate the process.""" diff --git a/libensemble/comms/logs.py b/libensemble/comms/logs.py index 10acbae07..47f85f351 100644 --- a/libensemble/comms/logs.py +++ b/libensemble/comms/logs.py @@ -203,6 +203,7 @@ def manager_logging_config(specs={}): def exit_logger(): stat_timer.stop() stat_logger.info(f"Exiting ensemble at: {stat_timer.date_end} Time Taken: {stat_timer.elapsed}") + stat_logger.handlers[0].close() # If closing logs - each libE() call will log to a new file. # fh.close() diff --git a/libensemble/manager.py b/libensemble/manager.py index d228d089f..094ef839b 100644 --- a/libensemble/manager.py +++ b/libensemble/manager.py @@ -302,6 +302,9 @@ def _kill_workers(self) -> None: """Kills the workers""" for w in self.W["worker_id"]: self.wcomms[w].send(STOP_TAG, MAN_SIGNAL_FINISH) + if w == 0: + self.wcomms[0].result() + self.wcomms[0] = None # --- Checkpointing logic @@ -691,6 +694,7 @@ def run(self, persis_info: dict) -> (dict, int, int): finally: # Return persis_info, exit_flag, elapsed time result = self._final_receive_and_kill(persis_info) + self.wcomms = None sys.stdout.flush() sys.stderr.flush() return result diff --git a/libensemble/tests/functionality_tests/test_GPU_gen_resources.py b/libensemble/tests/functionality_tests/test_GPU_gen_resources.py index a0ef24e15..bd40d5c4c 100644 --- a/libensemble/tests/functionality_tests/test_GPU_gen_resources.py +++ b/libensemble/tests/functionality_tests/test_GPU_gen_resources.py @@ -42,12 +42,6 @@ from libensemble.sim_funcs.var_resources import gpu_variable_resources_from_gen as sim_f from libensemble.tools import add_unique_random_streams, parse_args -# TODO: multiple libE calls with gen-on-manager currently not supported with spawn on macOS -if sys.platform == "darwin": - from multiprocessing import set_start_method - - set_start_method("fork", force=True) - # from libensemble import logger # logger.set_level("DEBUG") # For testing the test From f30233c6a892aaa9f32d557dcd57b4f0ca870ef5 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 8 Mar 2024 15:20:59 -0600 Subject: [PATCH 058/288] debugging...... --- libensemble/comms/comms.py | 6 ++++++ libensemble/comms/logs.py | 1 + libensemble/libE.py | 3 +++ .../tests/functionality_tests/test_GPU_gen_resources.py | 2 ++ 4 files changed, 12 insertions(+) diff --git a/libensemble/comms/comms.py b/libensemble/comms/comms.py index 51042c463..2b31cf5b9 100644 --- a/libensemble/comms/comms.py +++ b/libensemble/comms/comms.py @@ -255,6 +255,9 @@ def terminate(self, timeout=None): self.handle.join(timeout=timeout) if self.running: raise Timeout() + self.handle = None + self.inbox = None + self.outbox = None class QCommProcess(QCommLocal): @@ -274,3 +277,6 @@ def terminate(self, timeout=None): self.handle.join(timeout=timeout) if self.running: raise Timeout() + self.handle = None + self.inbox = None + self.outbox = None diff --git a/libensemble/comms/logs.py b/libensemble/comms/logs.py index 47f85f351..de2454f8d 100644 --- a/libensemble/comms/logs.py +++ b/libensemble/comms/logs.py @@ -204,6 +204,7 @@ def exit_logger(): stat_timer.stop() stat_logger.info(f"Exiting ensemble at: {stat_timer.date_end} Time Taken: {stat_timer.elapsed}") stat_logger.handlers[0].close() + print("Manager logger closed") # If closing logs - each libE() call will log to a new file. # fh.close() diff --git a/libensemble/libE.py b/libensemble/libE.py index b283a82b4..b5ddaa330 100644 --- a/libensemble/libE.py +++ b/libensemble/libE.py @@ -460,6 +460,9 @@ def kill_proc_team(wcomms, timeout): wcomm.result(timeout=timeout) except Timeout: wcomm.terminate() + wcomm.handle = None + wcomm.inbox = None + wcomm.outbox = None def libE_local(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs, H0): diff --git a/libensemble/tests/functionality_tests/test_GPU_gen_resources.py b/libensemble/tests/functionality_tests/test_GPU_gen_resources.py index bd40d5c4c..0fc8192f7 100644 --- a/libensemble/tests/functionality_tests/test_GPU_gen_resources.py +++ b/libensemble/tests/functionality_tests/test_GPU_gen_resources.py @@ -110,6 +110,8 @@ if run == 0: libE_specs["gen_num_procs"] = 2 elif run == 1: + if gen_on_manager: + print("SECOND LIBE CALL WITH GEN ON MANAGER") libE_specs["gen_num_gpus"] = 1 elif run == 2: persis_info["gen_num_gpus"] = 1 From 6d0f9d2849c63f69fb14f3ec14d3f35e86dfed57 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 8 Mar 2024 16:08:56 -0600 Subject: [PATCH 059/288] cleaning up debugging, removing comm from Executor upon worker exiting --- libensemble/comms/comms.py | 6 ------ libensemble/comms/logs.py | 1 - libensemble/libE.py | 3 --- libensemble/manager.py | 1 - libensemble/worker.py | 1 + 5 files changed, 1 insertion(+), 11 deletions(-) diff --git a/libensemble/comms/comms.py b/libensemble/comms/comms.py index 2b31cf5b9..51042c463 100644 --- a/libensemble/comms/comms.py +++ b/libensemble/comms/comms.py @@ -255,9 +255,6 @@ def terminate(self, timeout=None): self.handle.join(timeout=timeout) if self.running: raise Timeout() - self.handle = None - self.inbox = None - self.outbox = None class QCommProcess(QCommLocal): @@ -277,6 +274,3 @@ def terminate(self, timeout=None): self.handle.join(timeout=timeout) if self.running: raise Timeout() - self.handle = None - self.inbox = None - self.outbox = None diff --git a/libensemble/comms/logs.py b/libensemble/comms/logs.py index de2454f8d..47f85f351 100644 --- a/libensemble/comms/logs.py +++ b/libensemble/comms/logs.py @@ -204,7 +204,6 @@ def exit_logger(): stat_timer.stop() stat_logger.info(f"Exiting ensemble at: {stat_timer.date_end} Time Taken: {stat_timer.elapsed}") stat_logger.handlers[0].close() - print("Manager logger closed") # If closing logs - each libE() call will log to a new file. # fh.close() diff --git a/libensemble/libE.py b/libensemble/libE.py index b5ddaa330..b283a82b4 100644 --- a/libensemble/libE.py +++ b/libensemble/libE.py @@ -460,9 +460,6 @@ def kill_proc_team(wcomms, timeout): wcomm.result(timeout=timeout) except Timeout: wcomm.terminate() - wcomm.handle = None - wcomm.inbox = None - wcomm.outbox = None def libE_local(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs, H0): diff --git a/libensemble/manager.py b/libensemble/manager.py index 094ef839b..69117916d 100644 --- a/libensemble/manager.py +++ b/libensemble/manager.py @@ -304,7 +304,6 @@ def _kill_workers(self) -> None: self.wcomms[w].send(STOP_TAG, MAN_SIGNAL_FINISH) if w == 0: self.wcomms[0].result() - self.wcomms[0] = None # --- Checkpointing logic diff --git a/libensemble/worker.py b/libensemble/worker.py index fcf0a5c57..1a96dbdd5 100644 --- a/libensemble/worker.py +++ b/libensemble/worker.py @@ -415,3 +415,4 @@ def run(self) -> None: self.gen_runner.shutdown() self.sim_runner.shutdown() self.EnsembleDirectory.copy_back() + Executor.executor.comm = None From 97c2c53aceed96b7b70e6501bd21661d510edb46 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 8 Mar 2024 16:12:10 -0600 Subject: [PATCH 060/288] clarification comment --- libensemble/worker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libensemble/worker.py b/libensemble/worker.py index 1a96dbdd5..bfbb82659 100644 --- a/libensemble/worker.py +++ b/libensemble/worker.py @@ -415,4 +415,4 @@ def run(self) -> None: self.gen_runner.shutdown() self.sim_runner.shutdown() self.EnsembleDirectory.copy_back() - Executor.executor.comm = None + Executor.executor.comm = None # so Executor can be pickled upon further libE calls From 5dc8dbd316f9c99252582f64800b1883b9b8e43e Mon Sep 17 00:00:00 2001 From: shudson Date: Fri, 8 Mar 2024 18:30:38 -0600 Subject: [PATCH 061/288] Refactor gpCAM gen to ask/tell and add wrapper --- .../gen_funcs/persistent_gen_wrapper.py | 28 ++++ libensemble/gen_funcs/persistent_gpCAM.py | 136 +++++++++--------- .../tests/regression_tests/test_gpCAM.py | 8 +- 3 files changed, 106 insertions(+), 66 deletions(-) create mode 100644 libensemble/gen_funcs/persistent_gen_wrapper.py diff --git a/libensemble/gen_funcs/persistent_gen_wrapper.py b/libensemble/gen_funcs/persistent_gen_wrapper.py new file mode 100644 index 000000000..9780a145f --- /dev/null +++ b/libensemble/gen_funcs/persistent_gen_wrapper.py @@ -0,0 +1,28 @@ +import inspect +from libensemble.tools.persistent_support import PersistentSupport +from libensemble.message_numbers import EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG + + +def persistent_gen_f(H, persis_info, gen_specs, libE_info): + + ps = PersistentSupport(libE_info, EVAL_GEN_TAG) + U = gen_specs["user"] + b = U.get("initial_batch_size") or U.get("batch_size") + calc_in = None + + generator = U["generator"] + if inspect.isclass(generator): + gen = generator(H, persis_info, gen_specs, libE_info) + else: + gen = generator + + tag = None + while tag not in [STOP_TAG, PERSIS_STOP]: + H_o = gen.ask(b) + tag, Work, calc_in = ps.send_recv(H_o) + gen.tell(calc_in) + + if hasattr(calc_in, "__len__"): + b = len(calc_in) + + return H_o, persis_info, FINISHED_PERSISTENT_GEN_TAG diff --git a/libensemble/gen_funcs/persistent_gpCAM.py b/libensemble/gen_funcs/persistent_gpCAM.py index 9b67798e9..5f4a8191b 100644 --- a/libensemble/gen_funcs/persistent_gpCAM.py +++ b/libensemble/gen_funcs/persistent_gpCAM.py @@ -10,7 +10,7 @@ from libensemble.tools.persistent_support import PersistentSupport __all__ = [ - "persistent_gpCAM_simple", + "GP_CAM_SIMPLE", "persistent_gpCAM_ask_tell", ] @@ -75,17 +75,12 @@ def _generate_mesh(lb, ub, num_points=10): return points -def _update_gp_and_eval_var(all_x, all_y, x_for_var, test_points, persis_info): +# TODO Make a class method +def _eval_var(my_gp2S, all_x, all_y, x_for_var, test_points, persis_info): """ - Update the GP using the points in all_x and their function values in - all_y. (We are assuming deterministic values in all_y, so we set the noise - to be 1e-8 when build the GP.) Then evaluates the posterior covariance at - points in x_for_var. If we have test points, calculate mean square error - at those points. + Evaluate the posterior covariance at points in x_for_var. + If we have test points, calculate mean square error at those points. """ - my_gp2S = GP(all_x, all_y, noise_variances=1e-12 * np.ones(len(all_y))) - my_gp2S.train() - # Obtain covariance in groups to prevent memory overload. n_rows = x_for_var.shape[0] var_vals = [] @@ -105,6 +100,7 @@ def _update_gp_and_eval_var(all_x, all_y, x_for_var, test_points, persis_info): f_est = my_gp2S.posterior_mean(test_points["x"])["f(x)"] mse = np.mean((f_est - test_points["f"]) ** 2) persis_info.setdefault("mean_squared_error", []).append(mse) + return np.array(var_vals) @@ -145,74 +141,86 @@ def _find_eligible_points(x_for_var, sorted_indices, r, batch_size): return np.array(eligible_points) -def persistent_gpCAM_simple(H_in, persis_info, gen_specs, libE_info): - """ - 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. - - .. seealso:: - `test_gpCAM.py `_ - """ # noqa - U = gen_specs["user"] - - test_points = _read_testpoints(U) - - batch_size, n, lb, ub, all_x, all_y, ps = _initialize_gpcAM(U, libE_info) - - # Send batches until manager sends stop tag - tag = None - persis_info["max_variance"] = [] - - if U.get("use_grid"): - num_points = 10 - x_for_var = _generate_mesh(lb, ub, num_points) - r_low_init, r_high_init = calculate_grid_distances(lb, ub, num_points) - - while tag not in [STOP_TAG, PERSIS_STOP]: - if all_x.shape[0] == 0: - x_new = persis_info["rand_stream"].uniform(lb, ub, (batch_size, n)) +class GP_CAM_SIMPLE: + # Choose whether functions are internal methods or not + def _initialize_gpcAM(self, user_specs): + """Extract user params""" + self.lb = np.array(user_specs["lb"]) + self.ub = np.array(user_specs["ub"]) + 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" + self.all_x = np.empty((0, self.n)) + self.all_y = np.empty((0, 1)) + np.random.seed(0) + + def __init__(self, H, persis_info, gen_specs, libE_info=None): + self.H = H + self.persis_info = persis_info + self.gen_specs = gen_specs + self.libE_info = libE_info + + self.U = self.gen_specs["user"] + self.test_points = _read_testpoints(self.U) + self._initialize_gpcAM(self.U) + self.my_gp2S = None + self.noise = 1e-12 + self.x_for_var = None + self.var_vals = None + + if self.U.get("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 ask(self, n_trials): + if self.all_x.shape[0] == 0: + x_new = self.persis_info["rand_stream"].uniform(self.lb, self.ub, (n_trials, self.n)) else: - if not U.get("use_grid"): - x_for_var = persis_info["rand_stream"].uniform(lb, ub, (10 * batch_size, n)) - var_vals = _update_gp_and_eval_var(all_x, all_y, x_for_var, test_points, persis_info) - - if U.get("use_grid"): - r_high = r_high_init - r_low = r_low_init + if not self.U.get("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(-var_vals) - while len(x_new) < batch_size: - x_new = _find_eligible_points(x_for_var, sorted_indices, r_cand, batch_size) - if len(x_new) < batch_size: + 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 - else: - x_new = x_for_var[np.argsort(var_vals)[-batch_size:]] - H_o = np.zeros(batch_size, dtype=gen_specs["out"]) - H_o["x"] = x_new - tag, Work, calc_in = ps.send_recv(H_o) + H_o = np.zeros(n_trials, dtype=self.gen_specs["out"]) + self.x_new = x_new + H_o["x"] = self.x_new + return H_o + def tell(self, calc_in): if calc_in is not None: y_new = np.atleast_2d(calc_in["f"]).T nan_indices = [i for i, fval in enumerate(y_new) if np.isnan(fval)] - x_new = np.delete(x_new, nan_indices, axis=0) + x_new = np.delete(self.x_new, nan_indices, axis=0) y_new = np.delete(y_new, nan_indices, axis=0) - all_x = np.vstack((all_x, x_new)) - all_y = np.vstack((all_y, y_new)) - # If final points are sent with PERSIS_STOP, update model and get final var_vals - if calc_in is not None: - # H_o not updated by default - is persis_info - if not U.get("use_grid"): - x_for_var = persis_info["rand_stream"].uniform(lb, ub, (10 * batch_size, n)) - var_vals = _update_gp_and_eval_var(all_x, all_y, x_for_var, test_points, persis_info) + self.all_x = np.vstack((self.all_x, x_new)) + self.all_y = np.vstack((self.all_y, y_new)) - return H_o, persis_info, FINISHED_PERSISTENT_GEN_TAG + if self.my_gp2S is None: + self.my_gp2S = GP(self.all_x, self.all_y, noise_variances=self.noise * np.ones(len(self.all_y))) + else: + self.my_gp2S.tell(self.all_x, self.all_y, noise_variances=self.noise * np.ones(len(self.all_y))) + self.my_gp2S.train() + + if not self.U.get("use_grid"): + n_trials = len(y_new) + self.x_for_var = self.persis_info["rand_stream"].uniform(self.lb, self.ub, (10 * n_trials, self.n)) + + self.var_vals = _eval_var( + self.my_gp2S, self.all_x, self.all_y, self.x_for_var, self.test_points, self.persis_info + ) def persistent_gpCAM_ask_tell(H_in, persis_info, gen_specs, libE_info): diff --git a/libensemble/tests/regression_tests/test_gpCAM.py b/libensemble/tests/regression_tests/test_gpCAM.py index 06c49ea5a..2504f6a1f 100644 --- a/libensemble/tests/regression_tests/test_gpCAM.py +++ b/libensemble/tests/regression_tests/test_gpCAM.py @@ -23,7 +23,9 @@ import numpy as np from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f -from libensemble.gen_funcs.persistent_gpCAM import persistent_gpCAM_ask_tell, persistent_gpCAM_simple + +from libensemble.gen_funcs.persistent_gen_wrapper import persistent_gen_f +from libensemble.gen_funcs.persistent_gpCAM import GP_CAM_SIMPLE, persistent_gpCAM_ask_tell # Import libEnsemble items for this test from libensemble.libE import libE @@ -62,11 +64,13 @@ for inst in range(3): if inst == 0: - gen_specs["gen_f"] = persistent_gpCAM_simple + gen_specs["gen_f"] = persistent_gen_f + gen_specs["user"]["generator"] = GP_CAM_SIMPLE num_batches = 10 exit_criteria = {"sim_max": num_batches * batch_size, "wallclock_max": 300} libE_specs["save_every_k_gens"] = 150 libE_specs["H_file_prefix"] = "gpCAM_nongrid" + if inst == 1: gen_specs["user"]["use_grid"] = True gen_specs["user"]["test_points_file"] = "gpCAM_nongrid_after_gen_150.npy" From 73d4b4c6d1d0f92d86f7956351f6aa5b8cab7069 Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 11 Mar 2024 10:06:26 -0500 Subject: [PATCH 062/288] bugfix --- libensemble/worker.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libensemble/worker.py b/libensemble/worker.py index bfbb82659..10823ad8a 100644 --- a/libensemble/worker.py +++ b/libensemble/worker.py @@ -415,4 +415,5 @@ def run(self) -> None: self.gen_runner.shutdown() self.sim_runner.shutdown() self.EnsembleDirectory.copy_back() - Executor.executor.comm = None # so Executor can be pickled upon further libE calls + if Executor.executor is not None: + Executor.executor.comm = None # so Executor can be pickled upon further libE calls From 6984383836ba560004f032277678155ce75f2b73 Mon Sep 17 00:00:00 2001 From: shudson Date: Tue, 19 Mar 2024 11:41:26 -0500 Subject: [PATCH 063/288] Mirror gpCAM renaming --- libensemble/gen_funcs/persistent_gpCAM.py | 34 +++++++++++------------ 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/libensemble/gen_funcs/persistent_gpCAM.py b/libensemble/gen_funcs/persistent_gpCAM.py index 5f4a8191b..013b5885f 100644 --- a/libensemble/gen_funcs/persistent_gpCAM.py +++ b/libensemble/gen_funcs/persistent_gpCAM.py @@ -76,7 +76,7 @@ def _generate_mesh(lb, ub, num_points=10): # TODO Make a class method -def _eval_var(my_gp2S, all_x, all_y, x_for_var, test_points, persis_info): +def _eval_var(my_gp, all_x, all_y, x_for_var, test_points, persis_info): """ Evaluate the posterior covariance at points in x_for_var. If we have test points, calculate mean square error at those points. @@ -88,7 +88,7 @@ def _eval_var(my_gp2S, all_x, all_y, x_for_var, test_points, persis_info): for start_idx in range(0, n_rows, group_size): end_idx = min(start_idx + group_size, n_rows) - var_vals_group = my_gp2S.posterior_covariance(x_for_var[start_idx:end_idx], variance_only=True)["v(x)"] + var_vals_group = my_gp.posterior_covariance(x_for_var[start_idx:end_idx], variance_only=True)["v(x)"] var_vals.extend(var_vals_group) assert len(var_vals) == n_rows, "Something wrong with the grouping" @@ -97,14 +97,14 @@ def _eval_var(my_gp2S, all_x, all_y, x_for_var, test_points, persis_info): persis_info.setdefault("mean_variance", []).append(np.mean(var_vals)) if test_points is not None: - f_est = my_gp2S.posterior_mean(test_points["x"])["f(x)"] + f_est = my_gp.posterior_mean(test_points["x"])["f(x)"] mse = np.mean((f_est - test_points["f"]) ** 2) persis_info.setdefault("mean_squared_error", []).append(mse) return np.array(var_vals) -def calculate_grid_distances(lb, ub, num_points): +def _calculate_grid_distances(lb, ub, num_points): """Calculate minimum and maximum distances between points in grid""" num_points = [num_points] * len(lb) spacings = [(ub[i] - lb[i]) / (num_points[i] - 1) for i in range(len(lb))] @@ -113,7 +113,7 @@ def calculate_grid_distances(lb, ub, num_points): return min_distance, max_distance -def is_point_far_enough(point, eligible_points, r): +def _is_point_far_enough(point, eligible_points, r): """Check if point is at least r distance away from all points in eligible_points.""" for ep in eligible_points: if np.linalg.norm(point - ep) < r: @@ -134,7 +134,7 @@ def _find_eligible_points(x_for_var, sorted_indices, r, batch_size): eligible_points = [] for idx in sorted_indices: point = x_for_var[idx] - if is_point_far_enough(point, eligible_points, r): + if _is_point_far_enough(point, eligible_points, r): eligible_points.append(point) if len(eligible_points) == batch_size: break @@ -164,7 +164,7 @@ def __init__(self, H, persis_info, gen_specs, libE_info=None): self.U = self.gen_specs["user"] self.test_points = _read_testpoints(self.U) self._initialize_gpcAM(self.U) - self.my_gp2S = None + self.my_gp = None self.noise = 1e-12 self.x_for_var = None self.var_vals = None @@ -172,7 +172,7 @@ def __init__(self, H, persis_info, gen_specs, libE_info=None): if self.U.get("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) + self.r_low_init, self.r_high_init = _calculate_grid_distances(self.lb, self.ub, self.num_points) def ask(self, n_trials): if self.all_x.shape[0] == 0: @@ -208,18 +208,18 @@ def tell(self, calc_in): self.all_x = np.vstack((self.all_x, x_new)) self.all_y = np.vstack((self.all_y, y_new)) - if self.my_gp2S is None: - self.my_gp2S = GP(self.all_x, self.all_y, noise_variances=self.noise * np.ones(len(self.all_y))) + if self.my_gp is None: + self.my_gp = GP(self.all_x, self.all_y, noise_variances=self.noise * np.ones(len(self.all_y))) else: - self.my_gp2S.tell(self.all_x, self.all_y, noise_variances=self.noise * np.ones(len(self.all_y))) - self.my_gp2S.train() + self.my_gp.tell(self.all_x, self.all_y, noise_variances=self.noise * np.ones(len(self.all_y))) + self.my_gp.train() if not self.U.get("use_grid"): n_trials = len(y_new) self.x_for_var = self.persis_info["rand_stream"].uniform(self.lb, self.ub, (10 * n_trials, self.n)) self.var_vals = _eval_var( - self.my_gp2S, self.all_x, self.all_y, self.x_for_var, self.test_points, self.persis_info + self.my_gp, self.all_x, self.all_y, self.x_for_var, self.test_points, self.persis_info ) @@ -250,15 +250,15 @@ def persistent_gpCAM_ask_tell(H_in, persis_info, gen_specs, libE_info): if first_call: # Initialize GP - my_gp2S = GP(all_x, all_y, noise_variances=1e-8 * np.ones(len(all_y))) + my_gp = GP(all_x, all_y, noise_variances=1e-8 * np.ones(len(all_y))) first_call = False else: - my_gp2S.tell(all_x, all_y, noise_variances=1e-8 * np.ones(len(all_y))) + my_gp.tell(all_x, all_y, noise_variances=1e-8 * np.ones(len(all_y))) - my_gp2S.train() + my_gp.train() start = time.time() - x_new = my_gp2S.ask( + x_new = my_gp.ask( bounds=np.column_stack((lb, ub)), n=batch_size, pop_size=batch_size, From 1fbc03f5945da20825f405163c6ec5cb8af977c7 Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 25 Mar 2024 16:03:12 -0500 Subject: [PATCH 064/288] make generator a field of gen_specs (instead of passing in class-instance to gen_f field) --- libensemble/specs.py | 8 +++++++- libensemble/utils/runners.py | 2 +- libensemble/utils/validators.py | 2 ++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/libensemble/specs.py b/libensemble/specs.py index bd80e5e00..546b63dca 100644 --- a/libensemble/specs.py +++ b/libensemble/specs.py @@ -4,6 +4,7 @@ from pydantic import BaseModel, Field +from libensemble import Generator from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first from libensemble.resources.platforms import Platform @@ -72,12 +73,17 @@ class GenSpecs(BaseModel): Specifications for configuring a Generator Function. """ - gen_f: Optional[Any] = None + gen_f: Optional[Callable] = None """ Python function matching the ``gen_f`` interface. Produces parameters for evaluation by a simulator function, and makes decisions based on simulator function output. """ + generator: Optional[Generator] = None + """ + A pre-initialized generator object. + """ + inputs: Optional[List[str]] = Field(default=[], alias="in") """ List of **field names** out of the complete history to pass diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 25e0d7392..df91ae81b 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -18,7 +18,7 @@ def __new__(cls, specs): return super(Runner, GlobusComputeRunner).__new__(GlobusComputeRunner) if specs.get("threaded"): # TODO: undecided interface return super(Runner, ThreadRunner).__new__(ThreadRunner) - if hasattr(specs.get("gen_f", None), "ask"): + if hasattr(specs.get("generator", None), "ask"): return super(Runner, AskTellGenRunner).__new__(AskTellGenRunner) else: return super().__new__(Runner) diff --git a/libensemble/utils/validators.py b/libensemble/utils/validators.py index 0ecc2ef13..e6f7f9133 100644 --- a/libensemble/utils/validators.py +++ b/libensemble/utils/validators.py @@ -137,6 +137,7 @@ def check_provided_ufuncs(cls, values): if values.get("alloc_specs").alloc_f.__name__ != "give_pregenerated_sim_work": gen_specs = values.get("gen_specs") assert hasattr(gen_specs, "gen_f"), "Generator function not provided to GenSpecs." + assert isinstance(gen_specs.gen_f, Callable), "Generator function is not callable." return values @@ -229,6 +230,7 @@ def check_provided_ufuncs(self): if self.alloc_specs.alloc_f.__name__ != "give_pregenerated_sim_work": assert hasattr(self.gen_specs, "gen_f"), "Generator function not provided to GenSpecs." + assert isinstance(self.gen_specs.gen_f, Callable), "Generator function is not callable." return self From 3518e66dd147632dec69101f1d34d49732b689a9 Mon Sep 17 00:00:00 2001 From: jlnav Date: Tue, 26 Mar 2024 12:02:29 -0500 Subject: [PATCH 065/288] misc fixes --- libensemble/specs.py | 3 +-- .../test_1d_asktell_gen.py | 25 ++++++++----------- libensemble/utils/runners.py | 15 +++++------ libensemble/utils/validators.py | 10 +++----- 4 files changed, 23 insertions(+), 30 deletions(-) diff --git a/libensemble/specs.py b/libensemble/specs.py index 546b63dca..eeb65826d 100644 --- a/libensemble/specs.py +++ b/libensemble/specs.py @@ -4,7 +4,6 @@ from pydantic import BaseModel, Field -from libensemble import Generator from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first from libensemble.resources.platforms import Platform @@ -79,7 +78,7 @@ class GenSpecs(BaseModel): simulator function, and makes decisions based on simulator function output. """ - generator: Optional[Generator] = None + generator: Optional[object] = None """ A pre-initialized generator object. """ diff --git a/libensemble/tests/functionality_tests/test_1d_asktell_gen.py b/libensemble/tests/functionality_tests/test_1d_asktell_gen.py index efd515939..a20bc10fa 100644 --- a/libensemble/tests/functionality_tests/test_1d_asktell_gen.py +++ b/libensemble/tests/functionality_tests/test_1d_asktell_gen.py @@ -19,7 +19,6 @@ from libensemble import Generator from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f from libensemble.gen_funcs.persistent_sampling import _get_user_params -from libensemble.gen_funcs.sampling import latin_hypercube_sample as gen_f from libensemble.gen_funcs.sampling import lhs_sample from libensemble.libE import libE from libensemble.sim_funcs.rosenbrock import rosenbrock_eval as sim_f2 @@ -82,20 +81,16 @@ def final_tell(self, H_in): "out": [("f", float)], } - gen_specs_normal = { - "gen_f": gen_f, - "out": [("x", float, (1,))], - "user": { - "gen_batch_size": 500, - "lb": np.array([-3]), - "ub": np.array([3]), - }, - } + gen_out = [("x", float, (1,))] persis_info = add_unique_random_streams({}, nworkers + 1, seed=1234) - gen_one = LHS(persis_info[1]["rand_stream"], np.array([3]), np.array([-3]), 500, gen_specs_normal["out"]) - gen_specs_normal["gen_f"] = gen_one + GenOne = LHS(persis_info[1]["rand_stream"], np.array([3]), np.array([-3]), 500, gen_out) + + gen_specs_normal = { + "generator": GenOne, + "out": [("x", float, (1,))], + } exit_criteria = {"gen_max": 201} @@ -104,7 +99,7 @@ def final_tell(self, H_in): if is_manager: assert len(H) >= 201 print("\nlibEnsemble with NORMAL random sampling has generated enough points") - print(H[:20]) + print(H[:10]) sim_specs = { "sim_f": sim_f2, @@ -125,7 +120,7 @@ def final_tell(self, H_in): persis_info = add_unique_random_streams({}, nworkers + 1, seed=1234) gen_two = PersistentUniform(persis_info[1], gen_specs_persistent) - gen_specs_persistent["gen_f"] = gen_two + gen_specs_persistent["generator"] = gen_two alloc_specs = {"alloc_f": alloc_f} @@ -136,4 +131,4 @@ def final_tell(self, H_in): if is_manager: assert len(H) >= 201 print("\nlibEnsemble with PERSISTENT random sampling has generated enough points") - print(H[:20]) + print(H[:10]) diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index df91ae81b..802905786 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -93,21 +93,22 @@ def shutdown(self) -> None: class AskTellGenRunner(Runner): def __init__(self, specs): super().__init__(specs) + self.gen = specs.get("generator") def _persistent_result(self, calc_in, persis_info, libE_info): self.ps = PersistentSupport(libE_info, EVAL_GEN_TAG) tag = None - initial_batch = getattr(self.f, "initial_batch_size", 0) or libE_info["batch_size"] - H_out = self.f.initial_ask(initial_batch, calc_in) + initial_batch = getattr(self.gen, "initial_batch_size", 0) or libE_info["batch_size"] + H_out = self.gen.initial_ask(initial_batch, calc_in) tag, Work, H_in = self.ps.send_recv(H_out) while tag not in [STOP_TAG, PERSIS_STOP]: - batch_size = getattr(self.f, "batch_size", 0) or Work["libE_info"]["batch_size"] - self.f.tell(H_in) - H_out = self.f.ask(batch_size) + batch_size = getattr(self.gen, "batch_size", 0) or Work["libE_info"]["batch_size"] + self.gen.tell(H_in) + H_out = self.gen.ask(batch_size) tag, Work, H_in = self.ps.send_recv(H_out) - return self.f.final_tell(H_in), FINISHED_PERSISTENT_GEN_TAG + return self.gen.final_tell(H_in), FINISHED_PERSISTENT_GEN_TAG def _result(self, calc_in: npt.NDArray, persis_info: dict, libE_info: dict) -> (npt.NDArray, dict, Optional[int]): if libE_info.get("persistent"): return self._persistent_result(calc_in, persis_info, libE_info) - return self.f.ask(getattr(self.f, "batch_size", 0) or libE_info["batch_size"]) + return self.gen.ask(getattr(self.gen, "batch_size", 0) or libE_info["batch_size"]) diff --git a/libensemble/utils/validators.py b/libensemble/utils/validators.py index e6f7f9133..987102aeb 100644 --- a/libensemble/utils/validators.py +++ b/libensemble/utils/validators.py @@ -131,13 +131,12 @@ def check_H0(cls, values): @root_validator def check_provided_ufuncs(cls, values): sim_specs = values.get("sim_specs") - assert hasattr(sim_specs, "sim_f"), "Simulation function not provided to SimSpecs." assert isinstance(sim_specs.sim_f, Callable), "Simulation function is not callable." if values.get("alloc_specs").alloc_f.__name__ != "give_pregenerated_sim_work": gen_specs = values.get("gen_specs") - assert hasattr(gen_specs, "gen_f"), "Generator function not provided to GenSpecs." - assert isinstance(gen_specs.gen_f, Callable), "Generator function is not callable." + if gen_specs.gen_f is not None: + assert isinstance(gen_specs.gen_f, Callable), "Generator function is not callable." return values @@ -225,12 +224,11 @@ def check_H0(self): @model_validator(mode="after") def check_provided_ufuncs(self): - assert hasattr(self.sim_specs, "sim_f"), "Simulation function not provided to SimSpecs." assert isinstance(self.sim_specs.sim_f, Callable), "Simulation function is not callable." if self.alloc_specs.alloc_f.__name__ != "give_pregenerated_sim_work": - assert hasattr(self.gen_specs, "gen_f"), "Generator function not provided to GenSpecs." - assert isinstance(self.gen_specs.gen_f, Callable), "Generator function is not callable." + if self.gen_specs.gen_f is not None: + assert isinstance(self.gen_specs.gen_f, Callable), "Generator function is not callable." return self From 5eaa4eeb2c0082c81120fe78e8a84f013750598e Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 27 Mar 2024 15:49:54 -0500 Subject: [PATCH 066/288] first round of trying to write a class that interacts with a traditional persistent gen_f via sends and recvs --- libensemble/generators.py | 50 ++++++++++++++++++++++++++++++++++-- libensemble/utils/runners.py | 20 +++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 9bcc465dd..822281420 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -1,6 +1,10 @@ +import queue as thread_queue from abc import ABC, abstractmethod from typing import Iterable, Optional +from libensemble.comms.comms import QComm, QCommThread +from libensemble.gen_funcs.aposmm_localopt_support import simulate_recv_from_manager + class Generator(ABC): """ @@ -73,15 +77,57 @@ def ask(self, num_points: int) -> Iterable: Request the next set of points to evaluate. """ - def tell(self, results: Iterable) -> None: + def tell(self, results: Iterable, *args, **kwargs) -> None: """ Send the results of evaluations to the generator. """ - def final_tell(self, results: Iterable) -> Optional[Iterable]: + def final_tell(self, results: Iterable, *args, **kwargs) -> Optional[Iterable]: """ Send the last set of results to the generator, instruct it to cleanup, and optionally retrieve an updated final state of evaluations. This is a separate method to simplify the common pattern of noting internally if a specific tell is the last. This will be called only once. """ + + +class PersistentGenHandler(Generator): + """Implement ask/tell for traditionally written persistent generator functions""" + + def __init__(self, gen_f, H, persis_info, gen_specs, libE_info): + self.gen_f = gen_f + self.H = H + self.persis_info = persis_info + self.gen_specs = gen_specs + self.libE_info = libE_info + self.inbox = thread_queue.Queue() # sending betweween HERE and gen + self.outbox = thread_queue.Queue() + + self.comm = QComm(self.inbox, self.outbox) + self.libE_info["comm"] = self.comm # replacing comm so gen sends HERE instead of manager + self.gen = QCommThread( + self.gen_f, + None, + self.H, + self.persis_info, # note that self.gen's inbox/outbox are unused by the underlying gen + self.gen_specs, + self.libE_info, + user_function=True, + ) + self.gen.run() + + def initial_ask(self, num_points: int) -> Iterable: + return self.ask(num_points) + + def ask(self, num_points: int) -> Iterable: + _, self.last_ask = self.outbox.get() + return self.last_ask["calc_out"] + + def tell(self, results: Iterable) -> None: + tag, Work, H_in = simulate_recv_from_manager(results, self.gen_specs) + self.inbox.put(tag, Work) + self.inbox.put(tag, H_in) + + def final_tell(self, results: Iterable) -> Optional[Iterable]: + self.tell(results) + return self.handle.result() diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 802905786..d25f79170 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -6,6 +6,7 @@ import numpy.typing as npt from libensemble.comms.comms import QCommThread +from libensemble.generators import PersistentGenHandler from libensemble.message_numbers import EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG from libensemble.tools.persistent_support import PersistentSupport @@ -112,3 +113,22 @@ def _result(self, calc_in: npt.NDArray, persis_info: dict, libE_info: dict) -> ( if libE_info.get("persistent"): return self._persistent_result(calc_in, persis_info, libE_info) return self.gen.ask(getattr(self.gen, "batch_size", 0) or libE_info["batch_size"]) + + +class WrappedTraditionalGenRunner(Runner): + def __init__(self, specs): + super().__init__(specs) + + def _result(self, calc_in: npt.NDArray, persis_info: dict, libE_info: dict) -> (npt.NDArray, dict, Optional[int]): + self.ps = PersistentSupport(libE_info, EVAL_GEN_TAG) + tag = None + initial_batch = getattr(self.gen, "initial_batch_size", 0) or libE_info["batch_size"] + wrapper = PersistentGenHandler(self.f, calc_in, persis_info, self.specs, libE_info) + out = wrapper.ask(initial_batch) + tag, Work, H_in = self.ps.send_recv(out) + while tag not in [STOP_TAG, PERSIS_STOP]: + batch_size = getattr(self.gen, "batch_size", 0) or Work["libE_info"]["batch_size"] + wrapper.tell(H_in) + out = wrapper.ask(batch_size) + tag, Work, H_in = self.ps.send_recv(out) + return wrapper.final_tell(H_in), FINISHED_PERSISTENT_GEN_TAG From 4309409760b3589f5da95511db3c5c87b969d58c Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 28 Mar 2024 14:36:28 -0500 Subject: [PATCH 067/288] remove tentative code from runners.py to run a wrapped generator, start persistent_gen upon calling initial_ask --- libensemble/generators.py | 47 ++++++++++++++++++------------------ libensemble/utils/runners.py | 20 --------------- 2 files changed, 24 insertions(+), 43 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 822281420..73db2d319 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -3,7 +3,7 @@ from typing import Iterable, Optional from libensemble.comms.comms import QComm, QCommThread -from libensemble.gen_funcs.aposmm_localopt_support import simulate_recv_from_manager +from libensemble.message_numbers import EVAL_GEN_TAG, PERSIS_STOP class Generator(ABC): @@ -91,43 +91,44 @@ def final_tell(self, results: Iterable, *args, **kwargs) -> Optional[Iterable]: """ -class PersistentGenHandler(Generator): - """Implement ask/tell for traditionally written persistent generator functions""" +class LibEnsembleGenTranslator(Generator): + """Implement ask/tell for traditionally written libEnsemble persistent generator functions. + Still requires a handful of libEnsemble-specific data-structures on initialization. + """ - def __init__(self, gen_f, H, persis_info, gen_specs, libE_info): - self.gen_f = gen_f - self.H = H - self.persis_info = persis_info + def __init__(self, gen_f, History, persis_info, gen_specs, libE_info): self.gen_specs = gen_specs - self.libE_info = libE_info self.inbox = thread_queue.Queue() # sending betweween HERE and gen self.outbox = thread_queue.Queue() - self.comm = QComm(self.inbox, self.outbox) - self.libE_info["comm"] = self.comm # replacing comm so gen sends HERE instead of manager + comm = QComm(self.inbox, self.outbox) + libE_info["comm"] = comm # replacing comm so gen sends HERE instead of manager self.gen = QCommThread( - self.gen_f, + gen_f, None, - self.H, - self.persis_info, # note that self.gen's inbox/outbox are unused by the underlying gen + History, + persis_info, # note that self.gen's inbox/outbox are unused by the underlying gen self.gen_specs, - self.libE_info, + libE_info, user_function=True, ) - self.gen.run() - def initial_ask(self, num_points: int) -> Iterable: + def initial_ask(self, num_points: int, *args) -> Iterable: + if not self.gen.running: + self.gen.run() return self.ask(num_points) def ask(self, num_points: int) -> Iterable: _, self.last_ask = self.outbox.get() - return self.last_ask["calc_out"] + return self.last_ask["calc_out"][:num_points] - def tell(self, results: Iterable) -> None: - tag, Work, H_in = simulate_recv_from_manager(results, self.gen_specs) - self.inbox.put(tag, Work) - self.inbox.put(tag, H_in) + def tell(self, results: Iterable, tag=EVAL_GEN_TAG) -> None: + if results is not None: + self.inbox.put((tag, {"libE_info": {"H_rows": results["sim_id"], "persistent": True}})) + else: + self.inbox.put((tag, None)) + self.inbox.put((0, results)) def final_tell(self, results: Iterable) -> Optional[Iterable]: - self.tell(results) - return self.handle.result() + self.tell(results, PERSIS_STOP) + return self.gen.result() diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index d25f79170..802905786 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -6,7 +6,6 @@ import numpy.typing as npt from libensemble.comms.comms import QCommThread -from libensemble.generators import PersistentGenHandler from libensemble.message_numbers import EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG from libensemble.tools.persistent_support import PersistentSupport @@ -113,22 +112,3 @@ def _result(self, calc_in: npt.NDArray, persis_info: dict, libE_info: dict) -> ( if libE_info.get("persistent"): return self._persistent_result(calc_in, persis_info, libE_info) return self.gen.ask(getattr(self.gen, "batch_size", 0) or libE_info["batch_size"]) - - -class WrappedTraditionalGenRunner(Runner): - def __init__(self, specs): - super().__init__(specs) - - def _result(self, calc_in: npt.NDArray, persis_info: dict, libE_info: dict) -> (npt.NDArray, dict, Optional[int]): - self.ps = PersistentSupport(libE_info, EVAL_GEN_TAG) - tag = None - initial_batch = getattr(self.gen, "initial_batch_size", 0) or libE_info["batch_size"] - wrapper = PersistentGenHandler(self.f, calc_in, persis_info, self.specs, libE_info) - out = wrapper.ask(initial_batch) - tag, Work, H_in = self.ps.send_recv(out) - while tag not in [STOP_TAG, PERSIS_STOP]: - batch_size = getattr(self.gen, "batch_size", 0) or Work["libE_info"]["batch_size"] - wrapper.tell(H_in) - out = wrapper.ask(batch_size) - tag, Work, H_in = self.ps.send_recv(out) - return wrapper.final_tell(H_in), FINISHED_PERSISTENT_GEN_TAG From c74f6802b2df2ede402562529fbf3ecd9cbafc20 Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 28 Mar 2024 15:10:31 -0500 Subject: [PATCH 068/288] separate ask/tell aposmm into another test, add init_comms method for manager/worker to initialize itself, more adjustments --- libensemble/generators.py | 33 ++++-- libensemble/libE.py | 2 +- .../test_persistent_aposmm_nlopt_asktell.py | 109 ++++++++++++++++++ libensemble/utils/runners.py | 4 + 4 files changed, 136 insertions(+), 12 deletions(-) create mode 100644 libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py diff --git a/libensemble/generators.py b/libensemble/generators.py index 73db2d319..708d84514 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -3,6 +3,7 @@ from typing import Iterable, Optional from libensemble.comms.comms import QComm, QCommThread +from libensemble.executors import Executor from libensemble.message_numbers import EVAL_GEN_TAG, PERSIS_STOP @@ -96,35 +97,45 @@ class LibEnsembleGenTranslator(Generator): Still requires a handful of libEnsemble-specific data-structures on initialization. """ - def __init__(self, gen_f, History, persis_info, gen_specs, libE_info): + def __init__(self, gen_f, gen_specs, History=[], persis_info={}, libE_info={}): + self.gen_f = gen_f self.gen_specs = gen_specs + self.History = History + self.persis_info = persis_info + self.libE_info = libE_info + + def init_comms(self): self.inbox = thread_queue.Queue() # sending betweween HERE and gen self.outbox = thread_queue.Queue() comm = QComm(self.inbox, self.outbox) - libE_info["comm"] = comm # replacing comm so gen sends HERE instead of manager + self.libE_info["comm"] = comm # replacing comm so gen sends HERE instead of manager + self.libE_info["executor"] = Executor.executor + self.gen = QCommThread( - gen_f, + self.gen_f, None, - History, - persis_info, # note that self.gen's inbox/outbox are unused by the underlying gen + self.History, + self.persis_info, self.gen_specs, - libE_info, + self.libE_info, user_function=True, - ) + ) # note that self.gen's inbox/outbox are unused by the underlying gen - def initial_ask(self, num_points: int, *args) -> Iterable: + def initial_ask(self, num_points: int = 0, *args) -> Iterable: if not self.gen.running: self.gen.run() return self.ask(num_points) - def ask(self, num_points: int) -> Iterable: + def ask(self, num_points: int = 0) -> Iterable: _, self.last_ask = self.outbox.get() - return self.last_ask["calc_out"][:num_points] + if num_points: + return self.last_ask["calc_out"][:num_points] + return self.last_ask["calc_out"] def tell(self, results: Iterable, tag=EVAL_GEN_TAG) -> None: if results is not None: - self.inbox.put((tag, {"libE_info": {"H_rows": results["sim_id"], "persistent": True}})) + self.inbox.put((tag, {"libE_info": {"H_rows": results["sim_id"], "persistent": True, "executor": None}})) else: self.inbox.put((tag, None)) self.inbox.put((0, results)) diff --git a/libensemble/libE.py b/libensemble/libE.py index b283a82b4..22755c0b8 100644 --- a/libensemble/libE.py +++ b/libensemble/libE.py @@ -275,7 +275,7 @@ def manager( logger.info(f"libE version v{__version__}") if "out" in gen_specs and ("sim_id", int) in gen_specs["out"]: - if "libensemble.gen_funcs" not in gen_specs["gen_f"].__module__: + if hasattr(gen_specs["gen_f"], "__module__") and "libensemble.gen_funcs" not in gen_specs["gen_f"].__module__: logger.manager_warning(_USER_SIM_ID_WARNING) try: diff --git a/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py b/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py new file mode 100644 index 000000000..840c3f00d --- /dev/null +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py @@ -0,0 +1,109 @@ +""" +Runs libEnsemble with APOSMM with the NLopt local optimizer. + +Execute via one of the following commands (e.g. 3 workers): + mpiexec -np 4 python test_persistent_aposmm_nlopt.py + python test_persistent_aposmm_nlopt.py --nworkers 3 --comms local + python test_persistent_aposmm_nlopt.py --nworkers 3 --comms tcp + +When running with the above commands, the number of concurrent evaluations of +the objective function will be 2, as one of the three workers will be the +persistent generator. +""" + +# Do not change these lines - they are parsed by run-tests.sh +# TESTSUITE_COMMS: local mpi tcp +# TESTSUITE_NPROCS: 3 + +import sys +from math import gamma, pi, sqrt + +import numpy as np + +import libensemble.gen_funcs + +# Import libEnsemble items for this test +from libensemble.libE import libE +from libensemble.sim_funcs.six_hump_camel import six_hump_camel as sim_f + +libensemble.gen_funcs.rc.aposmm_optimizers = "nlopt" +from time import time + +from libensemble.alloc_funcs.persistent_aposmm_alloc import persistent_aposmm_alloc as alloc_f +from libensemble.gen_funcs.persistent_aposmm import aposmm as gen_f +from libensemble.generators import LibEnsembleGenTranslator +from libensemble.tests.regression_tests.support import six_hump_camel_minima as minima +from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output + +# Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). +if __name__ == "__main__": + nworkers, is_manager, libE_specs, _ = parse_args() + + if is_manager: + start_time = time() + + if nworkers < 2: + sys.exit("Cannot run with a persistent worker if only one worker -- aborting...") + + n = 2 + sim_specs = { + "sim_f": sim_f, + "in": ["x"], + "out": [("f", float)], + } + + gen_out = [ + ("x", float, n), + ("x_on_cube", float, n), + ("sim_id", int), + ("local_min", bool), + ("local_pt", bool), + ] + + gen_specs = { + "gen_f": gen_f, + "persis_in": ["f"] + [n[0] for n in gen_out], + "out": gen_out, + "user": { + "initial_sample_size": 100, + "sample_points": np.round(minima, 1), + "localopt_method": "LN_BOBYQA", + "rk_const": 0.5 * ((gamma(1 + (n / 2)) * 5) ** (1 / n)) / sqrt(pi), + "xtol_abs": 1e-6, + "ftol_abs": 1e-6, + "dist_to_bound_multiple": 0.5, + "max_active_runs": 6, + "lb": np.array([-3, -2]), + "ub": np.array([3, 2]), + }, + } + + alloc_specs = {"alloc_f": alloc_f} + + persis_info = add_unique_random_streams({}, nworkers + 1) + + aposmm_persis_info = persis_info[1] + + exit_criteria = {"sim_max": 2000} + + gen_specs.pop("gen_f") + gen_specs["generator"] = LibEnsembleGenTranslator(gen_f, gen_specs, persis_info=persis_info[1]) + gen_specs["generator"].initial_batch_size = gen_specs["user"]["initial_sample_size"] + + libE_specs["gen_on_manager"] = True + + # Perform the run + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) + + if is_manager: + print("[Manager]:", H[np.where(H["local_min"])]["x"]) + print("[Manager]: Time taken =", time() - start_time, flush=True) + + tol = 1e-5 + for m in minima: + # The minima are known on this test problem. + # We use their values to test APOSMM has identified all minima + print(np.min(np.sum((H[H["local_min"]]["x"] - m) ** 2, 1)), flush=True) + assert np.min(np.sum((H[H["local_min"]]["x"] - m) ** 2, 1)) < tol + + save_libE_output(H, persis_info, __file__, nworkers) diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 802905786..49c634440 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -99,6 +99,10 @@ def _persistent_result(self, calc_in, persis_info, libE_info): self.ps = PersistentSupport(libE_info, EVAL_GEN_TAG) tag = None initial_batch = getattr(self.gen, "initial_batch_size", 0) or libE_info["batch_size"] + if hasattr(self.gen, "init_comms"): + self.gen.persis_info = persis_info + self.gen.libE_info = persis_info + self.gen.init_comms() H_out = self.gen.initial_ask(initial_batch, calc_in) tag, Work, H_in = self.ps.send_recv(H_out) while tag not in [STOP_TAG, PERSIS_STOP]: From 4004138b1e6f5e964d4125a7093cf31352b31505 Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 1 Apr 2024 16:25:35 -0500 Subject: [PATCH 069/288] first round of adding a unit test for APOSMM wrapped with the translator class --- .../unit_tests/test_persistent_aposmm.py | 100 +++++++++++++++++- 1 file changed, 96 insertions(+), 4 deletions(-) diff --git a/libensemble/tests/unit_tests/test_persistent_aposmm.py b/libensemble/tests/unit_tests/test_persistent_aposmm.py index b08bc85fa..2c18bff80 100644 --- a/libensemble/tests/unit_tests/test_persistent_aposmm.py +++ b/libensemble/tests/unit_tests/test_persistent_aposmm.py @@ -168,8 +168,100 @@ def test_standalone_persistent_aposmm_combined_func(): assert persis_info.get("run_order"), "Standalone persistent_aposmm didn't do any localopt runs" +@pytest.mark.extra +def test_asktell_with_persistent_aposmm(): + from math import gamma, pi, sqrt + + import libensemble.gen_funcs + from libensemble.generators import LibEnsembleGenTranslator + from libensemble.message_numbers import FINISHED_PERSISTENT_GEN_TAG + from libensemble.sim_funcs.six_hump_camel import six_hump_camel_func, six_hump_camel_grad + from libensemble.tests.regression_tests.support import six_hump_camel_minima as minima + + libensemble.gen_funcs.rc.aposmm_optimizers = "nlopt" + from libensemble.gen_funcs.persistent_aposmm import aposmm + + persis_info = {"rand_stream": np.random.default_rng(1), "nworkers": 4} + + n = 2 + eval_max = 2000 + + gen_out = [("x", float, n), ("x_on_cube", float, n), ("sim_id", int), ("local_min", bool), ("local_pt", bool)] + + gen_specs = { + "in": ["x", "f", "grad", "local_pt", "sim_id", "sim_ended", "x_on_cube", "local_min"], + "out": gen_out, + "user": { + "initial_sample_size": 100, + # 'localopt_method': 'LD_MMA', # Needs gradients + "sample_points": np.round(minima, 1), + "localopt_method": "LN_BOBYQA", + "rk_const": 0.5 * ((gamma(1 + (n / 2)) * 5) ** (1 / n)) / sqrt(pi), + "xtol_abs": 1e-6, + "ftol_abs": 1e-6, + "dist_to_bound_multiple": 0.5, + "max_active_runs": 6, + "lb": np.array([-3, -2]), + "ub": np.array([3, 2]), + }, + } + + APOSMM = LibEnsembleGenTranslator(aposmm, gen_specs, persis_info=persis_info) + APOSMM.init_comms() + initial_sample = APOSMM.initial_ask() + initial_results = np.zeros( + len(initial_sample), dtype=gen_out + [("sim_ended", bool), ("f", float), ("grad", float, 2)] + ) + + total_evals = 0 + eval_max = 300 + + for field in gen_specs["out"]: + initial_results[field[0]] = initial_sample[field[0]] + + for i in initial_sample["sim_id"]: + initial_results[i]["sim_ended"] = True + initial_results[i]["f"] = six_hump_camel_func(initial_sample["x"][i]) + initial_results[i]["grad"] = six_hump_camel_grad(initial_sample["x"][i]) + total_evals += 1 + + APOSMM.tell(initial_results) + + while total_evals < eval_max: + if total_evals >= 105: + import ipdb + + ipdb.set_trace() + sample = APOSMM.ask() + results = np.zeros(len(sample), dtype=gen_out + [("sim_ended", bool), ("f", float), ("grad", float, 2)]) + for field in gen_specs["out"]: + results[field[0]] = sample[field[0]] + for i in range(len(sample)): + results[i]["sim_ended"] = True + results[i]["f"] = six_hump_camel_func(sample["x"][i]) + results[i]["grad"] = six_hump_camel_grad(sample["x"][i]) + total_evals += 1 + APOSMM.tell(results) + H, persis_info, exit_code = APOSMM.final_tell(None) + + assert exit_code == FINISHED_PERSISTENT_GEN_TAG, "Standalone persistent_aposmm didn't exit correctly" + assert np.sum(H["sim_ended"]) >= eval_max, "Standalone persistent_aposmm, didn't evaluate enough points" + assert persis_info.get("run_order"), "Standalone persistent_aposmm didn't do any localopt runs" + + tol = 1e-3 + min_found = 0 + for m in minima: + # The minima are known on this test problem. + # We use their values to test APOSMM has identified all minima + print(np.min(np.sum((H[H["local_min"]]["x"] - m) ** 2, 1)), flush=True) + if np.min(np.sum((H[H["local_min"]]["x"] - m) ** 2, 1)) < tol: + min_found += 1 + assert min_found >= 6, f"Found {min_found} minima" + + if __name__ == "__main__": - test_persis_aposmm_localopt_test() - test_update_history_optimal() - test_standalone_persistent_aposmm() - test_standalone_persistent_aposmm_combined_func() + # test_persis_aposmm_localopt_test() + # test_update_history_optimal() + # test_standalone_persistent_aposmm() + # test_standalone_persistent_aposmm_combined_func() + test_asktell_with_persistent_aposmm() From e33391b4eb89cd4ee8e8f4c2a01b5e436d99d7d8 Mon Sep 17 00:00:00 2001 From: jlnav Date: Tue, 2 Apr 2024 14:47:14 -0500 Subject: [PATCH 070/288] dont need gradient eval, note tentative minima from aposmm, fix for loop temp var overwriting import, bump number of "sims" --- .../unit_tests/test_persistent_aposmm.py | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/libensemble/tests/unit_tests/test_persistent_aposmm.py b/libensemble/tests/unit_tests/test_persistent_aposmm.py index 2c18bff80..c71c4ea45 100644 --- a/libensemble/tests/unit_tests/test_persistent_aposmm.py +++ b/libensemble/tests/unit_tests/test_persistent_aposmm.py @@ -175,7 +175,7 @@ def test_asktell_with_persistent_aposmm(): import libensemble.gen_funcs from libensemble.generators import LibEnsembleGenTranslator from libensemble.message_numbers import FINISHED_PERSISTENT_GEN_TAG - from libensemble.sim_funcs.six_hump_camel import six_hump_camel_func, six_hump_camel_grad + from libensemble.sim_funcs.six_hump_camel import six_hump_camel_func from libensemble.tests.regression_tests.support import six_hump_camel_minima as minima libensemble.gen_funcs.rc.aposmm_optimizers = "nlopt" @@ -189,7 +189,7 @@ def test_asktell_with_persistent_aposmm(): gen_out = [("x", float, n), ("x_on_cube", float, n), ("sim_id", int), ("local_min", bool), ("local_pt", bool)] gen_specs = { - "in": ["x", "f", "grad", "local_pt", "sim_id", "sim_ended", "x_on_cube", "local_min"], + "in": ["x", "f", "local_pt", "sim_id", "sim_ended", "x_on_cube", "local_min"], "out": gen_out, "user": { "initial_sample_size": 100, @@ -209,12 +209,10 @@ def test_asktell_with_persistent_aposmm(): APOSMM = LibEnsembleGenTranslator(aposmm, gen_specs, persis_info=persis_info) APOSMM.init_comms() initial_sample = APOSMM.initial_ask() - initial_results = np.zeros( - len(initial_sample), dtype=gen_out + [("sim_ended", bool), ("f", float), ("grad", float, 2)] - ) + initial_results = np.zeros(len(initial_sample), dtype=gen_out + [("sim_ended", bool), ("f", float)]) total_evals = 0 - eval_max = 300 + eval_max = 2000 for field in gen_specs["out"]: initial_results[field[0]] = initial_sample[field[0]] @@ -222,32 +220,34 @@ def test_asktell_with_persistent_aposmm(): for i in initial_sample["sim_id"]: initial_results[i]["sim_ended"] = True initial_results[i]["f"] = six_hump_camel_func(initial_sample["x"][i]) - initial_results[i]["grad"] = six_hump_camel_grad(initial_sample["x"][i]) total_evals += 1 APOSMM.tell(initial_results) + potential_minima = [] + while total_evals < eval_max: - if total_evals >= 105: - import ipdb - ipdb.set_trace() sample = APOSMM.ask() - results = np.zeros(len(sample), dtype=gen_out + [("sim_ended", bool), ("f", float), ("grad", float, 2)]) + results = np.zeros(len(sample), dtype=gen_out + [("sim_ended", bool), ("f", float)]) for field in gen_specs["out"]: results[field[0]] = sample[field[0]] for i in range(len(sample)): results[i]["sim_ended"] = True results[i]["f"] = six_hump_camel_func(sample["x"][i]) - results[i]["grad"] = six_hump_camel_grad(sample["x"][i]) total_evals += 1 + if any(results["local_min"]): # some points were passsed back to us newly marked as local minima + for m in results["x"][results["local_min"]]: + potential_minima.append(m) + results = results[~results["local_min"]] APOSMM.tell(results) H, persis_info, exit_code = APOSMM.final_tell(None) assert exit_code == FINISHED_PERSISTENT_GEN_TAG, "Standalone persistent_aposmm didn't exit correctly" - assert np.sum(H["sim_ended"]) >= eval_max, "Standalone persistent_aposmm, didn't evaluate enough points" assert persis_info.get("run_order"), "Standalone persistent_aposmm didn't do any localopt runs" + assert len(potential_minima) >= 6, f"Found {len(potential_minima)} minima" + tol = 1e-3 min_found = 0 for m in minima: From 1a9e676f7ce28dd7cc09c5e60dd2be1b342b021c Mon Sep 17 00:00:00 2001 From: jlnav Date: Tue, 2 Apr 2024 14:49:54 -0500 Subject: [PATCH 071/288] reenable other aposmm unit tests --- libensemble/tests/unit_tests/test_persistent_aposmm.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libensemble/tests/unit_tests/test_persistent_aposmm.py b/libensemble/tests/unit_tests/test_persistent_aposmm.py index c71c4ea45..5e9ad1659 100644 --- a/libensemble/tests/unit_tests/test_persistent_aposmm.py +++ b/libensemble/tests/unit_tests/test_persistent_aposmm.py @@ -260,8 +260,8 @@ def test_asktell_with_persistent_aposmm(): if __name__ == "__main__": - # test_persis_aposmm_localopt_test() - # test_update_history_optimal() - # test_standalone_persistent_aposmm() - # test_standalone_persistent_aposmm_combined_func() + test_persis_aposmm_localopt_test() + test_update_history_optimal() + test_standalone_persistent_aposmm() + test_standalone_persistent_aposmm_combined_func() test_asktell_with_persistent_aposmm() From 3b0c60a61cf9d09040508c456b3b2aa1f4750cc1 Mon Sep 17 00:00:00 2001 From: jlnav Date: Tue, 2 Apr 2024 15:49:21 -0500 Subject: [PATCH 072/288] fixup aposmm_nlopt ask/tell version regression test --- .../regression_tests/test_persistent_aposmm_nlopt_asktell.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py b/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py index 840c3f00d..afc4209f6 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py @@ -61,7 +61,6 @@ ] gen_specs = { - "gen_f": gen_f, "persis_in": ["f"] + [n[0] for n in gen_out], "out": gen_out, "user": { @@ -86,9 +85,9 @@ exit_criteria = {"sim_max": 2000} - gen_specs.pop("gen_f") gen_specs["generator"] = LibEnsembleGenTranslator(gen_f, gen_specs, persis_info=persis_info[1]) gen_specs["generator"].initial_batch_size = gen_specs["user"]["initial_sample_size"] + gen_specs["generator"].batch_size = gen_specs["user"]["max_active_runs"] libE_specs["gen_on_manager"] = True @@ -106,4 +105,5 @@ print(np.min(np.sum((H[H["local_min"]]["x"] - m) ** 2, 1)), flush=True) assert np.min(np.sum((H[H["local_min"]]["x"] - m) ** 2, 1)) < tol + persis_info[0]["comm"] = None save_libE_output(H, persis_info, __file__, nworkers) From daa4e7bbe0bdcbcd3b05a687a7ba844417706b5e Mon Sep 17 00:00:00 2001 From: jlnav Date: Tue, 2 Apr 2024 15:53:39 -0500 Subject: [PATCH 073/288] typo (thanks typos!) --- libensemble/tests/unit_tests/test_persistent_aposmm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libensemble/tests/unit_tests/test_persistent_aposmm.py b/libensemble/tests/unit_tests/test_persistent_aposmm.py index 5e9ad1659..ba4c949c6 100644 --- a/libensemble/tests/unit_tests/test_persistent_aposmm.py +++ b/libensemble/tests/unit_tests/test_persistent_aposmm.py @@ -236,7 +236,7 @@ def test_asktell_with_persistent_aposmm(): results[i]["sim_ended"] = True results[i]["f"] = six_hump_camel_func(sample["x"][i]) total_evals += 1 - if any(results["local_min"]): # some points were passsed back to us newly marked as local minima + if any(results["local_min"]): # some points were passed back to us newly marked as local minima for m in results["x"][results["local_min"]]: potential_minima.append(m) results = results[~results["local_min"]] From 87a7fc555947bfc0cff1ddda85662e8ecee891aa Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 3 Apr 2024 15:48:17 -0500 Subject: [PATCH 074/288] rename init_comms to setup, tentative APOSMM-specific ask/tell interface as subclass of LibEnsembleGenTranslator --- libensemble/generators.py | 48 +++++++++++++++++-- .../test_persistent_aposmm_nlopt_asktell.py | 2 - .../unit_tests/test_persistent_aposmm.py | 2 +- libensemble/utils/runners.py | 4 +- 4 files changed, 48 insertions(+), 8 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 708d84514..454c0f9d5 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -2,9 +2,13 @@ from abc import ABC, abstractmethod from typing import Iterable, Optional +import numpy as np + from libensemble.comms.comms import QComm, QCommThread from libensemble.executors import Executor +from libensemble.gen_funcs import persistent_aposmm from libensemble.message_numbers import EVAL_GEN_TAG, PERSIS_STOP +from libensemble.tools import add_unique_random_streams class Generator(ABC): @@ -97,14 +101,14 @@ class LibEnsembleGenTranslator(Generator): Still requires a handful of libEnsemble-specific data-structures on initialization. """ - def __init__(self, gen_f, gen_specs, History=[], persis_info={}, libE_info={}): - self.gen_f = gen_f + def __init__(self, gen_specs, History=[], persis_info={}, libE_info={}): + self.gen_f = gen_specs["gen_f"] self.gen_specs = gen_specs self.History = History self.persis_info = persis_info self.libE_info = libE_info - def init_comms(self): + def setup(self): self.inbox = thread_queue.Queue() # sending betweween HERE and gen self.outbox = thread_queue.Queue() @@ -143,3 +147,41 @@ def tell(self, results: Iterable, tag=EVAL_GEN_TAG) -> None: def final_tell(self, results: Iterable) -> Optional[Iterable]: self.tell(results, PERSIS_STOP) return self.gen.result() + + +class APOSMM(LibEnsembleGenTranslator): + def __init__(self, gen_specs, History=[], persis_info={}, libE_info={}): + gen_specs["gen_f"] = persistent_aposmm + if not persis_info: + persis_info = add_unique_random_streams({}, 1) + self.initial_batch_size = gen_specs["user"]["initial_sample_size"] + self.batch_size = gen_specs["user"]["max_active_runs"] + super().__init__(gen_specs, History, persis_info[1], libE_info) + + def setup(self): + super().setup() + + def initial_ask(self) -> Iterable: + return super().initial_ask() + + def ask(self) -> (Iterable, Iterable): + results = super().ask() + if any(results["local_min"]): + minima = results["x"][results["local_min"]] + results = results[~results["local_min"]] + return results, minima + return results, [] + + def tell(self, results: Iterable) -> None: + if "sim_ended" in results.dtype.names: + results["sim_ended"] = True + else: + new_results = np.zeros(len(results), dtype=results.dtype + [("sim_ended", bool)]) + for field in results.dtype.names: + new_results[field] = results[field] + new_results["sim_ended"] = True + results = new_results + super().tell(results) + + def final_tell(self, results: Iterable) -> (Iterable, dict, int): + return super().final_tell(results) diff --git a/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py b/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py index afc4209f6..d2442247e 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py @@ -81,8 +81,6 @@ persis_info = add_unique_random_streams({}, nworkers + 1) - aposmm_persis_info = persis_info[1] - exit_criteria = {"sim_max": 2000} gen_specs["generator"] = LibEnsembleGenTranslator(gen_f, gen_specs, persis_info=persis_info[1]) diff --git a/libensemble/tests/unit_tests/test_persistent_aposmm.py b/libensemble/tests/unit_tests/test_persistent_aposmm.py index ba4c949c6..31d76bc57 100644 --- a/libensemble/tests/unit_tests/test_persistent_aposmm.py +++ b/libensemble/tests/unit_tests/test_persistent_aposmm.py @@ -207,7 +207,7 @@ def test_asktell_with_persistent_aposmm(): } APOSMM = LibEnsembleGenTranslator(aposmm, gen_specs, persis_info=persis_info) - APOSMM.init_comms() + APOSMM.setup() initial_sample = APOSMM.initial_ask() initial_results = np.zeros(len(initial_sample), dtype=gen_out + [("sim_ended", bool), ("f", float)]) diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 49c634440..4872032e6 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -99,10 +99,10 @@ def _persistent_result(self, calc_in, persis_info, libE_info): self.ps = PersistentSupport(libE_info, EVAL_GEN_TAG) tag = None initial_batch = getattr(self.gen, "initial_batch_size", 0) or libE_info["batch_size"] - if hasattr(self.gen, "init_comms"): + if hasattr(self.gen, "setup"): self.gen.persis_info = persis_info self.gen.libE_info = persis_info - self.gen.init_comms() + self.gen.setup() H_out = self.gen.initial_ask(initial_batch, calc_in) tag, Work, H_in = self.ps.send_recv(H_out) while tag not in [STOP_TAG, PERSIS_STOP]: From 28c202591066705391261b6e53c6158d97e8ec58 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 3 Apr 2024 16:37:46 -0500 Subject: [PATCH 075/288] various fixes, update unit test --- libensemble/generators.py | 47 ++++++++++--------- .../unit_tests/test_persistent_aposmm.py | 32 +++++-------- 2 files changed, 37 insertions(+), 42 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 454c0f9d5..1caaa081d 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -6,7 +6,7 @@ from libensemble.comms.comms import QComm, QCommThread from libensemble.executors import Executor -from libensemble.gen_funcs import persistent_aposmm +from libensemble.gen_funcs.persistent_aposmm import aposmm from libensemble.message_numbers import EVAL_GEN_TAG, PERSIS_STOP from libensemble.tools import add_unique_random_streams @@ -129,7 +129,9 @@ def setup(self): def initial_ask(self, num_points: int = 0, *args) -> Iterable: if not self.gen.running: self.gen.run() - return self.ask(num_points) + if num_points: + return self.ask(num_points, *args) + return self.ask(*args) def ask(self, num_points: int = 0) -> Iterable: _, self.last_ask = self.outbox.get() @@ -151,37 +153,36 @@ def final_tell(self, results: Iterable) -> Optional[Iterable]: class APOSMM(LibEnsembleGenTranslator): def __init__(self, gen_specs, History=[], persis_info={}, libE_info={}): - gen_specs["gen_f"] = persistent_aposmm + gen_specs["gen_f"] = aposmm if not persis_info: - persis_info = add_unique_random_streams({}, 1) + persis_info = add_unique_random_streams({}, 4)[1] + persis_info["nworkers"] = 4 self.initial_batch_size = gen_specs["user"]["initial_sample_size"] self.batch_size = gen_specs["user"]["max_active_runs"] - super().__init__(gen_specs, History, persis_info[1], libE_info) - - def setup(self): - super().setup() + super().__init__(gen_specs, History, persis_info, libE_info) - def initial_ask(self) -> Iterable: - return super().initial_ask() + def initial_ask(self, *args) -> Iterable: + return super().initial_ask(args)[0] - def ask(self) -> (Iterable, Iterable): - results = super().ask() + def ask(self, *args) -> (Iterable, Iterable): + results = super().ask(args) if any(results["local_min"]): - minima = results["x"][results["local_min"]] + minima = results[results["local_min"]] results = results[~results["local_min"]] return results, minima return results, [] - def tell(self, results: Iterable) -> None: - if "sim_ended" in results.dtype.names: - results["sim_ended"] = True - else: - new_results = np.zeros(len(results), dtype=results.dtype + [("sim_ended", bool)]) - for field in results.dtype.names: - new_results[field] = results[field] - new_results["sim_ended"] = True - results = new_results - super().tell(results) + def tell(self, results: Iterable, tag=EVAL_GEN_TAG) -> None: + if results is not None: + if "sim_ended" in results.dtype.names: + results["sim_ended"] = True + else: + new_results = np.zeros(len(results), dtype=self.gen_specs["out"] + [("sim_ended", bool), ("f", float)]) + for field in results.dtype.names: + new_results[field] = results[field] + new_results["sim_ended"] = True + results = new_results + super().tell(results, tag) def final_tell(self, results: Iterable) -> (Iterable, dict, int): return super().final_tell(results) diff --git a/libensemble/tests/unit_tests/test_persistent_aposmm.py b/libensemble/tests/unit_tests/test_persistent_aposmm.py index 31d76bc57..e6b03eac1 100644 --- a/libensemble/tests/unit_tests/test_persistent_aposmm.py +++ b/libensemble/tests/unit_tests/test_persistent_aposmm.py @@ -173,15 +173,12 @@ def test_asktell_with_persistent_aposmm(): from math import gamma, pi, sqrt import libensemble.gen_funcs - from libensemble.generators import LibEnsembleGenTranslator + from libensemble.generators import APOSMM from libensemble.message_numbers import FINISHED_PERSISTENT_GEN_TAG from libensemble.sim_funcs.six_hump_camel import six_hump_camel_func from libensemble.tests.regression_tests.support import six_hump_camel_minima as minima libensemble.gen_funcs.rc.aposmm_optimizers = "nlopt" - from libensemble.gen_funcs.persistent_aposmm import aposmm - - persis_info = {"rand_stream": np.random.default_rng(1), "nworkers": 4} n = 2 eval_max = 2000 @@ -206,10 +203,10 @@ def test_asktell_with_persistent_aposmm(): }, } - APOSMM = LibEnsembleGenTranslator(aposmm, gen_specs, persis_info=persis_info) - APOSMM.setup() - initial_sample = APOSMM.initial_ask() - initial_results = np.zeros(len(initial_sample), dtype=gen_out + [("sim_ended", bool), ("f", float)]) + my_APOSMM = APOSMM(gen_specs) + my_APOSMM.setup() + initial_sample = my_APOSMM.initial_ask() + initial_results = np.zeros(len(initial_sample), dtype=gen_out + [("f", float)]) total_evals = 0 eval_max = 2000 @@ -218,30 +215,27 @@ def test_asktell_with_persistent_aposmm(): initial_results[field[0]] = initial_sample[field[0]] for i in initial_sample["sim_id"]: - initial_results[i]["sim_ended"] = True initial_results[i]["f"] = six_hump_camel_func(initial_sample["x"][i]) total_evals += 1 - APOSMM.tell(initial_results) + my_APOSMM.tell(initial_results) potential_minima = [] while total_evals < eval_max: - sample = APOSMM.ask() - results = np.zeros(len(sample), dtype=gen_out + [("sim_ended", bool), ("f", float)]) + sample, detected_minima = my_APOSMM.ask() + if len(detected_minima): + for m in detected_minima: + potential_minima.append(m) + results = np.zeros(len(sample), dtype=gen_out + [("f", float)]) for field in gen_specs["out"]: results[field[0]] = sample[field[0]] for i in range(len(sample)): - results[i]["sim_ended"] = True results[i]["f"] = six_hump_camel_func(sample["x"][i]) total_evals += 1 - if any(results["local_min"]): # some points were passed back to us newly marked as local minima - for m in results["x"][results["local_min"]]: - potential_minima.append(m) - results = results[~results["local_min"]] - APOSMM.tell(results) - H, persis_info, exit_code = APOSMM.final_tell(None) + my_APOSMM.tell(results) + H, persis_info, exit_code = my_APOSMM.final_tell(None) assert exit_code == FINISHED_PERSISTENT_GEN_TAG, "Standalone persistent_aposmm didn't exit correctly" assert persis_info.get("run_order"), "Standalone persistent_aposmm didn't do any localopt runs" From c34e95217e6546bc9d72fe1fcd66794013730230 Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 4 Apr 2024 10:29:37 -0500 Subject: [PATCH 076/288] .ask() interface now returns two arrays: first is points to evaluate, second is "updates" e.g. minima found or points to cancel. better typing. reg test uses APOSMM class --- libensemble/generators.py | 45 ++++++++++--------- .../test_persistent_aposmm_nlopt_asktell.py | 10 ++--- .../unit_tests/test_persistent_aposmm.py | 1 - libensemble/utils/runners.py | 6 ++- 4 files changed, 30 insertions(+), 32 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 1caaa081d..3a04466bd 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -3,6 +3,7 @@ from typing import Iterable, Optional import numpy as np +from numpy import typing as npt from libensemble.comms.comms import QComm, QCommThread from libensemble.executors import Executor @@ -68,7 +69,7 @@ def __init__(self, *args, **kwargs): my_generator = MyGenerator(my_parameter, batch_size=10) """ - def initial_ask(self, num_points: int, previous_results: Optional[Iterable]) -> Iterable: + def initial_ask(self, num_points: int, previous_results: Optional[Iterable], *args, **kwargs) -> Iterable: """ The initial set of generated points is often produced differently than subsequent sets. This is a separate method to simplify the common pattern of noting internally if a @@ -77,9 +78,9 @@ def initial_ask(self, num_points: int, previous_results: Optional[Iterable]) -> """ @abstractmethod - def ask(self, num_points: int) -> Iterable: + def ask(self, num_points: int, *args, **kwargs) -> (Iterable, Optional[Iterable]): """ - Request the next set of points to evaluate. + Request the next set of points to evaluate, and optionally any previous points to update. """ def tell(self, results: Iterable, *args, **kwargs) -> None: @@ -101,14 +102,16 @@ class LibEnsembleGenTranslator(Generator): Still requires a handful of libEnsemble-specific data-structures on initialization. """ - def __init__(self, gen_specs, History=[], persis_info={}, libE_info={}): + def __init__( + self, gen_specs: dict, History: npt.NDArray = [], persis_info: dict = {}, libE_info: dict = {} + ) -> None: self.gen_f = gen_specs["gen_f"] self.gen_specs = gen_specs self.History = History self.persis_info = persis_info self.libE_info = libE_info - def setup(self): + def setup(self) -> None: self.inbox = thread_queue.Queue() # sending betweween HERE and gen self.outbox = thread_queue.Queue() @@ -126,33 +129,31 @@ def setup(self): user_function=True, ) # note that self.gen's inbox/outbox are unused by the underlying gen - def initial_ask(self, num_points: int = 0, *args) -> Iterable: + def initial_ask(self, num_points: int = 0, *args) -> npt.NDArray: if not self.gen.running: self.gen.run() - if num_points: - return self.ask(num_points, *args) - return self.ask(*args) + return self.ask(num_points) - def ask(self, num_points: int = 0) -> Iterable: + def ask(self, num_points: int = 0) -> (Iterable, Optional[npt.NDArray]): _, self.last_ask = self.outbox.get() - if num_points: - return self.last_ask["calc_out"][:num_points] return self.last_ask["calc_out"] - def tell(self, results: Iterable, tag=EVAL_GEN_TAG) -> None: + def tell(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: if results is not None: self.inbox.put((tag, {"libE_info": {"H_rows": results["sim_id"], "persistent": True, "executor": None}})) else: self.inbox.put((tag, None)) self.inbox.put((0, results)) - def final_tell(self, results: Iterable) -> Optional[Iterable]: + def final_tell(self, results: npt.NDArray) -> (npt.NDArray, dict, int): self.tell(results, PERSIS_STOP) return self.gen.result() class APOSMM(LibEnsembleGenTranslator): - def __init__(self, gen_specs, History=[], persis_info={}, libE_info={}): + def __init__( + self, gen_specs: dict, History: npt.NDArray = [], persis_info: dict = {}, libE_info: dict = {} + ) -> None: gen_specs["gen_f"] = aposmm if not persis_info: persis_info = add_unique_random_streams({}, 4)[1] @@ -161,18 +162,18 @@ def __init__(self, gen_specs, History=[], persis_info={}, libE_info={}): self.batch_size = gen_specs["user"]["max_active_runs"] super().__init__(gen_specs, History, persis_info, libE_info) - def initial_ask(self, *args) -> Iterable: - return super().initial_ask(args)[0] + def initial_ask(self, num_points: int = 0, *args) -> npt.NDArray: + return super().initial_ask(num_points, args)[0] - def ask(self, *args) -> (Iterable, Iterable): - results = super().ask(args) + def ask(self, num_points: int = 0) -> (npt.NDArray, npt.NDArray): + results = super().ask(num_points) if any(results["local_min"]): minima = results[results["local_min"]] results = results[~results["local_min"]] return results, minima - return results, [] + return results, np.empty(0, dtype=self.gen_specs["out"]) - def tell(self, results: Iterable, tag=EVAL_GEN_TAG) -> None: + def tell(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: if results is not None: if "sim_ended" in results.dtype.names: results["sim_ended"] = True @@ -184,5 +185,5 @@ def tell(self, results: Iterable, tag=EVAL_GEN_TAG) -> None: results = new_results super().tell(results, tag) - def final_tell(self, results: Iterable) -> (Iterable, dict, int): + def final_tell(self, results: npt.NDArray) -> (npt.NDArray, dict, int): return super().final_tell(results) diff --git a/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py b/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py index d2442247e..b93920c78 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py @@ -30,8 +30,7 @@ from time import time from libensemble.alloc_funcs.persistent_aposmm_alloc import persistent_aposmm_alloc as alloc_f -from libensemble.gen_funcs.persistent_aposmm import aposmm as gen_f -from libensemble.generators import LibEnsembleGenTranslator +from libensemble.generators import APOSMM from libensemble.tests.regression_tests.support import six_hump_camel_minima as minima from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output @@ -77,15 +76,12 @@ }, } - alloc_specs = {"alloc_f": alloc_f} - persis_info = add_unique_random_streams({}, nworkers + 1) + alloc_specs = {"alloc_f": alloc_f} exit_criteria = {"sim_max": 2000} - gen_specs["generator"] = LibEnsembleGenTranslator(gen_f, gen_specs, persis_info=persis_info[1]) - gen_specs["generator"].initial_batch_size = gen_specs["user"]["initial_sample_size"] - gen_specs["generator"].batch_size = gen_specs["user"]["max_active_runs"] + gen_specs["generator"] = APOSMM(gen_specs, persis_info=persis_info[1]) libE_specs["gen_on_manager"] = True diff --git a/libensemble/tests/unit_tests/test_persistent_aposmm.py b/libensemble/tests/unit_tests/test_persistent_aposmm.py index e6b03eac1..da15e0b14 100644 --- a/libensemble/tests/unit_tests/test_persistent_aposmm.py +++ b/libensemble/tests/unit_tests/test_persistent_aposmm.py @@ -190,7 +190,6 @@ def test_asktell_with_persistent_aposmm(): "out": gen_out, "user": { "initial_sample_size": 100, - # 'localopt_method': 'LD_MMA', # Needs gradients "sample_points": np.round(minima, 1), "localopt_method": "LN_BOBYQA", "rk_const": 0.5 * ((gamma(1 + (n / 2)) * 5) ** (1 / n)) / sqrt(pi), diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 4872032e6..73d431164 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -3,6 +3,7 @@ import logging.handlers from typing import Optional +import numpy as np import numpy.typing as npt from libensemble.comms.comms import QCommThread @@ -101,14 +102,15 @@ def _persistent_result(self, calc_in, persis_info, libE_info): initial_batch = getattr(self.gen, "initial_batch_size", 0) or libE_info["batch_size"] if hasattr(self.gen, "setup"): self.gen.persis_info = persis_info - self.gen.libE_info = persis_info + self.gen.libE_info = libE_info self.gen.setup() H_out = self.gen.initial_ask(initial_batch, calc_in) tag, Work, H_in = self.ps.send_recv(H_out) while tag not in [STOP_TAG, PERSIS_STOP]: batch_size = getattr(self.gen, "batch_size", 0) or Work["libE_info"]["batch_size"] self.gen.tell(H_in) - H_out = self.gen.ask(batch_size) + points, updates = self.gen.ask(batch_size) + H_out = np.append(points, updates) tag, Work, H_in = self.ps.send_recv(H_out) return self.gen.final_tell(H_in), FINISHED_PERSISTENT_GEN_TAG From 1ac16c0b83666ccbd252ec24f8ce3b55b3f17837 Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 4 Apr 2024 11:03:23 -0500 Subject: [PATCH 077/288] fix AskTellGenRunner to combine two arrays as output from .ask --- libensemble/utils/runners.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 73d431164..8a0d21bde 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -109,8 +109,11 @@ def _persistent_result(self, calc_in, persis_info, libE_info): while tag not in [STOP_TAG, PERSIS_STOP]: batch_size = getattr(self.gen, "batch_size", 0) or Work["libE_info"]["batch_size"] self.gen.tell(H_in) - points, updates = self.gen.ask(batch_size) - H_out = np.append(points, updates) + points = self.gen.ask(batch_size) + if len(points) == 2: + H_out = np.append(points[0], points[1]) + else: + H_out = points tag, Work, H_in = self.ps.send_recv(H_out) return self.gen.final_tell(H_in), FINISHED_PERSISTENT_GEN_TAG From 1cad07d6362819c44fa866e034db08f567a621e0 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 5 Apr 2024 10:12:22 -0500 Subject: [PATCH 078/288] try fixing pounders import --- libensemble/gen_funcs/aposmm_localopt_support.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libensemble/gen_funcs/aposmm_localopt_support.py b/libensemble/gen_funcs/aposmm_localopt_support.py index 909bbccd7..d21bebef2 100644 --- a/libensemble/gen_funcs/aposmm_localopt_support.py +++ b/libensemble/gen_funcs/aposmm_localopt_support.py @@ -43,7 +43,7 @@ class APOSMMException(Exception): if "dfols" in optimizers: import dfols # noqa: F401 if "ibcdfo" in optimizers: - from ibcdfo import pounders # noqa: F401 + from ibcdfo.pounders import pounders # noqa: F401 if "scipy" in optimizers: from scipy import optimize as sp_opt # noqa: F401 if "external_localopt" in optimizers: From 604d4ddcb0171510c6f16ccb8e967bbb79e80fe1 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 5 Apr 2024 13:31:44 -0500 Subject: [PATCH 079/288] rearrange sim_ended setting logic, add a comment, try wrapping Surmise with translator class, add corresponding regression test --- libensemble/generators.py | 44 ++++-- ...est_persistent_surmise_killsims_asktell.py | 143 ++++++++++++++++++ libensemble/utils/runners.py | 2 +- 3 files changed, 177 insertions(+), 12 deletions(-) create mode 100644 libensemble/tests/regression_tests/test_persistent_surmise_killsims_asktell.py diff --git a/libensemble/generators.py b/libensemble/generators.py index 3a04466bd..5a82f91dd 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -8,6 +8,7 @@ from libensemble.comms.comms import QComm, QCommThread from libensemble.executors import Executor from libensemble.gen_funcs.persistent_aposmm import aposmm +from libensemble.gen_funcs.persistent_surmise_calib import surmise_calib from libensemble.message_numbers import EVAL_GEN_TAG, PERSIS_STOP from libensemble.tools import add_unique_random_streams @@ -129,6 +130,17 @@ def setup(self) -> None: user_function=True, ) # note that self.gen's inbox/outbox are unused by the underlying gen + def _set_sim_ended(self, results: npt.NDArray) -> npt.NDArray: + if "sim_ended" in results.dtype.names: + results["sim_ended"] = True + else: + new_results = np.zeros(len(results), dtype=self.gen_specs["out"] + [("sim_ended", bool), ("f", float)]) + for field in results.dtype.names: + new_results[field] = results[field] + new_results["sim_ended"] = True + results = new_results + return results + def initial_ask(self, num_points: int = 0, *args) -> npt.NDArray: if not self.gen.running: self.gen.run() @@ -140,6 +152,7 @@ def ask(self, num_points: int = 0) -> (Iterable, Optional[npt.NDArray]): def tell(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: if results is not None: + results = self._set_sim_ended(results) self.inbox.put((tag, {"libE_info": {"H_rows": results["sim_id"], "persistent": True, "executor": None}})) else: self.inbox.put((tag, None)) @@ -158,8 +171,6 @@ def __init__( if not persis_info: persis_info = add_unique_random_streams({}, 4)[1] persis_info["nworkers"] = 4 - self.initial_batch_size = gen_specs["user"]["initial_sample_size"] - self.batch_size = gen_specs["user"]["max_active_runs"] super().__init__(gen_specs, History, persis_info, libE_info) def initial_ask(self, num_points: int = 0, *args) -> npt.NDArray: @@ -174,15 +185,26 @@ def ask(self, num_points: int = 0) -> (npt.NDArray, npt.NDArray): return results, np.empty(0, dtype=self.gen_specs["out"]) def tell(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: - if results is not None: - if "sim_ended" in results.dtype.names: - results["sim_ended"] = True - else: - new_results = np.zeros(len(results), dtype=self.gen_specs["out"] + [("sim_ended", bool), ("f", float)]) - for field in results.dtype.names: - new_results[field] = results[field] - new_results["sim_ended"] = True - results = new_results + super().tell(results, tag) + + def final_tell(self, results: npt.NDArray) -> (npt.NDArray, dict, int): + return super().final_tell(results) + + +class Surmise(LibEnsembleGenTranslator): + def __init__( + self, gen_specs: dict, History: npt.NDArray = [], persis_info: dict = {}, libE_info: dict = {} + ) -> None: + gen_specs["gen_f"] = surmise_calib + super().__init__(gen_specs, History, persis_info, libE_info) + + def initial_ask(self, num_points: int = 0, *args) -> npt.NDArray: + return super().initial_ask(num_points, args)[0] + + def ask(self, num_points: int = 0) -> (npt.NDArray): + return super().ask(num_points) + + def tell(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: super().tell(results, tag) def final_tell(self, results: npt.NDArray) -> (npt.NDArray, dict, int): diff --git a/libensemble/tests/regression_tests/test_persistent_surmise_killsims_asktell.py b/libensemble/tests/regression_tests/test_persistent_surmise_killsims_asktell.py new file mode 100644 index 000000000..4116b5b6d --- /dev/null +++ b/libensemble/tests/regression_tests/test_persistent_surmise_killsims_asktell.py @@ -0,0 +1,143 @@ +""" +Tests libEnsemble's capability to kill/cancel simulations that are in progress. + +Execute via one of the following commands (e.g. 3 workers): + mpiexec -np 4 python test_persistent_surmise_killsims.py + python test_persistent_surmise_killsims.py --nworkers 3 --comms local + python test_persistent_surmise_killsims.py --nworkers 3 --comms tcp + +When running with the above commands, the number of concurrent evaluations of +the objective function will be 2, as one of the three workers will be the +persistent generator. + +This test is a smaller variant of test_persistent_surmise_calib.py, but which +subprocesses a compiled version of the borehole simulation. A delay is +added to simulations after the initial batch, so that the killing of running +simulations can be tested. This will only affect simulations that have already +been issued to a worker when the cancel request is registesred by the manager. + +See more information, see tutorial: +"Borehole Calibration with Selective Simulation Cancellation" +in the libEnsemble documentation. +""" + +# Do not change these lines - they are parsed by run-tests.sh +# TESTSUITE_COMMS: mpi local tcp +# TESTSUITE_NPROCS: 3 4 +# TESTSUITE_EXTRA: true +# TESTSUITE_OS_SKIP: OSX + +# Requires: +# Install Surmise package + +import os + +import numpy as np + +from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f +from libensemble.executors.executor import Executor +from libensemble.generators import Surmise + +# Import libEnsemble items for this test +from libensemble.libE import libE +from libensemble.sim_funcs.borehole_kills import borehole as sim_f +from libensemble.tests.regression_tests.common import build_borehole # current location +from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output + +# from libensemble import logger +# logger.set_level("DEBUG") # To get debug logging in ensemble.log + +if __name__ == "__main__": + nworkers, is_manager, libE_specs, _ = parse_args() + + n_init_thetas = 15 # Initial batch of thetas + n_x = 5 # No. of x values + nparams = 4 # No. of theta params + ndims = 3 # No. of x coordinates. + max_add_thetas = 20 # Max no. of thetas added for evaluation + step_add_theta = 10 # No. of thetas to generate per step, before emulator is rebuilt + n_explore_theta = 200 # No. of thetas to explore while selecting the next theta + obsvar = 10 ** (-1) # Constant for generating noise in obs + + # Batch mode until after init_sample_size (add one theta to batch for observations) + init_sample_size = (n_init_thetas + 1) * n_x + + # Stop after max_emul_runs runs of the emulator + max_evals = init_sample_size + max_add_thetas * n_x + + sim_app = os.path.join(os.getcwd(), "borehole.x") + if not os.path.isfile(sim_app): + build_borehole() + + exctr = Executor() # Run serial sub-process in place + exctr.register_app(full_path=sim_app, app_name="borehole") + + # Subprocess variant creates input and output files for each sim + libE_specs["sim_dirs_make"] = True # To keep all - make sim dirs + # libE_specs["use_worker_dirs"] = True # To overwrite - make worker dirs only + + # Rename ensemble dir for non-interference with other regression tests + libE_specs["ensemble_dir_path"] = "ensemble_calib_kills" + + sim_specs = { + "sim_f": sim_f, + "in": ["x", "thetas"], + "out": [ + ("f", float), + ("sim_killed", bool), # "sim_killed" is used only for display at the end of this test + ], + "user": { + "num_obs": n_x, + "init_sample_size": init_sample_size, + }, + } + + gen_out = [ + ("x", float, ndims), + ("thetas", float, nparams), + ("priority", int), + ("obs", float, n_x), + ("obsvar", float, n_x), + ] + + gen_specs = { + "persis_in": [o[0] for o in gen_out] + ["f", "sim_ended", "sim_id"], + "out": gen_out, + "user": { + "n_init_thetas": n_init_thetas, # Num thetas in initial batch + "num_x_vals": n_x, # Num x points to create + "step_add_theta": step_add_theta, # No. of thetas to generate per step + "n_explore_theta": n_explore_theta, # No. of thetas to explore each step + "obsvar": obsvar, # Variance for generating noise in obs + "init_sample_size": init_sample_size, # Initial batch size inc. observations + "priorloc": 1, # Prior location in the unit cube. + "priorscale": 0.2, # Standard deviation of prior + }, + } + + alloc_specs = { + "alloc_f": alloc_f, + "user": { + "init_sample_size": init_sample_size, + "async_return": True, # True = Return results to gen as they come in (after sample) + "active_recv_gen": True, # Persistent gen can handle irregular communications + }, + } + + persis_info = add_unique_random_streams({}, nworkers + 1) + gen_specs["generator"] = Surmise(gen_specs, persis_info=persis_info) + + exit_criteria = {"sim_max": max_evals} + + # Perform the run + H, persis_info, flag = libE( + sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs=alloc_specs, libE_specs=libE_specs + ) + + if is_manager: + print("Cancelled sims", H["sim_id"][H["cancel_requested"]]) + print("Kills sent by manager to running simulations", H["sim_id"][H["kill_sent"]]) + print("Killed sims", H["sim_id"][H["sim_killed"]]) + sims_done = np.count_nonzero(H["sim_ended"]) + save_libE_output(H, persis_info, __file__, nworkers) + assert sims_done == max_evals, f"Num of completed simulations should be {max_evals}. Is {sims_done}" diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 8a0d21bde..389210f2e 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -110,7 +110,7 @@ def _persistent_result(self, calc_in, persis_info, libE_info): batch_size = getattr(self.gen, "batch_size", 0) or Work["libE_info"]["batch_size"] self.gen.tell(H_in) points = self.gen.ask(batch_size) - if len(points) == 2: + if len(points) == 2: # returned "samples" and "updates". can combine if same dtype H_out = np.append(points[0], points[1]) else: H_out = points From c38f39645be41875db2650ecbf95b765f950cfbb Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 5 Apr 2024 16:34:28 -0500 Subject: [PATCH 080/288] Attempted refactor where worker can process multiple contiguous messages from the manager to the gen, or from the gen to the manager. e.g. Surmise sends points and immediately follows up with requesting cancellations --- libensemble/utils/runners.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 389210f2e..1660b1903 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -1,6 +1,7 @@ import inspect import logging import logging.handlers +import time from typing import Optional import numpy as np @@ -105,16 +106,25 @@ def _persistent_result(self, calc_in, persis_info, libE_info): self.gen.libE_info = libE_info self.gen.setup() H_out = self.gen.initial_ask(initial_batch, calc_in) - tag, Work, H_in = self.ps.send_recv(H_out) - while tag not in [STOP_TAG, PERSIS_STOP]: - batch_size = getattr(self.gen, "batch_size", 0) or Work["libE_info"]["batch_size"] - self.gen.tell(H_in) - points = self.gen.ask(batch_size) - if len(points) == 2: # returned "samples" and "updates". can combine if same dtype - H_out = np.append(points[0], points[1]) - else: - H_out = points - tag, Work, H_in = self.ps.send_recv(H_out) + tag, Work, H_in = self.ps.send_recv(H_out) # evaluate the initial sample + self.gen.tell(H_in) # tell the gen the initial sample results + batch_size = getattr(self.gen, "batch_size", 0) or Work["libE_info"]["batch_size"] + STOP = False + while not STOP: + time.sleep(0.0025) # dont need to ping the gen relentlessly. Let it calculate. 400hz + for _ in range(self.gen.outbox.qsize()): # send any outstanding messages + points = self.gen.ask(batch_size) + if len(points) == 2: # returned "samples" and "updates". can combine if same dtype + H_out = np.append(points[0], points[1]) + else: + H_out = points + self.ps.send(H_out) + while self.ps.comm.mail_flag(): # receive any new messages, give all to gen + tag, _, H_in = self.ps.recv() + if tag in [STOP_TAG, PERSIS_STOP]: + STOP = True + break + self.gen.tell(H_in) return self.gen.final_tell(H_in), FINISHED_PERSISTENT_GEN_TAG def _result(self, calc_in: npt.NDArray, persis_info: dict, libE_info: dict) -> (npt.NDArray, dict, Optional[int]): From 99ad53ef8d7534206ad69a476850b52e440288ba Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 10 Apr 2024 14:49:33 -0500 Subject: [PATCH 081/288] initial construction of test_asktell_surmise. try intercepting and pack points and cancellations together within class-based Surmise. clarify a comment --- libensemble/generators.py | 33 ++++-- .../tests/unit_tests/test_asktell_surmise.py | 109 ++++++++++++++++++ libensemble/utils/runners.py | 2 +- 3 files changed, 135 insertions(+), 9 deletions(-) create mode 100644 libensemble/tests/unit_tests/test_asktell_surmise.py diff --git a/libensemble/generators.py b/libensemble/generators.py index 5a82f91dd..1eda6afe5 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -98,7 +98,7 @@ def final_tell(self, results: Iterable, *args, **kwargs) -> Optional[Iterable]: """ -class LibEnsembleGenTranslator(Generator): +class LibEnsembleGenInterfacer(Generator): """Implement ask/tell for traditionally written libEnsemble persistent generator functions. Still requires a handful of libEnsemble-specific data-structures on initialization. """ @@ -141,12 +141,12 @@ def _set_sim_ended(self, results: npt.NDArray) -> npt.NDArray: results = new_results return results - def initial_ask(self, num_points: int = 0, *args) -> npt.NDArray: + def initial_ask(self, num_points: int = 0, *args, **kwargs) -> npt.NDArray: if not self.gen.running: self.gen.run() return self.ask(num_points) - def ask(self, num_points: int = 0) -> (Iterable, Optional[npt.NDArray]): + def ask(self, num_points: int = 0, *args, **kwargs) -> (Iterable, Optional[npt.NDArray]): _, self.last_ask = self.outbox.get() return self.last_ask["calc_out"] @@ -163,7 +163,7 @@ def final_tell(self, results: npt.NDArray) -> (npt.NDArray, dict, int): return self.gen.result() -class APOSMM(LibEnsembleGenTranslator): +class APOSMM(LibEnsembleGenInterfacer): def __init__( self, gen_specs: dict, History: npt.NDArray = [], persis_info: dict = {}, libE_info: dict = {} ) -> None: @@ -191,18 +191,35 @@ def final_tell(self, results: npt.NDArray) -> (npt.NDArray, dict, int): return super().final_tell(results) -class Surmise(LibEnsembleGenTranslator): +class Surmise(LibEnsembleGenInterfacer): def __init__( self, gen_specs: dict, History: npt.NDArray = [], persis_info: dict = {}, libE_info: dict = {} ) -> None: gen_specs["gen_f"] = surmise_calib + if ("sim_id", int) not in gen_specs["out"]: + gen_specs["out"].append(("sim_id", int)) super().__init__(gen_specs, History, persis_info, libE_info) + self.sim_id_index = 0 + + def _add_sim_ids(self, array: npt.NDArray) -> npt.NDArray: + new_array_with_sim_ids = np.zeros(len(array), dtype=array.dtype.descr + [("sim_id", int)]) + new_array_with_sim_ids["sim_id"] = np.arange(self.sim_id_index, self.sim_id_index + len(array)) + for field in array.dtype.names: + new_array_with_sim_ids[field] = array[field] + self.sim_id_index += len(array) + return new_array_with_sim_ids def initial_ask(self, num_points: int = 0, *args) -> npt.NDArray: - return super().initial_ask(num_points, args)[0] + return self._add_sim_ids(super().initial_ask(num_points, args)[0]) - def ask(self, num_points: int = 0) -> (npt.NDArray): - return super().ask(num_points) + def ask(self, num_points: int = 0) -> (npt.NDArray, Optional[npt.NDArray]): + _, self.last_ask = self.outbox.get() + points = self._add_sim_ids(self.last_ask["calc_out"]) + try: + cancels = self.outbox.get(timeout=0.1) + return points, cancels + except thread_queue.Empty: + return points, np.empty(0, dtype=[("sim_id", int), ("cancel_requested", bool)]) def tell(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: super().tell(results, tag) diff --git a/libensemble/tests/unit_tests/test_asktell_surmise.py b/libensemble/tests/unit_tests/test_asktell_surmise.py new file mode 100644 index 000000000..09195846b --- /dev/null +++ b/libensemble/tests/unit_tests/test_asktell_surmise.py @@ -0,0 +1,109 @@ +import numpy as np +import pytest + +from libensemble.message_numbers import FINISHED_PERSISTENT_GEN_TAG + + +@pytest.mark.extra +def test_asktell_surmise(): + + from libensemble.generators import Surmise + + # Import libEnsemble items for this test + from libensemble.sim_funcs.borehole import borehole + from libensemble.tools import add_unique_random_streams + + n_init_thetas = 15 # Initial batch of thetas + n_x = 5 # No. of x values + nparams = 4 # No. of theta params + ndims = 3 # No. of x coordinates. + max_add_thetas = 20 # Max no. of thetas added for evaluation + step_add_theta = 10 # No. of thetas to generate per step, before emulator is rebuilt + n_explore_theta = 200 # No. of thetas to explore while selecting the next theta + obsvar = 10 ** (-1) # Constant for generating noise in obs + + # Batch mode until after init_sample_size (add one theta to batch for observations) + init_sample_size = (n_init_thetas + 1) * n_x + + # Stop after max_emul_runs runs of the emulator + max_evals = init_sample_size + max_add_thetas * n_x + + # Rename ensemble dir for non-interference with other regression tests + sim_specs = { + "in": ["x", "thetas"], + "out": [ + ("f", float), + ], + "user": { + "num_obs": n_x, + "init_sample_size": init_sample_size, + }, + } + + gen_out = [ + ("x", float, ndims), + ("thetas", float, nparams), + ("priority", int), + ("obs", float, n_x), + ("obsvar", float, n_x), + ] + + gen_specs = { + "persis_in": [o[0] for o in gen_out] + ["f", "sim_ended", "sim_id"], + "out": gen_out, + "user": { + "n_init_thetas": n_init_thetas, # Num thetas in initial batch + "num_x_vals": n_x, # Num x points to create + "step_add_theta": step_add_theta, # No. of thetas to generate per step + "n_explore_theta": n_explore_theta, # No. of thetas to explore each step + "obsvar": obsvar, # Variance for generating noise in obs + "init_sample_size": init_sample_size, # Initial batch size inc. observations + "priorloc": 1, # Prior location in the unit cube. + "priorscale": 0.2, # Standard deviation of prior + }, + } + + persis_info = add_unique_random_streams({}, 5) + surmise = Surmise(gen_specs, persis_info=persis_info) + surmise.setup() + + initial_sample = surmise.ask() + + initial_results = np.zeros(len(initial_sample), dtype=gen_out + [("f", float), ("sim_id", int)]) + + for field in gen_specs["out"]: + initial_results[field[0]] = initial_sample[field[0]] + + total_evals = 0 + + for i in len(initial_sample): + initial_results[i] = borehole(initial_sample[i], {}, sim_specs, {}) + initial_results[i]["sim_id"] = i + total_evals += 1 + + surmise.tell(initial_results) + + requested_canceled_sim_ids = [] + + while total_evals < max_evals: + + sample, cancels = surmise.ask() + if len(cancels): + for m in cancels: + requested_canceled_sim_ids.append(m) + results = np.zeros(len(sample), dtype=gen_out + [("f", float), ("sim_id", int)]) + for field in gen_specs["out"]: + results[field[0]] = sample[field[0]] + for i in range(len(sample)): + results[i]["f"] = borehole(sample[i], {}, sim_specs, {}) + results[i]["sim_id"] = total_evals + total_evals += 1 + surmise.tell(results) + H, persis_info, exit_code = surmise.final_tell(None) + + assert exit_code == FINISHED_PERSISTENT_GEN_TAG, "Standalone persistent_aposmm didn't exit correctly" + assert len(requested_canceled_sim_ids), "No cancellations sent by Surmise" + + +if __name__ == "__main__": + test_asktell_surmise() diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 1660b1903..0e2114946 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -112,7 +112,7 @@ def _persistent_result(self, calc_in, persis_info, libE_info): STOP = False while not STOP: time.sleep(0.0025) # dont need to ping the gen relentlessly. Let it calculate. 400hz - for _ in range(self.gen.outbox.qsize()): # send any outstanding messages + for _ in range(self.gen.outbox.qsize()): # recv/send any outstanding messages points = self.gen.ask(batch_size) if len(points) == 2: # returned "samples" and "updates". can combine if same dtype H_out = np.append(points[0], points[1]) From 486318beccbf4e421e2f98423e734fd905a4cf8c Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 10 Apr 2024 15:53:28 -0500 Subject: [PATCH 082/288] bugfixes --- libensemble/generators.py | 7 ++----- libensemble/tests/unit_tests/test_asktell_surmise.py | 4 ++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 1eda6afe5..4be61e9f6 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -202,12 +202,9 @@ def __init__( self.sim_id_index = 0 def _add_sim_ids(self, array: npt.NDArray) -> npt.NDArray: - new_array_with_sim_ids = np.zeros(len(array), dtype=array.dtype.descr + [("sim_id", int)]) - new_array_with_sim_ids["sim_id"] = np.arange(self.sim_id_index, self.sim_id_index + len(array)) - for field in array.dtype.names: - new_array_with_sim_ids[field] = array[field] + array["sim_id"] = np.arange(self.sim_id_index, self.sim_id_index + len(array)) self.sim_id_index += len(array) - return new_array_with_sim_ids + return array def initial_ask(self, num_points: int = 0, *args) -> npt.NDArray: return self._add_sim_ids(super().initial_ask(num_points, args)[0]) diff --git a/libensemble/tests/unit_tests/test_asktell_surmise.py b/libensemble/tests/unit_tests/test_asktell_surmise.py index 09195846b..a2a05ba92 100644 --- a/libensemble/tests/unit_tests/test_asktell_surmise.py +++ b/libensemble/tests/unit_tests/test_asktell_surmise.py @@ -64,10 +64,10 @@ def test_asktell_surmise(): } persis_info = add_unique_random_streams({}, 5) - surmise = Surmise(gen_specs, persis_info=persis_info) + surmise = Surmise(gen_specs, persis_info=persis_info[1]) surmise.setup() - initial_sample = surmise.ask() + initial_sample = surmise.initial_ask() initial_results = np.zeros(len(initial_sample), dtype=gen_out + [("f", float), ("sim_id", int)]) From 66c2549a1b913618c793082aaf5f3bd1eb9dd644 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 10 Apr 2024 16:35:31 -0500 Subject: [PATCH 083/288] fixes and clarifications --- libensemble/generators.py | 2 +- libensemble/tests/unit_tests/test_asktell_surmise.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 4be61e9f6..24febcc1c 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -207,7 +207,7 @@ def _add_sim_ids(self, array: npt.NDArray) -> npt.NDArray: return array def initial_ask(self, num_points: int = 0, *args) -> npt.NDArray: - return self._add_sim_ids(super().initial_ask(num_points, args)[0]) + return super().initial_ask(num_points, args)[0] def ask(self, num_points: int = 0) -> (npt.NDArray, Optional[npt.NDArray]): _, self.last_ask = self.outbox.get() diff --git a/libensemble/tests/unit_tests/test_asktell_surmise.py b/libensemble/tests/unit_tests/test_asktell_surmise.py index a2a05ba92..d78d4cd20 100644 --- a/libensemble/tests/unit_tests/test_asktell_surmise.py +++ b/libensemble/tests/unit_tests/test_asktell_surmise.py @@ -64,12 +64,12 @@ def test_asktell_surmise(): } persis_info = add_unique_random_streams({}, 5) - surmise = Surmise(gen_specs, persis_info=persis_info[1]) + surmise = Surmise(gen_specs, persis_info=persis_info[1]) # we add sim_id as a field to gen_specs["out"] surmise.setup() initial_sample = surmise.initial_ask() - initial_results = np.zeros(len(initial_sample), dtype=gen_out + [("f", float), ("sim_id", int)]) + initial_results = np.zeros(len(initial_sample), dtype=gen_out + [("f", float)]) for field in gen_specs["out"]: initial_results[field[0]] = initial_sample[field[0]] @@ -78,7 +78,6 @@ def test_asktell_surmise(): for i in len(initial_sample): initial_results[i] = borehole(initial_sample[i], {}, sim_specs, {}) - initial_results[i]["sim_id"] = i total_evals += 1 surmise.tell(initial_results) @@ -91,7 +90,7 @@ def test_asktell_surmise(): if len(cancels): for m in cancels: requested_canceled_sim_ids.append(m) - results = np.zeros(len(sample), dtype=gen_out + [("f", float), ("sim_id", int)]) + results = np.zeros(len(sample), dtype=gen_out + [("f", float)]) for field in gen_specs["out"]: results[field[0]] = sample[field[0]] for i in range(len(sample)): From 0ddf761348e91876bb646378f33bbef18e75cf3a Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 10 Apr 2024 16:46:18 -0500 Subject: [PATCH 084/288] lets try the exeucutor sim_f... i dont know correct dimensions I guess? --- .../tests/unit_tests/test_asktell_surmise.py | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/libensemble/tests/unit_tests/test_asktell_surmise.py b/libensemble/tests/unit_tests/test_asktell_surmise.py index d78d4cd20..9daa57ce5 100644 --- a/libensemble/tests/unit_tests/test_asktell_surmise.py +++ b/libensemble/tests/unit_tests/test_asktell_surmise.py @@ -1,3 +1,5 @@ +import os + import numpy as np import pytest @@ -7,12 +9,21 @@ @pytest.mark.extra def test_asktell_surmise(): + from libensemble.executors import Executor from libensemble.generators import Surmise # Import libEnsemble items for this test - from libensemble.sim_funcs.borehole import borehole + from libensemble.sim_funcs.borehole_kills import borehole as sim_f + from libensemble.tests.regression_tests.common import build_borehole # current location from libensemble.tools import add_unique_random_streams + sim_app = os.path.join(os.getcwd(), "borehole.x") + if not os.path.isfile(sim_app): + build_borehole() + + exctr = Executor() # Run serial sub-process in place + exctr.register_app(full_path=sim_app, app_name="borehole") + n_init_thetas = 15 # Initial batch of thetas n_x = 5 # No. of x values nparams = 4 # No. of theta params @@ -33,6 +44,7 @@ def test_asktell_surmise(): "in": ["x", "thetas"], "out": [ ("f", float), + ("sim_killed", bool), ], "user": { "num_obs": n_x, @@ -76,8 +88,8 @@ def test_asktell_surmise(): total_evals = 0 - for i in len(initial_sample): - initial_results[i] = borehole(initial_sample[i], {}, sim_specs, {}) + for i in initial_sample["sim_id"]: + initial_results[i] = sim_f(initial_sample[i], {}, sim_specs, {}) total_evals += 1 surmise.tell(initial_results) @@ -94,7 +106,7 @@ def test_asktell_surmise(): for field in gen_specs["out"]: results[field[0]] = sample[field[0]] for i in range(len(sample)): - results[i]["f"] = borehole(sample[i], {}, sim_specs, {}) + results[i]["f"] = sim_f(sample[i], {}, sim_specs, {}) results[i]["sim_id"] = total_evals total_evals += 1 surmise.tell(results) From b540ba4282ca475dcaba5fb72546ed5a2406bb65 Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 11 Apr 2024 11:12:18 -0500 Subject: [PATCH 085/288] fixes, including not polling the manager in a unit test --- libensemble/sim_funcs/borehole_kills.py | 6 +++--- libensemble/tests/unit_tests/test_asktell_surmise.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/libensemble/sim_funcs/borehole_kills.py b/libensemble/sim_funcs/borehole_kills.py index 54a31256b..477a7bf42 100644 --- a/libensemble/sim_funcs/borehole_kills.py +++ b/libensemble/sim_funcs/borehole_kills.py @@ -5,7 +5,7 @@ from libensemble.sim_funcs.surmise_test_function import borehole_true -def subproc_borehole(H, delay): +def subproc_borehole(H, delay, poll_manager): """This evaluates the Borehole function using a subprocess running compiled code. @@ -22,7 +22,7 @@ def subproc_borehole(H, delay): args = "input" + " " + str(delay) task = exctr.submit(app_name="borehole", app_args=args, stdout="out.txt", stderr="err.txt") - calc_status = exctr.polling_loop(task, delay=0.01, poll_manager=True) + calc_status = exctr.polling_loop(task, delay=0.01, poll_manager=poll_manager) if calc_status in MAN_KILL_SIGNALS + [TASK_FAILED]: f = np.inf @@ -45,7 +45,7 @@ def borehole(H, persis_info, sim_specs, libE_info): if sim_id > sim_specs["user"]["init_sample_size"]: delay = 2 + np.random.normal(scale=0.5) - f, calc_status = subproc_borehole(H, delay) + f, calc_status = subproc_borehole(H, delay, sim_specs["user"].get("poll_manager", True)) if calc_status in MAN_KILL_SIGNALS and "sim_killed" in H_o.dtype.names: H_o["sim_killed"] = True # For calling script to print only. diff --git a/libensemble/tests/unit_tests/test_asktell_surmise.py b/libensemble/tests/unit_tests/test_asktell_surmise.py index 9daa57ce5..a8d8f6604 100644 --- a/libensemble/tests/unit_tests/test_asktell_surmise.py +++ b/libensemble/tests/unit_tests/test_asktell_surmise.py @@ -49,6 +49,7 @@ def test_asktell_surmise(): "user": { "num_obs": n_x, "init_sample_size": init_sample_size, + "poll_manager": False, }, } @@ -89,7 +90,7 @@ def test_asktell_surmise(): total_evals = 0 for i in initial_sample["sim_id"]: - initial_results[i] = sim_f(initial_sample[i], {}, sim_specs, {}) + initial_results[i] = sim_f(initial_sample[i], {}, sim_specs, {"H_rows": initial_sample["sim_id"]}) total_evals += 1 surmise.tell(initial_results) From c7810f84be4d04df9fa6c9dbd24938b30349cd09 Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 11 Apr 2024 11:25:20 -0500 Subject: [PATCH 086/288] fix returns from simf --- libensemble/tests/unit_tests/test_asktell_surmise.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/libensemble/tests/unit_tests/test_asktell_surmise.py b/libensemble/tests/unit_tests/test_asktell_surmise.py index a8d8f6604..500406b26 100644 --- a/libensemble/tests/unit_tests/test_asktell_surmise.py +++ b/libensemble/tests/unit_tests/test_asktell_surmise.py @@ -90,7 +90,7 @@ def test_asktell_surmise(): total_evals = 0 for i in initial_sample["sim_id"]: - initial_results[i] = sim_f(initial_sample[i], {}, sim_specs, {"H_rows": initial_sample["sim_id"]}) + initial_results[i], _a, _b = sim_f(initial_sample[i], {}, sim_specs, {"H_rows": initial_sample["sim_id"]}) total_evals += 1 surmise.tell(initial_results) @@ -107,8 +107,7 @@ def test_asktell_surmise(): for field in gen_specs["out"]: results[field[0]] = sample[field[0]] for i in range(len(sample)): - results[i]["f"] = sim_f(sample[i], {}, sim_specs, {}) - results[i]["sim_id"] = total_evals + results[i], _a, _b = sim_f(sample[i], {}, sim_specs, {"H_rows": sample["sim_id"]}) total_evals += 1 surmise.tell(results) H, persis_info, exit_code = surmise.final_tell(None) From aff10be0561e6f78413c2f0f4374b9cb1a54ab68 Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 11 Apr 2024 11:51:52 -0500 Subject: [PATCH 087/288] trying again to fix simf outputs, move some imports to within their wrapper classes --- libensemble/generators.py | 6 ++++-- libensemble/tests/unit_tests/test_asktell_surmise.py | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 24febcc1c..151677be3 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -7,8 +7,6 @@ from libensemble.comms.comms import QComm, QCommThread from libensemble.executors import Executor -from libensemble.gen_funcs.persistent_aposmm import aposmm -from libensemble.gen_funcs.persistent_surmise_calib import surmise_calib from libensemble.message_numbers import EVAL_GEN_TAG, PERSIS_STOP from libensemble.tools import add_unique_random_streams @@ -167,6 +165,8 @@ class APOSMM(LibEnsembleGenInterfacer): def __init__( self, gen_specs: dict, History: npt.NDArray = [], persis_info: dict = {}, libE_info: dict = {} ) -> None: + from libensemble.gen_funcs.persistent_aposmm import aposmm + gen_specs["gen_f"] = aposmm if not persis_info: persis_info = add_unique_random_streams({}, 4)[1] @@ -195,6 +195,8 @@ class Surmise(LibEnsembleGenInterfacer): def __init__( self, gen_specs: dict, History: npt.NDArray = [], persis_info: dict = {}, libE_info: dict = {} ) -> None: + from libensemble.gen_funcs.persistent_surmise_calib import surmise_calib + gen_specs["gen_f"] = surmise_calib if ("sim_id", int) not in gen_specs["out"]: gen_specs["out"].append(("sim_id", int)) diff --git a/libensemble/tests/unit_tests/test_asktell_surmise.py b/libensemble/tests/unit_tests/test_asktell_surmise.py index 500406b26..9d7e9cc5b 100644 --- a/libensemble/tests/unit_tests/test_asktell_surmise.py +++ b/libensemble/tests/unit_tests/test_asktell_surmise.py @@ -90,7 +90,8 @@ def test_asktell_surmise(): total_evals = 0 for i in initial_sample["sim_id"]: - initial_results[i], _a, _b = sim_f(initial_sample[i], {}, sim_specs, {"H_rows": initial_sample["sim_id"]}) + H_out, _a, _b = sim_f(initial_sample[i], {}, sim_specs, {"H_rows": initial_sample["sim_id"]}) + initial_results[i] = H_out total_evals += 1 surmise.tell(initial_results) @@ -107,7 +108,8 @@ def test_asktell_surmise(): for field in gen_specs["out"]: results[field[0]] = sample[field[0]] for i in range(len(sample)): - results[i], _a, _b = sim_f(sample[i], {}, sim_specs, {"H_rows": sample["sim_id"]}) + H_out, _a, _b = sim_f(sample[i], {}, sim_specs, {"H_rows": sample["sim_id"]}) + results[i] = H_out total_evals += 1 surmise.tell(results) H, persis_info, exit_code = surmise.final_tell(None) From 2583b1565f08583731c83237990d2a5900f19eea Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 11 Apr 2024 13:07:27 -0500 Subject: [PATCH 088/288] borehole seems to output an oddly-shaped array, can we just use the first identical value? --- libensemble/tests/unit_tests/test_asktell_surmise.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/libensemble/tests/unit_tests/test_asktell_surmise.py b/libensemble/tests/unit_tests/test_asktell_surmise.py index 9d7e9cc5b..dc6643088 100644 --- a/libensemble/tests/unit_tests/test_asktell_surmise.py +++ b/libensemble/tests/unit_tests/test_asktell_surmise.py @@ -13,7 +13,7 @@ def test_asktell_surmise(): from libensemble.generators import Surmise # Import libEnsemble items for this test - from libensemble.sim_funcs.borehole_kills import borehole as sim_f + from libensemble.sim_funcs.borehole_kills import borehole from libensemble.tests.regression_tests.common import build_borehole # current location from libensemble.tools import add_unique_random_streams @@ -90,8 +90,8 @@ def test_asktell_surmise(): total_evals = 0 for i in initial_sample["sim_id"]: - H_out, _a, _b = sim_f(initial_sample[i], {}, sim_specs, {"H_rows": initial_sample["sim_id"]}) - initial_results[i] = H_out + H_out, _a, _b = borehole(initial_sample[i], {}, sim_specs, {"H_rows": np.array([initial_sample[i]["sim_id"]])}) + initial_results[i]["f"] = H_out["f"][0] # some "bugginess" with output shape of array in simf total_evals += 1 surmise.tell(initial_results) @@ -108,8 +108,8 @@ def test_asktell_surmise(): for field in gen_specs["out"]: results[field[0]] = sample[field[0]] for i in range(len(sample)): - H_out, _a, _b = sim_f(sample[i], {}, sim_specs, {"H_rows": sample["sim_id"]}) - results[i] = H_out + H_out, _a, _b = borehole(sample[i], {}, sim_specs, {"H_rows": np.array([initial_sample[i]["sim_id"]])}) + results[i]["f"] = H_out["f"][0] total_evals += 1 surmise.tell(results) H, persis_info, exit_code = surmise.final_tell(None) From e9dcf0fdd70a3e070bc39686a00f06d7c5f49b34 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 12 Apr 2024 09:10:06 -0500 Subject: [PATCH 089/288] process two initial samples from surmise, arrange points/cancels as output from .ask by internally determining what type of array we got first --- libensemble/generators.py | 20 ++++++++-- libensemble/sim_funcs/borehole_kills.py | 4 +- .../tests/unit_tests/test_asktell_surmise.py | 38 ++++++++++++++----- 3 files changed, 46 insertions(+), 16 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 151677be3..d7191d0f4 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -213,12 +213,24 @@ def initial_ask(self, num_points: int = 0, *args) -> npt.NDArray: def ask(self, num_points: int = 0) -> (npt.NDArray, Optional[npt.NDArray]): _, self.last_ask = self.outbox.get() - points = self._add_sim_ids(self.last_ask["calc_out"]) + output = self.last_ask["calc_out"] + if "cancel_requested" in output.dtype.names: + cancels = output + got_cancels_first = True + else: + points = self._add_sim_ids(output) + got_cancels_first = False try: - cancels = self.outbox.get(timeout=0.1) - return points, cancels + additional = self.outbox.get(timeout=0.2) # either cancels or new points + if got_cancels_first: + return additional, cancels + else: + return points, additional except thread_queue.Empty: - return points, np.empty(0, dtype=[("sim_id", int), ("cancel_requested", bool)]) + if got_cancels_first: + return np.empty(0, dtype=self.gen_specs["out"]), cancels + else: + return points, np.empty(0, dtype=[("sim_id", int), ("cancel_requested", bool)]) def tell(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: super().tell(results, tag) diff --git a/libensemble/sim_funcs/borehole_kills.py b/libensemble/sim_funcs/borehole_kills.py index 477a7bf42..47a00af90 100644 --- a/libensemble/sim_funcs/borehole_kills.py +++ b/libensemble/sim_funcs/borehole_kills.py @@ -15,8 +15,8 @@ def subproc_borehole(H, delay, poll_manager): """ with open("input", "w") as f: - H["thetas"][0].tofile(f) - H["x"][0].tofile(f) + H["thetas"].tofile(f) + H["x"].tofile(f) exctr = Executor.executor args = "input" + " " + str(delay) diff --git a/libensemble/tests/unit_tests/test_asktell_surmise.py b/libensemble/tests/unit_tests/test_asktell_surmise.py index dc6643088..83aa43aca 100644 --- a/libensemble/tests/unit_tests/test_asktell_surmise.py +++ b/libensemble/tests/unit_tests/test_asktell_surmise.py @@ -98,20 +98,38 @@ def test_asktell_surmise(): requested_canceled_sim_ids = [] + next_sample, cancels = surmise.ask() + next_results = np.zeros(len(next_sample), dtype=gen_out + [("f", float)]) + + for field in gen_specs["out"]: + next_results[field[0]] = next_sample[field[0]] + + for i in range(len(next_sample)): + H_out, _a, _b = borehole(next_sample[i], {}, sim_specs, {"H_rows": np.array([next_sample[i]["sim_id"]])}) + next_results[i]["f"] = H_out["f"][0] + total_evals += 1 + + surmise.tell(next_results) + sample, cancels = surmise.ask() + while total_evals < max_evals: - sample, cancels = surmise.ask() - if len(cancels): - for m in cancels: - requested_canceled_sim_ids.append(m) - results = np.zeros(len(sample), dtype=gen_out + [("f", float)]) - for field in gen_specs["out"]: - results[field[0]] = sample[field[0]] for i in range(len(sample)): - H_out, _a, _b = borehole(sample[i], {}, sim_specs, {"H_rows": np.array([initial_sample[i]["sim_id"]])}) - results[i]["f"] = H_out["f"][0] + result = np.zeros(1, dtype=gen_out + [("f", float)]) + for field in gen_specs["out"]: + result[field[0]] = sample[i][field[0]] + H_out, _a, _b = borehole(sample[i], {}, sim_specs, {"H_rows": np.array([sample[i]["sim_id"]])}) + result["f"] = H_out["f"][0] total_evals += 1 - surmise.tell(results) + surmise.tell(result) + new_sample, cancels = surmise.ask() + if len(cancels): + for m in cancels: + requested_canceled_sim_ids.append(m) + if len(new_sample): + sample = new_sample + break + H, persis_info, exit_code = surmise.final_tell(None) assert exit_code == FINISHED_PERSISTENT_GEN_TAG, "Standalone persistent_aposmm didn't exit correctly" From 4a9c2348f119ae8d20c22330fd775b9cd93dac1a Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 12 Apr 2024 10:08:41 -0500 Subject: [PATCH 090/288] implement ready_to_be_asked for surmise --- libensemble/generators.py | 3 +++ .../tests/unit_tests/test_asktell_surmise.py | 14 ++++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index d7191d0f4..2c53f76f6 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -211,6 +211,9 @@ def _add_sim_ids(self, array: npt.NDArray) -> npt.NDArray: def initial_ask(self, num_points: int = 0, *args) -> npt.NDArray: return super().initial_ask(num_points, args)[0] + def ready_to_be_asked(self) -> bool: + return not self.outbox.empty() + def ask(self, num_points: int = 0) -> (npt.NDArray, Optional[npt.NDArray]): _, self.last_ask = self.outbox.get() output = self.last_ask["calc_out"] diff --git a/libensemble/tests/unit_tests/test_asktell_surmise.py b/libensemble/tests/unit_tests/test_asktell_surmise.py index 83aa43aca..9792b5bc0 100644 --- a/libensemble/tests/unit_tests/test_asktell_surmise.py +++ b/libensemble/tests/unit_tests/test_asktell_surmise.py @@ -114,7 +114,9 @@ def test_asktell_surmise(): while total_evals < max_evals: - for i in range(len(sample)): + samples_iter = range(len(sample)) + + for i in samples_iter: result = np.zeros(1, dtype=gen_out + [("f", float)]) for field in gen_specs["out"]: result[field[0]] = sample[i][field[0]] @@ -122,13 +124,13 @@ def test_asktell_surmise(): result["f"] = H_out["f"][0] total_evals += 1 surmise.tell(result) - new_sample, cancels = surmise.ask() - if len(cancels): + if surmise.ready_to_be_asked(): + new_sample, cancels = surmise.ask() for m in cancels: requested_canceled_sim_ids.append(m) - if len(new_sample): - sample = new_sample - break + if len(new_sample): + sample = new_sample + break H, persis_info, exit_code = surmise.final_tell(None) From d0c158ffe7c11d2445468bece89ab7cebf501692 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 12 Apr 2024 12:20:12 -0500 Subject: [PATCH 091/288] split runner result loops into that for a "normal" ask/tell gen that doesnt communicate with a thread, and a "persistent interfacer" one that does --- libensemble/utils/runners.py | 46 +++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 0e2114946..639ace4f3 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -8,6 +8,7 @@ import numpy.typing as npt from libensemble.comms.comms import QCommThread +from libensemble.generators import LibEnsembleGenInterfacer from libensemble.message_numbers import EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG from libensemble.tools.persistent_support import PersistentSupport @@ -97,23 +98,23 @@ def __init__(self, specs): super().__init__(specs) self.gen = specs.get("generator") - def _persistent_result(self, calc_in, persis_info, libE_info): - self.ps = PersistentSupport(libE_info, EVAL_GEN_TAG) - tag = None - initial_batch = getattr(self.gen, "initial_batch_size", 0) or libE_info["batch_size"] - if hasattr(self.gen, "setup"): - self.gen.persis_info = persis_info - self.gen.libE_info = libE_info - self.gen.setup() - H_out = self.gen.initial_ask(initial_batch, calc_in) - tag, Work, H_in = self.ps.send_recv(H_out) # evaluate the initial sample - self.gen.tell(H_in) # tell the gen the initial sample results - batch_size = getattr(self.gen, "batch_size", 0) or Work["libE_info"]["batch_size"] + def _loop_over_normal_generator(self, tag, Work): + while tag not in [PERSIS_STOP, STOP_TAG]: + batch_size = getattr(self.gen, "batch_size", 0) or Work["libE_info"]["batch_size"] + points = self.gen.ask(batch_size) + if len(points) == 2: # returned "samples" and "updates". can combine if same dtype + H_out = np.append(points[0], points[1]) + else: + H_out = points + tag, Work, H_in = self.ps.send_recv(H_out) + return H_in + + def _loop_over_persistent_interfacer(self): STOP = False while not STOP: time.sleep(0.0025) # dont need to ping the gen relentlessly. Let it calculate. 400hz for _ in range(self.gen.outbox.qsize()): # recv/send any outstanding messages - points = self.gen.ask(batch_size) + points = self.gen.ask() if len(points) == 2: # returned "samples" and "updates". can combine if same dtype H_out = np.append(points[0], points[1]) else: @@ -125,7 +126,24 @@ def _persistent_result(self, calc_in, persis_info, libE_info): STOP = True break self.gen.tell(H_in) - return self.gen.final_tell(H_in), FINISHED_PERSISTENT_GEN_TAG + return H_in + + def _persistent_result(self, calc_in, persis_info, libE_info): + self.ps = PersistentSupport(libE_info, EVAL_GEN_TAG) + tag = None + if hasattr(self.gen, "setup"): + self.gen.persis_info = persis_info + self.gen.libE_info = libE_info + self.gen.setup() + initial_batch = getattr(self.gen, "initial_batch_size", 0) or libE_info["batch_size"] + H_out = self.gen.initial_ask(initial_batch, calc_in) + tag, Work, H_in = self.ps.send_recv(H_out) # evaluate the initial sample + self.gen.tell(H_in) # tell the gen the initial sample results + if issubclass(type(self.gen), LibEnsembleGenInterfacer): + final_H_in = self._loop_over_persistent_interfacer() + else: + final_H_in = self._loop_over_normal_generator(tag, Work) + return self.gen.final_tell(final_H_in), FINISHED_PERSISTENT_GEN_TAG def _result(self, calc_in: npt.NDArray, persis_info: dict, libE_info: dict) -> (npt.NDArray, dict, Optional[int]): if libE_info.get("persistent"): From 3f0f89f186bde5aa6503c5e0ee4f46e9f152f5cc Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 12 Apr 2024 13:21:11 -0500 Subject: [PATCH 092/288] fix pounders import to be module instead of function --- libensemble/gen_funcs/aposmm_localopt_support.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libensemble/gen_funcs/aposmm_localopt_support.py b/libensemble/gen_funcs/aposmm_localopt_support.py index d21bebef2..909bbccd7 100644 --- a/libensemble/gen_funcs/aposmm_localopt_support.py +++ b/libensemble/gen_funcs/aposmm_localopt_support.py @@ -43,7 +43,7 @@ class APOSMMException(Exception): if "dfols" in optimizers: import dfols # noqa: F401 if "ibcdfo" in optimizers: - from ibcdfo.pounders import pounders # noqa: F401 + from ibcdfo import pounders # noqa: F401 if "scipy" in optimizers: from scipy import optimize as sp_opt # noqa: F401 if "external_localopt" in optimizers: From 263dce9d3a433d21703eb04b85de06302b257011 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 12 Apr 2024 13:49:26 -0500 Subject: [PATCH 093/288] refactoring --- libensemble/generators.py | 6 ++---- libensemble/utils/runners.py | 24 ++++++++++++------------ 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 2c53f76f6..7fc5f17f4 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -227,13 +227,11 @@ def ask(self, num_points: int = 0) -> (npt.NDArray, Optional[npt.NDArray]): additional = self.outbox.get(timeout=0.2) # either cancels or new points if got_cancels_first: return additional, cancels - else: - return points, additional + return points, additional except thread_queue.Empty: if got_cancels_first: return np.empty(0, dtype=self.gen_specs["out"]), cancels - else: - return points, np.empty(0, dtype=[("sim_id", int), ("cancel_requested", bool)]) + return points, np.empty(0, dtype=[("sim_id", int), ("cancel_requested", bool)]) def tell(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: super().tell(results, tag) diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 639ace4f3..14e015e7f 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -109,24 +109,24 @@ def _loop_over_normal_generator(self, tag, Work): tag, Work, H_in = self.ps.send_recv(H_out) return H_in + def _ask_and_send(self): + for _ in range(self.gen.outbox.qsize()): # recv/send any outstanding messages + points = self.gen.ask() + if len(points) == 2: # returned "samples" and "updates". can combine if same dtype + H_out = np.append(points[0], points[1]) + else: + H_out = points + self.ps.send(H_out) + def _loop_over_persistent_interfacer(self): - STOP = False - while not STOP: + while True: time.sleep(0.0025) # dont need to ping the gen relentlessly. Let it calculate. 400hz - for _ in range(self.gen.outbox.qsize()): # recv/send any outstanding messages - points = self.gen.ask() - if len(points) == 2: # returned "samples" and "updates". can combine if same dtype - H_out = np.append(points[0], points[1]) - else: - H_out = points - self.ps.send(H_out) + self._ask_and_send() while self.ps.comm.mail_flag(): # receive any new messages, give all to gen tag, _, H_in = self.ps.recv() if tag in [STOP_TAG, PERSIS_STOP]: - STOP = True - break + return H_in self.gen.tell(H_in) - return H_in def _persistent_result(self, calc_in, persis_info, libE_info): self.ps = PersistentSupport(libE_info, EVAL_GEN_TAG) From 215403ec1304896cfc7bc93107f49e59df5055ee Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 12 Apr 2024 16:03:23 -0500 Subject: [PATCH 094/288] add set_history() to generator standard --- libensemble/generators.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 7fc5f17f4..9fe29e862 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -68,12 +68,16 @@ def __init__(self, *args, **kwargs): my_generator = MyGenerator(my_parameter, batch_size=10) """ - def initial_ask(self, num_points: int, previous_results: Optional[Iterable], *args, **kwargs) -> Iterable: + def set_history(self, new_history: Iterable): + """ + Replace/initialize the generator's history. + """ + + def initial_ask(self, num_points: int, *args, **kwargs) -> Iterable: """ The initial set of generated points is often produced differently than subsequent sets. This is a separate method to simplify the common pattern of noting internally if a - specific ask was the first. Previous results can be provided to build a foundation - for the initial sample. This will be called only once. + specific ask was the first. """ @abstractmethod From 5ba5457beec7dabf71fe589e5eb9c1e2c50fae9d Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 12 Apr 2024 16:05:16 -0500 Subject: [PATCH 095/288] for Surmise and APOSMM, the input_H of final_tell *defaults* as None --- libensemble/generators.py | 4 ++-- libensemble/tests/unit_tests/test_asktell_surmise.py | 2 +- libensemble/tests/unit_tests/test_persistent_aposmm.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 9fe29e862..7a394aa6d 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -191,7 +191,7 @@ def ask(self, num_points: int = 0) -> (npt.NDArray, npt.NDArray): def tell(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: super().tell(results, tag) - def final_tell(self, results: npt.NDArray) -> (npt.NDArray, dict, int): + def final_tell(self, results: npt.NDArray = None) -> (npt.NDArray, dict, int): return super().final_tell(results) @@ -240,5 +240,5 @@ def ask(self, num_points: int = 0) -> (npt.NDArray, Optional[npt.NDArray]): def tell(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: super().tell(results, tag) - def final_tell(self, results: npt.NDArray) -> (npt.NDArray, dict, int): + def final_tell(self, results: npt.NDArray = None) -> (npt.NDArray, dict, int): return super().final_tell(results) diff --git a/libensemble/tests/unit_tests/test_asktell_surmise.py b/libensemble/tests/unit_tests/test_asktell_surmise.py index 9792b5bc0..c44130ff2 100644 --- a/libensemble/tests/unit_tests/test_asktell_surmise.py +++ b/libensemble/tests/unit_tests/test_asktell_surmise.py @@ -132,7 +132,7 @@ def test_asktell_surmise(): sample = new_sample break - H, persis_info, exit_code = surmise.final_tell(None) + H, persis_info, exit_code = surmise.final_tell() assert exit_code == FINISHED_PERSISTENT_GEN_TAG, "Standalone persistent_aposmm didn't exit correctly" assert len(requested_canceled_sim_ids), "No cancellations sent by Surmise" diff --git a/libensemble/tests/unit_tests/test_persistent_aposmm.py b/libensemble/tests/unit_tests/test_persistent_aposmm.py index da15e0b14..08d75a019 100644 --- a/libensemble/tests/unit_tests/test_persistent_aposmm.py +++ b/libensemble/tests/unit_tests/test_persistent_aposmm.py @@ -234,7 +234,7 @@ def test_asktell_with_persistent_aposmm(): results[i]["f"] = six_hump_camel_func(sample["x"][i]) total_evals += 1 my_APOSMM.tell(results) - H, persis_info, exit_code = my_APOSMM.final_tell(None) + H, persis_info, exit_code = my_APOSMM.final_tell() assert exit_code == FINISHED_PERSISTENT_GEN_TAG, "Standalone persistent_aposmm didn't exit correctly" assert persis_info.get("run_order"), "Standalone persistent_aposmm didn't do any localopt runs" From 14c4a382c4c3716bd7633455c0edb4f568f61848 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 12 Apr 2024 16:33:13 -0500 Subject: [PATCH 096/288] add create_results_array to interfacer class to create an already-ready array to slot sim results into immediately --- libensemble/generators.py | 45 ++++++++++--------- .../tests/unit_tests/test_asktell_surmise.py | 15 ++----- .../unit_tests/test_persistent_aposmm.py | 9 +--- 3 files changed, 29 insertions(+), 40 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 7a394aa6d..9067be792 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -13,6 +13,7 @@ class Generator(ABC): """ + v 0.4.12.24 Tentative generator interface for use with libEnsemble, and generic enough to be broadly compatible with other workflow packages. @@ -28,8 +29,11 @@ def __init__(self, param): self.param = param self.model = None + def set_history(self, yesterdays_points): + self.history = new_history + def initial_ask(self, num_points, yesterdays_points): - return create_initial_points(num_points, self.param, yesterdays_points) + return create_initial_points(num_points, self.param, self.history) def ask(self, num_points): return create_points(num_points, self.param) @@ -44,17 +48,6 @@ def final_tell(self, results): my_generator = MyGenerator(my_parameter=100) my_ensemble = Ensemble(generator=my_generator) - - Pattern of operations: - 0. User initializes the generator class in their script, provides object to workflow/libEnsemble - 1. Initial ask for points from the generator - 2. Send initial points to workflow for evaluation - while not instructed to cleanup: - 3. Tell results to generator - 4. Ask generator for subsequent points - 5. Send points to workflow for evaluation. Get results and any cleanup instruction. - 6. Perform final_tell to generator, retrieve any final results/points if any. - """ @abstractmethod @@ -164,8 +157,18 @@ def final_tell(self, results: npt.NDArray) -> (npt.NDArray, dict, int): self.tell(results, PERSIS_STOP) return self.gen.result() + def create_results_array(self, addtl_fields: list = [("f", float)]) -> npt.NDArray: + new_results = np.zeros(len(self.results), dtype=self.gen_specs["out"] + addtl_fields) + for field in self.gen_specs["out"]: + new_results[field[0]] = self.results[field[0]] + return new_results + class APOSMM(LibEnsembleGenInterfacer): + """ + Standalone object-oriented APOSMM generator + """ + def __init__( self, gen_specs: dict, History: npt.NDArray = [], persis_info: dict = {}, libE_info: dict = {} ) -> None: @@ -181,12 +184,12 @@ def initial_ask(self, num_points: int = 0, *args) -> npt.NDArray: return super().initial_ask(num_points, args)[0] def ask(self, num_points: int = 0) -> (npt.NDArray, npt.NDArray): - results = super().ask(num_points) - if any(results["local_min"]): - minima = results[results["local_min"]] - results = results[~results["local_min"]] - return results, minima - return results, np.empty(0, dtype=self.gen_specs["out"]) + self.results = super().ask(num_points) + if any(self.results["local_min"]): + minima = self.results[self.results["local_min"]] + self.results = self.results[~self.results["local_min"]] + return self.results, minima + return self.results, np.empty(0, dtype=self.gen_specs["out"]) def tell(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: super().tell(results, tag) @@ -225,17 +228,17 @@ def ask(self, num_points: int = 0) -> (npt.NDArray, Optional[npt.NDArray]): cancels = output got_cancels_first = True else: - points = self._add_sim_ids(output) + self.results = self._add_sim_ids(output) got_cancels_first = False try: additional = self.outbox.get(timeout=0.2) # either cancels or new points if got_cancels_first: return additional, cancels - return points, additional + return self.results, additional except thread_queue.Empty: if got_cancels_first: return np.empty(0, dtype=self.gen_specs["out"]), cancels - return points, np.empty(0, dtype=[("sim_id", int), ("cancel_requested", bool)]) + return self.results, np.empty(0, dtype=[("sim_id", int), ("cancel_requested", bool)]) def tell(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: super().tell(results, tag) diff --git a/libensemble/tests/unit_tests/test_asktell_surmise.py b/libensemble/tests/unit_tests/test_asktell_surmise.py index c44130ff2..966912d89 100644 --- a/libensemble/tests/unit_tests/test_asktell_surmise.py +++ b/libensemble/tests/unit_tests/test_asktell_surmise.py @@ -81,11 +81,7 @@ def test_asktell_surmise(): surmise.setup() initial_sample = surmise.initial_ask() - - initial_results = np.zeros(len(initial_sample), dtype=gen_out + [("f", float)]) - - for field in gen_specs["out"]: - initial_results[field[0]] = initial_sample[field[0]] + initial_results = surmise.create_results_array() total_evals = 0 @@ -99,10 +95,7 @@ def test_asktell_surmise(): requested_canceled_sim_ids = [] next_sample, cancels = surmise.ask() - next_results = np.zeros(len(next_sample), dtype=gen_out + [("f", float)]) - - for field in gen_specs["out"]: - next_results[field[0]] = next_sample[field[0]] + next_results = surmise.create_results_array() for i in range(len(next_sample)): H_out, _a, _b = borehole(next_sample[i], {}, sim_specs, {"H_rows": np.array([next_sample[i]["sim_id"]])}) @@ -117,9 +110,7 @@ def test_asktell_surmise(): samples_iter = range(len(sample)) for i in samples_iter: - result = np.zeros(1, dtype=gen_out + [("f", float)]) - for field in gen_specs["out"]: - result[field[0]] = sample[i][field[0]] + result = surmise.create_results_array() H_out, _a, _b = borehole(sample[i], {}, sim_specs, {"H_rows": np.array([sample[i]["sim_id"]])}) result["f"] = H_out["f"][0] total_evals += 1 diff --git a/libensemble/tests/unit_tests/test_persistent_aposmm.py b/libensemble/tests/unit_tests/test_persistent_aposmm.py index 08d75a019..aff7e93be 100644 --- a/libensemble/tests/unit_tests/test_persistent_aposmm.py +++ b/libensemble/tests/unit_tests/test_persistent_aposmm.py @@ -205,14 +205,11 @@ def test_asktell_with_persistent_aposmm(): my_APOSMM = APOSMM(gen_specs) my_APOSMM.setup() initial_sample = my_APOSMM.initial_ask() - initial_results = np.zeros(len(initial_sample), dtype=gen_out + [("f", float)]) + initial_results = my_APOSMM.create_results_array() total_evals = 0 eval_max = 2000 - for field in gen_specs["out"]: - initial_results[field[0]] = initial_sample[field[0]] - for i in initial_sample["sim_id"]: initial_results[i]["f"] = six_hump_camel_func(initial_sample["x"][i]) total_evals += 1 @@ -227,9 +224,7 @@ def test_asktell_with_persistent_aposmm(): if len(detected_minima): for m in detected_minima: potential_minima.append(m) - results = np.zeros(len(sample), dtype=gen_out + [("f", float)]) - for field in gen_specs["out"]: - results[field[0]] = sample[field[0]] + results = my_APOSMM.create_results_array() for i in range(len(sample)): results[i]["f"] = six_hump_camel_func(sample["x"][i]) total_evals += 1 From 8e482d4b0f1fd4bd8413ba332efafcc0fc122563 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 17 Apr 2024 16:35:59 -0500 Subject: [PATCH 097/288] tentative remove of initial_ask, make num_points in ask optional --- libensemble/generators.py | 37 +++---------------- .../test_1d_asktell_gen.py | 3 -- .../tests/unit_tests/test_asktell_surmise.py | 2 +- .../unit_tests/test_persistent_aposmm.py | 2 +- libensemble/utils/runners.py | 3 +- 5 files changed, 9 insertions(+), 38 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 9067be792..3ea192e91 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -29,12 +29,6 @@ def __init__(self, param): self.param = param self.model = None - def set_history(self, yesterdays_points): - self.history = new_history - - def initial_ask(self, num_points, yesterdays_points): - return create_initial_points(num_points, self.param, self.history) - def ask(self, num_points): return create_points(num_points, self.param) @@ -61,20 +55,8 @@ def __init__(self, *args, **kwargs): my_generator = MyGenerator(my_parameter, batch_size=10) """ - def set_history(self, new_history: Iterable): - """ - Replace/initialize the generator's history. - """ - - def initial_ask(self, num_points: int, *args, **kwargs) -> Iterable: - """ - The initial set of generated points is often produced differently than subsequent sets. - This is a separate method to simplify the common pattern of noting internally if a - specific ask was the first. - """ - @abstractmethod - def ask(self, num_points: int, *args, **kwargs) -> (Iterable, Optional[Iterable]): + def ask(self, num_points: Optional[int], *args, **kwargs) -> (Iterable, Optional[Iterable]): """ Request the next set of points to evaluate, and optionally any previous points to update. """ @@ -136,12 +118,9 @@ def _set_sim_ended(self, results: npt.NDArray) -> npt.NDArray: results = new_results return results - def initial_ask(self, num_points: int = 0, *args, **kwargs) -> npt.NDArray: + def ask(self, num_points: Optional[int] = 0, *args, **kwargs) -> (Iterable, Optional[npt.NDArray]): if not self.gen.running: self.gen.run() - return self.ask(num_points) - - def ask(self, num_points: int = 0, *args, **kwargs) -> (Iterable, Optional[npt.NDArray]): _, self.last_ask = self.outbox.get() return self.last_ask["calc_out"] @@ -180,11 +159,8 @@ def __init__( persis_info["nworkers"] = 4 super().__init__(gen_specs, History, persis_info, libE_info) - def initial_ask(self, num_points: int = 0, *args) -> npt.NDArray: - return super().initial_ask(num_points, args)[0] - - def ask(self, num_points: int = 0) -> (npt.NDArray, npt.NDArray): - self.results = super().ask(num_points) + def ask(self) -> (npt.NDArray, npt.NDArray): + self.results = super().ask() if any(self.results["local_min"]): minima = self.results[self.results["local_min"]] self.results = self.results[~self.results["local_min"]] @@ -215,13 +191,10 @@ def _add_sim_ids(self, array: npt.NDArray) -> npt.NDArray: self.sim_id_index += len(array) return array - def initial_ask(self, num_points: int = 0, *args) -> npt.NDArray: - return super().initial_ask(num_points, args)[0] - def ready_to_be_asked(self) -> bool: return not self.outbox.empty() - def ask(self, num_points: int = 0) -> (npt.NDArray, Optional[npt.NDArray]): + def ask(self) -> (npt.NDArray, Optional[npt.NDArray]): _, self.last_ask = self.outbox.get() output = self.last_ask["calc_out"] if "cancel_requested" in output.dtype.names: diff --git a/libensemble/tests/functionality_tests/test_1d_asktell_gen.py b/libensemble/tests/functionality_tests/test_1d_asktell_gen.py index a20bc10fa..ab6dfe1bb 100644 --- a/libensemble/tests/functionality_tests/test_1d_asktell_gen.py +++ b/libensemble/tests/functionality_tests/test_1d_asktell_gen.py @@ -53,9 +53,6 @@ def __init__(self, persis_info, gen_specs): self.gen_specs = gen_specs _, self.n, self.lb, self.ub = _get_user_params(gen_specs["user"]) - def initial_ask(self, num_points, *args): - return self.ask(num_points) - def ask(self, num_points): H_o = np.zeros(num_points, dtype=self.gen_specs["out"]) H_o["x"] = self.persis_info["rand_stream"].uniform(self.lb, self.ub, (num_points, self.n)) diff --git a/libensemble/tests/unit_tests/test_asktell_surmise.py b/libensemble/tests/unit_tests/test_asktell_surmise.py index 966912d89..783d86e3d 100644 --- a/libensemble/tests/unit_tests/test_asktell_surmise.py +++ b/libensemble/tests/unit_tests/test_asktell_surmise.py @@ -80,7 +80,7 @@ def test_asktell_surmise(): surmise = Surmise(gen_specs, persis_info=persis_info[1]) # we add sim_id as a field to gen_specs["out"] surmise.setup() - initial_sample = surmise.initial_ask() + initial_sample = surmise.ask() initial_results = surmise.create_results_array() total_evals = 0 diff --git a/libensemble/tests/unit_tests/test_persistent_aposmm.py b/libensemble/tests/unit_tests/test_persistent_aposmm.py index aff7e93be..2a6a9d098 100644 --- a/libensemble/tests/unit_tests/test_persistent_aposmm.py +++ b/libensemble/tests/unit_tests/test_persistent_aposmm.py @@ -204,7 +204,7 @@ def test_asktell_with_persistent_aposmm(): my_APOSMM = APOSMM(gen_specs) my_APOSMM.setup() - initial_sample = my_APOSMM.initial_ask() + initial_sample = my_APOSMM.ask() initial_results = my_APOSMM.create_results_array() total_evals = 0 diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 14e015e7f..77f848442 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -107,6 +107,7 @@ def _loop_over_normal_generator(self, tag, Work): else: H_out = points tag, Work, H_in = self.ps.send_recv(H_out) + self.gen.tell(H_in) return H_in def _ask_and_send(self): @@ -136,7 +137,7 @@ def _persistent_result(self, calc_in, persis_info, libE_info): self.gen.libE_info = libE_info self.gen.setup() initial_batch = getattr(self.gen, "initial_batch_size", 0) or libE_info["batch_size"] - H_out = self.gen.initial_ask(initial_batch, calc_in) + H_out = self.gen.ask(initial_batch, calc_in) tag, Work, H_in = self.ps.send_recv(H_out) # evaluate the initial sample self.gen.tell(H_in) # tell the gen the initial sample results if issubclass(type(self.gen), LibEnsembleGenInterfacer): From d92d7a571437bcfd4922e1f6de40b0029272bf9a Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 17 Apr 2024 16:37:25 -0500 Subject: [PATCH 098/288] disregard "updates" from aposmm/surmise's first ask --- libensemble/tests/unit_tests/test_asktell_surmise.py | 2 +- libensemble/tests/unit_tests/test_persistent_aposmm.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libensemble/tests/unit_tests/test_asktell_surmise.py b/libensemble/tests/unit_tests/test_asktell_surmise.py index 783d86e3d..bfbf8eff0 100644 --- a/libensemble/tests/unit_tests/test_asktell_surmise.py +++ b/libensemble/tests/unit_tests/test_asktell_surmise.py @@ -80,7 +80,7 @@ def test_asktell_surmise(): surmise = Surmise(gen_specs, persis_info=persis_info[1]) # we add sim_id as a field to gen_specs["out"] surmise.setup() - initial_sample = surmise.ask() + initial_sample, _ = surmise.ask() initial_results = surmise.create_results_array() total_evals = 0 diff --git a/libensemble/tests/unit_tests/test_persistent_aposmm.py b/libensemble/tests/unit_tests/test_persistent_aposmm.py index 2a6a9d098..c6129e615 100644 --- a/libensemble/tests/unit_tests/test_persistent_aposmm.py +++ b/libensemble/tests/unit_tests/test_persistent_aposmm.py @@ -204,7 +204,7 @@ def test_asktell_with_persistent_aposmm(): my_APOSMM = APOSMM(gen_specs) my_APOSMM.setup() - initial_sample = my_APOSMM.ask() + initial_sample, _ = my_APOSMM.ask() initial_results = my_APOSMM.create_results_array() total_evals = 0 From 904ca3950157c13021d724226f0d1e07d97cf707 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 17 Apr 2024 16:42:36 -0500 Subject: [PATCH 099/288] various fixes --- libensemble/generators.py | 4 ++-- libensemble/utils/runners.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 3ea192e91..30232eff0 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -159,7 +159,7 @@ def __init__( persis_info["nworkers"] = 4 super().__init__(gen_specs, History, persis_info, libE_info) - def ask(self) -> (npt.NDArray, npt.NDArray): + def ask(self, *args) -> (npt.NDArray, npt.NDArray): self.results = super().ask() if any(self.results["local_min"]): minima = self.results[self.results["local_min"]] @@ -194,7 +194,7 @@ def _add_sim_ids(self, array: npt.NDArray) -> npt.NDArray: def ready_to_be_asked(self) -> bool: return not self.outbox.empty() - def ask(self) -> (npt.NDArray, Optional[npt.NDArray]): + def ask(self, *args) -> (npt.NDArray, Optional[npt.NDArray]): _, self.last_ask = self.outbox.get() output = self.last_ask["calc_out"] if "cancel_requested" in output.dtype.names: diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 77f848442..6bc0304e4 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -137,9 +137,9 @@ def _persistent_result(self, calc_in, persis_info, libE_info): self.gen.libE_info = libE_info self.gen.setup() initial_batch = getattr(self.gen, "initial_batch_size", 0) or libE_info["batch_size"] - H_out = self.gen.ask(initial_batch, calc_in) + H_out, _ = self.gen.ask(initial_batch) # updates can probably be ignored when asking the first time tag, Work, H_in = self.ps.send_recv(H_out) # evaluate the initial sample - self.gen.tell(H_in) # tell the gen the initial sample results + self.gen.tell(H_in) if issubclass(type(self.gen), LibEnsembleGenInterfacer): final_H_in = self._loop_over_persistent_interfacer() else: From 6ef87682a18df7a4cc1365e4c55c735592d900c7 Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 18 Apr 2024 10:49:54 -0500 Subject: [PATCH 100/288] removing some redundant method defs, removing surmise unit test on macos jobs --- .github/workflows/extra.yml | 1 + libensemble/generators.py | 12 ------------ 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/.github/workflows/extra.yml b/.github/workflows/extra.yml index bfe7f7441..80e0395fb 100644 --- a/.github/workflows/extra.yml +++ b/.github/workflows/extra.yml @@ -233,6 +233,7 @@ jobs: env: CONDA_BUILD_SYSROOT: /Users/runner/work/libensemble/sdk/MacOSX10.15.sdk run: | + rm ./libensemble/tests/unit_tests/test_asktell_surmise.py ./libensemble/tests/run-tests.sh -e -z -${{ matrix.comms-type }} - name: Merge coverage diff --git a/libensemble/generators.py b/libensemble/generators.py index 30232eff0..0086ca44a 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -167,12 +167,6 @@ def ask(self, *args) -> (npt.NDArray, npt.NDArray): return self.results, minima return self.results, np.empty(0, dtype=self.gen_specs["out"]) - def tell(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: - super().tell(results, tag) - - def final_tell(self, results: npt.NDArray = None) -> (npt.NDArray, dict, int): - return super().final_tell(results) - class Surmise(LibEnsembleGenInterfacer): def __init__( @@ -212,9 +206,3 @@ def ask(self, *args) -> (npt.NDArray, Optional[npt.NDArray]): if got_cancels_first: return np.empty(0, dtype=self.gen_specs["out"]), cancels return self.results, np.empty(0, dtype=[("sim_id", int), ("cancel_requested", bool)]) - - def tell(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: - super().tell(results, tag) - - def final_tell(self, results: npt.NDArray = None) -> (npt.NDArray, dict, int): - return super().final_tell(results) From 7f7c4b3786cdb9db26989be6f51f28360b162561 Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 18 Apr 2024 16:12:12 -0500 Subject: [PATCH 101/288] first attempt to implement .ask_updates(), add to current ask/tell gens --- libensemble/generators.py | 45 ++++++++++++++----- .../tests/unit_tests/test_asktell_surmise.py | 10 ++--- .../unit_tests/test_persistent_aposmm.py | 6 +-- libensemble/utils/runners.py | 18 ++++---- 4 files changed, 49 insertions(+), 30 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 0086ca44a..899ad2274 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -1,3 +1,4 @@ +import copy import queue as thread_queue from abc import ABC, abstractmethod from typing import Iterable, Optional @@ -56,11 +57,16 @@ def __init__(self, *args, **kwargs): """ @abstractmethod - def ask(self, num_points: Optional[int], *args, **kwargs) -> (Iterable, Optional[Iterable]): + def ask(self, num_points: Optional[int], *args, **kwargs) -> Iterable: """ Request the next set of points to evaluate, and optionally any previous points to update. """ + def ask_updates(self) -> Iterable: + """ + Request any updates to previous points, e.g. minima discovered, points to cancel. + """ + def tell(self, results: Iterable, *args, **kwargs) -> None: """ Send the results of evaluations to the generator. @@ -118,12 +124,15 @@ def _set_sim_ended(self, results: npt.NDArray) -> npt.NDArray: results = new_results return results - def ask(self, num_points: Optional[int] = 0, *args, **kwargs) -> (Iterable, Optional[npt.NDArray]): + def ask(self, num_points: Optional[int] = 0, *args, **kwargs) -> npt.NDArray: if not self.gen.running: self.gen.run() _, self.last_ask = self.outbox.get() return self.last_ask["calc_out"] + def ask_updates(self) -> npt.NDArray: + return self.ask() + def tell(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: if results is not None: results = self._set_sim_ended(results) @@ -158,14 +167,20 @@ def __init__( persis_info = add_unique_random_streams({}, 4)[1] persis_info["nworkers"] = 4 super().__init__(gen_specs, History, persis_info, libE_info) + self.all_local_minima = [] - def ask(self, *args) -> (npt.NDArray, npt.NDArray): + def ask(self, *args) -> npt.NDArray: self.results = super().ask() if any(self.results["local_min"]): - minima = self.results[self.results["local_min"]] - self.results = self.results[~self.results["local_min"]] - return self.results, minima - return self.results, np.empty(0, dtype=self.gen_specs["out"]) + min_idxs = self.results["local_min"] + self.all_local_minima.append(self.results[min_idxs]) + self.results = self.results[~min_idxs] + return self.results + + def ask_updates(self) -> npt.NDArray: + minima = copy.deepcopy(self.all_local_minima) + self.all_local_minima = [] + return minima class Surmise(LibEnsembleGenInterfacer): @@ -179,6 +194,7 @@ def __init__( gen_specs["out"].append(("sim_id", int)) super().__init__(gen_specs, History, persis_info, libE_info) self.sim_id_index = 0 + self.all_cancels = [] def _add_sim_ids(self, array: npt.NDArray) -> npt.NDArray: array["sim_id"] = np.arange(self.sim_id_index, self.sim_id_index + len(array)) @@ -194,15 +210,20 @@ def ask(self, *args) -> (npt.NDArray, Optional[npt.NDArray]): if "cancel_requested" in output.dtype.names: cancels = output got_cancels_first = True + self.all_cancels.append(cancels) else: self.results = self._add_sim_ids(output) got_cancels_first = False try: additional = self.outbox.get(timeout=0.2) # either cancels or new points if got_cancels_first: - return additional, cancels - return self.results, additional + return additional + self.all_cancels.append(additional) + return self.results except thread_queue.Empty: - if got_cancels_first: - return np.empty(0, dtype=self.gen_specs["out"]), cancels - return self.results, np.empty(0, dtype=[("sim_id", int), ("cancel_requested", bool)]) + return self.results + + def ask_updates(self) -> npt.NDArray: + cancels = copy.deepcopy(self.all_cancels) + self.all_cancels = [] + return cancels diff --git a/libensemble/tests/unit_tests/test_asktell_surmise.py b/libensemble/tests/unit_tests/test_asktell_surmise.py index bfbf8eff0..b422b9011 100644 --- a/libensemble/tests/unit_tests/test_asktell_surmise.py +++ b/libensemble/tests/unit_tests/test_asktell_surmise.py @@ -80,7 +80,7 @@ def test_asktell_surmise(): surmise = Surmise(gen_specs, persis_info=persis_info[1]) # we add sim_id as a field to gen_specs["out"] surmise.setup() - initial_sample, _ = surmise.ask() + initial_sample = surmise.ask() initial_results = surmise.create_results_array() total_evals = 0 @@ -94,7 +94,7 @@ def test_asktell_surmise(): requested_canceled_sim_ids = [] - next_sample, cancels = surmise.ask() + next_sample, cancels = surmise.ask(), surmise.ask_updates() next_results = surmise.create_results_array() for i in range(len(next_sample)): @@ -103,7 +103,7 @@ def test_asktell_surmise(): total_evals += 1 surmise.tell(next_results) - sample, cancels = surmise.ask() + sample, cancels = surmise.ask(), surmise.ask_updates() while total_evals < max_evals: @@ -116,14 +116,14 @@ def test_asktell_surmise(): total_evals += 1 surmise.tell(result) if surmise.ready_to_be_asked(): - new_sample, cancels = surmise.ask() + new_sample, cancels = surmise.ask(), surmise.ask_updates() for m in cancels: requested_canceled_sim_ids.append(m) if len(new_sample): sample = new_sample break - H, persis_info, exit_code = surmise.final_tell() + H, persis_info, exit_code = surmise.final_tell(None) assert exit_code == FINISHED_PERSISTENT_GEN_TAG, "Standalone persistent_aposmm didn't exit correctly" assert len(requested_canceled_sim_ids), "No cancellations sent by Surmise" diff --git a/libensemble/tests/unit_tests/test_persistent_aposmm.py b/libensemble/tests/unit_tests/test_persistent_aposmm.py index c6129e615..fe065554d 100644 --- a/libensemble/tests/unit_tests/test_persistent_aposmm.py +++ b/libensemble/tests/unit_tests/test_persistent_aposmm.py @@ -204,7 +204,7 @@ def test_asktell_with_persistent_aposmm(): my_APOSMM = APOSMM(gen_specs) my_APOSMM.setup() - initial_sample, _ = my_APOSMM.ask() + initial_sample = my_APOSMM.ask() initial_results = my_APOSMM.create_results_array() total_evals = 0 @@ -220,7 +220,7 @@ def test_asktell_with_persistent_aposmm(): while total_evals < eval_max: - sample, detected_minima = my_APOSMM.ask() + sample, detected_minima = my_APOSMM.ask(), my_APOSMM.ask_updates() if len(detected_minima): for m in detected_minima: potential_minima.append(m) @@ -229,7 +229,7 @@ def test_asktell_with_persistent_aposmm(): results[i]["f"] = six_hump_camel_func(sample["x"][i]) total_evals += 1 my_APOSMM.tell(results) - H, persis_info, exit_code = my_APOSMM.final_tell() + H, persis_info, exit_code = my_APOSMM.final_tell(results) assert exit_code == FINISHED_PERSISTENT_GEN_TAG, "Standalone persistent_aposmm didn't exit correctly" assert persis_info.get("run_order"), "Standalone persistent_aposmm didn't do any localopt runs" diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 6bc0304e4..c7a796bb9 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -101,9 +101,9 @@ def __init__(self, specs): def _loop_over_normal_generator(self, tag, Work): while tag not in [PERSIS_STOP, STOP_TAG]: batch_size = getattr(self.gen, "batch_size", 0) or Work["libE_info"]["batch_size"] - points = self.gen.ask(batch_size) - if len(points) == 2: # returned "samples" and "updates". can combine if same dtype - H_out = np.append(points[0], points[1]) + points, updates = self.gen.ask(batch_size), self.gen.ask_updates() + if len(updates): # returned "samples" and "updates". can combine if same dtype + H_out = np.append(points, updates) else: H_out = points tag, Work, H_in = self.ps.send_recv(H_out) @@ -112,12 +112,10 @@ def _loop_over_normal_generator(self, tag, Work): def _ask_and_send(self): for _ in range(self.gen.outbox.qsize()): # recv/send any outstanding messages - points = self.gen.ask() - if len(points) == 2: # returned "samples" and "updates". can combine if same dtype - H_out = np.append(points[0], points[1]) - else: - H_out = points - self.ps.send(H_out) + points, updates = self.gen.ask(), self.gen.ask_updates() + self.ps.send(points) + if len(updates): # returned "samples" and "updates". can combine if same dtype + self.ps.send(updates) def _loop_over_persistent_interfacer(self): while True: @@ -137,7 +135,7 @@ def _persistent_result(self, calc_in, persis_info, libE_info): self.gen.libE_info = libE_info self.gen.setup() initial_batch = getattr(self.gen, "initial_batch_size", 0) or libE_info["batch_size"] - H_out, _ = self.gen.ask(initial_batch) # updates can probably be ignored when asking the first time + H_out = self.gen.ask(initial_batch) # updates can probably be ignored when asking the first time tag, Work, H_in = self.ps.send_recv(H_out) # evaluate the initial sample self.gen.tell(H_in) if issubclass(type(self.gen), LibEnsembleGenInterfacer): From 858c5ad934c877afee5d6881ca542f6237c4401d Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 19 Apr 2024 09:41:14 -0500 Subject: [PATCH 102/288] only combine points and updates if we get updates back. otherwise just send points. if we get updates and have trouble combining them, send them separately --- libensemble/utils/runners.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index c7a796bb9..53065aeb5 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -113,9 +113,15 @@ def _loop_over_normal_generator(self, tag, Work): def _ask_and_send(self): for _ in range(self.gen.outbox.qsize()): # recv/send any outstanding messages points, updates = self.gen.ask(), self.gen.ask_updates() - self.ps.send(points) - if len(updates): # returned "samples" and "updates". can combine if same dtype - self.ps.send(updates) + if len(updates): + try: + self.ps.send(np.append(points, updates)) + except np.exceptions.DTypePromotionError: # points/updates have different dtypes + self.ps.send(points) + for i in updates: + self.ps.send(i) + else: + self.ps.send(points) def _loop_over_persistent_interfacer(self): while True: From 02e60c4588a155a50bc5bc31945187dae72b7a90 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 19 Apr 2024 09:55:46 -0500 Subject: [PATCH 103/288] only try combining also if updates is not None --- libensemble/utils/runners.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 53065aeb5..6f2500b44 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -102,7 +102,7 @@ def _loop_over_normal_generator(self, tag, Work): while tag not in [PERSIS_STOP, STOP_TAG]: batch_size = getattr(self.gen, "batch_size", 0) or Work["libE_info"]["batch_size"] points, updates = self.gen.ask(batch_size), self.gen.ask_updates() - if len(updates): # returned "samples" and "updates". can combine if same dtype + if updates is not None and len(updates): # returned "samples" and "updates". can combine if same dtype H_out = np.append(points, updates) else: H_out = points @@ -113,7 +113,7 @@ def _loop_over_normal_generator(self, tag, Work): def _ask_and_send(self): for _ in range(self.gen.outbox.qsize()): # recv/send any outstanding messages points, updates = self.gen.ask(), self.gen.ask_updates() - if len(updates): + if updates is not None and len(updates): try: self.ps.send(np.append(points, updates)) except np.exceptions.DTypePromotionError: # points/updates have different dtypes From 1ed90229e80fe3e8fc5a6a625ceaddc7bd8fbda2 Mon Sep 17 00:00:00 2001 From: shudson Date: Fri, 19 Apr 2024 13:46:54 -0500 Subject: [PATCH 104/288] Add RandSample ask/tell generator --- libensemble/gen_funcs/persistent_sampling.py | 32 ++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/libensemble/gen_funcs/persistent_sampling.py b/libensemble/gen_funcs/persistent_sampling.py index fcbcba090..fec2c3e06 100644 --- a/libensemble/gen_funcs/persistent_sampling.py +++ b/libensemble/gen_funcs/persistent_sampling.py @@ -29,6 +29,38 @@ def _get_user_params(user_specs): return b, n, lb, ub +class RandSample(): + def __init__(self, _, persis_info, gen_specs, libE_info=None): + # self.H = H + self.persis_info = persis_info + self.gen_specs = gen_specs + self.libE_info = libE_info + self._get_user_params(self.gen_specs["user"]) + + def ask(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)) + + if "obj_component" in H_o.dtype.fields: # needs H_o - needs to be created in here. + H_o["obj_component"] = self.persis_info["rand_stream"].integers( + low=0, high=self.gen_specs["user"]["num_components"], size=n_trials + ) + return H_o + + def tell(self, calc_in): + pass # random sample so nothing to tell + + 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" + + @persistent_input_fields(["f", "x", "sim_id"]) @output_data([("x", float, (2,))]) def persistent_uniform(_, persis_info, gen_specs, libE_info): From 57db8c6c6ef631d757dee6e96fbf6a6a0d862a55 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 19 Apr 2024 14:55:12 -0500 Subject: [PATCH 105/288] surmise needs to start first for ask to work - do so by calling superclass's ask for contents --- libensemble/generators.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 899ad2274..72fff45c3 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -14,7 +14,7 @@ class Generator(ABC): """ - v 0.4.12.24 + v 0.4.19.24 Tentative generator interface for use with libEnsemble, and generic enough to be broadly compatible with other workflow packages. @@ -205,8 +205,7 @@ def ready_to_be_asked(self) -> bool: return not self.outbox.empty() def ask(self, *args) -> (npt.NDArray, Optional[npt.NDArray]): - _, self.last_ask = self.outbox.get() - output = self.last_ask["calc_out"] + output = super().ask() if "cancel_requested" in output.dtype.names: cancels = output got_cancels_first = True From ce79b2a6cce949df1c13c2ad4a626da2995a3de3 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 19 Apr 2024 16:13:16 -0500 Subject: [PATCH 106/288] surmise was creating a too-big template result array. let user specify length --- libensemble/generators.py | 6 ++++-- libensemble/tests/unit_tests/test_asktell_surmise.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 72fff45c3..fc00dea01 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -145,8 +145,10 @@ def final_tell(self, results: npt.NDArray) -> (npt.NDArray, dict, int): self.tell(results, PERSIS_STOP) return self.gen.result() - def create_results_array(self, addtl_fields: list = [("f", float)]) -> npt.NDArray: - new_results = np.zeros(len(self.results), dtype=self.gen_specs["out"] + addtl_fields) + def create_results_array(self, length: int = 0, addtl_fields: list = [("f", float)]) -> npt.NDArray: + if not length: + in_length = len(self.results) + new_results = np.zeros(in_length, dtype=self.gen_specs["out"] + addtl_fields) for field in self.gen_specs["out"]: new_results[field[0]] = self.results[field[0]] return new_results diff --git a/libensemble/tests/unit_tests/test_asktell_surmise.py b/libensemble/tests/unit_tests/test_asktell_surmise.py index b422b9011..688c6878a 100644 --- a/libensemble/tests/unit_tests/test_asktell_surmise.py +++ b/libensemble/tests/unit_tests/test_asktell_surmise.py @@ -110,7 +110,7 @@ def test_asktell_surmise(): samples_iter = range(len(sample)) for i in samples_iter: - result = surmise.create_results_array() + result = surmise.create_results_array(1) H_out, _a, _b = borehole(sample[i], {}, sim_specs, {"H_rows": np.array([sample[i]["sim_id"]])}) result["f"] = H_out["f"][0] total_evals += 1 From c23476f21ca540384274130b79548005ad1dd7c8 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 19 Apr 2024 16:15:33 -0500 Subject: [PATCH 107/288] fix --- libensemble/generators.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index fc00dea01..ca3af2e37 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -146,8 +146,7 @@ def final_tell(self, results: npt.NDArray) -> (npt.NDArray, dict, int): return self.gen.result() def create_results_array(self, length: int = 0, addtl_fields: list = [("f", float)]) -> npt.NDArray: - if not length: - in_length = len(self.results) + in_length = len(self.results) if not length else length new_results = np.zeros(in_length, dtype=self.gen_specs["out"] + addtl_fields) for field in self.gen_specs["out"]: new_results[field[0]] = self.results[field[0]] From 9353e00c3626e9cca34da96af72cd4caccaeec1a Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 19 Apr 2024 16:35:50 -0500 Subject: [PATCH 108/288] lets just make this simple for now....... --- libensemble/tests/unit_tests/test_asktell_surmise.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libensemble/tests/unit_tests/test_asktell_surmise.py b/libensemble/tests/unit_tests/test_asktell_surmise.py index 688c6878a..05464f2ff 100644 --- a/libensemble/tests/unit_tests/test_asktell_surmise.py +++ b/libensemble/tests/unit_tests/test_asktell_surmise.py @@ -110,7 +110,9 @@ def test_asktell_surmise(): samples_iter = range(len(sample)) for i in samples_iter: - result = surmise.create_results_array(1) + result = np.zeros(1, dtype=gen_specs["out"] + [("f", float)]) + for field in gen_specs["out"]: + result[field[0]] = sample[i][field[0]] H_out, _a, _b = borehole(sample[i], {}, sim_specs, {"H_rows": np.array([sample[i]["sim_id"]])}) result["f"] = H_out["f"][0] total_evals += 1 From 7f1ef574036f8d35da998b02e501d264695962b9 Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 22 Apr 2024 12:54:12 -0500 Subject: [PATCH 109/288] move test_asktell_surmise in-place test to regression_tests --- .github/workflows/extra.yml | 1 - .../test_asktell_surmise.py | 14 ++++++-------- 2 files changed, 6 insertions(+), 9 deletions(-) rename libensemble/tests/{unit_tests => regression_tests}/test_asktell_surmise.py (98%) diff --git a/.github/workflows/extra.yml b/.github/workflows/extra.yml index 80e0395fb..bfe7f7441 100644 --- a/.github/workflows/extra.yml +++ b/.github/workflows/extra.yml @@ -233,7 +233,6 @@ jobs: env: CONDA_BUILD_SYSROOT: /Users/runner/work/libensemble/sdk/MacOSX10.15.sdk run: | - rm ./libensemble/tests/unit_tests/test_asktell_surmise.py ./libensemble/tests/run-tests.sh -e -z -${{ matrix.comms-type }} - name: Merge coverage diff --git a/libensemble/tests/unit_tests/test_asktell_surmise.py b/libensemble/tests/regression_tests/test_asktell_surmise.py similarity index 98% rename from libensemble/tests/unit_tests/test_asktell_surmise.py rename to libensemble/tests/regression_tests/test_asktell_surmise.py index 05464f2ff..fe48d02c9 100644 --- a/libensemble/tests/unit_tests/test_asktell_surmise.py +++ b/libensemble/tests/regression_tests/test_asktell_surmise.py @@ -1,13 +1,15 @@ +# TESTSUITE_COMMS: local +# TESTSUITE_NPROCS: 4 +# TESTSUITE_EXTRA: true +# TESTSUITE_OS_SKIP: OSX + import os import numpy as np -import pytest from libensemble.message_numbers import FINISHED_PERSISTENT_GEN_TAG - -@pytest.mark.extra -def test_asktell_surmise(): +if __name__ == "__main__": from libensemble.executors import Executor from libensemble.generators import Surmise @@ -129,7 +131,3 @@ def test_asktell_surmise(): assert exit_code == FINISHED_PERSISTENT_GEN_TAG, "Standalone persistent_aposmm didn't exit correctly" assert len(requested_canceled_sim_ids), "No cancellations sent by Surmise" - - -if __name__ == "__main__": - test_asktell_surmise() From f54dbc6af34dfef4649165b101776af80ea44779 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 24 Apr 2024 12:35:05 -0500 Subject: [PATCH 110/288] unique ensemble_dir_path --- .../test_persistent_surmise_killsims_asktell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libensemble/tests/regression_tests/test_persistent_surmise_killsims_asktell.py b/libensemble/tests/regression_tests/test_persistent_surmise_killsims_asktell.py index 4116b5b6d..40cf7a28a 100644 --- a/libensemble/tests/regression_tests/test_persistent_surmise_killsims_asktell.py +++ b/libensemble/tests/regression_tests/test_persistent_surmise_killsims_asktell.py @@ -77,7 +77,7 @@ # libE_specs["use_worker_dirs"] = True # To overwrite - make worker dirs only # Rename ensemble dir for non-interference with other regression tests - libE_specs["ensemble_dir_path"] = "ensemble_calib_kills" + libE_specs["ensemble_dir_path"] = "ensemble_calib_kills_asktell" sim_specs = { "sim_f": sim_f, From 3d977907c0ad2a8e5a5d7d61e0876c0d2458fe53 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 24 Apr 2024 13:09:15 -0500 Subject: [PATCH 111/288] dunno why this error occurs on tcp, but may be worth investigating... --- .../test_persistent_surmise_killsims_asktell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libensemble/tests/regression_tests/test_persistent_surmise_killsims_asktell.py b/libensemble/tests/regression_tests/test_persistent_surmise_killsims_asktell.py index 40cf7a28a..0dcbd55df 100644 --- a/libensemble/tests/regression_tests/test_persistent_surmise_killsims_asktell.py +++ b/libensemble/tests/regression_tests/test_persistent_surmise_killsims_asktell.py @@ -22,7 +22,7 @@ """ # Do not change these lines - they are parsed by run-tests.sh -# TESTSUITE_COMMS: mpi local tcp +# TESTSUITE_COMMS: mpi local # TESTSUITE_NPROCS: 3 4 # TESTSUITE_EXTRA: true # TESTSUITE_OS_SKIP: OSX From 9932e0aa1a68d9faf78cf427ab2f31fa9c88a000 Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 25 Apr 2024 13:50:47 -0500 Subject: [PATCH 112/288] perhaps we dont need to combine points and updates for libE. just simply send points first, then updates --- libensemble/utils/runners.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 6f2500b44..f51fdd38c 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -114,12 +114,9 @@ def _ask_and_send(self): for _ in range(self.gen.outbox.qsize()): # recv/send any outstanding messages points, updates = self.gen.ask(), self.gen.ask_updates() if updates is not None and len(updates): - try: - self.ps.send(np.append(points, updates)) - except np.exceptions.DTypePromotionError: # points/updates have different dtypes - self.ps.send(points) - for i in updates: - self.ps.send(i) + self.ps.send(points) + for i in updates: + self.ps.send(i) else: self.ps.send(points) From aeb28db06dc5735711d651616aacfe9b531cef3b Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 25 Apr 2024 15:22:34 -0500 Subject: [PATCH 113/288] be more careful with returning updates from surmise back to libE. keep_state when sending updates --- libensemble/generators.py | 6 +++--- .../test_persistent_surmise_killsims_asktell.py | 1 + libensemble/utils/runners.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index ca3af2e37..8e98daa18 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -215,10 +215,10 @@ def ask(self, *args) -> (npt.NDArray, Optional[npt.NDArray]): self.results = self._add_sim_ids(output) got_cancels_first = False try: - additional = self.outbox.get(timeout=0.2) # either cancels or new points + _, additional = self.outbox.get(timeout=0.2) # either cancels or new points if got_cancels_first: - return additional - self.all_cancels.append(additional) + return additional["calc_out"] + self.all_cancels.append(additional["calc_out"]) return self.results except thread_queue.Empty: return self.results diff --git a/libensemble/tests/regression_tests/test_persistent_surmise_killsims_asktell.py b/libensemble/tests/regression_tests/test_persistent_surmise_killsims_asktell.py index 0dcbd55df..8d971fe91 100644 --- a/libensemble/tests/regression_tests/test_persistent_surmise_killsims_asktell.py +++ b/libensemble/tests/regression_tests/test_persistent_surmise_killsims_asktell.py @@ -78,6 +78,7 @@ # Rename ensemble dir for non-interference with other regression tests libE_specs["ensemble_dir_path"] = "ensemble_calib_kills_asktell" + libE_specs["gen_on_manager"] = True sim_specs = { "sim_f": sim_f, diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index f51fdd38c..9aa827886 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -116,7 +116,7 @@ def _ask_and_send(self): if updates is not None and len(updates): self.ps.send(points) for i in updates: - self.ps.send(i) + self.ps.send(i, keep_state=True) else: self.ps.send(points) From bba59bb734afc0c927ae9098e9e74d239b9555f6 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 26 Apr 2024 14:56:50 -0500 Subject: [PATCH 114/288] the "for" condition is evaluated once, and may be inaccurate if ask/ask_updates takes two items from the queue. the next time around will hang. use "while qsize()" instead. --- libensemble/utils/runners.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 9aa827886..92f95c52e 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -111,7 +111,7 @@ def _loop_over_normal_generator(self, tag, Work): return H_in def _ask_and_send(self): - for _ in range(self.gen.outbox.qsize()): # recv/send any outstanding messages + while self.gen.outbox.qsize(): # recv/send any outstanding messages points, updates = self.gen.ask(), self.gen.ask_updates() if updates is not None and len(updates): self.ps.send(points) From 9591365826cf2be743c69e4dbeffc0d03b0a288d Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 8 May 2024 15:20:45 -0500 Subject: [PATCH 115/288] Make RandSample and ask/tell GPCAM subclasses of Generator, make test_1d_asktell_gen test the RandSample class in persistent_sampling --- libensemble/gen_funcs/persistent_gpCAM.py | 3 +- libensemble/gen_funcs/persistent_sampling.py | 6 +- .../test_1d_asktell_gen.py | 71 ++----------------- 3 files changed, 11 insertions(+), 69 deletions(-) diff --git a/libensemble/gen_funcs/persistent_gpCAM.py b/libensemble/gen_funcs/persistent_gpCAM.py index 013b5885f..0bab89c35 100644 --- a/libensemble/gen_funcs/persistent_gpCAM.py +++ b/libensemble/gen_funcs/persistent_gpCAM.py @@ -6,6 +6,7 @@ from gpcam import GPOptimizer as GP from numpy.lib.recfunctions import repack_fields +from libensemble import Generator from libensemble.message_numbers import EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG from libensemble.tools.persistent_support import PersistentSupport @@ -141,7 +142,7 @@ def _find_eligible_points(x_for_var, sorted_indices, r, batch_size): return np.array(eligible_points) -class GP_CAM_SIMPLE: +class GP_CAM_SIMPLE(Generator): # Choose whether functions are internal methods or not def _initialize_gpcAM(self, user_specs): """Extract user params""" diff --git a/libensemble/gen_funcs/persistent_sampling.py b/libensemble/gen_funcs/persistent_sampling.py index fec2c3e06..74338bbc9 100644 --- a/libensemble/gen_funcs/persistent_sampling.py +++ b/libensemble/gen_funcs/persistent_sampling.py @@ -2,6 +2,7 @@ import numpy as np +from libensemble import Generator from libensemble.message_numbers import EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG from libensemble.specs import output_data, persistent_input_fields from libensemble.tools.persistent_support import PersistentSupport @@ -29,7 +30,7 @@ def _get_user_params(user_specs): return b, n, lb, ub -class RandSample(): +class RandSample(Generator): def __init__(self, _, persis_info, gen_specs, libE_info=None): # self.H = H self.persis_info = persis_info @@ -60,6 +61,9 @@ def _get_user_params(self, user_specs): assert isinstance(self.lb, np.ndarray), "lb must be a numpy array" assert isinstance(self.ub, np.ndarray), "ub must be a numpy array" + def final_tell(self, results): + pass + @persistent_input_fields(["f", "x", "sim_id"]) @output_data([("x", float, (2,))]) diff --git a/libensemble/tests/functionality_tests/test_1d_asktell_gen.py b/libensemble/tests/functionality_tests/test_1d_asktell_gen.py index ab6dfe1bb..793cec368 100644 --- a/libensemble/tests/functionality_tests/test_1d_asktell_gen.py +++ b/libensemble/tests/functionality_tests/test_1d_asktell_gen.py @@ -16,10 +16,8 @@ import numpy as np # Import libEnsemble items for this test -from libensemble import Generator from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f -from libensemble.gen_funcs.persistent_sampling import _get_user_params -from libensemble.gen_funcs.sampling import lhs_sample +from libensemble.gen_funcs.persistent_sampling import RandSample from libensemble.libE import libE from libensemble.sim_funcs.rosenbrock import rosenbrock_eval as sim_f2 from libensemble.tools import add_unique_random_streams, parse_args @@ -31,73 +29,10 @@ def sim_f(In): return Out -class LHS(Generator): - def __init__(self, rand_stream, ub, lb, b, dtype): - self.rand_stream = rand_stream - self.ub = ub - self.lb = lb - self.batch_size = b - self.dtype = dtype - - def ask(self, *args): - n = len(self.lb) - H_o = np.zeros(self.batch_size, dtype=self.dtype) - A = lhs_sample(n, self.batch_size, self.rand_stream) - H_o["x"] = A * (self.ub - self.lb) + self.lb - return H_o - - -class PersistentUniform(Generator): - def __init__(self, persis_info, gen_specs): - self.persis_info = persis_info - self.gen_specs = gen_specs - _, self.n, self.lb, self.ub = _get_user_params(gen_specs["user"]) - - def ask(self, num_points): - H_o = np.zeros(num_points, dtype=self.gen_specs["out"]) - H_o["x"] = self.persis_info["rand_stream"].uniform(self.lb, self.ub, (num_points, self.n)) - self.last_H = H_o - return H_o - - def tell(self, H_in): - if hasattr(H_in, "__len__"): - self.batch_size = len(H_in) - - def final_tell(self, H_in): - self.tell(H_in) - return self.last_H - - if __name__ == "__main__": nworkers, is_manager, libE_specs, _ = parse_args() libE_specs["gen_on_manager"] = True - sim_specs = { - "sim_f": sim_f, - "in": ["x"], - "out": [("f", float)], - } - - gen_out = [("x", float, (1,))] - - persis_info = add_unique_random_streams({}, nworkers + 1, seed=1234) - - GenOne = LHS(persis_info[1]["rand_stream"], np.array([3]), np.array([-3]), 500, gen_out) - - gen_specs_normal = { - "generator": GenOne, - "out": [("x", float, (1,))], - } - - exit_criteria = {"gen_max": 201} - - H, persis_info, flag = libE(sim_specs, gen_specs_normal, exit_criteria, persis_info, libE_specs=libE_specs) - - if is_manager: - assert len(H) >= 201 - print("\nlibEnsemble with NORMAL random sampling has generated enough points") - print(H[:10]) - sim_specs = { "sim_f": sim_f2, "in": ["x"], @@ -114,9 +49,11 @@ def final_tell(self, H_in): }, } + exit_criteria = {"gen_max": 201} + persis_info = add_unique_random_streams({}, nworkers + 1, seed=1234) - gen_two = PersistentUniform(persis_info[1], gen_specs_persistent) + gen_two = RandSample(None, persis_info[1], gen_specs_persistent, None) gen_specs_persistent["generator"] = gen_two alloc_specs = {"alloc_f": alloc_f} From e225e6cf68a4cd8b5db2b45de3581f6ba2c74814 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 10 May 2024 16:24:37 -0500 Subject: [PATCH 116/288] an attempt at allowing users (Optimas) to ask APOSMM for selections of points. cache an ask of aposmm, give out selections of that ask until all are given out, then ask aposmm for more --- libensemble/generators.py | 46 ++++++++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 8e98daa18..5cb336d16 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -87,7 +87,7 @@ class LibEnsembleGenInterfacer(Generator): """ def __init__( - self, gen_specs: dict, History: npt.NDArray = [], persis_info: dict = {}, libE_info: dict = {} + self, gen_specs: dict, History: npt.NDArray = [], persis_info: dict = {}, libE_info: dict = {}, **kwargs ) -> None: self.gen_f = gen_specs["gen_f"] self.gen_specs = gen_specs @@ -159,24 +159,54 @@ class APOSMM(LibEnsembleGenInterfacer): """ def __init__( - self, gen_specs: dict, History: npt.NDArray = [], persis_info: dict = {}, libE_info: dict = {} + self, gen_specs: dict = {}, History: npt.NDArray = [], persis_info: dict = {}, libE_info: dict = {}, **kwargs ) -> None: from libensemble.gen_funcs.persistent_aposmm import aposmm gen_specs["gen_f"] = aposmm + if len(kwargs) > 0: + gen_specs["user"] = kwargs + if not gen_specs.get("out"): + n = len(kwargs["lb"]) or len(kwargs["ub"]) + gen_specs["out"] = [ + ("x", float, n), + ("x_on_cube", float, n), + ("sim_id", int), + ("local_min", bool), + ("local_pt", bool), + ] + gen_specs["in"] = ["x", "f", "local_pt", "sim_id", "sim_ended", "x_on_cube", "local_min"] if not persis_info: persis_info = add_unique_random_streams({}, 4)[1] persis_info["nworkers"] = 4 super().__init__(gen_specs, History, persis_info, libE_info) self.all_local_minima = [] + self.cached_ask = None + self.results_idx = 0 + self.last_ask = None def ask(self, *args) -> npt.NDArray: - self.results = super().ask() - if any(self.results["local_min"]): - min_idxs = self.results["local_min"] - self.all_local_minima.append(self.results[min_idxs]) - self.results = self.results[~min_idxs] - return self.results + if not self.last_ask: # haven't been asked yet, or all previously enqueued points have been "asked" + self.last_ask = super().ask() + if any( + self.last_ask["local_min"] + ): # filter out local minima rows, but they're cached in self.all_local_minima + min_idxs = self.last_ask["local_min"] + self.all_local_minima.append(self.last_ask[min_idxs]) + self.last_ask = self.last_ask[~min_idxs] + if len(args) and isinstance(args[0], int): # we've been asked for a selection of the last ask + num_asked = args[0] + results = self.last_ask[self.results_idx : self.results_idx + num_asked] + self.results_idx += num_asked + if self.results_idx >= len( + self.last_ask + ): # all points have been asked out of the selection. next time around, get new points from aposmm + self.results_idx = 0 + self.last_ask = None + return results + results = copy.deepcopy(self.last_ask) + self.last_ask = None + return results def ask_updates(self) -> npt.NDArray: minima = copy.deepcopy(self.all_local_minima) From 960dd3f54bfb6af8ccda392175ea9f28e5ccf77a Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 10 May 2024 16:29:19 -0500 Subject: [PATCH 117/288] cache the copy of last_ask before clearing it, primarily for results_array creation purposes. can probably be simplified --- libensemble/generators.py | 1 + 1 file changed, 1 insertion(+) diff --git a/libensemble/generators.py b/libensemble/generators.py index 5cb336d16..5ca35dad9 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -205,6 +205,7 @@ def ask(self, *args) -> npt.NDArray: self.last_ask = None return results results = copy.deepcopy(self.last_ask) + self.results = results self.last_ask = None return results From 39c3ab2d594248235206bcc33f509aa449b0bdae Mon Sep 17 00:00:00 2001 From: jlnav Date: Tue, 14 May 2024 17:22:39 -0500 Subject: [PATCH 118/288] huh, not sure why this evaluation worked fine for me --- libensemble/generators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 5ca35dad9..bfa54e886 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -186,7 +186,7 @@ def __init__( self.last_ask = None def ask(self, *args) -> npt.NDArray: - if not self.last_ask: # haven't been asked yet, or all previously enqueued points have been "asked" + if self.last_ask is None: # haven't been asked yet, or all previously enqueued points have been "asked" self.last_ask = super().ask() if any( self.last_ask["local_min"] From 38721f142a578a1188808466b5d724e2cb179f8f Mon Sep 17 00:00:00 2001 From: jlnav Date: Tue, 14 May 2024 17:28:12 -0500 Subject: [PATCH 119/288] put back create_results_array(empty=True), which I disappeared somehow --- libensemble/generators.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index bfa54e886..f009a4de6 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -145,11 +145,14 @@ def final_tell(self, results: npt.NDArray) -> (npt.NDArray, dict, int): self.tell(results, PERSIS_STOP) return self.gen.result() - def create_results_array(self, length: int = 0, addtl_fields: list = [("f", float)]) -> npt.NDArray: + def create_results_array( + self, length: int = 0, addtl_fields: list = [("f", float)], empty: bool = False + ) -> npt.NDArray: in_length = len(self.results) if not length else length new_results = np.zeros(in_length, dtype=self.gen_specs["out"] + addtl_fields) - for field in self.gen_specs["out"]: - new_results[field[0]] = self.results[field[0]] + if not empty: + for field in self.gen_specs["out"]: + new_results[field[0]] = self.results[field[0]] return new_results From 0e7d6e2ac26932cec6fa1758d70ecd12e1fe4529 Mon Sep 17 00:00:00 2001 From: jlnav Date: Tue, 14 May 2024 17:41:54 -0500 Subject: [PATCH 120/288] ensure gens like APOSMM are allowed to return their entire initial sample --- libensemble/utils/runners.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 92f95c52e..50746f9f8 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -138,7 +138,10 @@ def _persistent_result(self, calc_in, persis_info, libE_info): self.gen.libE_info = libE_info self.gen.setup() initial_batch = getattr(self.gen, "initial_batch_size", 0) or libE_info["batch_size"] - H_out = self.gen.ask(initial_batch) # updates can probably be ignored when asking the first time + if not issubclass(type(self.gen), LibEnsembleGenInterfacer): + H_out = self.gen.ask(initial_batch) # updates can probably be ignored when asking the first time + else: + H_out = self.gen.ask() # libE really needs to recieve the *entire* initial batch tag, Work, H_in = self.ps.send_recv(H_out) # evaluate the initial sample self.gen.tell(H_in) if issubclass(type(self.gen), LibEnsembleGenInterfacer): From 4b2da4adfbfb0d264b4a70e426789b10461a8ec6 Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 16 May 2024 09:58:38 -0500 Subject: [PATCH 121/288] various adjustments to try being safer with numpy array memory --- libensemble/generators.py | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index f009a4de6..7ca117a89 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -127,8 +127,8 @@ def _set_sim_ended(self, results: npt.NDArray) -> npt.NDArray: def ask(self, num_points: Optional[int] = 0, *args, **kwargs) -> npt.NDArray: if not self.gen.running: self.gen.run() - _, self.last_ask = self.outbox.get() - return self.last_ask["calc_out"] + _, self.blast_ask = self.outbox.get() + return self.blast_ask["calc_out"] def ask_updates(self) -> npt.NDArray: return self.ask() @@ -136,10 +136,12 @@ def ask_updates(self) -> npt.NDArray: def tell(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: if results is not None: results = self._set_sim_ended(results) - self.inbox.put((tag, {"libE_info": {"H_rows": results["sim_id"], "persistent": True, "executor": None}})) + self.inbox.put( + (tag, {"libE_info": {"H_rows": np.copy(results["sim_id"]), "persistent": True, "executor": None}}) + ) else: self.inbox.put((tag, None)) - self.inbox.put((0, results)) + self.inbox.put((0, np.copy(results))) def final_tell(self, results: npt.NDArray) -> (npt.NDArray, dict, int): self.tell(results, PERSIS_STOP) @@ -189,25 +191,26 @@ def __init__( self.last_ask = None def ask(self, *args) -> npt.NDArray: - if self.last_ask is None: # haven't been asked yet, or all previously enqueued points have been "asked" + if (self.last_ask is None) or ( + self.results_idx >= len(self.last_ask) + ): # haven't been asked yet, or all previously enqueued points have been "asked" + self.results_idx = 0 self.last_ask = super().ask() - if any( - self.last_ask["local_min"] - ): # filter out local minima rows, but they're cached in self.all_local_minima + if self.last_ask[ + "local_min" + ].any(): # filter out local minima rows, but they're cached in self.all_local_minima + print("FOUND A MINIMA") min_idxs = self.last_ask["local_min"] self.all_local_minima.append(self.last_ask[min_idxs]) self.last_ask = self.last_ask[~min_idxs] if len(args) and isinstance(args[0], int): # we've been asked for a selection of the last ask num_asked = args[0] - results = self.last_ask[self.results_idx : self.results_idx + num_asked] + results = np.copy( + self.last_ask[self.results_idx : self.results_idx + num_asked] + ) # if resetting last_ask later, results may point to "None" self.results_idx += num_asked - if self.results_idx >= len( - self.last_ask - ): # all points have been asked out of the selection. next time around, get new points from aposmm - self.results_idx = 0 - self.last_ask = None return results - results = copy.deepcopy(self.last_ask) + results = np.copy(self.last_ask) self.results = results self.last_ask = None return results From cfdc077bf1794919fadabb7ad9e4ea000a2d0507 Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 16 May 2024 15:06:10 -0500 Subject: [PATCH 122/288] spellcheck --- libensemble/utils/runners.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 50746f9f8..4d4c1df2a 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -141,7 +141,7 @@ def _persistent_result(self, calc_in, persis_info, libE_info): if not issubclass(type(self.gen), LibEnsembleGenInterfacer): H_out = self.gen.ask(initial_batch) # updates can probably be ignored when asking the first time else: - H_out = self.gen.ask() # libE really needs to recieve the *entire* initial batch + H_out = self.gen.ask() # libE really needs to receive the *entire* initial batch tag, Work, H_in = self.ps.send_recv(H_out) # evaluate the initial sample self.gen.tell(H_in) if issubclass(type(self.gen), LibEnsembleGenInterfacer): From f4f8d95e4dd53ce71f93b0c4cfe0a79184bb10a7 Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 23 May 2024 14:53:02 -0500 Subject: [PATCH 123/288] rearrange parameters for RandSample --- libensemble/gen_funcs/persistent_gen_wrapper.py | 5 +++-- libensemble/gen_funcs/persistent_sampling.py | 5 +---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/libensemble/gen_funcs/persistent_gen_wrapper.py b/libensemble/gen_funcs/persistent_gen_wrapper.py index 9780a145f..f752bd081 100644 --- a/libensemble/gen_funcs/persistent_gen_wrapper.py +++ b/libensemble/gen_funcs/persistent_gen_wrapper.py @@ -1,6 +1,7 @@ import inspect -from libensemble.tools.persistent_support import PersistentSupport + from libensemble.message_numbers import EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG +from libensemble.tools.persistent_support import PersistentSupport def persistent_gen_f(H, persis_info, gen_specs, libE_info): @@ -12,7 +13,7 @@ def persistent_gen_f(H, persis_info, gen_specs, libE_info): generator = U["generator"] if inspect.isclass(generator): - gen = generator(H, persis_info, gen_specs, libE_info) + gen = generator(gen_specs, H, persis_info, libE_info) else: gen = generator diff --git a/libensemble/gen_funcs/persistent_sampling.py b/libensemble/gen_funcs/persistent_sampling.py index 74338bbc9..3d0c7e908 100644 --- a/libensemble/gen_funcs/persistent_sampling.py +++ b/libensemble/gen_funcs/persistent_sampling.py @@ -31,7 +31,7 @@ def _get_user_params(user_specs): class RandSample(Generator): - def __init__(self, _, persis_info, gen_specs, libE_info=None): + def __init__(self, gen_specs, _, persis_info, libE_info=None): # self.H = H self.persis_info = persis_info self.gen_specs = gen_specs @@ -61,9 +61,6 @@ def _get_user_params(self, user_specs): assert isinstance(self.lb, np.ndarray), "lb must be a numpy array" assert isinstance(self.ub, np.ndarray), "ub must be a numpy array" - def final_tell(self, results): - pass - @persistent_input_fields(["f", "x", "sim_id"]) @output_data([("x", float, (2,))]) From 4998094efecca0f2e525dc35a5e2ea692365603a Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 23 May 2024 15:45:16 -0500 Subject: [PATCH 124/288] rename thread attribute to self.thread for clarity --- libensemble/generators.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 7ca117a89..3041192a6 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -103,7 +103,7 @@ 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.gen = QCommThread( + self.thread = QCommThread( self.gen_f, None, self.History, @@ -111,7 +111,7 @@ def setup(self) -> None: self.gen_specs, self.libE_info, user_function=True, - ) # note that self.gen's inbox/outbox are unused by the underlying gen + ) # note that self.thread's inbox/outbox are unused by the underlying gen def _set_sim_ended(self, results: npt.NDArray) -> npt.NDArray: if "sim_ended" in results.dtype.names: @@ -125,8 +125,8 @@ def _set_sim_ended(self, results: npt.NDArray) -> npt.NDArray: return results def ask(self, num_points: Optional[int] = 0, *args, **kwargs) -> npt.NDArray: - if not self.gen.running: - self.gen.run() + if not self.thread.running: + self.thread.run() _, self.blast_ask = self.outbox.get() return self.blast_ask["calc_out"] @@ -145,7 +145,7 @@ def tell(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: def final_tell(self, results: npt.NDArray) -> (npt.NDArray, dict, int): self.tell(results, PERSIS_STOP) - return self.gen.result() + return self.thread.result() def create_results_array( self, length: int = 0, addtl_fields: list = [("f", float)], empty: bool = False @@ -199,7 +199,6 @@ def ask(self, *args) -> npt.NDArray: if self.last_ask[ "local_min" ].any(): # filter out local minima rows, but they're cached in self.all_local_minima - print("FOUND A MINIMA") min_idxs = self.last_ask["local_min"] self.all_local_minima.append(self.last_ask[min_idxs]) self.last_ask = self.last_ask[~min_idxs] From fa87f592af4a282929d1fb497443bedf2c8df323 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 24 May 2024 15:45:08 -0500 Subject: [PATCH 125/288] libE now should be able to continue with a "live" gen from a previous run; we needed to remove it temporarily from gen_specs right before that dict is serialized for the workers. --- libensemble/generators.py | 1 + libensemble/libE.py | 26 ++++++++++++++++++++++++++ libensemble/utils/runners.py | 3 ++- 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 3041192a6..8d8a086d7 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -94,6 +94,7 @@ def __init__( self.History = History self.persis_info = persis_info self.libE_info = libE_info + self.thread = None def setup(self) -> None: self.inbox = thread_queue.Queue() # sending betweween HERE and gen diff --git a/libensemble/libE.py b/libensemble/libE.py index f14a66b8f..d32644fee 100644 --- a/libensemble/libE.py +++ b/libensemble/libE.py @@ -441,6 +441,24 @@ def libE_mpi_worker(libE_comm, sim_specs, gen_specs, libE_specs): # ==================== Local version =============================== +def _retrieve_generator(gen_specs): + import copy + + gen_ref = gen_specs["user"].get("generator", None) or gen_specs.get("generator", None) + slot = "user" if gen_specs["user"].get("generator", None) is not None else "base" # where the key was found + gen_specs["user"]["generator"] = None + gen_specs["generator"] = None + gen_specs = copy.deepcopy(gen_specs) + return gen_ref, slot + + +def _slot_back_generator(gen_specs, gen_ref, slot): # unfortunately, "generator" can go in two different spots + if slot == "user": + gen_specs["user"]["generator"] = gen_ref + elif slot == "base": + gen_specs["generator"] = gen_ref + + def start_proc_team(nworkers, sim_specs, gen_specs, libE_specs, log_comm=True): """Launch a process worker team.""" resources = Resources.resources @@ -452,6 +470,11 @@ def start_proc_team(nworkers, sim_specs, gen_specs, libE_specs, log_comm=True): QCommLocal = QCommThread log_comm = False # Prevents infinite loop of logging. + if libE_specs.get("gen_on_manager"): # We dont need to (and can't) send "live" generators to workers + gen, slot = _retrieve_generator(gen_specs) + else: + gen = None + wcomms = [ QCommLocal(worker_main, nworkers, sim_specs, gen_specs, libE_specs, w, log_comm, resources, executor) for w in range(1, nworkers + 1) @@ -459,6 +482,9 @@ def start_proc_team(nworkers, sim_specs, gen_specs, libE_specs, log_comm=True): for wcomm in wcomms: wcomm.run() + + if gen is not None: # We still need the gen on the manager, so put it back + _slot_back_generator(gen_specs, gen, slot) return wcomms diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 4d4c1df2a..bb0d37024 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -136,7 +136,8 @@ def _persistent_result(self, calc_in, persis_info, libE_info): if hasattr(self.gen, "setup"): self.gen.persis_info = persis_info self.gen.libE_info = libE_info - self.gen.setup() + if self.gen.thread is None: + self.gen.setup() # maybe we're reusing a live gen from a previous run initial_batch = getattr(self.gen, "initial_batch_size", 0) or libE_info["batch_size"] if not issubclass(type(self.gen), LibEnsembleGenInterfacer): H_out = self.gen.ask(initial_batch) # updates can probably be ignored when asking the first time From 441cf06b4bc39271ec3f56ef3f153f04d0b1ea2f Mon Sep 17 00:00:00 2001 From: shudson Date: Thu, 30 May 2024 17:24:43 -0500 Subject: [PATCH 126/288] Making new gpCAM gen class --- libensemble/gen_classes/gpCAM.py | 152 ++++++++++++++++++ .../gen_funcs/persistent_gen_wrapper.py | 2 +- libensemble/gen_funcs/persistent_sampling.py | 2 +- .../regression_tests/test_gpCAM_class.py | 93 +++++++++++ 4 files changed, 247 insertions(+), 2 deletions(-) create mode 100644 libensemble/gen_classes/gpCAM.py create mode 100644 libensemble/tests/regression_tests/test_gpCAM_class.py diff --git a/libensemble/gen_classes/gpCAM.py b/libensemble/gen_classes/gpCAM.py new file mode 100644 index 000000000..6a6bf2614 --- /dev/null +++ b/libensemble/gen_classes/gpCAM.py @@ -0,0 +1,152 @@ +"""Generator class exposing gpCAM functionality""" + +import time + +import numpy as np +from gpcam import GPOptimizer as GP +from numpy.lib.recfunctions import repack_fields + +from libensemble import Generator +from libensemble.message_numbers import EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG +from libensemble.tools.persistent_support import PersistentSupport + +# While there are class / func duplicates - re-use functions. +from libensemble.gen_funcs.persistent_gpCAM import ( + _read_testpoints, + _generate_mesh, + _eval_var, + _calculate_grid_distances, + _is_point_far_enough, + _find_eligible_points, +) + +__all__ = [ + "GP_CAM", + "GP_CAM_Covar", +] + + +# Note - batch size is set in wrapper currently - and passed to ask as n_trials. +# To support empty ask(), add batch_size back in here. + + +# Equivalent to function persistent_gpCAM_ask_tell +class GP_CAM(Generator): + """ + 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 _initialize_gpcAM(self, user_specs): + """Extract user params""" + # self.b = user_specs["batch_size"] + self.lb = np.array(user_specs["lb"]) + self.ub = np.array(user_specs["ub"]) + 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" + self.all_x = np.empty((0, self.n)) + self.all_y = np.empty((0, 1)) + np.random.seed(0) + + def __init__(self, H, persis_info, gen_specs, libE_info=None): + self.H = H + self.persis_info = persis_info + self.gen_specs = gen_specs + self.libE_info = libE_info + + self.U = self.gen_specs["user"] + self._initialize_gpcAM(self.U) + self.my_gp = None + self.noise = 1e-8 # 1e-12 + + def ask(self, n_trials): + if self.all_x.shape[0] == 0: + x_new = self.persis_info["rand_stream"].uniform(self.lb, self.ub, (n_trials, self.n)) + else: + start = time.time() + self.x_new = my_gp.ask( + bounds=np.column_stack((self.lb, self.ub)), + n=n_trials, + pop_size=n_trials, + max_iter=1, + )["x"] + print(f"Ask time:{time.time() - start}") + H_o = np.zeros(n_trials, dtype=self.gen_specs["out"]) + H_o["x"] = self.x_new + return H_o + + def tell(self, calc_in): + if calc_in is not None: + 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)] + 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)) + + if self.my_gp is None: + self.my_gp = GP(self.all_x, self.all_y, noise_variances=self.noise * np.ones(len(self.all_y))) + else: + self.my_gp.tell(self.all_x, self.all_y, noise_variances=self.noise * np.ones(len(self.all_y))) + 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, H, persis_info, gen_specs, libE_info=None): + super().__init__(H, persis_info, gen_specs, libE_info) + self.test_points = _read_testpoints(self.U) + self.x_for_var = None + self.var_vals = None + if self.U.get("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 ask(self, n_trials): + if self.all_x.shape[0] == 0: + x_new = self.persis_info["rand_stream"].uniform(self.lb, self.ub, (n_trials, self.n)) + else: + if not self.U.get("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.gen_specs["out"]) + H_o["x"] = self.x_new + return H_o + + def tell(self, calc_in): + if calc_in is not None: + super().tell(calc_in) + if not self.U.get("use_grid"): + n_trials = len(self.y_new) + self.x_for_var = self.persis_info["rand_stream"].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 + ) diff --git a/libensemble/gen_funcs/persistent_gen_wrapper.py b/libensemble/gen_funcs/persistent_gen_wrapper.py index f752bd081..750a3baf6 100644 --- a/libensemble/gen_funcs/persistent_gen_wrapper.py +++ b/libensemble/gen_funcs/persistent_gen_wrapper.py @@ -13,7 +13,7 @@ def persistent_gen_f(H, persis_info, gen_specs, libE_info): generator = U["generator"] if inspect.isclass(generator): - gen = generator(gen_specs, H, persis_info, libE_info) + gen = generator(H, persis_info, gen_specs, libE_info) else: gen = generator diff --git a/libensemble/gen_funcs/persistent_sampling.py b/libensemble/gen_funcs/persistent_sampling.py index 3d0c7e908..db73e0474 100644 --- a/libensemble/gen_funcs/persistent_sampling.py +++ b/libensemble/gen_funcs/persistent_sampling.py @@ -31,7 +31,7 @@ def _get_user_params(user_specs): class RandSample(Generator): - def __init__(self, gen_specs, _, persis_info, libE_info=None): + def __init__(self, _, persis_info, gen_specs, libE_info=None): # self.H = H self.persis_info = persis_info self.gen_specs = gen_specs diff --git a/libensemble/tests/regression_tests/test_gpCAM_class.py b/libensemble/tests/regression_tests/test_gpCAM_class.py new file mode 100644 index 000000000..efbbfd52b --- /dev/null +++ b/libensemble/tests/regression_tests/test_gpCAM_class.py @@ -0,0 +1,93 @@ +""" +Tests libEnsemble with gpCAM + +Execute via one of the following commands (e.g. 3 workers): + mpiexec -np 4 python test_gpCAM.py + python test_gpCAM.py --nworkers 3 --comms local + +When running with the above commands, the number of concurrent evaluations of +the objective function will be 2, as one of the three workers will be the +persistent generator. + +See libensemble.gen_funcs.persistent_gpCAM for more details about the generator +setup. +""" + +# Do not change these lines - they are parsed by run-tests.sh +# TESTSUITE_COMMS: mpi local +# TESTSUITE_NPROCS: 4 +# TESTSUITE_EXTRA: true + +import sys + +import numpy as np + +from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f + +from libensemble.gen_funcs.persistent_gen_wrapper import persistent_gen_f +from libensemble.gen_classes.gpCAM import GP_CAM_Covar, GP_CAM + +# Import libEnsemble items for this test +from libensemble.libE import libE +from libensemble.sim_funcs.rosenbrock import rosenbrock_eval as sim_f +from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output + +# Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). +if __name__ == "__main__": + nworkers, is_manager, libE_specs, _ = parse_args() + + if nworkers < 2: + sys.exit("Cannot run with a persistent worker if only one worker -- aborting...") + + n = 4 + batch_size = 15 + + sim_specs = { + "sim_f": sim_f, + "in": ["x"], + "out": [ + ("f", float), + ], + } + + gen_specs = { + "persis_in": ["x", "f", "sim_id"], + "out": [("x", float, (n,))], + "user": { + "batch_size": batch_size, + "lb": np.array([-3, -2, -1, -1]), + "ub": np.array([3, 2, 1, 1]), + }, + } + + alloc_specs = {"alloc_f": alloc_f} + + for inst in range(3): + if inst == 0: + gen_specs["gen_f"] = persistent_gen_f + gen_specs["user"]["generator"] = GP_CAM_Covar + num_batches = 10 + exit_criteria = {"sim_max": num_batches * batch_size, "wallclock_max": 300} + libE_specs["save_every_k_gens"] = 150 + libE_specs["H_file_prefix"] = "gpCAM_nongrid" + + if inst == 1: + gen_specs["user"]["use_grid"] = True + gen_specs["user"]["test_points_file"] = "gpCAM_nongrid_after_gen_150.npy" + libE_specs["final_gen_send"] = True + del libE_specs["H_file_prefix"] + del libE_specs["save_every_k_gens"] + elif inst == 2: + gen_specs["generator"] = GP_CAM + num_batches = 3 # Few because the ask_tell gen can be slow + exit_criteria = {"sim_max": num_batches * batch_size, "wallclock_max": 300} + + persis_info = add_unique_random_streams({}, nworkers + 1) + + # Perform the run + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) + + if is_manager: + assert len(np.unique(H["gen_ended_time"])) == num_batches + + save_libE_output(H, persis_info, __file__, nworkers) From 34e9f4a8d66aa4cd7f08e78b0b52da8e9b3ef4ec Mon Sep 17 00:00:00 2001 From: shudson Date: Thu, 30 May 2024 22:26:21 -0500 Subject: [PATCH 127/288] Minor fixes to gpCAM class test --- libensemble/gen_classes/gpCAM.py | 4 ++-- libensemble/tests/regression_tests/test_gpCAM_class.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/libensemble/gen_classes/gpCAM.py b/libensemble/gen_classes/gpCAM.py index 6a6bf2614..303231754 100644 --- a/libensemble/gen_classes/gpCAM.py +++ b/libensemble/gen_classes/gpCAM.py @@ -66,10 +66,10 @@ def __init__(self, H, persis_info, gen_specs, libE_info=None): def ask(self, n_trials): if self.all_x.shape[0] == 0: - x_new = self.persis_info["rand_stream"].uniform(self.lb, self.ub, (n_trials, self.n)) + self.x_new = self.persis_info["rand_stream"].uniform(self.lb, self.ub, (n_trials, self.n)) else: start = time.time() - self.x_new = my_gp.ask( + self.x_new = self.my_gp.ask( bounds=np.column_stack((self.lb, self.ub)), n=n_trials, pop_size=n_trials, diff --git a/libensemble/tests/regression_tests/test_gpCAM_class.py b/libensemble/tests/regression_tests/test_gpCAM_class.py index efbbfd52b..8bf985de2 100644 --- a/libensemble/tests/regression_tests/test_gpCAM_class.py +++ b/libensemble/tests/regression_tests/test_gpCAM_class.py @@ -70,7 +70,6 @@ exit_criteria = {"sim_max": num_batches * batch_size, "wallclock_max": 300} libE_specs["save_every_k_gens"] = 150 libE_specs["H_file_prefix"] = "gpCAM_nongrid" - if inst == 1: gen_specs["user"]["use_grid"] = True gen_specs["user"]["test_points_file"] = "gpCAM_nongrid_after_gen_150.npy" @@ -78,7 +77,7 @@ del libE_specs["H_file_prefix"] del libE_specs["save_every_k_gens"] elif inst == 2: - gen_specs["generator"] = GP_CAM + gen_specs["user"]["generator"] = GP_CAM num_batches = 3 # Few because the ask_tell gen can be slow exit_criteria = {"sim_max": num_batches * batch_size, "wallclock_max": 300} From db36ab881a068c859be0a7451c48b9d038998dd3 Mon Sep 17 00:00:00 2001 From: shudson Date: Fri, 31 May 2024 10:58:21 -0500 Subject: [PATCH 128/288] Minor fixes to gpCAM class test --- libensemble/gen_classes/gpCAM.py | 4 ---- libensemble/tests/regression_tests/test_gpCAM_class.py | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/libensemble/gen_classes/gpCAM.py b/libensemble/gen_classes/gpCAM.py index 303231754..9ba102602 100644 --- a/libensemble/gen_classes/gpCAM.py +++ b/libensemble/gen_classes/gpCAM.py @@ -4,11 +4,8 @@ import numpy as np from gpcam import GPOptimizer as GP -from numpy.lib.recfunctions import repack_fields from libensemble import Generator -from libensemble.message_numbers import EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG -from libensemble.tools.persistent_support import PersistentSupport # While there are class / func duplicates - re-use functions. from libensemble.gen_funcs.persistent_gpCAM import ( @@ -16,7 +13,6 @@ _generate_mesh, _eval_var, _calculate_grid_distances, - _is_point_far_enough, _find_eligible_points, ) diff --git a/libensemble/tests/regression_tests/test_gpCAM_class.py b/libensemble/tests/regression_tests/test_gpCAM_class.py index 8bf985de2..40f58b52b 100644 --- a/libensemble/tests/regression_tests/test_gpCAM_class.py +++ b/libensemble/tests/regression_tests/test_gpCAM_class.py @@ -2,8 +2,8 @@ Tests libEnsemble with gpCAM Execute via one of the following commands (e.g. 3 workers): - mpiexec -np 4 python test_gpCAM.py - python test_gpCAM.py --nworkers 3 --comms local + mpiexec -np 4 python test_gpCAM_class.py + python test_gpCAM_class.py --nworkers 3 --comms local When running with the above commands, the number of concurrent evaluations of the objective function will be 2, as one of the three workers will be the From cfe217aa062b4a964eafbf1589580990697ab918 Mon Sep 17 00:00:00 2001 From: shudson Date: Fri, 31 May 2024 17:39:17 -0500 Subject: [PATCH 129/288] Make rand sample test both wrapper and asktell --- libensemble/gen_classes/gpCAM.py | 3 +- .../gen_funcs/persistent_gen_wrapper.py | 2 +- .../test_1d_asktell_gen.py | 68 --------------- .../test_sampling_asktell_gen.py | 83 +++++++++++++++++++ .../regression_tests/test_gpCAM_class.py | 2 +- 5 files changed, 87 insertions(+), 71 deletions(-) delete mode 100644 libensemble/tests/functionality_tests/test_1d_asktell_gen.py create mode 100644 libensemble/tests/functionality_tests/test_sampling_asktell_gen.py diff --git a/libensemble/gen_classes/gpCAM.py b/libensemble/gen_classes/gpCAM.py index 9ba102602..b22e2aece 100644 --- a/libensemble/gen_classes/gpCAM.py +++ b/libensemble/gen_classes/gpCAM.py @@ -34,7 +34,8 @@ class GP_CAM(Generator): 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.""" + (relative to the simulation evaluation time) for some use cases. + """ def _initialize_gpcAM(self, user_specs): """Extract user params""" diff --git a/libensemble/gen_funcs/persistent_gen_wrapper.py b/libensemble/gen_funcs/persistent_gen_wrapper.py index 750a3baf6..434a6ae6a 100644 --- a/libensemble/gen_funcs/persistent_gen_wrapper.py +++ b/libensemble/gen_funcs/persistent_gen_wrapper.py @@ -9,7 +9,6 @@ def persistent_gen_f(H, persis_info, gen_specs, libE_info): ps = PersistentSupport(libE_info, EVAL_GEN_TAG) U = gen_specs["user"] b = U.get("initial_batch_size") or U.get("batch_size") - calc_in = None generator = U["generator"] if inspect.isclass(generator): @@ -18,6 +17,7 @@ def persistent_gen_f(H, persis_info, gen_specs, libE_info): gen = generator tag = None + calc_in = None while tag not in [STOP_TAG, PERSIS_STOP]: H_o = gen.ask(b) tag, Work, calc_in = ps.send_recv(H_o) diff --git a/libensemble/tests/functionality_tests/test_1d_asktell_gen.py b/libensemble/tests/functionality_tests/test_1d_asktell_gen.py deleted file mode 100644 index 793cec368..000000000 --- a/libensemble/tests/functionality_tests/test_1d_asktell_gen.py +++ /dev/null @@ -1,68 +0,0 @@ -""" -Runs libEnsemble with Latin hypercube sampling on a simple 1D problem - -Execute via one of the following commands (e.g. 3 workers): - mpiexec -np 4 python test_1d_sampling.py - python test_1d_sampling.py --nworkers 3 --comms local - python test_1d_sampling.py --nworkers 3 --comms tcp - -The number of concurrent evaluations of the objective function will be 4-1=3. -""" - -# Do not change these lines - they are parsed by run-tests.sh -# TESTSUITE_COMMS: mpi local -# TESTSUITE_NPROCS: 2 4 - -import numpy as np - -# Import libEnsemble items for this test -from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f -from libensemble.gen_funcs.persistent_sampling import RandSample -from libensemble.libE import libE -from libensemble.sim_funcs.rosenbrock import rosenbrock_eval as sim_f2 -from libensemble.tools import add_unique_random_streams, parse_args - - -def sim_f(In): - Out = np.zeros(1, dtype=[("f", float)]) - Out["f"] = np.linalg.norm(In) - return Out - - -if __name__ == "__main__": - nworkers, is_manager, libE_specs, _ = parse_args() - libE_specs["gen_on_manager"] = True - - sim_specs = { - "sim_f": sim_f2, - "in": ["x"], - "out": [("f", float), ("grad", float, 2)], - } - - gen_specs_persistent = { - "persis_in": ["x", "f", "grad", "sim_id"], - "out": [("x", float, (2,))], - "user": { - "initial_batch_size": 20, - "lb": np.array([-3, -2]), - "ub": np.array([3, 2]), - }, - } - - exit_criteria = {"gen_max": 201} - - persis_info = add_unique_random_streams({}, nworkers + 1, seed=1234) - - gen_two = RandSample(None, persis_info[1], gen_specs_persistent, None) - gen_specs_persistent["generator"] = gen_two - - alloc_specs = {"alloc_f": alloc_f} - - H, persis_info, flag = libE( - sim_specs, gen_specs_persistent, exit_criteria, persis_info, alloc_specs, libE_specs=libE_specs - ) - - if is_manager: - assert len(H) >= 201 - print("\nlibEnsemble with PERSISTENT random sampling has generated enough points") - print(H[:10]) diff --git a/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py b/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py new file mode 100644 index 000000000..93cad6829 --- /dev/null +++ b/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py @@ -0,0 +1,83 @@ +""" +Runs libEnsemble with Latin hypercube sampling on a simple 1D problem + +Execute via one of the following commands (e.g. 3 workers): + mpiexec -np 4 python test_sampling_asktell_gen.py + python test_sampling_asktell_gen.py --nworkers 3 --comms local + python test_sampling_asktell_gen.py --nworkers 3 --comms tcp + +The number of concurrent evaluations of the objective function will be 4-1=3. +""" + +# Do not change these lines - they are parsed by run-tests.sh +# TESTSUITE_COMMS: mpi local +# TESTSUITE_NPROCS: 2 4 + +import numpy as np + +# Import libEnsemble items for this test +from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f +from libensemble.gen_funcs.persistent_gen_wrapper import persistent_gen_f as gen_f +from libensemble.gen_classes.sampling import RandSample +from libensemble.libE import libE +from libensemble.sim_funcs.rosenbrock import rosenbrock_eval as sim_f +from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output + + +def sim_f(In): + Out = np.zeros(1, dtype=[("f", float)]) + Out["f"] = np.linalg.norm(In) + return Out + + +if __name__ == "__main__": + nworkers, is_manager, libE_specs, _ = parse_args() + libE_specs["gen_on_manager"] = True + + sim_specs = { + "sim_f": sim_f, + "in": ["x"], + "out": [("f", float), ("grad", float, 2)], + } + + gen_specs = { + "persis_in": ["x", "f", "grad", "sim_id"], + "out": [("x", float, (2,))], + "user": { + "initial_batch_size": 20, + "lb": np.array([-3, -2]), + "ub": np.array([3, 2]), + }, + } + + alloc_specs = {"alloc_f": alloc_f} + exit_criteria = {"gen_max": 201} + + for inst in range(3): + persis_info = add_unique_random_streams({}, nworkers + 1, seed=1234) + + if inst == 0: + # Using wrapper - pass class + generator = RandSample + gen_specs["gen_f"] = gen_f + gen_specs["user"]["generator"] = generator + if inst == 1: + # Using wrapper - pass object + gen_specs["gen_f"] = gen_f + generator = RandSample(None, persis_info[1], gen_specs, None) + gen_specs["user"]["generator"] = generator + elif inst == 2: + del gen_specs["gen_f"] + generator = RandSample(None, persis_info[1], gen_specs, None) + gen_specs["generator"] = generator # use asktell runner + print(f'{gen_specs=}, {hasattr(generator, "ask")}') + + H, persis_info, flag = libE( + sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs=libE_specs + ) + + if is_manager: + assert len(H) >= 201 + print("\nlibEnsemble with PERSISTENT random sampling has generated enough points") + print(H[:10]) + assert not np.isclose(H["f"][0], 3.23720733e+02) diff --git a/libensemble/tests/regression_tests/test_gpCAM_class.py b/libensemble/tests/regression_tests/test_gpCAM_class.py index 40f58b52b..3ff3da5b0 100644 --- a/libensemble/tests/regression_tests/test_gpCAM_class.py +++ b/libensemble/tests/regression_tests/test_gpCAM_class.py @@ -24,7 +24,7 @@ from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f -from libensemble.gen_funcs.persistent_gen_wrapper import persistent_gen_f +from libensemble.gen_funcs.persistent_gen_wrapper import persistent_gen_f as gen_f from libensemble.gen_classes.gpCAM import GP_CAM_Covar, GP_CAM # Import libEnsemble items for this test From e999c10d51e405ca7ec8ab3af1d4b1c39c11a880 Mon Sep 17 00:00:00 2001 From: shudson Date: Fri, 31 May 2024 17:48:04 -0500 Subject: [PATCH 130/288] Add gen_classes sampling --- libensemble/gen_classes/sampling.py | 51 +++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 libensemble/gen_classes/sampling.py diff --git a/libensemble/gen_classes/sampling.py b/libensemble/gen_classes/sampling.py new file mode 100644 index 000000000..7d4212273 --- /dev/null +++ b/libensemble/gen_classes/sampling.py @@ -0,0 +1,51 @@ +"""Generator classes providing points using sampling""" + +import numpy as np + +from libensemble import Generator +from libensemble.message_numbers import EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG +from libensemble.specs import output_data, persistent_input_fields +from libensemble.tools.persistent_support import PersistentSupport + +__all__ = [ + #"persistent_uniform", + "RandSample", # TODO - naming - should base class be e.g., UniformSample +] + +class RandSample(Generator): + """ + This generator returns ``gen_specs["initial_batch_size"]`` uniformly + 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. + """ + + def __init__(self, _, persis_info, gen_specs, libE_info=None): + # self.H = H + self.persis_info = persis_info + self.gen_specs = gen_specs + self.libE_info = libE_info + self._get_user_params(self.gen_specs["user"]) + + def ask(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)) + + if "obj_component" in H_o.dtype.fields: # needs H_o - needs to be created in here. + H_o["obj_component"] = self.persis_info["rand_stream"].integers( + low=0, high=self.gen_specs["user"]["num_components"], size=n_trials + ) + return H_o + + def tell(self, calc_in): + pass # random sample so nothing to tell + + 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" From c2859205e1a7d9ee76a57986b6a3193ce975d49d Mon Sep 17 00:00:00 2001 From: shudson Date: Sat, 1 Jun 2024 14:57:01 -0500 Subject: [PATCH 131/288] Make gen_specs generator take precedence over user specs --- libensemble/libE.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/libensemble/libE.py b/libensemble/libE.py index d32644fee..a21d8dc65 100644 --- a/libensemble/libE.py +++ b/libensemble/libE.py @@ -443,9 +443,8 @@ def libE_mpi_worker(libE_comm, sim_specs, gen_specs, libE_specs): def _retrieve_generator(gen_specs): import copy - - gen_ref = gen_specs["user"].get("generator", None) or gen_specs.get("generator", None) - slot = "user" if gen_specs["user"].get("generator", None) is not None else "base" # where the key was found + gen_ref = gen_specs.get("generator") or gen_specs["user"].get("generator") + slot = "base" if gen_specs.get("generator") is not None else "user" # where the key was found gen_specs["user"]["generator"] = None gen_specs["generator"] = None gen_specs = copy.deepcopy(gen_specs) From 687ea85f2641703eb8bb878d7e2cb50b3815f49a Mon Sep 17 00:00:00 2001 From: shudson Date: Sat, 1 Jun 2024 15:10:48 -0500 Subject: [PATCH 132/288] Remove redundant generator redirection --- libensemble/libE.py | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/libensemble/libE.py b/libensemble/libE.py index a21d8dc65..bfa2da574 100644 --- a/libensemble/libE.py +++ b/libensemble/libE.py @@ -440,24 +440,6 @@ def libE_mpi_worker(libE_comm, sim_specs, gen_specs, libE_specs): # ==================== Local version =============================== - -def _retrieve_generator(gen_specs): - import copy - gen_ref = gen_specs.get("generator") or gen_specs["user"].get("generator") - slot = "base" if gen_specs.get("generator") is not None else "user" # where the key was found - gen_specs["user"]["generator"] = None - gen_specs["generator"] = None - gen_specs = copy.deepcopy(gen_specs) - return gen_ref, slot - - -def _slot_back_generator(gen_specs, gen_ref, slot): # unfortunately, "generator" can go in two different spots - if slot == "user": - gen_specs["user"]["generator"] = gen_ref - elif slot == "base": - gen_specs["generator"] = gen_ref - - def start_proc_team(nworkers, sim_specs, gen_specs, libE_specs, log_comm=True): """Launch a process worker team.""" resources = Resources.resources @@ -469,11 +451,6 @@ def start_proc_team(nworkers, sim_specs, gen_specs, libE_specs, log_comm=True): QCommLocal = QCommThread log_comm = False # Prevents infinite loop of logging. - if libE_specs.get("gen_on_manager"): # We dont need to (and can't) send "live" generators to workers - gen, slot = _retrieve_generator(gen_specs) - else: - gen = None - wcomms = [ QCommLocal(worker_main, nworkers, sim_specs, gen_specs, libE_specs, w, log_comm, resources, executor) for w in range(1, nworkers + 1) @@ -482,8 +459,6 @@ def start_proc_team(nworkers, sim_specs, gen_specs, libE_specs, log_comm=True): for wcomm in wcomms: wcomm.run() - if gen is not None: # We still need the gen on the manager, so put it back - _slot_back_generator(gen_specs, gen, slot) return wcomms From 0772deb3419c3a1bc9ee195295addfeffa6d41ac Mon Sep 17 00:00:00 2001 From: shudson Date: Mon, 3 Jun 2024 15:55:54 -0500 Subject: [PATCH 133/288] Revert original gen funcs --- libensemble/gen_funcs/persistent_gpCAM.py | 123 ++++++++---------- libensemble/gen_funcs/persistent_sampling.py | 33 ----- .../tests/regression_tests/test_gpCAM.py | 8 +- 3 files changed, 59 insertions(+), 105 deletions(-) diff --git a/libensemble/gen_funcs/persistent_gpCAM.py b/libensemble/gen_funcs/persistent_gpCAM.py index 0bab89c35..23eeb3f5e 100644 --- a/libensemble/gen_funcs/persistent_gpCAM.py +++ b/libensemble/gen_funcs/persistent_gpCAM.py @@ -6,12 +6,11 @@ from gpcam import GPOptimizer as GP from numpy.lib.recfunctions import repack_fields -from libensemble import Generator from libensemble.message_numbers import EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG from libensemble.tools.persistent_support import PersistentSupport __all__ = [ - "GP_CAM_SIMPLE", + "persistent_gpCAM_simple", "persistent_gpCAM_ask_tell", ] @@ -76,7 +75,6 @@ def _generate_mesh(lb, ub, num_points=10): return points -# TODO Make a class method def _eval_var(my_gp, all_x, all_y, x_for_var, test_points, persis_info): """ Evaluate the posterior covariance at points in x_for_var. @@ -142,86 +140,79 @@ def _find_eligible_points(x_for_var, sorted_indices, r, batch_size): return np.array(eligible_points) -class GP_CAM_SIMPLE(Generator): - # Choose whether functions are internal methods or not - def _initialize_gpcAM(self, user_specs): - """Extract user params""" - self.lb = np.array(user_specs["lb"]) - self.ub = np.array(user_specs["ub"]) - 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" - self.all_x = np.empty((0, self.n)) - self.all_y = np.empty((0, 1)) - np.random.seed(0) - - def __init__(self, H, persis_info, gen_specs, libE_info=None): - self.H = H - self.persis_info = persis_info - self.gen_specs = gen_specs - self.libE_info = libE_info - - self.U = self.gen_specs["user"] - self.test_points = _read_testpoints(self.U) - self._initialize_gpcAM(self.U) - self.my_gp = None - self.noise = 1e-12 - self.x_for_var = None - self.var_vals = None - - if self.U.get("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 ask(self, n_trials): - if self.all_x.shape[0] == 0: - x_new = self.persis_info["rand_stream"].uniform(self.lb, self.ub, (n_trials, self.n)) +def persistent_gpCAM_simple(H_in, persis_info, gen_specs, libE_info): + """ + 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. + + .. seealso:: + `test_gpCAM.py `_ + """ # noqa + U = gen_specs["user"] + my_gp = None + noise = 1e-12 + + test_points = _read_testpoints(U) + + batch_size, n, lb, ub, all_x, all_y, ps = _initialize_gpcAM(U, libE_info) + + # Send batches until manager sends stop tag + tag = None + var_vals = None + + if U.get("use_grid"): + num_points = 10 + x_for_var = _generate_mesh(lb, ub, num_points) + r_low_init, r_high_init = _calculate_grid_distances(lb, ub, num_points) + else: + x_for_var = persis_info["rand_stream"].uniform(lb, ub, (10 * batch_size, n)) + + while tag not in [STOP_TAG, PERSIS_STOP]: + if all_x.shape[0] == 0: + x_new = persis_info["rand_stream"].uniform(lb, ub, (batch_size, n)) else: - if not self.U.get("use_grid"): - x_new = self.x_for_var[np.argsort(self.var_vals)[-n_trials:]] + if not U.get("use_grid"): + x_for_var = persis_info["rand_stream"].uniform(lb, ub, (10 * batch_size, n)) + x_new = x_for_var[np.argsort(var_vals)[-batch_size:]] else: - r_high = self.r_high_init - r_low = self.r_low_init + r_high = r_high_init + r_low = 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: + sorted_indices = np.argsort(-var_vals) + while len(x_new) < batch_size: + x_new = _find_eligible_points(x_for_var, sorted_indices, r_cand, batch_size) + if len(x_new) < batch_size: r_high = r_cand r_cand = (r_high + r_low) / 2.0 - H_o = np.zeros(n_trials, dtype=self.gen_specs["out"]) - self.x_new = x_new - H_o["x"] = self.x_new - return H_o + H_o = np.zeros(batch_size, dtype=gen_specs["out"]) + H_o["x"] = x_new + tag, Work, calc_in = ps.send_recv(H_o) - def tell(self, calc_in): + # This works with or without final_gen_send if calc_in is not None: y_new = np.atleast_2d(calc_in["f"]).T nan_indices = [i for i, fval in enumerate(y_new) if np.isnan(fval)] - x_new = np.delete(self.x_new, nan_indices, axis=0) + x_new = np.delete(x_new, nan_indices, axis=0) y_new = np.delete(y_new, nan_indices, axis=0) + all_x = np.vstack((all_x, x_new)) + all_y = np.vstack((all_y, y_new)) - self.all_x = np.vstack((self.all_x, x_new)) - self.all_y = np.vstack((self.all_y, y_new)) - - if self.my_gp is None: - self.my_gp = GP(self.all_x, self.all_y, noise_variances=self.noise * np.ones(len(self.all_y))) + if my_gp is None: + my_gp = GP(all_x, all_y, noise_variances=noise * np.ones(len(all_y))) else: - self.my_gp.tell(self.all_x, self.all_y, noise_variances=self.noise * np.ones(len(self.all_y))) - self.my_gp.train() + my_gp.tell(all_x, all_y, noise_variances=noise * np.ones(len(all_y))) + my_gp.train() - if not self.U.get("use_grid"): - n_trials = len(y_new) - self.x_for_var = self.persis_info["rand_stream"].uniform(self.lb, self.ub, (10 * n_trials, self.n)) + if not U.get("use_grid"): + x_for_var = persis_info["rand_stream"].uniform(lb, ub, (10 * batch_size, n)) + var_vals = _eval_var(my_gp, all_x, all_y, x_for_var, test_points, persis_info) - self.var_vals = _eval_var( - self.my_gp, self.all_x, self.all_y, self.x_for_var, self.test_points, self.persis_info - ) + return H_o, persis_info, FINISHED_PERSISTENT_GEN_TAG def persistent_gpCAM_ask_tell(H_in, persis_info, gen_specs, libE_info): diff --git a/libensemble/gen_funcs/persistent_sampling.py b/libensemble/gen_funcs/persistent_sampling.py index db73e0474..fcbcba090 100644 --- a/libensemble/gen_funcs/persistent_sampling.py +++ b/libensemble/gen_funcs/persistent_sampling.py @@ -2,7 +2,6 @@ import numpy as np -from libensemble import Generator from libensemble.message_numbers import EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG from libensemble.specs import output_data, persistent_input_fields from libensemble.tools.persistent_support import PersistentSupport @@ -30,38 +29,6 @@ def _get_user_params(user_specs): return b, n, lb, ub -class RandSample(Generator): - def __init__(self, _, persis_info, gen_specs, libE_info=None): - # self.H = H - self.persis_info = persis_info - self.gen_specs = gen_specs - self.libE_info = libE_info - self._get_user_params(self.gen_specs["user"]) - - def ask(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)) - - if "obj_component" in H_o.dtype.fields: # needs H_o - needs to be created in here. - H_o["obj_component"] = self.persis_info["rand_stream"].integers( - low=0, high=self.gen_specs["user"]["num_components"], size=n_trials - ) - return H_o - - def tell(self, calc_in): - pass # random sample so nothing to tell - - 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" - - @persistent_input_fields(["f", "x", "sim_id"]) @output_data([("x", float, (2,))]) def persistent_uniform(_, persis_info, gen_specs, libE_info): diff --git a/libensemble/tests/regression_tests/test_gpCAM.py b/libensemble/tests/regression_tests/test_gpCAM.py index 2504f6a1f..06c49ea5a 100644 --- a/libensemble/tests/regression_tests/test_gpCAM.py +++ b/libensemble/tests/regression_tests/test_gpCAM.py @@ -23,9 +23,7 @@ import numpy as np from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f - -from libensemble.gen_funcs.persistent_gen_wrapper import persistent_gen_f -from libensemble.gen_funcs.persistent_gpCAM import GP_CAM_SIMPLE, persistent_gpCAM_ask_tell +from libensemble.gen_funcs.persistent_gpCAM import persistent_gpCAM_ask_tell, persistent_gpCAM_simple # Import libEnsemble items for this test from libensemble.libE import libE @@ -64,13 +62,11 @@ for inst in range(3): if inst == 0: - gen_specs["gen_f"] = persistent_gen_f - gen_specs["user"]["generator"] = GP_CAM_SIMPLE + gen_specs["gen_f"] = persistent_gpCAM_simple num_batches = 10 exit_criteria = {"sim_max": num_batches * batch_size, "wallclock_max": 300} libE_specs["save_every_k_gens"] = 150 libE_specs["H_file_prefix"] = "gpCAM_nongrid" - if inst == 1: gen_specs["user"]["use_grid"] = True gen_specs["user"]["test_points_file"] = "gpCAM_nongrid_after_gen_150.npy" From 91033bed221cc404524b29b3838746dc89c1e1b8 Mon Sep 17 00:00:00 2001 From: shudson Date: Mon, 3 Jun 2024 16:20:07 -0500 Subject: [PATCH 134/288] Fix imports --- libensemble/gen_classes/sampling.py | 10 +++------- .../test_sampling_asktell_gen.py | 13 ++++++------- .../tests/regression_tests/test_gpCAM_class.py | 2 +- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/libensemble/gen_classes/sampling.py b/libensemble/gen_classes/sampling.py index 7d4212273..e565b528d 100644 --- a/libensemble/gen_classes/sampling.py +++ b/libensemble/gen_classes/sampling.py @@ -1,18 +1,14 @@ """Generator classes providing points using sampling""" import numpy as np - from libensemble import Generator -from libensemble.message_numbers import EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG -from libensemble.specs import output_data, persistent_input_fields -from libensemble.tools.persistent_support import PersistentSupport __all__ = [ - #"persistent_uniform", - "RandSample", # TODO - naming - should base class be e.g., UniformSample + "UniformSample", ] -class RandSample(Generator): + +class UniformSample(Generator): """ This generator returns ``gen_specs["initial_batch_size"]`` uniformly sampled points the first time it is called. Afterwards, it returns the diff --git a/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py b/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py index 93cad6829..3a3f71c70 100644 --- a/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py +++ b/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py @@ -18,10 +18,9 @@ # Import libEnsemble items for this test from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f from libensemble.gen_funcs.persistent_gen_wrapper import persistent_gen_f as gen_f -from libensemble.gen_classes.sampling import RandSample +from libensemble.gen_classes.sampling import UniformSample from libensemble.libE import libE -from libensemble.sim_funcs.rosenbrock import rosenbrock_eval as sim_f -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +from libensemble.tools import add_unique_random_streams, parse_args def sim_f(In): @@ -58,17 +57,17 @@ def sim_f(In): if inst == 0: # Using wrapper - pass class - generator = RandSample + generator = UniformSample gen_specs["gen_f"] = gen_f gen_specs["user"]["generator"] = generator if inst == 1: # Using wrapper - pass object gen_specs["gen_f"] = gen_f - generator = RandSample(None, persis_info[1], gen_specs, None) + generator = UniformSample(None, persis_info[1], gen_specs, None) gen_specs["user"]["generator"] = generator elif inst == 2: del gen_specs["gen_f"] - generator = RandSample(None, persis_info[1], gen_specs, None) + generator = UniformSample(None, persis_info[1], gen_specs, None) gen_specs["generator"] = generator # use asktell runner print(f'{gen_specs=}, {hasattr(generator, "ask")}') @@ -78,6 +77,6 @@ def sim_f(In): if is_manager: assert len(H) >= 201 - print("\nlibEnsemble with PERSISTENT random sampling has generated enough points") + print("\nlibEnsemble with PERSISTENT random sampling has generated enough points\n") print(H[:10]) assert not np.isclose(H["f"][0], 3.23720733e+02) diff --git a/libensemble/tests/regression_tests/test_gpCAM_class.py b/libensemble/tests/regression_tests/test_gpCAM_class.py index 3ff3da5b0..a2a63bef5 100644 --- a/libensemble/tests/regression_tests/test_gpCAM_class.py +++ b/libensemble/tests/regression_tests/test_gpCAM_class.py @@ -64,7 +64,7 @@ for inst in range(3): if inst == 0: - gen_specs["gen_f"] = persistent_gen_f + gen_specs["gen_f"] = gen_f gen_specs["user"]["generator"] = GP_CAM_Covar num_batches = 10 exit_criteria = {"sim_max": num_batches * batch_size, "wallclock_max": 300} From 2e3253a2f964943f9c190e0aabc22038fea186b9 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 5 Jun 2024 09:45:32 -0500 Subject: [PATCH 135/288] small adjusts, plus add same seed to two aposmm tests (1 classic, 1 ask/tell) --- libensemble/generators.py | 7 +++---- .../tests/regression_tests/test_persistent_aposmm_nlopt.py | 2 +- .../test_persistent_aposmm_nlopt_asktell.py | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 8d8a086d7..738415564 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -128,8 +128,8 @@ def _set_sim_ended(self, results: npt.NDArray) -> npt.NDArray: def ask(self, num_points: Optional[int] = 0, *args, **kwargs) -> npt.NDArray: if not self.thread.running: self.thread.run() - _, self.blast_ask = self.outbox.get() - return self.blast_ask["calc_out"] + _, ask_full = self.outbox.get() + return ask_full["calc_out"] def ask_updates(self) -> npt.NDArray: return self.ask() @@ -183,11 +183,10 @@ def __init__( ] gen_specs["in"] = ["x", "f", "local_pt", "sim_id", "sim_ended", "x_on_cube", "local_min"] if not persis_info: - persis_info = add_unique_random_streams({}, 4)[1] + persis_info = add_unique_random_streams({}, 4, seed="aposmm")[1] persis_info["nworkers"] = 4 super().__init__(gen_specs, History, persis_info, libE_info) self.all_local_minima = [] - self.cached_ask = None self.results_idx = 0 self.last_ask = None diff --git a/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt.py b/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt.py index 681133016..c37d05090 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt.py @@ -79,7 +79,7 @@ alloc_specs = {"alloc_f": alloc_f} - persis_info = add_unique_random_streams({}, nworkers + 1) + persis_info = add_unique_random_streams({}, nworkers + 1, seed="aposmm") exit_criteria = {"sim_max": 2000} diff --git a/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py b/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py index b93920c78..0e3e981a9 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py @@ -76,7 +76,7 @@ }, } - persis_info = add_unique_random_streams({}, nworkers + 1) + persis_info = add_unique_random_streams({}, nworkers + 1, seed="aposmm") alloc_specs = {"alloc_f": alloc_f} exit_criteria = {"sim_max": 2000} From f0451f7df2410d9900674a630a4e03b3d4d8997e Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 5 Jun 2024 11:45:01 -0500 Subject: [PATCH 136/288] fix seeds --- libensemble/generators.py | 2 +- .../tests/regression_tests/test_persistent_aposmm_nlopt.py | 2 +- .../regression_tests/test_persistent_aposmm_nlopt_asktell.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 738415564..1c2206881 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -183,7 +183,7 @@ def __init__( ] gen_specs["in"] = ["x", "f", "local_pt", "sim_id", "sim_ended", "x_on_cube", "local_min"] if not persis_info: - persis_info = add_unique_random_streams({}, 4, seed="aposmm")[1] + persis_info = add_unique_random_streams({}, 4, seed=4321)[1] persis_info["nworkers"] = 4 super().__init__(gen_specs, History, persis_info, libE_info) self.all_local_minima = [] diff --git a/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt.py b/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt.py index c37d05090..2bcd7bf6b 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt.py @@ -79,7 +79,7 @@ alloc_specs = {"alloc_f": alloc_f} - persis_info = add_unique_random_streams({}, nworkers + 1, seed="aposmm") + persis_info = add_unique_random_streams({}, nworkers + 1, seed=4321) exit_criteria = {"sim_max": 2000} diff --git a/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py b/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py index 0e3e981a9..74f24ec5d 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py @@ -76,7 +76,7 @@ }, } - persis_info = add_unique_random_streams({}, nworkers + 1, seed="aposmm") + persis_info = add_unique_random_streams({}, nworkers + 1, seed=4321) alloc_specs = {"alloc_f": alloc_f} exit_criteria = {"sim_max": 2000} From f0769f9b3615ddffcf9dfcedc0ce18ac1ca1c0f8 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 12 Jun 2024 13:32:08 -0500 Subject: [PATCH 137/288] first experiment with creating a RandomSample class that fits the current consensus --- libensemble/gen_classes/sampling.py | 14 ++++++++++++++ libensemble/gen_funcs/persistent_gen_wrapper.py | 7 +++++++ libensemble/generators.py | 4 +++- .../test_sampling_asktell_gen.py | 12 +++++++++--- 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/libensemble/gen_classes/sampling.py b/libensemble/gen_classes/sampling.py index e565b528d..e5e9aae43 100644 --- a/libensemble/gen_classes/sampling.py +++ b/libensemble/gen_classes/sampling.py @@ -1,6 +1,7 @@ """Generator classes providing points using sampling""" import numpy as np + from libensemble import Generator __all__ = [ @@ -45,3 +46,16 @@ def _get_user_params(self, user_specs): 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" + + +class StandardUniformSample(UniformSample): + """ + This generator returns ``gen_specs["initial_batch_size"]`` uniformly + 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. + """ + + def ask(self, n_trials): + out = super().ask(n_trials) + return [{"x": x.tolist()} for x in out["x"]] diff --git a/libensemble/gen_funcs/persistent_gen_wrapper.py b/libensemble/gen_funcs/persistent_gen_wrapper.py index 434a6ae6a..c5e89762d 100644 --- a/libensemble/gen_funcs/persistent_gen_wrapper.py +++ b/libensemble/gen_funcs/persistent_gen_wrapper.py @@ -1,5 +1,7 @@ import inspect +import numpy as np + from libensemble.message_numbers import EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG from libensemble.tools.persistent_support import PersistentSupport @@ -20,6 +22,11 @@ def persistent_gen_f(H, persis_info, gen_specs, libE_info): calc_in = None while tag not in [STOP_TAG, PERSIS_STOP]: H_o = gen.ask(b) + if isinstance(H_o, list): + H_o_arr = np.zeros(len(H_o), dtype=gen_specs["out"]) + for i in range(len(H_o)): + H_o_arr[i] = H_o[i]["x"] + H_o = H_o_arr tag, Work, calc_in = ps.send_recv(H_o) gen.tell(calc_in) diff --git a/libensemble/generators.py b/libensemble/generators.py index 1c2206881..16dea3770 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -11,12 +11,14 @@ from libensemble.message_numbers import EVAL_GEN_TAG, PERSIS_STOP from libensemble.tools import add_unique_random_streams +# TODO: Refactor below-class to wrap StandardGenerator and possibly convert in/out data to list-of-dicts + class Generator(ABC): """ v 0.4.19.24 - Tentative generator interface for use with libEnsemble, and generic enough to be + Tentative generator interface for use with libEnsemble, and gene∂ric enough to be broadly compatible with other workflow packages. .. code-block:: python diff --git a/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py b/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py index 3a3f71c70..e9ea18418 100644 --- a/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py +++ b/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py @@ -17,8 +17,8 @@ # Import libEnsemble items for this test from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f +from libensemble.gen_classes.sampling import StandardUniformSample, UniformSample from libensemble.gen_funcs.persistent_gen_wrapper import persistent_gen_f as gen_f -from libensemble.gen_classes.sampling import UniformSample from libensemble.libE import libE from libensemble.tools import add_unique_random_streams, parse_args @@ -52,7 +52,7 @@ def sim_f(In): alloc_specs = {"alloc_f": alloc_f} exit_criteria = {"gen_max": 201} - for inst in range(3): + for inst in range(4): persis_info = add_unique_random_streams({}, nworkers + 1, seed=1234) if inst == 0: @@ -70,6 +70,12 @@ def sim_f(In): generator = UniformSample(None, persis_info[1], gen_specs, None) gen_specs["generator"] = generator # use asktell runner print(f'{gen_specs=}, {hasattr(generator, "ask")}') + elif inst == 3: + generator = StandardUniformSample + gen_specs["gen_f"] = gen_f + gen_specs["user"]["generator"] = generator + gen_specs["generator"] = None + print(f'{gen_specs=}, {hasattr(generator, "ask")}') H, persis_info, flag = libE( sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs=libE_specs @@ -79,4 +85,4 @@ def sim_f(In): assert len(H) >= 201 print("\nlibEnsemble with PERSISTENT random sampling has generated enough points\n") print(H[:10]) - assert not np.isclose(H["f"][0], 3.23720733e+02) + assert not np.isclose(H["f"][0], 3.23720733e02) From bc458daf2f7b6e8b678841193ae4c7f32adf7906 Mon Sep 17 00:00:00 2001 From: jlnav Date: Tue, 25 Jun 2024 09:44:44 -0500 Subject: [PATCH 138/288] wrapper now presumably generic enough for non-x keys? --- libensemble/gen_funcs/persistent_gen_wrapper.py | 3 ++- libensemble/generators.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/libensemble/gen_funcs/persistent_gen_wrapper.py b/libensemble/gen_funcs/persistent_gen_wrapper.py index c5e89762d..ebf22cf1a 100644 --- a/libensemble/gen_funcs/persistent_gen_wrapper.py +++ b/libensemble/gen_funcs/persistent_gen_wrapper.py @@ -25,7 +25,8 @@ def persistent_gen_f(H, persis_info, gen_specs, libE_info): if isinstance(H_o, list): H_o_arr = np.zeros(len(H_o), dtype=gen_specs["out"]) for i in range(len(H_o)): - H_o_arr[i] = H_o[i]["x"] + for key in H_o[0].keys(): + H_o_arr[i][key] = H_o[i][key] H_o = H_o_arr tag, Work, calc_in = ps.send_recv(H_o) gen.tell(calc_in) diff --git a/libensemble/generators.py b/libensemble/generators.py index 16dea3770..36aa6c1da 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -18,7 +18,7 @@ class Generator(ABC): """ v 0.4.19.24 - Tentative generator interface for use with libEnsemble, and gene∂ric enough to be + Tentative generator interface for use with libEnsemble, and generic enough to be broadly compatible with other workflow packages. .. code-block:: python From 714c177cbc3c00a3adcd5bd4052f42f727dbf97a Mon Sep 17 00:00:00 2001 From: jlnav Date: Tue, 25 Jun 2024 11:25:38 -0500 Subject: [PATCH 139/288] adds list-to-array conversion to runners.py --- .../test_sampling_asktell_gen.py | 8 ++++-- libensemble/utils/runners.py | 27 ++++++++++++++----- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py b/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py index e9ea18418..d39b13b1c 100644 --- a/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py +++ b/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py @@ -52,7 +52,7 @@ def sim_f(In): alloc_specs = {"alloc_f": alloc_f} exit_criteria = {"gen_max": 201} - for inst in range(4): + for inst in range(5): persis_info = add_unique_random_streams({}, nworkers + 1, seed=1234) if inst == 0: @@ -76,6 +76,11 @@ def sim_f(In): gen_specs["user"]["generator"] = generator gen_specs["generator"] = None print(f'{gen_specs=}, {hasattr(generator, "ask")}') + elif inst == 4: + del gen_specs["gen_f"] + generator = StandardUniformSample(None, persis_info[1], gen_specs, None) + gen_specs["generator"] = generator # use asktell runner + print(f'{gen_specs=}, {hasattr(generator, "ask")}') H, persis_info, flag = libE( sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs=libE_specs @@ -83,6 +88,5 @@ def sim_f(In): if is_manager: assert len(H) >= 201 - print("\nlibEnsemble with PERSISTENT random sampling has generated enough points\n") print(H[:10]) assert not np.isclose(H["f"][0], 3.23720733e02) diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index bb0d37024..905cc3b6c 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -98,10 +98,19 @@ def __init__(self, specs): super().__init__(specs) self.gen = specs.get("generator") + def _to_array(self, x): + if isinstance(x, list): + arr = np.zeros(len(x), dtype=self.specs["out"]) + for i in range(len(x)): + for key in x[0].keys(): + arr[i][key] = x[i][key] + return arr + return x + def _loop_over_normal_generator(self, tag, Work): while tag not in [PERSIS_STOP, STOP_TAG]: batch_size = getattr(self.gen, "batch_size", 0) or Work["libE_info"]["batch_size"] - points, updates = self.gen.ask(batch_size), self.gen.ask_updates() + points, updates = self._to_array(self.gen.ask(batch_size)), self._to_array(self.gen.ask_updates()) if updates is not None and len(updates): # returned "samples" and "updates". can combine if same dtype H_out = np.append(points, updates) else: @@ -112,7 +121,7 @@ def _loop_over_normal_generator(self, tag, Work): def _ask_and_send(self): while self.gen.outbox.qsize(): # recv/send any outstanding messages - points, updates = self.gen.ask(), self.gen.ask_updates() + points, updates = self._to_array(self.gen.ask()), self._to_array(self.gen.ask_updates()) if updates is not None and len(updates): self.ps.send(points) for i in updates: @@ -134,15 +143,19 @@ def _persistent_result(self, calc_in, persis_info, libE_info): self.ps = PersistentSupport(libE_info, EVAL_GEN_TAG) tag = None if hasattr(self.gen, "setup"): - self.gen.persis_info = persis_info + self.gen.persis_info = persis_info # passthrough, setup() uses the gen attributes self.gen.libE_info = libE_info if self.gen.thread is None: self.gen.setup() # maybe we're reusing a live gen from a previous run initial_batch = getattr(self.gen, "initial_batch_size", 0) or libE_info["batch_size"] - if not issubclass(type(self.gen), LibEnsembleGenInterfacer): - H_out = self.gen.ask(initial_batch) # updates can probably be ignored when asking the first time + if not issubclass( + type(self.gen), LibEnsembleGenInterfacer + ): # we can't control how many points created by a threaded gen + H_out = self._to_array( + self.gen.ask(initial_batch) + ) # updates can probably be ignored when asking the first time else: - H_out = self.gen.ask() # libE really needs to receive the *entire* initial batch + H_out = self._to_array(self.gen.ask()) # libE really needs to receive the *entire* initial batch tag, Work, H_in = self.ps.send_recv(H_out) # evaluate the initial sample self.gen.tell(H_in) if issubclass(type(self.gen), LibEnsembleGenInterfacer): @@ -154,4 +167,4 @@ def _persistent_result(self, calc_in, persis_info, libE_info): def _result(self, calc_in: npt.NDArray, persis_info: dict, libE_info: dict) -> (npt.NDArray, dict, Optional[int]): if libE_info.get("persistent"): return self._persistent_result(calc_in, persis_info, libE_info) - return self.gen.ask(getattr(self.gen, "batch_size", 0) or libE_info["batch_size"]) + return self._to_array(self.gen.ask(getattr(self.gen, "batch_size", 0) or libE_info["batch_size"])) From c791acd21ff845c3b21b0e76f6b2849584d9dd2f Mon Sep 17 00:00:00 2001 From: jlnav Date: Tue, 25 Jun 2024 12:59:23 -0500 Subject: [PATCH 140/288] more type checks --- libensemble/utils/runners.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 905cc3b6c..d3b5b4020 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -99,7 +99,7 @@ def __init__(self, specs): self.gen = specs.get("generator") def _to_array(self, x): - if isinstance(x, list): + if isinstance(x, list) and len(x) and isinstance(x[0], dict): arr = np.zeros(len(x), dtype=self.specs["out"]) for i in range(len(x)): for key in x[0].keys(): From d4fb064e4952031bea482c0ad5a6bb17ad766340 Mon Sep 17 00:00:00 2001 From: jlnav Date: Tue, 2 Jul 2024 14:09:48 -0500 Subject: [PATCH 141/288] pair of functions for converting between numpy and list_of_dicts --- libensemble/generators.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/libensemble/generators.py b/libensemble/generators.py index 36aa6c1da..f837ff3ad 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -83,6 +83,33 @@ def final_tell(self, results: Iterable, *args, **kwargs) -> Optional[Iterable]: """ +def list_dicts_to_np(list_dicts: Iterable) -> npt.NDArray: + new_dtype = [] + new_dtype_names = [i for i in list_dicts[0].keys()] + for i, entry in enumerate(list_dicts[0].values()): # must inspect values to get presumptive types + if hasattr(entry, "shape") and len(entry.shape): + entry_dtype = (new_dtype_names[i], entry.dtype, entry.shape) + else: + entry_dtype = (new_dtype_names[i], type(entry)) + new_dtype.append(entry_dtype) + + out = np.zeros(len(list_dicts), dtype=new_dtype) + for i, entry in enumerate(list_dicts): + for field in entry.keys(): + out[field][i] = entry[field] + return out + + +def np_to_list_dicts(array: npt.NDArray) -> Iterable: + out = [] + for row in array: + new_dict = {} + for field in row.dtype.names: + new_dict[field] = row[field] + out.append(new_dict) + return out + + class LibEnsembleGenInterfacer(Generator): """Implement ask/tell for traditionally written libEnsemble persistent generator functions. Still requires a handful of libEnsemble-specific data-structures on initialization. From bfd25af7625cc8a9be80e1d54238a24ca6332f18 Mon Sep 17 00:00:00 2001 From: jlnav Date: Tue, 2 Jul 2024 15:07:35 -0500 Subject: [PATCH 142/288] initial changes for APOSMM returning/accepting lists of dicts --- libensemble/generators.py | 39 +++++++------------ .../unit_tests/test_persistent_aposmm.py | 16 ++++---- 2 files changed, 22 insertions(+), 33 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index f837ff3ad..b02c72ed6 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -1,7 +1,7 @@ import copy import queue as thread_queue from abc import ABC, abstractmethod -from typing import Iterable, Optional +from typing import Iterable, List, Optional import numpy as np from numpy import typing as npt @@ -16,7 +16,7 @@ class Generator(ABC): """ - v 0.4.19.24 + v 0.7.2.24 Tentative generator interface for use with libEnsemble, and generic enough to be broadly compatible with other workflow packages. @@ -59,22 +59,22 @@ def __init__(self, *args, **kwargs): """ @abstractmethod - def ask(self, num_points: Optional[int], *args, **kwargs) -> Iterable: + def ask(self, num_points: Optional[int], *args, **kwargs) -> List[dict]: """ Request the next set of points to evaluate, and optionally any previous points to update. """ - def ask_updates(self) -> Iterable: + def ask_updates(self) -> npt.NDArray: """ Request any updates to previous points, e.g. minima discovered, points to cancel. """ - def tell(self, results: Iterable, *args, **kwargs) -> None: + def tell(self, results: List[dict], *args, **kwargs) -> None: """ Send the results of evaluations to the generator. """ - def final_tell(self, results: Iterable, *args, **kwargs) -> Optional[Iterable]: + def final_tell(self, results: List[dict], *args, **kwargs) -> Optional[npt.NDArray]: """ Send the last set of results to the generator, instruct it to cleanup, and optionally retrieve an updated final state of evaluations. This is a separate @@ -163,7 +163,8 @@ def ask(self, num_points: Optional[int] = 0, *args, **kwargs) -> npt.NDArray: def ask_updates(self) -> npt.NDArray: return self.ask() - def tell(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: + def tell(self, results: List[dict], tag: int = EVAL_GEN_TAG) -> None: + results = list_dicts_to_np(results) if results is not None: results = self._set_sim_ended(results) self.inbox.put( @@ -173,20 +174,10 @@ def tell(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: self.inbox.put((tag, None)) self.inbox.put((0, np.copy(results))) - def final_tell(self, results: npt.NDArray) -> (npt.NDArray, dict, int): - self.tell(results, PERSIS_STOP) + def final_tell(self, results: List[dict]) -> (npt.NDArray, dict, int): + self.tell(list_dicts_to_np(results), PERSIS_STOP) return self.thread.result() - def create_results_array( - self, length: int = 0, addtl_fields: list = [("f", float)], empty: bool = False - ) -> npt.NDArray: - in_length = len(self.results) if not length else length - new_results = np.zeros(in_length, dtype=self.gen_specs["out"] + addtl_fields) - if not empty: - for field in self.gen_specs["out"]: - new_results[field[0]] = self.results[field[0]] - return new_results - class APOSMM(LibEnsembleGenInterfacer): """ @@ -219,7 +210,7 @@ def __init__( self.results_idx = 0 self.last_ask = None - def ask(self, *args) -> npt.NDArray: + def ask(self, *args) -> List[dict]: if (self.last_ask is None) or ( self.results_idx >= len(self.last_ask) ): # haven't been asked yet, or all previously enqueued points have been "asked" @@ -241,7 +232,7 @@ def ask(self, *args) -> npt.NDArray: results = np.copy(self.last_ask) self.results = results self.last_ask = None - return results + return np_to_list_dicts(results) def ask_updates(self) -> npt.NDArray: minima = copy.deepcopy(self.all_local_minima) @@ -270,7 +261,7 @@ def _add_sim_ids(self, array: npt.NDArray) -> npt.NDArray: def ready_to_be_asked(self) -> bool: return not self.outbox.empty() - def ask(self, *args) -> (npt.NDArray, Optional[npt.NDArray]): + def ask(self, *args) -> npt.NDArray: output = super().ask() if "cancel_requested" in output.dtype.names: cancels = output @@ -284,9 +275,9 @@ def ask(self, *args) -> (npt.NDArray, Optional[npt.NDArray]): if got_cancels_first: return additional["calc_out"] self.all_cancels.append(additional["calc_out"]) - return self.results + return np_to_list_dicts(self.results) except thread_queue.Empty: - return self.results + return np_to_list_dicts(self.results) def ask_updates(self) -> npt.NDArray: cancels = copy.deepcopy(self.all_cancels) diff --git a/libensemble/tests/unit_tests/test_persistent_aposmm.py b/libensemble/tests/unit_tests/test_persistent_aposmm.py index fe065554d..fccf1c26c 100644 --- a/libensemble/tests/unit_tests/test_persistent_aposmm.py +++ b/libensemble/tests/unit_tests/test_persistent_aposmm.py @@ -205,16 +205,15 @@ def test_asktell_with_persistent_aposmm(): my_APOSMM = APOSMM(gen_specs) my_APOSMM.setup() initial_sample = my_APOSMM.ask() - initial_results = my_APOSMM.create_results_array() total_evals = 0 eval_max = 2000 - for i in initial_sample["sim_id"]: - initial_results[i]["f"] = six_hump_camel_func(initial_sample["x"][i]) + for point in initial_sample: + point["f"] = six_hump_camel_func(point["x"]) total_evals += 1 - my_APOSMM.tell(initial_results) + my_APOSMM.tell(initial_sample) potential_minima = [] @@ -224,12 +223,11 @@ def test_asktell_with_persistent_aposmm(): if len(detected_minima): for m in detected_minima: potential_minima.append(m) - results = my_APOSMM.create_results_array() - for i in range(len(sample)): - results[i]["f"] = six_hump_camel_func(sample["x"][i]) + for point in sample: + point["f"] = six_hump_camel_func(point["x"]) total_evals += 1 - my_APOSMM.tell(results) - H, persis_info, exit_code = my_APOSMM.final_tell(results) + my_APOSMM.tell(sample) + H, persis_info, exit_code = my_APOSMM.final_tell(sample) assert exit_code == FINISHED_PERSISTENT_GEN_TAG, "Standalone persistent_aposmm didn't exit correctly" assert persis_info.get("run_order"), "Standalone persistent_aposmm didn't do any localopt runs" From 50a37893772338d1d58af2f12124ecda7415daa7 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 3 Jul 2024 11:38:06 -0500 Subject: [PATCH 143/288] bugfix --- libensemble/generators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index b02c72ed6..6eda27374 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -175,7 +175,7 @@ def tell(self, results: List[dict], tag: int = EVAL_GEN_TAG) -> None: self.inbox.put((0, np.copy(results))) def final_tell(self, results: List[dict]) -> (npt.NDArray, dict, int): - self.tell(list_dicts_to_np(results), PERSIS_STOP) + self.tell(results, PERSIS_STOP) # conversion happens in tell return self.thread.result() From 7581cc001037cdafd0277f96f18a64324fd2c763 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 3 Jul 2024 15:25:14 -0500 Subject: [PATCH 144/288] adjust runner/persistent-wrapper for new datatypes --- libensemble/gen_funcs/persistent_gen_wrapper.py | 3 ++- libensemble/generators.py | 2 ++ libensemble/utils/runners.py | 8 ++++---- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/libensemble/gen_funcs/persistent_gen_wrapper.py b/libensemble/gen_funcs/persistent_gen_wrapper.py index ebf22cf1a..3140e39c7 100644 --- a/libensemble/gen_funcs/persistent_gen_wrapper.py +++ b/libensemble/gen_funcs/persistent_gen_wrapper.py @@ -2,6 +2,7 @@ import numpy as np +from libensemble.generators import np_to_list_dicts from libensemble.message_numbers import EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG from libensemble.tools.persistent_support import PersistentSupport @@ -29,7 +30,7 @@ def persistent_gen_f(H, persis_info, gen_specs, libE_info): H_o_arr[i][key] = H_o[i][key] H_o = H_o_arr tag, Work, calc_in = ps.send_recv(H_o) - gen.tell(calc_in) + gen.tell(np_to_list_dicts(calc_in)) if hasattr(calc_in, "__len__"): b = len(calc_in) diff --git a/libensemble/generators.py b/libensemble/generators.py index 6eda27374..c7f14e718 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -84,6 +84,8 @@ def final_tell(self, results: List[dict], *args, **kwargs) -> Optional[npt.NDArr def list_dicts_to_np(list_dicts: Iterable) -> npt.NDArray: + if not list_dicts: + return None new_dtype = [] new_dtype_names = [i for i in list_dicts[0].keys()] for i, entry in enumerate(list_dicts[0].values()): # must inspect values to get presumptive types diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index d3b5b4020..a2873f3b9 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -8,7 +8,7 @@ import numpy.typing as npt from libensemble.comms.comms import QCommThread -from libensemble.generators import LibEnsembleGenInterfacer +from libensemble.generators import LibEnsembleGenInterfacer, np_to_list_dicts from libensemble.message_numbers import EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG from libensemble.tools.persistent_support import PersistentSupport @@ -116,7 +116,7 @@ def _loop_over_normal_generator(self, tag, Work): else: H_out = points tag, Work, H_in = self.ps.send_recv(H_out) - self.gen.tell(H_in) + self.gen.tell(np_to_list_dicts(H_in)) return H_in def _ask_and_send(self): @@ -137,7 +137,7 @@ def _loop_over_persistent_interfacer(self): tag, _, H_in = self.ps.recv() if tag in [STOP_TAG, PERSIS_STOP]: return H_in - self.gen.tell(H_in) + self.gen.tell(np_to_list_dicts(H_in)) def _persistent_result(self, calc_in, persis_info, libE_info): self.ps = PersistentSupport(libE_info, EVAL_GEN_TAG) @@ -157,7 +157,7 @@ def _persistent_result(self, calc_in, persis_info, libE_info): else: H_out = self._to_array(self.gen.ask()) # libE really needs to receive the *entire* initial batch tag, Work, H_in = self.ps.send_recv(H_out) # evaluate the initial sample - self.gen.tell(H_in) + self.gen.tell(np_to_list_dicts(H_in)) if issubclass(type(self.gen), LibEnsembleGenInterfacer): final_H_in = self._loop_over_persistent_interfacer() else: From f3b02d0739c7ed6e5db866a8a65c72f058e738d2 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 5 Jul 2024 11:53:11 -0500 Subject: [PATCH 145/288] refactor gen classes to accept/return list-of-dicts --- libensemble/gen_classes/gpCAM.py | 16 +++++---- libensemble/gen_classes/sampling.py | 17 ++-------- libensemble/generators.py | 10 +++--- .../test_sampling_asktell_gen.py | 19 +++-------- .../regression_tests/test_asktell_surmise.py | 33 ++++++++----------- 5 files changed, 34 insertions(+), 61 deletions(-) diff --git a/libensemble/gen_classes/gpCAM.py b/libensemble/gen_classes/gpCAM.py index b22e2aece..c44ad190c 100644 --- a/libensemble/gen_classes/gpCAM.py +++ b/libensemble/gen_classes/gpCAM.py @@ -9,12 +9,13 @@ # While there are class / func duplicates - re-use functions. from libensemble.gen_funcs.persistent_gpCAM import ( - _read_testpoints, - _generate_mesh, - _eval_var, _calculate_grid_distances, + _eval_var, _find_eligible_points, + _generate_mesh, + _read_testpoints, ) +from libensemble.generators import list_dicts_to_np, np_to_list_dicts __all__ = [ "GP_CAM", @@ -61,7 +62,7 @@ def __init__(self, H, persis_info, gen_specs, libE_info=None): self.my_gp = None self.noise = 1e-8 # 1e-12 - def ask(self, n_trials): + def ask(self, n_trials) -> list: if self.all_x.shape[0] == 0: self.x_new = self.persis_info["rand_stream"].uniform(self.lb, self.ub, (n_trials, self.n)) else: @@ -75,10 +76,11 @@ def ask(self, n_trials): print(f"Ask time:{time.time() - start}") H_o = np.zeros(n_trials, dtype=self.gen_specs["out"]) H_o["x"] = self.x_new - return H_o + return np_to_list_dicts(H_o) def tell(self, calc_in): if calc_in is not None: + calc_in = list_dicts_to_np(calc_in) 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)] self.x_new = np.delete(self.x_new, nan_indices, axis=0) @@ -113,7 +115,7 @@ def __init__(self, H, persis_info, gen_specs, libE_info=None): 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 ask(self, n_trials): + def ask(self, n_trials) -> list: if self.all_x.shape[0] == 0: x_new = self.persis_info["rand_stream"].uniform(self.lb, self.ub, (n_trials, self.n)) else: @@ -135,7 +137,7 @@ def ask(self, n_trials): self.x_new = x_new H_o = np.zeros(n_trials, dtype=self.gen_specs["out"]) H_o["x"] = self.x_new - return H_o + return np_to_list_dicts(H_o) def tell(self, calc_in): if calc_in is not None: diff --git a/libensemble/gen_classes/sampling.py b/libensemble/gen_classes/sampling.py index e5e9aae43..4c47d3ed7 100644 --- a/libensemble/gen_classes/sampling.py +++ b/libensemble/gen_classes/sampling.py @@ -17,7 +17,7 @@ class UniformSample(Generator): mode by adjusting the allocation function. """ - def __init__(self, _, persis_info, gen_specs, libE_info=None): + def __init__(self, _, persis_info, gen_specs, libE_info=None) -> list: # self.H = H self.persis_info = persis_info self.gen_specs = gen_specs @@ -32,7 +32,7 @@ def ask(self, n_trials): H_o["obj_component"] = self.persis_info["rand_stream"].integers( low=0, high=self.gen_specs["user"]["num_components"], size=n_trials ) - return H_o + return [{"x": x.tolist()} for x in H_o["x"]] def tell(self, calc_in): pass # random sample so nothing to tell @@ -46,16 +46,3 @@ def _get_user_params(self, user_specs): 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" - - -class StandardUniformSample(UniformSample): - """ - This generator returns ``gen_specs["initial_batch_size"]`` uniformly - 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. - """ - - def ask(self, n_trials): - out = super().ask(n_trials) - return [{"x": x.tolist()} for x in out["x"]] diff --git a/libensemble/generators.py b/libensemble/generators.py index c7f14e718..b0131703f 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -1,7 +1,7 @@ import copy import queue as thread_queue from abc import ABC, abstractmethod -from typing import Iterable, List, Optional +from typing import List, Optional import numpy as np from numpy import typing as npt @@ -83,7 +83,7 @@ def final_tell(self, results: List[dict], *args, **kwargs) -> Optional[npt.NDArr """ -def list_dicts_to_np(list_dicts: Iterable) -> npt.NDArray: +def list_dicts_to_np(list_dicts: list) -> npt.NDArray: if not list_dicts: return None new_dtype = [] @@ -102,7 +102,9 @@ def list_dicts_to_np(list_dicts: Iterable) -> npt.NDArray: return out -def np_to_list_dicts(array: npt.NDArray) -> Iterable: +def np_to_list_dicts(array: npt.NDArray) -> list: + if array is None: + return None out = [] for row in array: new_dict = {} @@ -263,7 +265,7 @@ def _add_sim_ids(self, array: npt.NDArray) -> npt.NDArray: def ready_to_be_asked(self) -> bool: return not self.outbox.empty() - def ask(self, *args) -> npt.NDArray: + def ask(self, *args) -> List[dict]: output = super().ask() if "cancel_requested" in output.dtype.names: cancels = output diff --git a/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py b/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py index d39b13b1c..07854f3e0 100644 --- a/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py +++ b/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py @@ -17,7 +17,7 @@ # Import libEnsemble items for this test from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f -from libensemble.gen_classes.sampling import StandardUniformSample, UniformSample +from libensemble.gen_classes.sampling import UniformSample from libensemble.gen_funcs.persistent_gen_wrapper import persistent_gen_f as gen_f from libensemble.libE import libE from libensemble.tools import add_unique_random_streams, parse_args @@ -52,7 +52,7 @@ def sim_f(In): alloc_specs = {"alloc_f": alloc_f} exit_criteria = {"gen_max": 201} - for inst in range(5): + for inst in range(3): persis_info = add_unique_random_streams({}, nworkers + 1, seed=1234) if inst == 0: @@ -66,21 +66,10 @@ def sim_f(In): generator = UniformSample(None, persis_info[1], gen_specs, None) gen_specs["user"]["generator"] = generator elif inst == 2: + # use asktell runner - pass object del gen_specs["gen_f"] generator = UniformSample(None, persis_info[1], gen_specs, None) - gen_specs["generator"] = generator # use asktell runner - print(f'{gen_specs=}, {hasattr(generator, "ask")}') - elif inst == 3: - generator = StandardUniformSample - gen_specs["gen_f"] = gen_f - gen_specs["user"]["generator"] = generator - gen_specs["generator"] = None - print(f'{gen_specs=}, {hasattr(generator, "ask")}') - elif inst == 4: - del gen_specs["gen_f"] - generator = StandardUniformSample(None, persis_info[1], gen_specs, None) - gen_specs["generator"] = generator # use asktell runner - print(f'{gen_specs=}, {hasattr(generator, "ask")}') + gen_specs["generator"] = generator H, persis_info, flag = libE( sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs=libE_specs diff --git a/libensemble/tests/regression_tests/test_asktell_surmise.py b/libensemble/tests/regression_tests/test_asktell_surmise.py index fe48d02c9..27e633441 100644 --- a/libensemble/tests/regression_tests/test_asktell_surmise.py +++ b/libensemble/tests/regression_tests/test_asktell_surmise.py @@ -12,7 +12,7 @@ if __name__ == "__main__": from libensemble.executors import Executor - from libensemble.generators import Surmise + from libensemble.generators import Surmise, list_dicts_to_np # Import libEnsemble items for this test from libensemble.sim_funcs.borehole_kills import borehole @@ -83,42 +83,35 @@ surmise.setup() initial_sample = surmise.ask() - initial_results = surmise.create_results_array() total_evals = 0 - for i in initial_sample["sim_id"]: - H_out, _a, _b = borehole(initial_sample[i], {}, sim_specs, {"H_rows": np.array([initial_sample[i]["sim_id"]])}) - initial_results[i]["f"] = H_out["f"][0] # some "bugginess" with output shape of array in simf + for point in initial_sample: + H_out, _a, _b = borehole(list_dicts_to_np(point), {}, sim_specs, {"H_rows": np.array([point["sim_id"]])}) + point["f"] = H_out["f"][0] # some "bugginess" with output shape of array in simf total_evals += 1 - surmise.tell(initial_results) + surmise.tell(initial_sample) requested_canceled_sim_ids = [] next_sample, cancels = surmise.ask(), surmise.ask_updates() - next_results = surmise.create_results_array() - for i in range(len(next_sample)): - H_out, _a, _b = borehole(next_sample[i], {}, sim_specs, {"H_rows": np.array([next_sample[i]["sim_id"]])}) - next_results[i]["f"] = H_out["f"][0] + for point in next_sample: + H_out, _a, _b = borehole(list_dicts_to_np(point), {}, sim_specs, {"H_rows": np.array([point["sim_id"]])}) + point["f"] = H_out["f"][0] total_evals += 1 - surmise.tell(next_results) + surmise.tell(next_sample) sample, cancels = surmise.ask(), surmise.ask_updates() while total_evals < max_evals: - samples_iter = range(len(sample)) - - for i in samples_iter: - result = np.zeros(1, dtype=gen_specs["out"] + [("f", float)]) - for field in gen_specs["out"]: - result[field[0]] = sample[i][field[0]] - H_out, _a, _b = borehole(sample[i], {}, sim_specs, {"H_rows": np.array([sample[i]["sim_id"]])}) - result["f"] = H_out["f"][0] + for point in sample: + H_out, _a, _b = borehole(list_dicts_to_np(point), {}, sim_specs, {"H_rows": np.array([point["sim_id"]])}) + point["f"] = H_out["f"][0] total_evals += 1 - surmise.tell(result) + surmise.tell(point) if surmise.ready_to_be_asked(): new_sample, cancels = surmise.ask(), surmise.ask_updates() for m in cancels: From 815b6021cc4cf38a5bc56fa31647d04ce7e84ab4 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 10 Jul 2024 17:00:23 -0500 Subject: [PATCH 146/288] tentatively switching inline conversions to wrapped ask/tells --- libensemble/gen_classes/gpCAM.py | 20 +++++++++------ libensemble/generators.py | 43 +++++++++++++++++++++++++++----- libensemble/utils/runners.py | 8 +++--- 3 files changed, 53 insertions(+), 18 deletions(-) diff --git a/libensemble/gen_classes/gpCAM.py b/libensemble/gen_classes/gpCAM.py index c44ad190c..acb8d56c6 100644 --- a/libensemble/gen_classes/gpCAM.py +++ b/libensemble/gen_classes/gpCAM.py @@ -1,9 +1,11 @@ """Generator class exposing gpCAM functionality""" import time +from typing import List, Union import numpy as np from gpcam import GPOptimizer as GP +from numpy import typing as npt from libensemble import Generator @@ -15,7 +17,7 @@ _generate_mesh, _read_testpoints, ) -from libensemble.generators import list_dicts_to_np, np_to_list_dicts +from libensemble.generators import call_then_convert, convert_then_call __all__ = [ "GP_CAM", @@ -62,7 +64,8 @@ def __init__(self, H, persis_info, gen_specs, libE_info=None): self.my_gp = None self.noise = 1e-8 # 1e-12 - def ask(self, n_trials) -> list: + @call_then_convert + def ask(self, n_trials: int) -> List[dict]: if self.all_x.shape[0] == 0: self.x_new = self.persis_info["rand_stream"].uniform(self.lb, self.ub, (n_trials, self.n)) else: @@ -76,11 +79,11 @@ def ask(self, n_trials) -> list: print(f"Ask time:{time.time() - start}") H_o = np.zeros(n_trials, dtype=self.gen_specs["out"]) H_o["x"] = self.x_new - return np_to_list_dicts(H_o) + return H_o - def tell(self, calc_in): + @convert_then_call + def tell(self, calc_in: Union[List[dict], npt.NDArray]) -> None: if calc_in is not None: - calc_in = list_dicts_to_np(calc_in) 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)] self.x_new = np.delete(self.x_new, nan_indices, axis=0) @@ -115,7 +118,8 @@ def __init__(self, H, persis_info, gen_specs, libE_info=None): 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 ask(self, n_trials) -> list: + @call_then_convert + def ask(self, n_trials: int) -> List[dict]: if self.all_x.shape[0] == 0: x_new = self.persis_info["rand_stream"].uniform(self.lb, self.ub, (n_trials, self.n)) else: @@ -137,9 +141,9 @@ def ask(self, n_trials) -> list: self.x_new = x_new H_o = np.zeros(n_trials, dtype=self.gen_specs["out"]) H_o["x"] = self.x_new - return np_to_list_dicts(H_o) + return H_o - def tell(self, calc_in): + def tell(self, calc_in: Union[List[dict], npt.NDArray]): if calc_in is not None: super().tell(calc_in) if not self.U.get("use_grid"): diff --git a/libensemble/generators.py b/libensemble/generators.py index b0131703f..046c53810 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -1,7 +1,8 @@ import copy import queue as thread_queue from abc import ABC, abstractmethod -from typing import List, Optional +from functools import wraps +from typing import List, Optional, Union import numpy as np from numpy import typing as npt @@ -102,7 +103,7 @@ def list_dicts_to_np(list_dicts: list) -> npt.NDArray: return out -def np_to_list_dicts(array: npt.NDArray) -> list: +def np_to_list_dicts(array: npt.NDArray) -> List[dict]: if array is None: return None out = [] @@ -114,6 +115,34 @@ def np_to_list_dicts(array: npt.NDArray) -> list: return out +def _libE_convert(input: Union[List[dict], npt.NDArray]) -> Union[List[dict], npt.NDArray]: + if isinstance(input, list): + return list_dicts_to_np(input) + elif isinstance(input, np.ndarray): + return np_to_list_dicts(input) + else: + raise ValueError("input must be a list or numpy array") + + +def convert_then_call(func): + @wraps(func) + def wrapper(self, data, *args, **kwargs): + if isinstance(data, list): + data = _libE_convert(data) + return func(self, data, *args, **kwargs) + + return wrapper + + +def call_then_convert(func): + @wraps(func) + def wrapper(self, *args, **kwargs): + data = func(self, *args, **kwargs) + return _libE_convert(data) + + return wrapper + + class LibEnsembleGenInterfacer(Generator): """Implement ask/tell for traditionally written libEnsemble persistent generator functions. Still requires a handful of libEnsemble-specific data-structures on initialization. @@ -167,8 +196,8 @@ def ask(self, num_points: Optional[int] = 0, *args, **kwargs) -> npt.NDArray: def ask_updates(self) -> npt.NDArray: return self.ask() + @convert_then_call def tell(self, results: List[dict], tag: int = EVAL_GEN_TAG) -> None: - results = list_dicts_to_np(results) if results is not None: results = self._set_sim_ended(results) self.inbox.put( @@ -214,6 +243,7 @@ def __init__( self.results_idx = 0 self.last_ask = None + @call_then_convert def ask(self, *args) -> List[dict]: if (self.last_ask is None) or ( self.results_idx >= len(self.last_ask) @@ -236,7 +266,7 @@ def ask(self, *args) -> List[dict]: results = np.copy(self.last_ask) self.results = results self.last_ask = None - return np_to_list_dicts(results) + return results def ask_updates(self) -> npt.NDArray: minima = copy.deepcopy(self.all_local_minima) @@ -265,6 +295,7 @@ def _add_sim_ids(self, array: npt.NDArray) -> npt.NDArray: def ready_to_be_asked(self) -> bool: return not self.outbox.empty() + @call_then_convert def ask(self, *args) -> List[dict]: output = super().ask() if "cancel_requested" in output.dtype.names: @@ -279,9 +310,9 @@ def ask(self, *args) -> List[dict]: if got_cancels_first: return additional["calc_out"] self.all_cancels.append(additional["calc_out"]) - return np_to_list_dicts(self.results) + return self.results except thread_queue.Empty: - return np_to_list_dicts(self.results) + return self.results def ask_updates(self) -> npt.NDArray: cancels = copy.deepcopy(self.all_cancels) diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index a2873f3b9..d3b5b4020 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -8,7 +8,7 @@ import numpy.typing as npt from libensemble.comms.comms import QCommThread -from libensemble.generators import LibEnsembleGenInterfacer, np_to_list_dicts +from libensemble.generators import LibEnsembleGenInterfacer from libensemble.message_numbers import EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG from libensemble.tools.persistent_support import PersistentSupport @@ -116,7 +116,7 @@ def _loop_over_normal_generator(self, tag, Work): else: H_out = points tag, Work, H_in = self.ps.send_recv(H_out) - self.gen.tell(np_to_list_dicts(H_in)) + self.gen.tell(H_in) return H_in def _ask_and_send(self): @@ -137,7 +137,7 @@ def _loop_over_persistent_interfacer(self): tag, _, H_in = self.ps.recv() if tag in [STOP_TAG, PERSIS_STOP]: return H_in - self.gen.tell(np_to_list_dicts(H_in)) + self.gen.tell(H_in) def _persistent_result(self, calc_in, persis_info, libE_info): self.ps = PersistentSupport(libE_info, EVAL_GEN_TAG) @@ -157,7 +157,7 @@ def _persistent_result(self, calc_in, persis_info, libE_info): else: H_out = self._to_array(self.gen.ask()) # libE really needs to receive the *entire* initial batch tag, Work, H_in = self.ps.send_recv(H_out) # evaluate the initial sample - self.gen.tell(np_to_list_dicts(H_in)) + self.gen.tell(H_in) if issubclass(type(self.gen), LibEnsembleGenInterfacer): final_H_in = self._loop_over_persistent_interfacer() else: From 4497ec4c646d81a95992bcd97c27f952d8694d18 Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 11 Jul 2024 16:48:54 -0500 Subject: [PATCH 147/288] trying out _ask_np, and _tell_np, for more efficient data-transfer internal to libE --- libensemble/gen_classes/gpCAM.py | 27 ++++++++++------ libensemble/gen_classes/sampling.py | 9 +++++- libensemble/generators.py | 48 +++++++---------------------- 3 files changed, 37 insertions(+), 47 deletions(-) diff --git a/libensemble/gen_classes/gpCAM.py b/libensemble/gen_classes/gpCAM.py index acb8d56c6..99e1f5b95 100644 --- a/libensemble/gen_classes/gpCAM.py +++ b/libensemble/gen_classes/gpCAM.py @@ -1,7 +1,7 @@ """Generator class exposing gpCAM functionality""" import time -from typing import List, Union +from typing import List, Optional import numpy as np from gpcam import GPOptimizer as GP @@ -17,7 +17,7 @@ _generate_mesh, _read_testpoints, ) -from libensemble.generators import call_then_convert, convert_then_call +from libensemble.generators import list_dicts_to_np, np_to_list_dicts __all__ = [ "GP_CAM", @@ -64,8 +64,13 @@ def __init__(self, H, persis_info, gen_specs, libE_info=None): self.my_gp = None self.noise = 1e-8 # 1e-12 - @call_then_convert - def ask(self, n_trials: int) -> List[dict]: + def ask(self, num_points: Optional[int] = 0) -> List[dict]: + return np_to_list_dicts(self._ask_np(num_points)) + + def tell(self, calc_in: List[dict]) -> None: + self._tell_np(list_dicts_to_np(calc_in)) + + def _ask_np(self, n_trials: int) -> npt.NDArray: if self.all_x.shape[0] == 0: self.x_new = self.persis_info["rand_stream"].uniform(self.lb, self.ub, (n_trials, self.n)) else: @@ -81,8 +86,7 @@ def ask(self, n_trials: int) -> List[dict]: H_o["x"] = self.x_new return H_o - @convert_then_call - def tell(self, calc_in: Union[List[dict], npt.NDArray]) -> None: + def _tell_np(self, calc_in: npt.NDArray) -> None: if calc_in is not None: 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)] @@ -118,8 +122,13 @@ def __init__(self, H, persis_info, gen_specs, libE_info=None): 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) - @call_then_convert - def ask(self, n_trials: int) -> List[dict]: + def ask(self, num_points: Optional[int] = 0) -> List[dict]: + return np_to_list_dicts(self._ask_np(num_points)) + + def tell(self, calc_in: List[dict]) -> None: + self._tell_np(list_dicts_to_np(calc_in)) + + def _ask_np(self, n_trials: int) -> List[dict]: if self.all_x.shape[0] == 0: x_new = self.persis_info["rand_stream"].uniform(self.lb, self.ub, (n_trials, self.n)) else: @@ -143,7 +152,7 @@ def ask(self, n_trials: int) -> List[dict]: H_o["x"] = self.x_new return H_o - def tell(self, calc_in: Union[List[dict], npt.NDArray]): + def _tell_np(self, calc_in: npt.NDArray): if calc_in is not None: super().tell(calc_in) if not self.U.get("use_grid"): diff --git a/libensemble/gen_classes/sampling.py b/libensemble/gen_classes/sampling.py index 4c47d3ed7..c4b26e80c 100644 --- a/libensemble/gen_classes/sampling.py +++ b/libensemble/gen_classes/sampling.py @@ -25,6 +25,10 @@ def __init__(self, _, persis_info, gen_specs, libE_info=None) -> list: self._get_user_params(self.gen_specs["user"]) def ask(self, n_trials): + H_o = self._ask_np(n_trials) + return [{"x": x.tolist()} for x in H_o["x"]] + + def _ask_np(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)) @@ -32,11 +36,14 @@ def ask(self, n_trials): H_o["obj_component"] = self.persis_info["rand_stream"].integers( low=0, high=self.gen_specs["user"]["num_components"], size=n_trials ) - return [{"x": x.tolist()} for x in H_o["x"]] + return H_o def tell(self, calc_in): pass # random sample so nothing to tell + def _tell_np(self, calc_in): + self.tell(calc_in) + def _get_user_params(self, user_specs): """Extract user params""" # b = user_specs["initial_batch_size"] diff --git a/libensemble/generators.py b/libensemble/generators.py index 046c53810..d2a5d6f81 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -1,8 +1,7 @@ import copy import queue as thread_queue from abc import ABC, abstractmethod -from functools import wraps -from typing import List, Optional, Union +from typing import List, Optional import numpy as np from numpy import typing as npt @@ -115,34 +114,6 @@ def np_to_list_dicts(array: npt.NDArray) -> List[dict]: return out -def _libE_convert(input: Union[List[dict], npt.NDArray]) -> Union[List[dict], npt.NDArray]: - if isinstance(input, list): - return list_dicts_to_np(input) - elif isinstance(input, np.ndarray): - return np_to_list_dicts(input) - else: - raise ValueError("input must be a list or numpy array") - - -def convert_then_call(func): - @wraps(func) - def wrapper(self, data, *args, **kwargs): - if isinstance(data, list): - data = _libE_convert(data) - return func(self, data, *args, **kwargs) - - return wrapper - - -def call_then_convert(func): - @wraps(func) - def wrapper(self, *args, **kwargs): - data = func(self, *args, **kwargs) - return _libE_convert(data) - - return wrapper - - class LibEnsembleGenInterfacer(Generator): """Implement ask/tell for traditionally written libEnsemble persistent generator functions. Still requires a handful of libEnsemble-specific data-structures on initialization. @@ -187,7 +158,13 @@ def _set_sim_ended(self, results: npt.NDArray) -> npt.NDArray: results = new_results return results - def ask(self, num_points: Optional[int] = 0, *args, **kwargs) -> npt.NDArray: + def ask(self, num_points: Optional[int] = 0) -> List[dict]: + return np_to_list_dicts(self._ask_np(num_points)) + + def tell(self, calc_in: List[dict]) -> None: + self._tell_np(list_dicts_to_np(calc_in)) + + def _ask_np(self, num_points: Optional[int] = 0, *args, **kwargs) -> npt.NDArray: if not self.thread.running: self.thread.run() _, ask_full = self.outbox.get() @@ -196,8 +173,7 @@ def ask(self, num_points: Optional[int] = 0, *args, **kwargs) -> npt.NDArray: def ask_updates(self) -> npt.NDArray: return self.ask() - @convert_then_call - def tell(self, results: List[dict], tag: int = EVAL_GEN_TAG) -> None: + def _tell_np(self, results: List[dict], tag: int = EVAL_GEN_TAG) -> None: if results is not None: results = self._set_sim_ended(results) self.inbox.put( @@ -243,8 +219,7 @@ def __init__( self.results_idx = 0 self.last_ask = None - @call_then_convert - def ask(self, *args) -> List[dict]: + def _ask_np(self, *args) -> List[dict]: if (self.last_ask is None) or ( self.results_idx >= len(self.last_ask) ): # haven't been asked yet, or all previously enqueued points have been "asked" @@ -295,8 +270,7 @@ def _add_sim_ids(self, array: npt.NDArray) -> npt.NDArray: def ready_to_be_asked(self) -> bool: return not self.outbox.empty() - @call_then_convert - def ask(self, *args) -> List[dict]: + def _ask_np(self, *args) -> List[dict]: output = super().ask() if "cancel_requested" in output.dtype.names: cancels = output From e83d75aa6b8eae075e6dba963f6b4d31cc8b8caf Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 12 Jul 2024 13:30:34 -0500 Subject: [PATCH 148/288] upon using a LibEnsembleGenInterfacer, talk to that class using _ask_np and _tell_np. plus other fixes --- libensemble/generators.py | 27 +++++++++++++-------------- libensemble/utils/runners.py | 11 ++++++----- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index d2a5d6f81..5b153fbcd 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -158,20 +158,20 @@ def _set_sim_ended(self, results: npt.NDArray) -> npt.NDArray: results = new_results return results - def ask(self, num_points: Optional[int] = 0) -> List[dict]: - return np_to_list_dicts(self._ask_np(num_points)) + def ask(self, n_trials: Optional[int] = 0) -> List[dict]: + return np_to_list_dicts(self._ask_np(n_trials)) - def tell(self, calc_in: List[dict]) -> None: - self._tell_np(list_dicts_to_np(calc_in)) + def tell(self, calc_in: List[dict], tag: int = EVAL_GEN_TAG) -> None: + self._tell_np(list_dicts_to_np(calc_in), tag) - def _ask_np(self, num_points: Optional[int] = 0, *args, **kwargs) -> npt.NDArray: + def _ask_np(self, n_trials: int = 0) -> npt.NDArray: if not self.thread.running: self.thread.run() _, ask_full = self.outbox.get() return ask_full["calc_out"] def ask_updates(self) -> npt.NDArray: - return self.ask() + return self._ask_np() def _tell_np(self, results: List[dict], tag: int = EVAL_GEN_TAG) -> None: if results is not None: @@ -219,31 +219,30 @@ def __init__( self.results_idx = 0 self.last_ask = None - def _ask_np(self, *args) -> List[dict]: + def _ask_np(self, n_trials: int = 0) -> npt.NDArray: if (self.last_ask is None) or ( self.results_idx >= len(self.last_ask) ): # haven't been asked yet, or all previously enqueued points have been "asked" self.results_idx = 0 - self.last_ask = super().ask() + self.last_ask = super()._ask_np(n_trials) if self.last_ask[ "local_min" ].any(): # filter out local minima rows, but they're cached in self.all_local_minima min_idxs = self.last_ask["local_min"] self.all_local_minima.append(self.last_ask[min_idxs]) self.last_ask = self.last_ask[~min_idxs] - if len(args) and isinstance(args[0], int): # we've been asked for a selection of the last ask - num_asked = args[0] + if n_trials > 0: # we've been asked for a selection of the last ask results = np.copy( - self.last_ask[self.results_idx : self.results_idx + num_asked] + self.last_ask[self.results_idx : self.results_idx + n_trials] ) # if resetting last_ask later, results may point to "None" - self.results_idx += num_asked + self.results_idx += n_trials return results results = np.copy(self.last_ask) self.results = results self.last_ask = None return results - def ask_updates(self) -> npt.NDArray: + def ask_updates(self) -> List[npt.NDArray]: minima = copy.deepcopy(self.all_local_minima) self.all_local_minima = [] return minima @@ -271,7 +270,7 @@ def ready_to_be_asked(self) -> bool: return not self.outbox.empty() def _ask_np(self, *args) -> List[dict]: - output = super().ask() + output = super()._ask_np() if "cancel_requested" in output.dtype.names: cancels = output got_cancels_first = True diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index d3b5b4020..553e10326 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -8,7 +8,7 @@ import numpy.typing as npt from libensemble.comms.comms import QCommThread -from libensemble.generators import LibEnsembleGenInterfacer +from libensemble.generators import LibEnsembleGenInterfacer, np_to_list_dicts from libensemble.message_numbers import EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG from libensemble.tools.persistent_support import PersistentSupport @@ -121,7 +121,7 @@ def _loop_over_normal_generator(self, tag, Work): def _ask_and_send(self): while self.gen.outbox.qsize(): # recv/send any outstanding messages - points, updates = self._to_array(self.gen.ask()), self._to_array(self.gen.ask_updates()) + points, updates = self.gen._ask_np(), self.gen.ask_updates() # PersistentInterfacers each have _ask_np if updates is not None and len(updates): self.ps.send(points) for i in updates: @@ -137,7 +137,7 @@ def _loop_over_persistent_interfacer(self): tag, _, H_in = self.ps.recv() if tag in [STOP_TAG, PERSIS_STOP]: return H_in - self.gen.tell(H_in) + self.gen._tell_np(H_in) def _persistent_result(self, calc_in, persis_info, libE_info): self.ps = PersistentSupport(libE_info, EVAL_GEN_TAG) @@ -155,12 +155,13 @@ def _persistent_result(self, calc_in, persis_info, libE_info): self.gen.ask(initial_batch) ) # updates can probably be ignored when asking the first time else: - H_out = self._to_array(self.gen.ask()) # libE really needs to receive the *entire* initial batch + H_out = self.gen._ask_np() # libE really needs to receive the *entire* initial batch tag, Work, H_in = self.ps.send_recv(H_out) # evaluate the initial sample - self.gen.tell(H_in) if issubclass(type(self.gen), LibEnsembleGenInterfacer): + self.gen._tell_np(H_in) final_H_in = self._loop_over_persistent_interfacer() else: + self.gen.tell(np_to_list_dicts(H_in)) final_H_in = self._loop_over_normal_generator(tag, Work) return self.gen.final_tell(final_H_in), FINISHED_PERSISTENT_GEN_TAG From 0fda5db28c0e6a3138f67b1e642e12bef4255a40 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 17 Jul 2024 13:24:15 -0500 Subject: [PATCH 149/288] tentative additional subclass of Generator, with abstractmethods _ask_np, _tell_np --- libensemble/gen_classes/gpCAM.py | 20 +++----------------- libensemble/generators.py | 21 +++++++++++++++++---- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/libensemble/gen_classes/gpCAM.py b/libensemble/gen_classes/gpCAM.py index 99e1f5b95..398a0c247 100644 --- a/libensemble/gen_classes/gpCAM.py +++ b/libensemble/gen_classes/gpCAM.py @@ -1,14 +1,12 @@ """Generator class exposing gpCAM functionality""" import time -from typing import List, Optional +from typing import List import numpy as np from gpcam import GPOptimizer as GP from numpy import typing as npt -from libensemble import Generator - # While there are class / func duplicates - re-use functions. from libensemble.gen_funcs.persistent_gpCAM import ( _calculate_grid_distances, @@ -17,7 +15,7 @@ _generate_mesh, _read_testpoints, ) -from libensemble.generators import list_dicts_to_np, np_to_list_dicts +from libensemble.generators import LibensembleGenerator __all__ = [ "GP_CAM", @@ -30,7 +28,7 @@ # Equivalent to function persistent_gpCAM_ask_tell -class GP_CAM(Generator): +class GP_CAM(LibensembleGenerator): """ This generation function constructs a global surrogate of `f` values. @@ -64,12 +62,6 @@ def __init__(self, H, persis_info, gen_specs, libE_info=None): self.my_gp = None self.noise = 1e-8 # 1e-12 - def ask(self, num_points: Optional[int] = 0) -> List[dict]: - return np_to_list_dicts(self._ask_np(num_points)) - - def tell(self, calc_in: List[dict]) -> None: - self._tell_np(list_dicts_to_np(calc_in)) - def _ask_np(self, n_trials: int) -> npt.NDArray: if self.all_x.shape[0] == 0: self.x_new = self.persis_info["rand_stream"].uniform(self.lb, self.ub, (n_trials, self.n)) @@ -122,12 +114,6 @@ def __init__(self, H, persis_info, gen_specs, libE_info=None): 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 ask(self, num_points: Optional[int] = 0) -> List[dict]: - return np_to_list_dicts(self._ask_np(num_points)) - - def tell(self, calc_in: List[dict]) -> None: - self._tell_np(list_dicts_to_np(calc_in)) - def _ask_np(self, n_trials: int) -> List[dict]: if self.all_x.shape[0] == 0: x_new = self.persis_info["rand_stream"].uniform(self.lb, self.ub, (n_trials, self.n)) diff --git a/libensemble/generators.py b/libensemble/generators.py index 5b153fbcd..8141f8b65 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -114,7 +114,23 @@ def np_to_list_dicts(array: npt.NDArray) -> List[dict]: return out -class LibEnsembleGenInterfacer(Generator): +class LibensembleGenerator(Generator): + @abstractmethod + def _ask_np(self, num_points: Optional[int]) -> npt.NDArray: + pass + + @abstractmethod + def _tell_np(self, results: npt.NDArray) -> None: + pass + + def ask(self, num_points: Optional[int] = 0) -> List[dict]: + return np_to_list_dicts(self._ask_np(num_points)) + + def tell(self, calc_in: List[dict]) -> None: + self._tell_np(list_dicts_to_np(calc_in)) + + +class LibEnsembleGenInterfacer(LibensembleGenerator): """Implement ask/tell for traditionally written libEnsemble persistent generator functions. Still requires a handful of libEnsemble-specific data-structures on initialization. """ @@ -158,9 +174,6 @@ def _set_sim_ended(self, results: npt.NDArray) -> npt.NDArray: results = new_results return results - def ask(self, n_trials: Optional[int] = 0) -> List[dict]: - return np_to_list_dicts(self._ask_np(n_trials)) - def tell(self, calc_in: List[dict], tag: int = EVAL_GEN_TAG) -> None: self._tell_np(list_dicts_to_np(calc_in), tag) From 0750feceed41ee41e4c346a8dcb32f0f5f6ee968 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 17 Jul 2024 14:00:48 -0500 Subject: [PATCH 150/288] runners.py now interacts with LibensembleGenerator class, makes sure to communicate with _ask_np, _tell_np --- libensemble/gen_classes/sampling.py | 13 +++---------- libensemble/generators.py | 4 ++-- libensemble/utils/runners.py | 23 ++++++++++++++++------- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/libensemble/gen_classes/sampling.py b/libensemble/gen_classes/sampling.py index c4b26e80c..942f7e25c 100644 --- a/libensemble/gen_classes/sampling.py +++ b/libensemble/gen_classes/sampling.py @@ -2,14 +2,14 @@ import numpy as np -from libensemble import Generator +from libensemble.generators import LibensembleGenerator __all__ = [ "UniformSample", ] -class UniformSample(Generator): +class UniformSample(LibensembleGenerator): """ This generator returns ``gen_specs["initial_batch_size"]`` uniformly sampled points the first time it is called. Afterwards, it returns the @@ -24,10 +24,6 @@ def __init__(self, _, persis_info, gen_specs, libE_info=None) -> list: self.libE_info = libE_info self._get_user_params(self.gen_specs["user"]) - def ask(self, n_trials): - H_o = self._ask_np(n_trials) - return [{"x": x.tolist()} for x in H_o["x"]] - def _ask_np(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)) @@ -38,11 +34,8 @@ def _ask_np(self, n_trials): ) return H_o - def tell(self, calc_in): - pass # random sample so nothing to tell - def _tell_np(self, calc_in): - self.tell(calc_in) + pass # random sample so nothing to tell def _get_user_params(self, user_specs): """Extract user params""" diff --git a/libensemble/generators.py b/libensemble/generators.py index 8141f8b65..088478a1f 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -84,7 +84,7 @@ def final_tell(self, results: List[dict], *args, **kwargs) -> Optional[npt.NDArr def list_dicts_to_np(list_dicts: list) -> npt.NDArray: - if not list_dicts: + if list_dicts is None: return None new_dtype = [] new_dtype_names = [i for i in list_dicts[0].keys()] @@ -116,7 +116,7 @@ def np_to_list_dicts(array: npt.NDArray) -> List[dict]: class LibensembleGenerator(Generator): @abstractmethod - def _ask_np(self, num_points: Optional[int]) -> npt.NDArray: + def _ask_np(self, num_points: Optional[int] = 0) -> npt.NDArray: pass @abstractmethod diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 553e10326..92b0dd5f7 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -8,7 +8,7 @@ import numpy.typing as npt from libensemble.comms.comms import QCommThread -from libensemble.generators import LibEnsembleGenInterfacer, np_to_list_dicts +from libensemble.generators import LibensembleGenerator, LibEnsembleGenInterfacer, np_to_list_dicts from libensemble.message_numbers import EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG from libensemble.tools.persistent_support import PersistentSupport @@ -110,13 +110,19 @@ def _to_array(self, x): def _loop_over_normal_generator(self, tag, Work): while tag not in [PERSIS_STOP, STOP_TAG]: batch_size = getattr(self.gen, "batch_size", 0) or Work["libE_info"]["batch_size"] - points, updates = self._to_array(self.gen.ask(batch_size)), self._to_array(self.gen.ask_updates()) + if issubclass(type(self.gen), LibensembleGenerator): + points, updates = self.gen._ask_np(batch_size), self.gen.ask_updates() + else: + points, updates = self._to_array(self.gen.ask(batch_size)), self._to_array(self.gen.ask_updates()) if updates is not None and len(updates): # returned "samples" and "updates". can combine if same dtype H_out = np.append(points, updates) else: H_out = points tag, Work, H_in = self.ps.send_recv(H_out) - self.gen.tell(H_in) + if issubclass(type(self.gen), LibensembleGenerator): + self.gen._tell_np(H_in) + else: + self.gen.tell(np_to_list_dicts(H_in)) return H_in def _ask_and_send(self): @@ -149,18 +155,21 @@ def _persistent_result(self, calc_in, persis_info, libE_info): self.gen.setup() # maybe we're reusing a live gen from a previous run initial_batch = getattr(self.gen, "initial_batch_size", 0) or libE_info["batch_size"] if not issubclass( - type(self.gen), LibEnsembleGenInterfacer + type(self.gen), LibensembleGenerator ): # we can't control how many points created by a threaded gen H_out = self._to_array( self.gen.ask(initial_batch) ) # updates can probably be ignored when asking the first time else: - H_out = self.gen._ask_np() # libE really needs to receive the *entire* initial batch + H_out = self.gen._ask_np(initial_batch) # libE really needs to receive the *entire* initial batch tag, Work, H_in = self.ps.send_recv(H_out) # evaluate the initial sample - if issubclass(type(self.gen), LibEnsembleGenInterfacer): + if issubclass(type(self.gen), LibEnsembleGenInterfacer): # libE native-gens can ask/tell numpy arrays self.gen._tell_np(H_in) final_H_in = self._loop_over_persistent_interfacer() - else: + elif issubclass(type(self.gen), LibensembleGenerator): + self.gen._tell_np(H_in) + final_H_in = self._loop_over_normal_generator(tag, Work) + else: # non-native gen, needs list of dicts self.gen.tell(np_to_list_dicts(H_in)) final_H_in = self._loop_over_normal_generator(tag, Work) return self.gen.final_tell(final_H_in), FINISHED_PERSISTENT_GEN_TAG From cf06598050190a0766f65076b25827da91f84af5 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 17 Jul 2024 15:25:30 -0500 Subject: [PATCH 151/288] remove leading underscores, _ask_np and _tell_np are now ask_np and tell_np --- libensemble/gen_classes/gpCAM.py | 8 ++++---- libensemble/gen_classes/sampling.py | 4 ++-- libensemble/generators.py | 24 ++++++++++++------------ libensemble/utils/runners.py | 14 +++++++------- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/libensemble/gen_classes/gpCAM.py b/libensemble/gen_classes/gpCAM.py index 398a0c247..a0b273e52 100644 --- a/libensemble/gen_classes/gpCAM.py +++ b/libensemble/gen_classes/gpCAM.py @@ -62,7 +62,7 @@ def __init__(self, H, persis_info, gen_specs, libE_info=None): self.my_gp = None self.noise = 1e-8 # 1e-12 - def _ask_np(self, n_trials: int) -> npt.NDArray: + def ask_np(self, n_trials: int) -> npt.NDArray: if self.all_x.shape[0] == 0: self.x_new = self.persis_info["rand_stream"].uniform(self.lb, self.ub, (n_trials, self.n)) else: @@ -78,7 +78,7 @@ def _ask_np(self, n_trials: int) -> npt.NDArray: H_o["x"] = self.x_new return H_o - def _tell_np(self, calc_in: npt.NDArray) -> None: + def tell_np(self, calc_in: npt.NDArray) -> None: if calc_in is not None: 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)] @@ -114,7 +114,7 @@ def __init__(self, H, persis_info, gen_specs, libE_info=None): 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 _ask_np(self, n_trials: int) -> List[dict]: + def ask_np(self, n_trials: int) -> List[dict]: if self.all_x.shape[0] == 0: x_new = self.persis_info["rand_stream"].uniform(self.lb, self.ub, (n_trials, self.n)) else: @@ -138,7 +138,7 @@ def _ask_np(self, n_trials: int) -> List[dict]: H_o["x"] = self.x_new return H_o - def _tell_np(self, calc_in: npt.NDArray): + def tell_np(self, calc_in: npt.NDArray): if calc_in is not None: super().tell(calc_in) if not self.U.get("use_grid"): diff --git a/libensemble/gen_classes/sampling.py b/libensemble/gen_classes/sampling.py index 942f7e25c..5c4d2c2f4 100644 --- a/libensemble/gen_classes/sampling.py +++ b/libensemble/gen_classes/sampling.py @@ -24,7 +24,7 @@ def __init__(self, _, persis_info, gen_specs, libE_info=None) -> list: self.libE_info = libE_info self._get_user_params(self.gen_specs["user"]) - def _ask_np(self, n_trials): + def ask_np(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)) @@ -34,7 +34,7 @@ def _ask_np(self, n_trials): ) return H_o - def _tell_np(self, calc_in): + def tell_np(self, calc_in): pass # random sample so nothing to tell def _get_user_params(self, user_specs): diff --git a/libensemble/generators.py b/libensemble/generators.py index 088478a1f..7f64ab9da 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -116,18 +116,18 @@ def np_to_list_dicts(array: npt.NDArray) -> List[dict]: class LibensembleGenerator(Generator): @abstractmethod - def _ask_np(self, num_points: Optional[int] = 0) -> npt.NDArray: + def ask_np(self, num_points: Optional[int] = 0) -> npt.NDArray: pass @abstractmethod - def _tell_np(self, results: npt.NDArray) -> None: + def tell_np(self, results: npt.NDArray) -> None: pass def ask(self, num_points: Optional[int] = 0) -> List[dict]: - return np_to_list_dicts(self._ask_np(num_points)) + return np_to_list_dicts(self.ask_np(num_points)) def tell(self, calc_in: List[dict]) -> None: - self._tell_np(list_dicts_to_np(calc_in)) + self.tell_np(list_dicts_to_np(calc_in)) class LibEnsembleGenInterfacer(LibensembleGenerator): @@ -175,18 +175,18 @@ def _set_sim_ended(self, results: npt.NDArray) -> npt.NDArray: return results def tell(self, calc_in: List[dict], tag: int = EVAL_GEN_TAG) -> None: - self._tell_np(list_dicts_to_np(calc_in), tag) + self.tell_np(list_dicts_to_np(calc_in), tag) - def _ask_np(self, n_trials: int = 0) -> npt.NDArray: + def ask_np(self, n_trials: int = 0) -> npt.NDArray: if not self.thread.running: self.thread.run() _, ask_full = self.outbox.get() return ask_full["calc_out"] def ask_updates(self) -> npt.NDArray: - return self._ask_np() + return self.ask_np() - def _tell_np(self, results: List[dict], tag: int = EVAL_GEN_TAG) -> None: + def tell_np(self, results: List[dict], tag: int = EVAL_GEN_TAG) -> None: if results is not None: results = self._set_sim_ended(results) self.inbox.put( @@ -232,12 +232,12 @@ def __init__( self.results_idx = 0 self.last_ask = None - def _ask_np(self, n_trials: int = 0) -> npt.NDArray: + def ask_np(self, n_trials: int = 0) -> npt.NDArray: if (self.last_ask is None) or ( self.results_idx >= len(self.last_ask) ): # haven't been asked yet, or all previously enqueued points have been "asked" self.results_idx = 0 - self.last_ask = super()._ask_np(n_trials) + self.last_ask = super().ask_np(n_trials) if self.last_ask[ "local_min" ].any(): # filter out local minima rows, but they're cached in self.all_local_minima @@ -282,8 +282,8 @@ def _add_sim_ids(self, array: npt.NDArray) -> npt.NDArray: def ready_to_be_asked(self) -> bool: return not self.outbox.empty() - def _ask_np(self, *args) -> List[dict]: - output = super()._ask_np() + def ask_np(self, *args) -> List[dict]: + output = super().ask_np() if "cancel_requested" in output.dtype.names: cancels = output got_cancels_first = True diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 92b0dd5f7..c70b71547 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -111,7 +111,7 @@ def _loop_over_normal_generator(self, tag, Work): while tag not in [PERSIS_STOP, STOP_TAG]: batch_size = getattr(self.gen, "batch_size", 0) or Work["libE_info"]["batch_size"] if issubclass(type(self.gen), LibensembleGenerator): - points, updates = self.gen._ask_np(batch_size), self.gen.ask_updates() + points, updates = self.gen.ask_np(batch_size), self.gen.ask_updates() else: points, updates = self._to_array(self.gen.ask(batch_size)), self._to_array(self.gen.ask_updates()) if updates is not None and len(updates): # returned "samples" and "updates". can combine if same dtype @@ -120,14 +120,14 @@ def _loop_over_normal_generator(self, tag, Work): H_out = points tag, Work, H_in = self.ps.send_recv(H_out) if issubclass(type(self.gen), LibensembleGenerator): - self.gen._tell_np(H_in) + self.gen.tell_np(H_in) else: self.gen.tell(np_to_list_dicts(H_in)) return H_in def _ask_and_send(self): while self.gen.outbox.qsize(): # recv/send any outstanding messages - points, updates = self.gen._ask_np(), self.gen.ask_updates() # PersistentInterfacers each have _ask_np + points, updates = self.gen.ask_np(), self.gen.ask_updates() # PersistentInterfacers each have ask_np if updates is not None and len(updates): self.ps.send(points) for i in updates: @@ -143,7 +143,7 @@ def _loop_over_persistent_interfacer(self): tag, _, H_in = self.ps.recv() if tag in [STOP_TAG, PERSIS_STOP]: return H_in - self.gen._tell_np(H_in) + self.gen.tell_np(H_in) def _persistent_result(self, calc_in, persis_info, libE_info): self.ps = PersistentSupport(libE_info, EVAL_GEN_TAG) @@ -161,13 +161,13 @@ def _persistent_result(self, calc_in, persis_info, libE_info): self.gen.ask(initial_batch) ) # updates can probably be ignored when asking the first time else: - H_out = self.gen._ask_np(initial_batch) # libE really needs to receive the *entire* initial batch + H_out = self.gen.ask_np(initial_batch) # libE really needs to receive the *entire* initial batch tag, Work, H_in = self.ps.send_recv(H_out) # evaluate the initial sample if issubclass(type(self.gen), LibEnsembleGenInterfacer): # libE native-gens can ask/tell numpy arrays - self.gen._tell_np(H_in) + self.gen.tell_np(H_in) final_H_in = self._loop_over_persistent_interfacer() elif issubclass(type(self.gen), LibensembleGenerator): - self.gen._tell_np(H_in) + self.gen.tell_np(H_in) final_H_in = self._loop_over_normal_generator(tag, Work) else: # non-native gen, needs list of dicts self.gen.tell(np_to_list_dicts(H_in)) From fe6eeddd2ea6d886f99cb37062211ec1dfa0af95 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 17 Jul 2024 16:25:17 -0500 Subject: [PATCH 152/288] remember, the LibensembleGenInterfacer class needs to ask the entire initial batch --- libensemble/utils/runners.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index c70b71547..4285c323a 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -154,14 +154,14 @@ def _persistent_result(self, calc_in, persis_info, libE_info): if self.gen.thread is None: self.gen.setup() # maybe we're reusing a live gen from a previous run initial_batch = getattr(self.gen, "initial_batch_size", 0) or libE_info["batch_size"] - if not issubclass( - type(self.gen), LibensembleGenerator + if issubclass( + type(self.gen), LibEnsembleGenInterfacer ): # we can't control how many points created by a threaded gen - H_out = self._to_array( - self.gen.ask(initial_batch) - ) # updates can probably be ignored when asking the first time - else: + H_out = self.gen.ask_np() # updates can probably be ignored when asking the first time + elif issubclass(type(self.gen), LibensembleGenerator): H_out = self.gen.ask_np(initial_batch) # libE really needs to receive the *entire* initial batch + else: + H_out = self.gen.ask(initial_batch) tag, Work, H_in = self.ps.send_recv(H_out) # evaluate the initial sample if issubclass(type(self.gen), LibEnsembleGenInterfacer): # libE native-gens can ask/tell numpy arrays self.gen.tell_np(H_in) From 3363e4ade264c9851acb97dee981f4cb9256cd24 Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 18 Jul 2024 09:57:33 -0500 Subject: [PATCH 153/288] rename LibensembleGenInterfacer to LibensembleGenThreadInterfacer --- libensemble/generators.py | 6 +++--- libensemble/utils/runners.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 7f64ab9da..3ea2d8d63 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -130,7 +130,7 @@ def tell(self, calc_in: List[dict]) -> None: self.tell_np(list_dicts_to_np(calc_in)) -class LibEnsembleGenInterfacer(LibensembleGenerator): +class LibensembleGenThreadInterfacer(LibensembleGenerator): """Implement ask/tell for traditionally written libEnsemble persistent generator functions. Still requires a handful of libEnsemble-specific data-structures on initialization. """ @@ -201,7 +201,7 @@ def final_tell(self, results: List[dict]) -> (npt.NDArray, dict, int): return self.thread.result() -class APOSMM(LibEnsembleGenInterfacer): +class APOSMM(LibensembleGenThreadInterfacer): """ Standalone object-oriented APOSMM generator """ @@ -261,7 +261,7 @@ def ask_updates(self) -> List[npt.NDArray]: return minima -class Surmise(LibEnsembleGenInterfacer): +class Surmise(LibensembleGenThreadInterfacer): def __init__( self, gen_specs: dict, History: npt.NDArray = [], persis_info: dict = {}, libE_info: dict = {} ) -> None: diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 4285c323a..67b91bfe5 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -8,7 +8,7 @@ import numpy.typing as npt from libensemble.comms.comms import QCommThread -from libensemble.generators import LibensembleGenerator, LibEnsembleGenInterfacer, np_to_list_dicts +from libensemble.generators import LibensembleGenerator, LibensembleGenThreadInterfacer, np_to_list_dicts from libensemble.message_numbers import EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG from libensemble.tools.persistent_support import PersistentSupport @@ -155,7 +155,7 @@ def _persistent_result(self, calc_in, persis_info, libE_info): self.gen.setup() # maybe we're reusing a live gen from a previous run initial_batch = getattr(self.gen, "initial_batch_size", 0) or libE_info["batch_size"] if issubclass( - type(self.gen), LibEnsembleGenInterfacer + type(self.gen), LibensembleGenThreadInterfacer ): # we can't control how many points created by a threaded gen H_out = self.gen.ask_np() # updates can probably be ignored when asking the first time elif issubclass(type(self.gen), LibensembleGenerator): @@ -163,7 +163,7 @@ def _persistent_result(self, calc_in, persis_info, libE_info): else: H_out = self.gen.ask(initial_batch) tag, Work, H_in = self.ps.send_recv(H_out) # evaluate the initial sample - if issubclass(type(self.gen), LibEnsembleGenInterfacer): # libE native-gens can ask/tell numpy arrays + if issubclass(type(self.gen), LibensembleGenThreadInterfacer): # libE native-gens can ask/tell numpy arrays self.gen.tell_np(H_in) final_H_in = self._loop_over_persistent_interfacer() elif issubclass(type(self.gen), LibensembleGenerator): From 25b4d4d72916133c030fd413e49d28920277f9a1 Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 18 Jul 2024 10:06:14 -0500 Subject: [PATCH 154/288] move numpy-> list-of-dicts converters out of generators into libensemble.utils.misc --- .../gen_funcs/persistent_gen_wrapper.py | 2 +- libensemble/generators.py | 42 +++---------------- libensemble/utils/misc.py | 34 +++++++++++++++ 3 files changed, 41 insertions(+), 37 deletions(-) diff --git a/libensemble/gen_funcs/persistent_gen_wrapper.py b/libensemble/gen_funcs/persistent_gen_wrapper.py index 3140e39c7..2ad862864 100644 --- a/libensemble/gen_funcs/persistent_gen_wrapper.py +++ b/libensemble/gen_funcs/persistent_gen_wrapper.py @@ -2,9 +2,9 @@ import numpy as np -from libensemble.generators import np_to_list_dicts from libensemble.message_numbers import EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG from libensemble.tools.persistent_support import PersistentSupport +from libensemble.utils.misc import np_to_list_dicts def persistent_gen_f(H, persis_info, gen_specs, libE_info): diff --git a/libensemble/generators.py b/libensemble/generators.py index 3ea2d8d63..2c8da4224 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -10,17 +10,13 @@ from libensemble.executors import Executor from libensemble.message_numbers import EVAL_GEN_TAG, PERSIS_STOP from libensemble.tools import add_unique_random_streams - -# TODO: Refactor below-class to wrap StandardGenerator and possibly convert in/out data to list-of-dicts +from libensemble.utils.misc import list_dicts_to_np, np_to_list_dicts class Generator(ABC): """ v 0.7.2.24 - Tentative generator interface for use with libEnsemble, and generic enough to be - broadly compatible with other workflow packages. - .. code-block:: python from libensemble import Ensemble @@ -83,38 +79,12 @@ def final_tell(self, results: List[dict], *args, **kwargs) -> Optional[npt.NDArr """ -def list_dicts_to_np(list_dicts: list) -> npt.NDArray: - if list_dicts is None: - return None - new_dtype = [] - new_dtype_names = [i for i in list_dicts[0].keys()] - for i, entry in enumerate(list_dicts[0].values()): # must inspect values to get presumptive types - if hasattr(entry, "shape") and len(entry.shape): - entry_dtype = (new_dtype_names[i], entry.dtype, entry.shape) - else: - entry_dtype = (new_dtype_names[i], type(entry)) - new_dtype.append(entry_dtype) - - out = np.zeros(len(list_dicts), dtype=new_dtype) - for i, entry in enumerate(list_dicts): - for field in entry.keys(): - out[field][i] = entry[field] - return out - - -def np_to_list_dicts(array: npt.NDArray) -> List[dict]: - if array is None: - return None - out = [] - for row in array: - new_dict = {} - for field in row.dtype.names: - new_dict[field] = row[field] - out.append(new_dict) - return out - - class LibensembleGenerator(Generator): + """Internal implementation of Generator interface for use with libEnsemble, or for those who + prefer numpy arrays. ``ask/tell`` methods communicate lists of dictionaries, like the standard. + ``ask_np/tell_np`` methods communicate numpy arrays containing the same data. + """ + @abstractmethod def ask_np(self, num_points: Optional[int] = 0) -> npt.NDArray: pass diff --git a/libensemble/utils/misc.py b/libensemble/utils/misc.py index ca67095ac..79208b7cf 100644 --- a/libensemble/utils/misc.py +++ b/libensemble/utils/misc.py @@ -4,8 +4,11 @@ from itertools import groupby from operator import itemgetter +from typing import List +import numpy as np import pydantic +from numpy import typing as npt pydantic_version = pydantic.__version__[0] @@ -76,3 +79,34 @@ def specs_checker_setattr(obj, key, value): obj[key] = value else: # actual obj obj.__dict__[key] = value + + +def list_dicts_to_np(list_dicts: list) -> npt.NDArray: + if list_dicts is None: + return None + new_dtype = [] + new_dtype_names = [i for i in list_dicts[0].keys()] + for i, entry in enumerate(list_dicts[0].values()): # must inspect values to get presumptive types + if hasattr(entry, "shape") and len(entry.shape): + entry_dtype = (new_dtype_names[i], entry.dtype, entry.shape) + else: + entry_dtype = (new_dtype_names[i], type(entry)) + new_dtype.append(entry_dtype) + + out = np.zeros(len(list_dicts), dtype=new_dtype) + for i, entry in enumerate(list_dicts): + for field in entry.keys(): + out[field][i] = entry[field] + return out + + +def np_to_list_dicts(array: npt.NDArray) -> List[dict]: + if array is None: + return None + out = [] + for row in array: + new_dict = {} + for field in row.dtype.names: + new_dict[field] = row[field] + out.append(new_dict) + return out From 1ca123e2217f6133ab5ac93c4d21b87efdcc993e Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 18 Jul 2024 13:21:07 -0500 Subject: [PATCH 155/288] small docstrings for current classes in generators.py, should probably add code-samples --- libensemble/generators.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 2c8da4224..403b08f67 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -57,7 +57,7 @@ def __init__(self, *args, **kwargs): @abstractmethod def ask(self, num_points: Optional[int], *args, **kwargs) -> List[dict]: """ - Request the next set of points to evaluate, and optionally any previous points to update. + Request the next set of points to evaluate. """ def ask_updates(self) -> npt.NDArray: @@ -94,9 +94,11 @@ def tell_np(self, results: npt.NDArray) -> None: pass def ask(self, num_points: Optional[int] = 0) -> List[dict]: + """Request the next set of points to evaluate.""" return np_to_list_dicts(self.ask_np(num_points)) def tell(self, calc_in: List[dict]) -> None: + """Send the results of evaluations to the generator.""" self.tell_np(list_dicts_to_np(calc_in)) @@ -116,6 +118,7 @@ def __init__( 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() @@ -145,18 +148,22 @@ def _set_sim_ended(self, results: npt.NDArray) -> npt.NDArray: return results def tell(self, calc_in: List[dict], tag: int = EVAL_GEN_TAG) -> None: + """Send the results of evaluations to the generator.""" self.tell_np(list_dicts_to_np(calc_in), tag) def ask_np(self, n_trials: int = 0) -> npt.NDArray: + """Request the next set of points to evaluate, as a NumPy array.""" if not self.thread.running: self.thread.run() _, ask_full = self.outbox.get() return ask_full["calc_out"] def ask_updates(self) -> npt.NDArray: + """Request any updates to previous points, e.g. minima discovered, points to cancel.""" return self.ask_np() def tell_np(self, results: List[dict], tag: int = EVAL_GEN_TAG) -> None: + """Send the results of evaluations to the generator, as a NumPy array.""" if results is not None: results = self._set_sim_ended(results) self.inbox.put( @@ -167,6 +174,7 @@ def tell_np(self, results: List[dict], tag: int = EVAL_GEN_TAG) -> None: self.inbox.put((0, np.copy(results))) def final_tell(self, results: List[dict]) -> (npt.NDArray, dict, int): + """Send any last results to the generator, and it to close down.""" self.tell(results, PERSIS_STOP) # conversion happens in tell return self.thread.result() @@ -203,6 +211,7 @@ def __init__( self.last_ask = None def ask_np(self, n_trials: int = 0) -> npt.NDArray: + """Request the next set of points to evaluate, as a NumPy array.""" if (self.last_ask is None) or ( self.results_idx >= len(self.last_ask) ): # haven't been asked yet, or all previously enqueued points have been "asked" @@ -226,12 +235,17 @@ def ask_np(self, n_trials: int = 0) -> npt.NDArray: return results def ask_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 class Surmise(LibensembleGenThreadInterfacer): + """ + Standalone object-oriented Surmise generator + """ + def __init__( self, gen_specs: dict, History: npt.NDArray = [], persis_info: dict = {}, libE_info: dict = {} ) -> None: @@ -250,9 +264,11 @@ def _add_sim_ids(self, array: npt.NDArray) -> npt.NDArray: return array def ready_to_be_asked(self) -> bool: + """Check if the generator has the next batch of points ready.""" return not self.outbox.empty() - def ask_np(self, *args) -> List[dict]: + def ask_np(self, *args) -> npt.NDArray: + """Request the next set of points to evaluate, as a NumPy array.""" output = super().ask_np() if "cancel_requested" in output.dtype.names: cancels = output @@ -270,7 +286,8 @@ def ask_np(self, *args) -> List[dict]: except thread_queue.Empty: return self.results - def ask_updates(self) -> npt.NDArray: + def ask_updates(self) -> List[npt.NDArray]: + """Request a list of NumPy arrays containing points that should be cancelled by the workflow.""" cancels = copy.deepcopy(self.all_cancels) self.all_cancels = [] return cancels From 838057b7941e628d8a056b8aadad1c8a4700eec7 Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 18 Jul 2024 13:38:50 -0500 Subject: [PATCH 156/288] refactor + more comments throughout AskTellGenRunner --- libensemble/utils/runners.py | 55 ++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 67b91bfe5..a7cfc1ae1 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -98,7 +98,8 @@ def __init__(self, specs): super().__init__(specs) self.gen = specs.get("generator") - def _to_array(self, x): + def _to_array(self, x: list) -> npt.NDArray: + """fast-cast list-of-dicts to NumPy array""" if isinstance(x, list) and len(x) and isinstance(x[0], dict): arr = np.zeros(len(x), dtype=self.specs["out"]) for i in range(len(x)): @@ -108,9 +109,10 @@ def _to_array(self, x): return x def _loop_over_normal_generator(self, tag, Work): + """Interact with ask/tell generator that *does not* contain a background thread""" while tag not in [PERSIS_STOP, STOP_TAG]: batch_size = getattr(self.gen, "batch_size", 0) or Work["libE_info"]["batch_size"] - if issubclass(type(self.gen), LibensembleGenerator): + if issubclass(type(self.gen), LibensembleGenerator): # we can ask native numpy for efficiency points, updates = self.gen.ask_np(batch_size), self.gen.ask_updates() else: points, updates = self._to_array(self.gen.ask(batch_size)), self._to_array(self.gen.ask_updates()) @@ -126,43 +128,40 @@ def _loop_over_normal_generator(self, tag, Work): return H_in def _ask_and_send(self): + """Loop over generator's outbox contents, send to manager""" while self.gen.outbox.qsize(): # recv/send any outstanding messages - points, updates = self.gen.ask_np(), self.gen.ask_updates() # PersistentInterfacers each have ask_np + points, updates = self.gen.ask_np(), self.gen.ask_updates() if updates is not None and len(updates): self.ps.send(points) for i in updates: - self.ps.send(i, keep_state=True) + self.ps.send(i, keep_state=True) # keep_state since an update doesn't imply "new points" else: self.ps.send(points) def _loop_over_persistent_interfacer(self): + """Cycle between moving all outbound / inbound messages between threaded gen and manager""" while True: time.sleep(0.0025) # dont need to ping the gen relentlessly. Let it calculate. 400hz self._ask_and_send() - while self.ps.comm.mail_flag(): # receive any new messages, give all to gen + while self.ps.comm.mail_flag(): # receive any new messages from Manager, give all to gen tag, _, H_in = self.ps.recv() if tag in [STOP_TAG, PERSIS_STOP]: - return H_in + return H_in # this will get inserted into final_tell. this breaks loop self.gen.tell_np(H_in) - def _persistent_result(self, calc_in, persis_info, libE_info): - self.ps = PersistentSupport(libE_info, EVAL_GEN_TAG) - tag = None - if hasattr(self.gen, "setup"): - self.gen.persis_info = persis_info # passthrough, setup() uses the gen attributes - self.gen.libE_info = libE_info - if self.gen.thread is None: - self.gen.setup() # maybe we're reusing a live gen from a previous run + def _get_initial_ask(self, libE_info) -> npt.NDArray: + """Get initial batch from generator based on generator type""" initial_batch = getattr(self.gen, "initial_batch_size", 0) or libE_info["batch_size"] - if issubclass( - type(self.gen), LibensembleGenThreadInterfacer - ): # we can't control how many points created by a threaded gen - H_out = self.gen.ask_np() # updates can probably be ignored when asking the first time + if issubclass(type(self.gen), LibensembleGenThreadInterfacer): + H_out = self.gen.ask_np() # libE really needs to receive the *entire* initial batch from a threaded gen elif issubclass(type(self.gen), LibensembleGenerator): - H_out = self.gen.ask_np(initial_batch) # libE really needs to receive the *entire* initial batch - else: + H_out = self.gen.ask_np(initial_batch) + else: # these will likely be 3rd party gens H_out = self.gen.ask(initial_batch) - tag, Work, H_in = self.ps.send_recv(H_out) # evaluate the initial sample + return H_out + + def _start_generator_loop(self, tag, Work, H_in): + """Start the generator loop after choosing best way of giving initial results to gen""" if issubclass(type(self.gen), LibensembleGenThreadInterfacer): # libE native-gens can ask/tell numpy arrays self.gen.tell_np(H_in) final_H_in = self._loop_over_persistent_interfacer() @@ -172,6 +171,20 @@ def _persistent_result(self, calc_in, persis_info, libE_info): else: # non-native gen, needs list of dicts self.gen.tell(np_to_list_dicts(H_in)) final_H_in = self._loop_over_normal_generator(tag, Work) + return final_H_in + + def _persistent_result(self, calc_in, persis_info, libE_info): + """Setup comms with manager, setup gen, loop gen to completion, return gen's results""" + self.ps = PersistentSupport(libE_info, EVAL_GEN_TAG) + tag = None + if hasattr(self.gen, "setup"): + self.gen.persis_info = persis_info # passthrough, setup() uses the gen attributes + self.gen.libE_info = libE_info + if self.gen.thread is None: + self.gen.setup() # maybe we're reusing a live gen from a previous run + H_out = self._get_initial_ask(libE_info) + tag, Work, H_in = self.ps.send_recv(H_out) # evaluate the initial sample + final_H_in = self._start_generator_loop(tag, Work, H_in) return self.gen.final_tell(final_H_in), FINISHED_PERSISTENT_GEN_TAG def _result(self, calc_in: npt.NDArray, persis_info: dict, libE_info: dict) -> (npt.NDArray, dict, Optional[int]): From 2638d33070e60bea34c081f1c961acce36755192 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 19 Jul 2024 14:47:10 -0500 Subject: [PATCH 157/288] necessary, but currently unfunctional refactoring of the AskTellGenRunner, into subclasses depending on the type of ask/tell gen being interacted with --- libensemble/utils/runners.py | 126 +++++++++++++++++++++-------------- 1 file changed, 76 insertions(+), 50 deletions(-) diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index a7cfc1ae1..52b78e523 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -21,7 +21,11 @@ def __new__(cls, specs): return super(Runner, GlobusComputeRunner).__new__(GlobusComputeRunner) if specs.get("threaded"): # TODO: undecided interface return super(Runner, ThreadRunner).__new__(ThreadRunner) - if hasattr(specs.get("generator", None), "ask"): + if isinstance(specs.get("generator", None), LibensembleGenThreadInterfacer): + return super(AskTellGenRunner, LibensembleGenThreadInterfacer).__new__(LibensembleGenThreadInterfacer) + if isinstance(specs.get("generator", None), LibensembleGenerator): + return super(AskTellGenRunner, LibensembleGenRunner).__new__(LibensembleGenRunner) + if hasattr(specs.get("generator", None), "ask"): # all other ask/tell gens, third party return super(Runner, AskTellGenRunner).__new__(AskTellGenRunner) else: return super().__new__(Runner) @@ -94,9 +98,13 @@ def shutdown(self) -> None: class AskTellGenRunner(Runner): + """Interact with ask/tell generator. Base class initialized for third-party generators.""" + def __init__(self, specs): super().__init__(specs) self.gen = specs.get("generator") + self.inital_batch = getattr(self.gen, "initial_batch_size", 0) + self.batch = getattr(self.gen, "batch_size", 0) def _to_array(self, x: list) -> npt.NDArray: """fast-cast list-of-dicts to NumPy array""" @@ -108,75 +116,34 @@ def _to_array(self, x: list) -> npt.NDArray: return arr return x - def _loop_over_normal_generator(self, tag, Work): + def _loop_over_gen(self, tag, Work): """Interact with ask/tell generator that *does not* contain a background thread""" while tag not in [PERSIS_STOP, STOP_TAG]: - batch_size = getattr(self.gen, "batch_size", 0) or Work["libE_info"]["batch_size"] - if issubclass(type(self.gen), LibensembleGenerator): # we can ask native numpy for efficiency - points, updates = self.gen.ask_np(batch_size), self.gen.ask_updates() - else: - points, updates = self._to_array(self.gen.ask(batch_size)), self._to_array(self.gen.ask_updates()) + batch_size = self.batch or Work["libE_info"]["batch_size"] + points, updates = self._to_array(self.gen.ask(batch_size)), self._to_array(self.gen.ask_updates()) if updates is not None and len(updates): # returned "samples" and "updates". can combine if same dtype H_out = np.append(points, updates) else: H_out = points tag, Work, H_in = self.ps.send_recv(H_out) - if issubclass(type(self.gen), LibensembleGenerator): - self.gen.tell_np(H_in) - else: - self.gen.tell(np_to_list_dicts(H_in)) + self.gen.tell(np_to_list_dicts(H_in)) return H_in - def _ask_and_send(self): - """Loop over generator's outbox contents, send to manager""" - while self.gen.outbox.qsize(): # recv/send any outstanding messages - points, updates = self.gen.ask_np(), self.gen.ask_updates() - if updates is not None and len(updates): - self.ps.send(points) - for i in updates: - self.ps.send(i, keep_state=True) # keep_state since an update doesn't imply "new points" - else: - self.ps.send(points) - - def _loop_over_persistent_interfacer(self): - """Cycle between moving all outbound / inbound messages between threaded gen and manager""" - while True: - time.sleep(0.0025) # dont need to ping the gen relentlessly. Let it calculate. 400hz - self._ask_and_send() - while self.ps.comm.mail_flag(): # receive any new messages from Manager, give all to gen - tag, _, H_in = self.ps.recv() - if tag in [STOP_TAG, PERSIS_STOP]: - return H_in # this will get inserted into final_tell. this breaks loop - self.gen.tell_np(H_in) - def _get_initial_ask(self, libE_info) -> npt.NDArray: """Get initial batch from generator based on generator type""" - initial_batch = getattr(self.gen, "initial_batch_size", 0) or libE_info["batch_size"] - if issubclass(type(self.gen), LibensembleGenThreadInterfacer): - H_out = self.gen.ask_np() # libE really needs to receive the *entire* initial batch from a threaded gen - elif issubclass(type(self.gen), LibensembleGenerator): - H_out = self.gen.ask_np(initial_batch) - else: # these will likely be 3rd party gens - H_out = self.gen.ask(initial_batch) + initial_batch = self.inital_batch or libE_info["batch_size"] + H_out = self.gen.ask(initial_batch) return H_out def _start_generator_loop(self, tag, Work, H_in): """Start the generator loop after choosing best way of giving initial results to gen""" - if issubclass(type(self.gen), LibensembleGenThreadInterfacer): # libE native-gens can ask/tell numpy arrays - self.gen.tell_np(H_in) - final_H_in = self._loop_over_persistent_interfacer() - elif issubclass(type(self.gen), LibensembleGenerator): - self.gen.tell_np(H_in) - final_H_in = self._loop_over_normal_generator(tag, Work) - else: # non-native gen, needs list of dicts - self.gen.tell(np_to_list_dicts(H_in)) - final_H_in = self._loop_over_normal_generator(tag, Work) + self.gen.tell(np_to_list_dicts(H_in)) + final_H_in = self._loop_over_gen(tag, Work) return final_H_in def _persistent_result(self, calc_in, persis_info, libE_info): """Setup comms with manager, setup gen, loop gen to completion, return gen's results""" self.ps = PersistentSupport(libE_info, EVAL_GEN_TAG) - tag = None if hasattr(self.gen, "setup"): self.gen.persis_info = persis_info # passthrough, setup() uses the gen attributes self.gen.libE_info = libE_info @@ -191,3 +158,62 @@ def _result(self, calc_in: npt.NDArray, persis_info: dict, libE_info: dict) -> ( if libE_info.get("persistent"): return self._persistent_result(calc_in, persis_info, libE_info) return self._to_array(self.gen.ask(getattr(self.gen, "batch_size", 0) or libE_info["batch_size"])) + + +class LibensembleGenRunner(AskTellGenRunner): + def _get_initial_ask(self, libE_info) -> npt.NDArray: + """Get initial batch from generator based on generator type""" + H_out = self.gen.ask_np(self.inital_batch or libE_info["batch_size"]) + return H_out + + def _start_generator_loop(self, tag, Work, H_in) -> npt.NDArray: + """Start the generator loop after choosing best way of giving initial results to gen""" + self.gen.tell_np(H_in) + return self._loop_over_libe_asktell_gen(tag, Work) + + def _loop_over_libe_asktell_gen(self, tag, Work) -> npt.NDArray: + """Interact with ask/tell generator that *does not* contain a background thread""" + while tag not in [PERSIS_STOP, STOP_TAG]: + batch_size = self.batch or Work["libE_info"]["batch_size"] + points, updates = self.gen.ask_np(batch_size), self.gen.ask_updates() + if updates is not None and len(updates): # returned "samples" and "updates". can combine if same dtype + H_out = np.append(points, updates) + else: + H_out = points + tag, Work, H_in = self.ps.send_recv(H_out) + self.gen.tell_np(H_in) + return H_in + + +class LibensembleGenThreadRunner(AskTellGenRunner): + def _get_initial_ask(self, libE_info) -> npt.NDArray: + """Get initial batch from generator based on generator type""" + H_out = self.gen.ask_np() # libE really needs to receive the *entire* initial batch from a threaded gen + return H_out + + def _start_generator_loop(self, tag, Work, H_in): + """Start the generator loop after choosing best way of giving initial results to gen""" + self.gen.tell_np(H_in) + return self._loop_over_thread_interfacer() + + def _ask_and_send(self): + """Loop over generator's outbox contents, send to manager""" + while self.gen.outbox.qsize(): # recv/send any outstanding messages + points, updates = self.gen.ask_np(), self.gen.ask_updates() + if updates is not None and len(updates): + self.ps.send(points) + for i in updates: + self.ps.send(i, keep_state=True) # keep_state since an update doesn't imply "new points" + else: + self.ps.send(points) + + def _loop_over_thread_interfacer(self): + """Cycle between moving all outbound / inbound messages between threaded gen and manager""" + while True: + time.sleep(0.0025) # dont need to ping the gen relentlessly. Let it calculate. 400hz + self._ask_and_send() + while self.ps.comm.mail_flag(): # receive any new messages from Manager, give all to gen + tag, _, H_in = self.ps.recv() + if tag in [STOP_TAG, PERSIS_STOP]: + return H_in # this will get inserted into final_tell. this breaks loop + self.gen.tell_np(H_in) From c3c19e16cdb7317c6df412c899a345c05d2bfb96 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 19 Jul 2024 16:47:09 -0500 Subject: [PATCH 158/288] replacing __new__ magic in Runner superclass with factory function for better creation of subsubclasses --- libensemble/utils/runners.py | 27 ++++++++++++++------------- libensemble/worker.py | 4 ++-- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 52b78e523..45d14a9bd 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -16,19 +16,21 @@ class Runner: - def __new__(cls, specs): + @classmethod + def from_specs(cls, specs): if len(specs.get("globus_compute_endpoint", "")) > 0: - return super(Runner, GlobusComputeRunner).__new__(GlobusComputeRunner) + return GlobusComputeRunner(specs) if specs.get("threaded"): # TODO: undecided interface - return super(Runner, ThreadRunner).__new__(ThreadRunner) - if isinstance(specs.get("generator", None), LibensembleGenThreadInterfacer): - return super(AskTellGenRunner, LibensembleGenThreadInterfacer).__new__(LibensembleGenThreadInterfacer) - if isinstance(specs.get("generator", None), LibensembleGenerator): - return super(AskTellGenRunner, LibensembleGenRunner).__new__(LibensembleGenRunner) - if hasattr(specs.get("generator", None), "ask"): # all other ask/tell gens, third party - return super(Runner, AskTellGenRunner).__new__(AskTellGenRunner) + return ThreadRunner(specs) + if specs.get("generator") is not None: + if isinstance(specs.get("generator", None), LibensembleGenThreadInterfacer): + return LibensembleGenThreadRunner(specs) + if isinstance(specs.get("generator", None), LibensembleGenerator): + return LibensembleGenRunner(specs) + else: + return AskTellGenRunner(specs) else: - return super().__new__(Runner) + return Runner(specs) def __init__(self, specs): self.specs = specs @@ -138,8 +140,7 @@ def _get_initial_ask(self, libE_info) -> npt.NDArray: def _start_generator_loop(self, tag, Work, H_in): """Start the generator loop after choosing best way of giving initial results to gen""" self.gen.tell(np_to_list_dicts(H_in)) - final_H_in = self._loop_over_gen(tag, Work) - return final_H_in + return self._loop_over_gen(tag, Work) def _persistent_result(self, calc_in, persis_info, libE_info): """Setup comms with manager, setup gen, loop gen to completion, return gen's results""" @@ -191,7 +192,7 @@ def _get_initial_ask(self, libE_info) -> npt.NDArray: H_out = self.gen.ask_np() # libE really needs to receive the *entire* initial batch from a threaded gen return H_out - def _start_generator_loop(self, tag, Work, H_in): + def _start_generator_loop(self, _, _2, H_in): """Start the generator loop after choosing best way of giving initial results to gen""" self.gen.tell_np(H_in) return self._loop_over_thread_interfacer() diff --git a/libensemble/worker.py b/libensemble/worker.py index aea054999..2282ef74a 100644 --- a/libensemble/worker.py +++ b/libensemble/worker.py @@ -166,8 +166,8 @@ def __init__( self.workerID = workerID self.libE_specs = libE_specs self.stats_fmt = libE_specs.get("stats_fmt", {}) - self.sim_runner = Runner(sim_specs) - self.gen_runner = Runner(gen_specs) + self.sim_runner = Runner.from_specs(sim_specs) + self.gen_runner = Runner.from_specs(gen_specs) self.runners = {EVAL_SIM_TAG: self.sim_runner.run, EVAL_GEN_TAG: self.gen_runner.run} self.calc_iter = {EVAL_SIM_TAG: 0, EVAL_GEN_TAG: 0} Worker._set_executor(self.workerID, self.comm) From 4d48ab9c4870c3ba2fcd6715e17bd9ec2b3d8af0 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 19 Jul 2024 16:48:36 -0500 Subject: [PATCH 159/288] typo --- libensemble/utils/runners.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 45d14a9bd..6433a39e3 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -105,7 +105,7 @@ class AskTellGenRunner(Runner): def __init__(self, specs): super().__init__(specs) self.gen = specs.get("generator") - self.inital_batch = getattr(self.gen, "initial_batch_size", 0) + self.initial_batch = getattr(self.gen, "initial_batch_size", 0) self.batch = getattr(self.gen, "batch_size", 0) def _to_array(self, x: list) -> npt.NDArray: @@ -133,7 +133,7 @@ def _loop_over_gen(self, tag, Work): def _get_initial_ask(self, libE_info) -> npt.NDArray: """Get initial batch from generator based on generator type""" - initial_batch = self.inital_batch or libE_info["batch_size"] + initial_batch = self.initial_batch or libE_info["batch_size"] H_out = self.gen.ask(initial_batch) return H_out @@ -164,7 +164,7 @@ def _result(self, calc_in: npt.NDArray, persis_info: dict, libE_info: dict) -> ( class LibensembleGenRunner(AskTellGenRunner): def _get_initial_ask(self, libE_info) -> npt.NDArray: """Get initial batch from generator based on generator type""" - H_out = self.gen.ask_np(self.inital_batch or libE_info["batch_size"]) + H_out = self.gen.ask_np(self.initial_batch or libE_info["batch_size"]) return H_out def _start_generator_loop(self, tag, Work, H_in) -> npt.NDArray: From 9f7d4850b22d96ad2d6553fabfe09a3df1f95ed3 Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 22 Jul 2024 10:54:00 -0500 Subject: [PATCH 160/288] fix Runners unit test --- libensemble/tests/unit_tests/test_ufunc_runners.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/libensemble/tests/unit_tests/test_ufunc_runners.py b/libensemble/tests/unit_tests/test_ufunc_runners.py index 1d3cbb4b2..51aa8c65d 100644 --- a/libensemble/tests/unit_tests/test_ufunc_runners.py +++ b/libensemble/tests/unit_tests/test_ufunc_runners.py @@ -30,8 +30,8 @@ def get_ufunc_args(): def test_normal_runners(): calc_in, sim_specs, gen_specs = get_ufunc_args() - simrunner = Runner(sim_specs) - genrunner = Runner(gen_specs) + simrunner = Runner.from_specs(sim_specs) + genrunner = Runner.from_specs(gen_specs) assert not hasattr(simrunner, "globus_compute_executor") and not hasattr( genrunner, "globus_compute_executor" ), "Globus Compute use should not be detected without setting endpoint fields" @@ -47,7 +47,7 @@ def tupilize(arg1, arg2): sim_specs["sim_f"] = tupilize persis_info = {"hello": "threads"} - simrunner = Runner(sim_specs) + simrunner = Runner.from_specs(sim_specs) result = simrunner._result(calc_in, persis_info, {}) assert result == (calc_in, persis_info) assert hasattr(simrunner, "thread_handle") @@ -61,7 +61,7 @@ def test_globus_compute_runner_init(): sim_specs["globus_compute_endpoint"] = "1234" with mock.patch("globus_compute_sdk.Executor"): - runner = Runner(sim_specs) + runner = Runner.from_specs(sim_specs) assert hasattr( runner, "globus_compute_executor" @@ -75,7 +75,7 @@ def test_globus_compute_runner_pass(): sim_specs["globus_compute_endpoint"] = "1234" with mock.patch("globus_compute_sdk.Executor"): - runner = Runner(sim_specs) + runner = Runner.from_specs(sim_specs) # Creating Mock Globus ComputeExecutor and Globus Compute future object - no exception globus_compute_mock = mock.Mock() @@ -101,7 +101,7 @@ def test_globus_compute_runner_fail(): gen_specs["globus_compute_endpoint"] = "4321" with mock.patch("globus_compute_sdk.Executor"): - runner = Runner(gen_specs) + runner = Runner.from_specs(gen_specs) # Creating Mock Globus ComputeExecutor and Globus Compute future object - yes exception globus_compute_mock = mock.Mock() From 06995d049cce09b451b04754cb4b13c1a60ced64 Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 29 Jul 2024 15:30:12 -0500 Subject: [PATCH 161/288] type/bug fixes --- libensemble/generators.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 403b08f67..d6301c87e 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -162,7 +162,7 @@ def ask_updates(self) -> npt.NDArray: """Request any updates to previous points, e.g. minima discovered, points to cancel.""" return self.ask_np() - def tell_np(self, results: List[dict], tag: int = EVAL_GEN_TAG) -> None: + def tell_np(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: """Send the results of evaluations to the generator, as a NumPy array.""" if results is not None: results = self._set_sim_ended(results) @@ -173,9 +173,9 @@ def tell_np(self, results: List[dict], tag: int = EVAL_GEN_TAG) -> None: self.inbox.put((tag, None)) self.inbox.put((0, np.copy(results))) - def final_tell(self, results: List[dict]) -> (npt.NDArray, dict, int): + def final_tell(self, results: npt.NDArray) -> (npt.NDArray, dict, int): """Send any last results to the generator, and it to close down.""" - self.tell(results, PERSIS_STOP) # conversion happens in tell + self.tell_np(results, PERSIS_STOP) # conversion happens in tell return self.thread.result() From 9e6c63c15bcef1f9b47abc0ca9d4077de2904b57 Mon Sep 17 00:00:00 2001 From: Jeffrey Larson Date: Thu, 1 Aug 2024 11:12:35 -0500 Subject: [PATCH 162/288] isort --- libensemble/ensemble.py | 2 +- libensemble/gen_funcs/persistent_aposmm.py | 4 ++-- libensemble/sim_funcs/simple_sim.py | 1 + libensemble/tests/functionality_tests/test_mpi_warning.py | 7 +++---- libensemble/tests/regression_tests/test_gpCAM_class.py | 3 +-- 5 files changed, 8 insertions(+), 9 deletions(-) diff --git a/libensemble/ensemble.py b/libensemble/ensemble.py index 11faa3298..31549d5b5 100644 --- a/libensemble/ensemble.py +++ b/libensemble/ensemble.py @@ -12,8 +12,8 @@ from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, LibeSpecs, SimSpecs from libensemble.tools import add_unique_random_streams from libensemble.tools import parse_args as parse_args_f -from libensemble.tools.parse_args import mpi_init from libensemble.tools import save_libE_output +from libensemble.tools.parse_args import mpi_init from libensemble.utils.misc import specs_dump ATTR_ERR_MSG = 'Unable to load "{}". Is the function or submodule correctly named?' diff --git a/libensemble/gen_funcs/persistent_aposmm.py b/libensemble/gen_funcs/persistent_aposmm.py index 1a15b1676..c5c3aa5e6 100644 --- a/libensemble/gen_funcs/persistent_aposmm.py +++ b/libensemble/gen_funcs/persistent_aposmm.py @@ -14,12 +14,12 @@ import numpy as np from mpmath import gamma -# from scipy.spatial.distance import cdist - from libensemble.gen_funcs.aposmm_localopt_support import ConvergedMsg, LocalOptInterfacer, simulate_recv_from_manager from libensemble.message_numbers import EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG from libensemble.tools.persistent_support import PersistentSupport +# from scipy.spatial.distance import cdist + # Due to recursion error in scipy cdist function def cdist(XA, XB, metric="euclidean"): diff --git a/libensemble/sim_funcs/simple_sim.py b/libensemble/sim_funcs/simple_sim.py index 5bd91bb49..74e193283 100644 --- a/libensemble/sim_funcs/simple_sim.py +++ b/libensemble/sim_funcs/simple_sim.py @@ -5,6 +5,7 @@ __all__ = ["norm_eval"] import numpy as np + from libensemble.specs import input_fields, output_data diff --git a/libensemble/tests/functionality_tests/test_mpi_warning.py b/libensemble/tests/functionality_tests/test_mpi_warning.py index 325fa291e..daf6125b6 100644 --- a/libensemble/tests/functionality_tests/test_mpi_warning.py +++ b/libensemble/tests/functionality_tests/test_mpi_warning.py @@ -11,19 +11,18 @@ # TESTSUITE_COMMS: mpi # TESTSUITE_NPROCS: 4 -import numpy as np import os import time -from libensemble import Ensemble +import numpy as np + +from libensemble import Ensemble, logger from libensemble.gen_funcs.sampling import latin_hypercube_sample as gen_f # Import libEnsemble items for this test from libensemble.sim_funcs.simple_sim import norm_eval as sim_f from libensemble.specs import ExitCriteria, GenSpecs, SimSpecs -from libensemble import logger - # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": log_file = "ensemble_check_warning.log" diff --git a/libensemble/tests/regression_tests/test_gpCAM_class.py b/libensemble/tests/regression_tests/test_gpCAM_class.py index a2a63bef5..45ac49aa1 100644 --- a/libensemble/tests/regression_tests/test_gpCAM_class.py +++ b/libensemble/tests/regression_tests/test_gpCAM_class.py @@ -23,9 +23,8 @@ import numpy as np from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f - +from libensemble.gen_classes.gpCAM import GP_CAM, GP_CAM_Covar from libensemble.gen_funcs.persistent_gen_wrapper import persistent_gen_f as gen_f -from libensemble.gen_classes.gpCAM import GP_CAM_Covar, GP_CAM # Import libEnsemble items for this test from libensemble.libE import libE From a69838525703452d73e20a39c037ffc4d1563000 Mon Sep 17 00:00:00 2001 From: Jeffrey Larson Date: Thu, 1 Aug 2024 11:25:36 -0500 Subject: [PATCH 163/288] black --- libensemble/libE.py | 1 + libensemble/tools/alloc_support.py | 6 +----- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/libensemble/libE.py b/libensemble/libE.py index bfa2da574..ec97ba9ed 100644 --- a/libensemble/libE.py +++ b/libensemble/libE.py @@ -440,6 +440,7 @@ def libE_mpi_worker(libE_comm, sim_specs, gen_specs, libE_specs): # ==================== Local version =============================== + def start_proc_team(nworkers, sim_specs, gen_specs, libE_specs, log_comm=True): """Launch a process worker team.""" resources = Resources.resources diff --git a/libensemble/tools/alloc_support.py b/libensemble/tools/alloc_support.py index fed947885..9e2fb5d8c 100644 --- a/libensemble/tools/alloc_support.py +++ b/libensemble/tools/alloc_support.py @@ -280,11 +280,7 @@ def gen_work(self, wid, H_fields, H_rows, persis_info, **libE_info): H_fields = AllocSupport._check_H_fields(H_fields) libE_info["H_rows"] = AllocSupport._check_H_rows(H_rows) - libE_info["batch_size"] = len( - self.avail_worker_ids( - gen_workers=False, - ) - ) + libE_info["batch_size"] = len(self.avail_worker_ids(gen_workers=False)) work = { "H_fields": H_fields, From 9da1ab8ad4c4fb2d5fa3674485499a0482718d7f Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 1 Aug 2024 15:26:55 -0500 Subject: [PATCH 164/288] ask_np / tell_np to ask_numpy / tell_numpy --- libensemble/gen_classes/gpCAM.py | 8 +++---- libensemble/gen_classes/sampling.py | 4 ++-- libensemble/generators.py | 37 ++++++++++++++++------------- libensemble/utils/runners.py | 16 ++++++------- 4 files changed, 35 insertions(+), 30 deletions(-) diff --git a/libensemble/gen_classes/gpCAM.py b/libensemble/gen_classes/gpCAM.py index a0b273e52..7828cf8d8 100644 --- a/libensemble/gen_classes/gpCAM.py +++ b/libensemble/gen_classes/gpCAM.py @@ -62,7 +62,7 @@ def __init__(self, H, persis_info, gen_specs, libE_info=None): self.my_gp = None self.noise = 1e-8 # 1e-12 - def ask_np(self, n_trials: int) -> npt.NDArray: + def ask_numpy(self, n_trials: int) -> npt.NDArray: if self.all_x.shape[0] == 0: self.x_new = self.persis_info["rand_stream"].uniform(self.lb, self.ub, (n_trials, self.n)) else: @@ -78,7 +78,7 @@ def ask_np(self, n_trials: int) -> npt.NDArray: H_o["x"] = self.x_new return H_o - def tell_np(self, calc_in: npt.NDArray) -> None: + def tell_numpy(self, calc_in: npt.NDArray) -> None: if calc_in is not None: 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)] @@ -114,7 +114,7 @@ def __init__(self, H, persis_info, gen_specs, libE_info=None): 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 ask_np(self, n_trials: int) -> List[dict]: + def ask_numpy(self, n_trials: int) -> List[dict]: if self.all_x.shape[0] == 0: x_new = self.persis_info["rand_stream"].uniform(self.lb, self.ub, (n_trials, self.n)) else: @@ -138,7 +138,7 @@ def ask_np(self, n_trials: int) -> List[dict]: H_o["x"] = self.x_new return H_o - def tell_np(self, calc_in: npt.NDArray): + def tell_numpy(self, calc_in: npt.NDArray): if calc_in is not None: super().tell(calc_in) if not self.U.get("use_grid"): diff --git a/libensemble/gen_classes/sampling.py b/libensemble/gen_classes/sampling.py index 5c4d2c2f4..3753d1fbb 100644 --- a/libensemble/gen_classes/sampling.py +++ b/libensemble/gen_classes/sampling.py @@ -24,7 +24,7 @@ def __init__(self, _, persis_info, gen_specs, libE_info=None) -> list: self.libE_info = libE_info self._get_user_params(self.gen_specs["user"]) - def ask_np(self, n_trials): + 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)) @@ -34,7 +34,7 @@ def ask_np(self, n_trials): ) return H_o - def tell_np(self, calc_in): + def tell_numpy(self, calc_in): pass # random sample so nothing to tell def _get_user_params(self, user_specs): diff --git a/libensemble/generators.py b/libensemble/generators.py index d6301c87e..861baefbe 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -12,14 +12,20 @@ from libensemble.tools import add_unique_random_streams from libensemble.utils.misc import list_dicts_to_np, np_to_list_dicts +""" +NOTE: 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/generator_standard +""" + class Generator(ABC): """ - v 0.7.2.24 .. code-block:: python - from libensemble import Ensemble from libensemble.generators import Generator @@ -40,7 +46,6 @@ def final_tell(self, results): my_generator = MyGenerator(my_parameter=100) - my_ensemble = Ensemble(generator=my_generator) """ @abstractmethod @@ -86,20 +91,20 @@ class LibensembleGenerator(Generator): """ @abstractmethod - def ask_np(self, num_points: Optional[int] = 0) -> npt.NDArray: + def ask_numpy(self, num_points: Optional[int] = 0) -> npt.NDArray: pass @abstractmethod - def tell_np(self, results: npt.NDArray) -> None: + def tell_numpy(self, results: npt.NDArray) -> None: pass def ask(self, num_points: Optional[int] = 0) -> List[dict]: """Request the next set of points to evaluate.""" - return np_to_list_dicts(self.ask_np(num_points)) + return np_to_list_dicts(self.ask_numpy(num_points)) def tell(self, calc_in: List[dict]) -> None: """Send the results of evaluations to the generator.""" - self.tell_np(list_dicts_to_np(calc_in)) + self.tell_numpy(list_dicts_to_np(calc_in)) class LibensembleGenThreadInterfacer(LibensembleGenerator): @@ -149,9 +154,9 @@ def _set_sim_ended(self, results: npt.NDArray) -> npt.NDArray: def tell(self, calc_in: List[dict], tag: int = EVAL_GEN_TAG) -> None: """Send the results of evaluations to the generator.""" - self.tell_np(list_dicts_to_np(calc_in), tag) + self.tell_numpy(list_dicts_to_np(calc_in), tag) - def ask_np(self, n_trials: int = 0) -> npt.NDArray: + def ask_numpy(self, n_trials: int = 0) -> npt.NDArray: """Request the next set of points to evaluate, as a NumPy array.""" if not self.thread.running: self.thread.run() @@ -160,9 +165,9 @@ def ask_np(self, n_trials: int = 0) -> npt.NDArray: def ask_updates(self) -> npt.NDArray: """Request any updates to previous points, e.g. minima discovered, points to cancel.""" - return self.ask_np() + return self.ask_numpy() - def tell_np(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: + def tell_numpy(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: """Send the results of evaluations to the generator, as a NumPy array.""" if results is not None: results = self._set_sim_ended(results) @@ -175,7 +180,7 @@ def tell_np(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: def final_tell(self, results: npt.NDArray) -> (npt.NDArray, dict, int): """Send any last results to the generator, and it to close down.""" - self.tell_np(results, PERSIS_STOP) # conversion happens in tell + self.tell_numpy(results, PERSIS_STOP) # conversion happens in tell return self.thread.result() @@ -210,13 +215,13 @@ def __init__( self.results_idx = 0 self.last_ask = None - def ask_np(self, n_trials: int = 0) -> npt.NDArray: + def ask_numpy(self, n_trials: int = 0) -> npt.NDArray: """Request the next set of points to evaluate, as a NumPy array.""" if (self.last_ask is None) or ( self.results_idx >= len(self.last_ask) ): # haven't been asked yet, or all previously enqueued points have been "asked" self.results_idx = 0 - self.last_ask = super().ask_np(n_trials) + self.last_ask = super().ask_numpy(n_trials) if self.last_ask[ "local_min" ].any(): # filter out local minima rows, but they're cached in self.all_local_minima @@ -267,9 +272,9 @@ def ready_to_be_asked(self) -> bool: """Check if the generator has the next batch of points ready.""" return not self.outbox.empty() - def ask_np(self, *args) -> npt.NDArray: + def ask_numpy(self, *args) -> npt.NDArray: """Request the next set of points to evaluate, as a NumPy array.""" - output = super().ask_np() + output = super().ask_numpy() if "cancel_requested" in output.dtype.names: cancels = output got_cancels_first = True diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 6433a39e3..1858a6058 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -164,43 +164,43 @@ def _result(self, calc_in: npt.NDArray, persis_info: dict, libE_info: dict) -> ( class LibensembleGenRunner(AskTellGenRunner): def _get_initial_ask(self, libE_info) -> npt.NDArray: """Get initial batch from generator based on generator type""" - H_out = self.gen.ask_np(self.initial_batch or libE_info["batch_size"]) + H_out = self.gen.ask_numpy(self.initial_batch or libE_info["batch_size"]) return H_out def _start_generator_loop(self, tag, Work, H_in) -> npt.NDArray: """Start the generator loop after choosing best way of giving initial results to gen""" - self.gen.tell_np(H_in) + self.gen.tell_numpy(H_in) return self._loop_over_libe_asktell_gen(tag, Work) def _loop_over_libe_asktell_gen(self, tag, Work) -> npt.NDArray: """Interact with ask/tell generator that *does not* contain a background thread""" while tag not in [PERSIS_STOP, STOP_TAG]: batch_size = self.batch or Work["libE_info"]["batch_size"] - points, updates = self.gen.ask_np(batch_size), self.gen.ask_updates() + points, updates = self.gen.ask_numpy(batch_size), self.gen.ask_updates() if updates is not None and len(updates): # returned "samples" and "updates". can combine if same dtype H_out = np.append(points, updates) else: H_out = points tag, Work, H_in = self.ps.send_recv(H_out) - self.gen.tell_np(H_in) + self.gen.tell_numpy(H_in) return H_in class LibensembleGenThreadRunner(AskTellGenRunner): def _get_initial_ask(self, libE_info) -> npt.NDArray: """Get initial batch from generator based on generator type""" - H_out = self.gen.ask_np() # libE really needs to receive the *entire* initial batch from a threaded gen + H_out = self.gen.ask_numpy() # libE really needs to receive the *entire* initial batch from a threaded gen return H_out def _start_generator_loop(self, _, _2, H_in): """Start the generator loop after choosing best way of giving initial results to gen""" - self.gen.tell_np(H_in) + self.gen.tell_numpy(H_in) return self._loop_over_thread_interfacer() def _ask_and_send(self): """Loop over generator's outbox contents, send to manager""" while self.gen.outbox.qsize(): # recv/send any outstanding messages - points, updates = self.gen.ask_np(), self.gen.ask_updates() + points, updates = self.gen.ask_numpy(), self.gen.ask_updates() if updates is not None and len(updates): self.ps.send(points) for i in updates: @@ -217,4 +217,4 @@ def _loop_over_thread_interfacer(self): tag, _, H_in = self.ps.recv() if tag in [STOP_TAG, PERSIS_STOP]: return H_in # this will get inserted into final_tell. this breaks loop - self.gen.tell_np(H_in) + self.gen.tell_numpy(H_in) From c2c7feb2204f960351838e75e56d2c6d2b5a7348 Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 1 Aug 2024 15:49:37 -0500 Subject: [PATCH 165/288] an attempt at some anti-redundancy in runners.py :) --- libensemble/utils/runners.py | 41 +++++++++++++++--------------------- 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 1858a6058..de14883ff 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -118,17 +118,23 @@ def _to_array(self, x: list) -> npt.NDArray: return arr return x + def _get_points_updates(self, batch_size: int) -> (npt.NDArray, npt.NDArray): + return self._to_array(self.gen.ask(batch_size)), self._to_array(self.gen.ask_updates()) + + def _convert_tell(self, x: npt.NDArray) -> list: + self.gen.tell(np_to_list_dicts(x)) + def _loop_over_gen(self, tag, Work): """Interact with ask/tell generator that *does not* contain a background thread""" while tag not in [PERSIS_STOP, STOP_TAG]: batch_size = self.batch or Work["libE_info"]["batch_size"] - points, updates = self._to_array(self.gen.ask(batch_size)), self._to_array(self.gen.ask_updates()) + points, updates = self._get_points_updates(batch_size) if updates is not None and len(updates): # returned "samples" and "updates". can combine if same dtype H_out = np.append(points, updates) else: H_out = points tag, Work, H_in = self.ps.send_recv(H_out) - self.gen.tell(np_to_list_dicts(H_in)) + self._convert_tell(H_in) return H_in def _get_initial_ask(self, libE_info) -> npt.NDArray: @@ -167,35 +173,22 @@ def _get_initial_ask(self, libE_info) -> npt.NDArray: H_out = self.gen.ask_numpy(self.initial_batch or libE_info["batch_size"]) return H_out + def _get_points_updates(self, batch_size: int) -> (npt.NDArray, npt.NDArray): + return self.gen.ask_numpy(batch_size), self.gen.ask_updates() + + def _convert_tell(self, x: npt.NDArray) -> list: + self.gen.tell_numpy(x) + def _start_generator_loop(self, tag, Work, H_in) -> npt.NDArray: """Start the generator loop after choosing best way of giving initial results to gen""" self.gen.tell_numpy(H_in) - return self._loop_over_libe_asktell_gen(tag, Work) - - def _loop_over_libe_asktell_gen(self, tag, Work) -> npt.NDArray: - """Interact with ask/tell generator that *does not* contain a background thread""" - while tag not in [PERSIS_STOP, STOP_TAG]: - batch_size = self.batch or Work["libE_info"]["batch_size"] - points, updates = self.gen.ask_numpy(batch_size), self.gen.ask_updates() - if updates is not None and len(updates): # returned "samples" and "updates". can combine if same dtype - H_out = np.append(points, updates) - else: - H_out = points - tag, Work, H_in = self.ps.send_recv(H_out) - self.gen.tell_numpy(H_in) - return H_in + return self._loop_over_gen(tag, Work) class LibensembleGenThreadRunner(AskTellGenRunner): def _get_initial_ask(self, libE_info) -> npt.NDArray: """Get initial batch from generator based on generator type""" - H_out = self.gen.ask_numpy() # libE really needs to receive the *entire* initial batch from a threaded gen - return H_out - - def _start_generator_loop(self, _, _2, H_in): - """Start the generator loop after choosing best way of giving initial results to gen""" - self.gen.tell_numpy(H_in) - return self._loop_over_thread_interfacer() + return self.gen.ask_numpy() # libE really needs to receive the *entire* initial batch from a threaded gen def _ask_and_send(self): """Loop over generator's outbox contents, send to manager""" @@ -208,7 +201,7 @@ def _ask_and_send(self): else: self.ps.send(points) - def _loop_over_thread_interfacer(self): + def _loop_over_gen(self, _, _2): """Cycle between moving all outbound / inbound messages between threaded gen and manager""" while True: time.sleep(0.0025) # dont need to ping the gen relentlessly. Let it calculate. 400hz From 9691602493c2aa02f6cb880a76c8960b82087b6b Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 1 Aug 2024 16:16:47 -0500 Subject: [PATCH 166/288] small adjustments from PR, plus first doc-page for ask/tell generators base-class --- docs/function_guides/ask_tell_generator.rst | 21 ++++++++++++++++ docs/function_guides/function_guide_index.rst | 1 + libensemble/gen_classes/gpCAM.py | 24 +++++++++---------- libensemble/gen_classes/sampling.py | 1 - libensemble/generators.py | 6 +++-- 5 files changed, 38 insertions(+), 15 deletions(-) create mode 100644 docs/function_guides/ask_tell_generator.rst diff --git a/docs/function_guides/ask_tell_generator.rst b/docs/function_guides/ask_tell_generator.rst new file mode 100644 index 000000000..6212b24f5 --- /dev/null +++ b/docs/function_guides/ask_tell_generator.rst @@ -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/generator_standard + +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: diff --git a/docs/function_guides/function_guide_index.rst b/docs/function_guides/function_guide_index.rst index 621bf36d2..0539e24c6 100644 --- a/docs/function_guides/function_guide_index.rst +++ b/docs/function_guides/function_guide_index.rst @@ -13,6 +13,7 @@ These guides describe common development patterns and optional components: :caption: Writing User Functions generator + ask_tell_generator simulator allocator sim_gen_alloc_api diff --git a/libensemble/gen_classes/gpCAM.py b/libensemble/gen_classes/gpCAM.py index 7828cf8d8..3be070d07 100644 --- a/libensemble/gen_classes/gpCAM.py +++ b/libensemble/gen_classes/gpCAM.py @@ -38,7 +38,18 @@ class GP_CAM(LibensembleGenerator): (relative to the simulation evaluation time) for some use cases. """ - def _initialize_gpcAM(self, user_specs): + def __init__(self, H, persis_info, gen_specs, libE_info=None): + self.H = H + self.persis_info = persis_info + self.gen_specs = gen_specs + self.libE_info = libE_info + + self.U = self.gen_specs["user"] + self._initialize_gpcAM(self.U) + self.my_gp = None + self.noise = 1e-8 # 1e-12 + + def _initialize_gpCAM(self, user_specs): """Extract user params""" # self.b = user_specs["batch_size"] self.lb = np.array(user_specs["lb"]) @@ -51,17 +62,6 @@ def _initialize_gpcAM(self, user_specs): self.all_y = np.empty((0, 1)) np.random.seed(0) - def __init__(self, H, persis_info, gen_specs, libE_info=None): - self.H = H - self.persis_info = persis_info - self.gen_specs = gen_specs - self.libE_info = libE_info - - self.U = self.gen_specs["user"] - self._initialize_gpcAM(self.U) - self.my_gp = None - self.noise = 1e-8 # 1e-12 - def ask_numpy(self, n_trials: int) -> npt.NDArray: if self.all_x.shape[0] == 0: self.x_new = self.persis_info["rand_stream"].uniform(self.lb, self.ub, (n_trials, self.n)) diff --git a/libensemble/gen_classes/sampling.py b/libensemble/gen_classes/sampling.py index 3753d1fbb..fb7c23c8c 100644 --- a/libensemble/gen_classes/sampling.py +++ b/libensemble/gen_classes/sampling.py @@ -18,7 +18,6 @@ class UniformSample(LibensembleGenerator): """ def __init__(self, _, persis_info, gen_specs, libE_info=None) -> list: - # self.H = H self.persis_info = persis_info self.gen_specs = gen_specs self.libE_info = libE_info diff --git a/libensemble/generators.py b/libensemble/generators.py index 861baefbe..70d285344 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -26,6 +26,7 @@ class Generator(ABC): .. code-block:: python + from libensemble.specs import GenSpecs from libensemble.generators import Generator @@ -46,6 +47,7 @@ def final_tell(self, results): my_generator = MyGenerator(my_parameter=100) + gen_specs = GenSpecs(generator=my_generator, ...) """ @abstractmethod @@ -60,7 +62,7 @@ def __init__(self, *args, **kwargs): """ @abstractmethod - def ask(self, num_points: Optional[int], *args, **kwargs) -> List[dict]: + def ask(self, num_points: Optional[int]) -> List[dict]: """ Request the next set of points to evaluate. """ @@ -70,7 +72,7 @@ def ask_updates(self) -> npt.NDArray: Request any updates to previous points, e.g. minima discovered, points to cancel. """ - def tell(self, results: List[dict], *args, **kwargs) -> None: + def tell(self, results: List[dict]) -> None: """ Send the results of evaluations to the generator. """ From d5eaddb28ed86dc3af731c0be9763d6f468acf26 Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 5 Aug 2024 13:32:38 -0500 Subject: [PATCH 167/288] simplifications from codeclimate --- libensemble/generators.py | 2 +- libensemble/utils/misc.py | 32 +++++++++++++++++++++----------- libensemble/utils/runners.py | 8 ++++---- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 70d285344..e78e8114f 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -115,7 +115,7 @@ class LibensembleGenThreadInterfacer(LibensembleGenerator): """ def __init__( - self, gen_specs: dict, History: npt.NDArray = [], persis_info: dict = {}, libE_info: dict = {}, **kwargs + self, gen_specs: dict, History: npt.NDArray = [], persis_info: dict = {}, libE_info: dict = {} ) -> None: self.gen_f = gen_specs["gen_f"] self.gen_specs = gen_specs diff --git a/libensemble/utils/misc.py b/libensemble/utils/misc.py index 79208b7cf..db73ccf91 100644 --- a/libensemble/utils/misc.py +++ b/libensemble/utils/misc.py @@ -81,23 +81,33 @@ def specs_checker_setattr(obj, key, value): obj.__dict__[key] = value +def _copy_data(array, list_dicts): + for i, entry in enumerate(list_dicts): + for field in entry.keys(): + array[field][i] = entry[field] + return array + + +def _decide_dtype(name, entry): + if hasattr(entry, "shape") and len(entry.shape): # numpy type + return (name, entry.dtype, entry.shape) + else: + return (name, type(entry)) + + def list_dicts_to_np(list_dicts: list) -> npt.NDArray: if list_dicts is None: return None + + first = list_dicts[0] + new_dtype_names = [i for i in first.keys()] new_dtype = [] - new_dtype_names = [i for i in list_dicts[0].keys()] - for i, entry in enumerate(list_dicts[0].values()): # must inspect values to get presumptive types - if hasattr(entry, "shape") and len(entry.shape): - entry_dtype = (new_dtype_names[i], entry.dtype, entry.shape) - else: - entry_dtype = (new_dtype_names[i], type(entry)) - new_dtype.append(entry_dtype) + for i, entry in enumerate(first.values()): # must inspect values to get presumptive types + name = new_dtype_names[i] + new_dtype.append(_decide_dtype(name, entry)) out = np.zeros(len(list_dicts), dtype=new_dtype) - for i, entry in enumerate(list_dicts): - for field in entry.keys(): - out[field][i] = entry[field] - return out + return _copy_data(out, list_dicts) def np_to_list_dicts(array: npt.NDArray) -> List[dict]: diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index de14883ff..6d3fdef92 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -20,12 +20,12 @@ class Runner: def from_specs(cls, specs): if len(specs.get("globus_compute_endpoint", "")) > 0: return GlobusComputeRunner(specs) - if specs.get("threaded"): # TODO: undecided interface + if specs.get("threaded"): return ThreadRunner(specs) - if specs.get("generator") is not None: - if isinstance(specs.get("generator", None), LibensembleGenThreadInterfacer): + if (generator := specs.get("generator")) is not None: + if isinstance(generator, LibensembleGenThreadInterfacer): return LibensembleGenThreadRunner(specs) - if isinstance(specs.get("generator", None), LibensembleGenerator): + if isinstance(generator, LibensembleGenerator): return LibensembleGenRunner(specs) else: return AskTellGenRunner(specs) From 7bad6aaf8cad6798e7e3ea375ec4aafdd6835cee Mon Sep 17 00:00:00 2001 From: shudson Date: Mon, 5 Aug 2024 17:23:15 -0500 Subject: [PATCH 168/288] Update gpCAM class gen --- libensemble/gen_classes/gpCAM.py | 36 ++++++++++--------- .../tests/regression_tests/test_gpCAM.py | 3 +- .../regression_tests/test_gpCAM_class.py | 5 +++ 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/libensemble/gen_classes/gpCAM.py b/libensemble/gen_classes/gpCAM.py index 3be070d07..3f9ae915a 100644 --- a/libensemble/gen_classes/gpCAM.py +++ b/libensemble/gen_classes/gpCAM.py @@ -38,17 +38,6 @@ class GP_CAM(LibensembleGenerator): (relative to the simulation evaluation time) for some use cases. """ - def __init__(self, H, persis_info, gen_specs, libE_info=None): - self.H = H - self.persis_info = persis_info - self.gen_specs = gen_specs - self.libE_info = libE_info - - self.U = self.gen_specs["user"] - self._initialize_gpcAM(self.U) - self.my_gp = None - self.noise = 1e-8 # 1e-12 - def _initialize_gpCAM(self, user_specs): """Extract user params""" # self.b = user_specs["batch_size"] @@ -62,16 +51,30 @@ def _initialize_gpCAM(self, user_specs): self.all_y = np.empty((0, 1)) np.random.seed(0) + def __init__(self, H, persis_info, gen_specs, libE_info=None): + self.H = H # Currently not used - could be used for an H0 + self.persis_info = persis_info + self.gen_specs = gen_specs + self.libE_info = libE_info + + self.U = self.gen_specs["user"] + self._initialize_gpCAM(self.U) + + self.my_gp = None + self.noise = 1e-8 # 1e-12 + self.ask_max_iter = self.gen_specs["user"].get("ask_max_iter") or 10 + def ask_numpy(self, n_trials: int) -> npt.NDArray: if self.all_x.shape[0] == 0: self.x_new = self.persis_info["rand_stream"].uniform(self.lb, self.ub, (n_trials, self.n)) else: start = time.time() self.x_new = self.my_gp.ask( - bounds=np.column_stack((self.lb, self.ub)), + input_set=np.column_stack((self.lb, self.ub)), n=n_trials, pop_size=n_trials, - max_iter=1, + 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.gen_specs["out"]) @@ -88,10 +91,11 @@ def tell_numpy(self, calc_in: npt.NDArray) -> None: 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, noise_variances=self.noise * np.ones(len(self.all_y))) + 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, noise_variances=self.noise * np.ones(len(self.all_y))) + self.my_gp.tell(self.all_x, self.all_y.flatten(), noise_variances=noise_var) self.my_gp.train() @@ -140,7 +144,7 @@ def ask_numpy(self, n_trials: int) -> List[dict]: def tell_numpy(self, calc_in: npt.NDArray): if calc_in is not None: - super().tell(calc_in) + super().tell_numpy(calc_in) if not self.U.get("use_grid"): n_trials = len(self.y_new) self.x_for_var = self.persis_info["rand_stream"].uniform(self.lb, self.ub, (10 * n_trials, self.n)) diff --git a/libensemble/tests/regression_tests/test_gpCAM.py b/libensemble/tests/regression_tests/test_gpCAM.py index e1bc1e404..9e87211e8 100644 --- a/libensemble/tests/regression_tests/test_gpCAM.py +++ b/libensemble/tests/regression_tests/test_gpCAM.py @@ -34,11 +34,10 @@ from libensemble.sim_funcs.rosenbrock import rosenbrock_eval as sim_f from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output -# Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). - warnings.filterwarnings("ignore", message="Default hyperparameter_bounds") +# Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": nworkers, is_manager, libE_specs, _ = parse_args() diff --git a/libensemble/tests/regression_tests/test_gpCAM_class.py b/libensemble/tests/regression_tests/test_gpCAM_class.py index 45ac49aa1..1a609d525 100644 --- a/libensemble/tests/regression_tests/test_gpCAM_class.py +++ b/libensemble/tests/regression_tests/test_gpCAM_class.py @@ -19,6 +19,7 @@ # TESTSUITE_EXTRA: true import sys +import warnings import numpy as np @@ -31,6 +32,9 @@ from libensemble.sim_funcs.rosenbrock import rosenbrock_eval as sim_f from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +warnings.filterwarnings("ignore", message="Default hyperparameter_bounds") + + # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": nworkers, is_manager, libE_specs, _ = parse_args() @@ -78,6 +82,7 @@ elif inst == 2: gen_specs["user"]["generator"] = GP_CAM num_batches = 3 # Few because the ask_tell gen can be slow + gen_specs["user"]["ask_max_iter"] = 1 # For quicker test exit_criteria = {"sim_max": num_batches * batch_size, "wallclock_max": 300} persis_info = add_unique_random_streams({}, nworkers + 1) From bd996e2e008edca58c343bd82c500f15167552d2 Mon Sep 17 00:00:00 2001 From: jlnav Date: Tue, 6 Aug 2024 14:48:48 -0500 Subject: [PATCH 169/288] docs fix, plus refactor aposmm_nlopt_asktell reg test to use kwargs'd parameterization of aposmm, plus specs/ensemble objects --- libensemble/generators.py | 8 +- .../test_persistent_aposmm_nlopt_asktell.py | 90 +++++++++---------- 2 files changed, 47 insertions(+), 51 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index e78e8114f..5005e5eb6 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -89,7 +89,7 @@ def final_tell(self, results: List[dict], *args, **kwargs) -> Optional[npt.NDArr class LibensembleGenerator(Generator): """Internal implementation of Generator interface for use with libEnsemble, or for those who prefer numpy arrays. ``ask/tell`` methods communicate lists of dictionaries, like the standard. - ``ask_np/tell_np`` methods communicate numpy arrays containing the same data. + ``ask_numpy/tell_numpy`` methods communicate numpy arrays containing the same data. """ @abstractmethod @@ -197,9 +197,9 @@ def __init__( from libensemble.gen_funcs.persistent_aposmm import aposmm gen_specs["gen_f"] = aposmm - if len(kwargs) > 0: + if len(kwargs) > 0: # so user can specify aposmm-specific parameters as kwargs to constructor gen_specs["user"] = kwargs - if not gen_specs.get("out"): + 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"]) gen_specs["out"] = [ ("x", float, n), @@ -208,7 +208,7 @@ def __init__( ("local_min", bool), ("local_pt", bool), ] - gen_specs["in"] = ["x", "f", "local_pt", "sim_id", "sim_ended", "x_on_cube", "local_min"] + 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({}, 4, seed=4321)[1] persis_info["nworkers"] = 4 diff --git a/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py b/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py index 74f24ec5d..22fcc62e2 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py @@ -23,72 +23,68 @@ import libensemble.gen_funcs # Import libEnsemble items for this test -from libensemble.libE import libE from libensemble.sim_funcs.six_hump_camel import six_hump_camel as sim_f libensemble.gen_funcs.rc.aposmm_optimizers = "nlopt" from time import time +from libensemble import Ensemble from libensemble.alloc_funcs.persistent_aposmm_alloc import persistent_aposmm_alloc as alloc_f from libensemble.generators import APOSMM +from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, SimSpecs from libensemble.tests.regression_tests.support import six_hump_camel_minima as minima -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +from libensemble.tools import save_libE_output # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": - nworkers, is_manager, libE_specs, _ = parse_args() - if is_manager: + workflow = Ensemble(parse_args=True) + + if workflow.is_manager: start_time = time() - if nworkers < 2: + if workflow.nworkers < 2: sys.exit("Cannot run with a persistent worker if only one worker -- aborting...") n = 2 - sim_specs = { - "sim_f": sim_f, - "in": ["x"], - "out": [("f", float)], - } - - gen_out = [ - ("x", float, n), - ("x_on_cube", float, n), - ("sim_id", int), - ("local_min", bool), - ("local_pt", bool), - ] - - gen_specs = { - "persis_in": ["f"] + [n[0] for n in gen_out], - "out": gen_out, - "user": { - "initial_sample_size": 100, - "sample_points": np.round(minima, 1), - "localopt_method": "LN_BOBYQA", - "rk_const": 0.5 * ((gamma(1 + (n / 2)) * 5) ** (1 / n)) / sqrt(pi), - "xtol_abs": 1e-6, - "ftol_abs": 1e-6, - "dist_to_bound_multiple": 0.5, - "max_active_runs": 6, - "lb": np.array([-3, -2]), - "ub": np.array([3, 2]), - }, - } - - persis_info = add_unique_random_streams({}, nworkers + 1, seed=4321) - alloc_specs = {"alloc_f": alloc_f} - - exit_criteria = {"sim_max": 2000} - - gen_specs["generator"] = APOSMM(gen_specs, persis_info=persis_info[1]) - - libE_specs["gen_on_manager"] = True + workflow.sim_specs = SimSpecs(sim_f=sim_f, inputs=["x"], outputs=[("f", float)]) + workflow.alloc_specs = AllocSpecs(alloc_f=alloc_f) + workflow.exit_criteria = ExitCriteria(sim_max=2000) + + aposmm = APOSMM( + initial_sample_size=100, + sample_points=minima, + localopt_method="LN_BOBYQA", + rk_const=0.5 * ((gamma(1 + (n / 2)) * 5) ** (1 / n)) / sqrt(pi), + xtol_abs=1e-6, + ftol_abs=1e-6, + max_active_runs=6, + lb=np.array([-3, -2]), + ub=np.array([3, 2]), + ) + + workflow.gen_specs = GenSpecs( + persis_in=["x", "x_on_cube", "sim_id", "local_min", "local_pt", "f"], + outputs=[ + ("x", float, n), + ("x_on_cube", float, n), + ("sim_id", int), + ("local_min", bool), + ("local_pt", bool), + ("f", float), + ], + generator=aposmm, + user={"initial_sample_size": 100}, + ) + + workflow.libE_specs.gen_on_manager = True + workflow.add_random_streams() + + H, persis_info, _ = workflow.run() # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) - if is_manager: + if workflow.is_manager: print("[Manager]:", H[np.where(H["local_min"])]["x"]) print("[Manager]: Time taken =", time() - start_time, flush=True) @@ -100,4 +96,4 @@ assert np.min(np.sum((H[H["local_min"]]["x"] - m) ** 2, 1)) < tol persis_info[0]["comm"] = None - save_libE_output(H, persis_info, __file__, nworkers) + save_libE_output(H, persis_info, __file__, workflow.nworkers) From 7d0bcf8860d2e528c705f49b3472a117dc7f2d81 Mon Sep 17 00:00:00 2001 From: shudson Date: Tue, 6 Aug 2024 21:41:29 -0500 Subject: [PATCH 170/288] Trim RNG lines in gpCAM class --- libensemble/gen_classes/gpCAM.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/libensemble/gen_classes/gpCAM.py b/libensemble/gen_classes/gpCAM.py index 3f9ae915a..00e53c915 100644 --- a/libensemble/gen_classes/gpCAM.py +++ b/libensemble/gen_classes/gpCAM.py @@ -59,6 +59,7 @@ def __init__(self, H, persis_info, gen_specs, libE_info=None): self.U = self.gen_specs["user"] self._initialize_gpCAM(self.U) + self.rng = self.persis_info["rand_stream"] self.my_gp = None self.noise = 1e-8 # 1e-12 @@ -66,7 +67,7 @@ def __init__(self, H, persis_info, gen_specs, libE_info=None): def ask_numpy(self, n_trials: int) -> npt.NDArray: if self.all_x.shape[0] == 0: - self.x_new = self.persis_info["rand_stream"].uniform(self.lb, self.ub, (n_trials, self.n)) + 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( @@ -120,7 +121,7 @@ def __init__(self, H, persis_info, gen_specs, libE_info=None): def ask_numpy(self, n_trials: int) -> List[dict]: if self.all_x.shape[0] == 0: - x_new = self.persis_info["rand_stream"].uniform(self.lb, self.ub, (n_trials, self.n)) + x_new = self.rng.uniform(self.lb, self.ub, (n_trials, self.n)) else: if not self.U.get("use_grid"): x_new = self.x_for_var[np.argsort(self.var_vals)[-n_trials:]] @@ -147,7 +148,7 @@ def tell_numpy(self, calc_in: npt.NDArray): super().tell_numpy(calc_in) if not self.U.get("use_grid"): n_trials = len(self.y_new) - self.x_for_var = self.persis_info["rand_stream"].uniform(self.lb, self.ub, (10 * n_trials, self.n)) + 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 From 136c046c6bb22179198d06cbcf8a10514bdf91c0 Mon Sep 17 00:00:00 2001 From: shudson Date: Tue, 6 Aug 2024 22:14:20 -0500 Subject: [PATCH 171/288] Add and test a sampling gen in standardized interface --- libensemble/gen_classes/sampling.py | 49 ++++++++++++++++++- .../test_sampling_asktell_gen.py | 22 ++++++--- libensemble/utils/runners.py | 7 ++- 3 files changed, 66 insertions(+), 12 deletions(-) diff --git a/libensemble/gen_classes/sampling.py b/libensemble/gen_classes/sampling.py index fb7c23c8c..beaa7bf92 100644 --- a/libensemble/gen_classes/sampling.py +++ b/libensemble/gen_classes/sampling.py @@ -2,14 +2,29 @@ import numpy as np -from libensemble.generators import LibensembleGenerator +from libensemble.generators import Generator, LibensembleGenerator __all__ = [ "UniformSample", + "UniformSampleDicts", ] -class UniformSample(LibensembleGenerator): +class SampleBase(LibensembleGenerator): + """Base class for sampling generators""" + + 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" + + +class UniformSample(SampleBase): """ This generator returns ``gen_specs["initial_batch_size"]`` uniformly sampled points the first time it is called. Afterwards, it returns the @@ -36,6 +51,36 @@ def ask_numpy(self, n_trials): def tell_numpy(self, calc_in): pass # random sample so nothing to tell + +# List of dictionaries format for ask (constructor currently using numpy still) +# Mostly standard generator interface for libE generators will use the ask/tell wrappers +# to the classes above. This is for testing a function written directly with that interface. +class UniformSampleDicts(Generator): + """ + This generator returns ``gen_specs["initial_batch_size"]`` uniformly + 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. + """ + + def __init__(self, _, persis_info, gen_specs, libE_info=None) -> list: + self.persis_info = persis_info + self.gen_specs = gen_specs + self.libE_info = libE_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)} + 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"] diff --git a/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py b/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py index 07854f3e0..16ee33d4f 100644 --- a/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py +++ b/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py @@ -17,7 +17,7 @@ # Import libEnsemble items for this test from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f -from libensemble.gen_classes.sampling import UniformSample +from libensemble.gen_classes.sampling import UniformSample, UniformSampleDicts from libensemble.gen_funcs.persistent_gen_wrapper import persistent_gen_f as gen_f from libensemble.libE import libE from libensemble.tools import add_unique_random_streams, parse_args @@ -31,7 +31,7 @@ def sim_f(In): if __name__ == "__main__": nworkers, is_manager, libE_specs, _ = parse_args() - libE_specs["gen_on_manager"] = True + #libE_specs["gen_on_manager"] = True sim_specs = { "sim_f": sim_f, @@ -52,7 +52,7 @@ def sim_f(In): alloc_specs = {"alloc_f": alloc_f} exit_criteria = {"gen_max": 201} - for inst in range(3): + for inst in range(4): persis_info = add_unique_random_streams({}, nworkers + 1, seed=1234) if inst == 0: @@ -60,22 +60,28 @@ def sim_f(In): generator = UniformSample gen_specs["gen_f"] = gen_f gen_specs["user"]["generator"] = generator + if inst == 1: # Using wrapper - pass object gen_specs["gen_f"] = gen_f generator = UniformSample(None, persis_info[1], gen_specs, None) gen_specs["user"]["generator"] = generator elif inst == 2: - # use asktell runner - pass object - del gen_specs["gen_f"] + # Using asktell runner - pass object + gen_specs.pop("gen_f", None) generator = UniformSample(None, persis_info[1], gen_specs, None) gen_specs["generator"] = generator + elif inst == 3: + # Using asktell runner - pass object - with standardized interface. + gen_specs.pop("gen_f", None) + generator = UniformSampleDicts(None, persis_info[1], gen_specs, None) + gen_specs["generator"] = generator H, persis_info, flag = libE( sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs=libE_specs ) if is_manager: - assert len(H) >= 201 - print(H[:10]) - assert not np.isclose(H["f"][0], 3.23720733e02) + print(H[["sim_id", "x", "f"]][:10]) + assert len(H) >= 201, f"H has length {len(H)}" + assert np.isclose(H["f"][9], 1.96760289) diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 6d3fdef92..d0cc85c1a 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -8,10 +8,13 @@ import numpy.typing as npt from libensemble.comms.comms import QCommThread -from libensemble.generators import LibensembleGenerator, LibensembleGenThreadInterfacer, np_to_list_dicts +from libensemble.generators import LibensembleGenerator, LibensembleGenThreadInterfacer from libensemble.message_numbers import EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG from libensemble.tools.persistent_support import PersistentSupport +from libensemble.utils.misc import np_to_list_dicts + + logger = logging.getLogger(__name__) @@ -156,7 +159,7 @@ def _persistent_result(self, calc_in, persis_info, libE_info): self.gen.libE_info = libE_info if self.gen.thread is None: self.gen.setup() # maybe we're reusing a live gen from a previous run - H_out = self._get_initial_ask(libE_info) + H_out = self._to_array(self._get_initial_ask(libE_info)) tag, Work, H_in = self.ps.send_recv(H_out) # evaluate the initial sample final_H_in = self._start_generator_loop(tag, Work, H_in) return self.gen.final_tell(final_H_in), FINISHED_PERSISTENT_GEN_TAG From c930cde60e47917a0e4f05bd6fe5329d9594b5f0 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 7 Aug 2024 10:51:58 -0500 Subject: [PATCH 172/288] flake8...? --- .../tests/functionality_tests/test_sampling_asktell_gen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py b/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py index 16ee33d4f..0cb35ecb4 100644 --- a/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py +++ b/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py @@ -31,7 +31,7 @@ def sim_f(In): if __name__ == "__main__": nworkers, is_manager, libE_specs, _ = parse_args() - #libE_specs["gen_on_manager"] = True + libE_specs["gen_on_manager"] = True sim_specs = { "sim_f": sim_f, From 2697af9dbb91200b0798c27f38b90a96249320d3 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 7 Aug 2024 13:01:04 -0500 Subject: [PATCH 173/288] move aposmm/surmise asktell classes to gen_classes, a handful of parameter/variable renames based on pr suggestions --- libensemble/gen_classes/__init__.py | 4 + libensemble/gen_classes/aposmm.py | 70 ++++++++++ libensemble/gen_classes/surmise.py | 60 +++++++++ libensemble/generators.py | 126 +----------------- .../regression_tests/test_asktell_surmise.py | 2 +- .../test_persistent_aposmm_nlopt_asktell.py | 2 +- ...est_persistent_surmise_killsims_asktell.py | 2 +- .../RENAME_test_persistent_aposmm.py | 2 +- 8 files changed, 143 insertions(+), 125 deletions(-) create mode 100644 libensemble/gen_classes/__init__.py create mode 100644 libensemble/gen_classes/aposmm.py create mode 100644 libensemble/gen_classes/surmise.py diff --git a/libensemble/gen_classes/__init__.py b/libensemble/gen_classes/__init__.py new file mode 100644 index 000000000..120ca1448 --- /dev/null +++ b/libensemble/gen_classes/__init__.py @@ -0,0 +1,4 @@ +from .aposmm import APOSMM # noqa: F401 +from .gpCAM import GP_CAM, GP_CAM_Covar # noqa: F401 +from .sampling import UniformSample, UniformSampleDicts # noqa: F401 +from .surmise import Surmise # noqa: F401 diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py new file mode 100644 index 000000000..8e8fb47f0 --- /dev/null +++ b/libensemble/gen_classes/aposmm.py @@ -0,0 +1,70 @@ +import copy +from typing import List + +import numpy as np +from numpy import typing as npt + +from libensemble.generators import LibensembleGenThreadInterfacer +from libensemble.tools import add_unique_random_streams + + +class APOSMM(LibensembleGenThreadInterfacer): + """ + Standalone object-oriented APOSMM generator + """ + + def __init__( + self, gen_specs: dict = {}, History: npt.NDArray = [], persis_info: dict = {}, libE_info: dict = {}, **kwargs + ) -> None: + from libensemble.gen_funcs.persistent_aposmm import aposmm + + gen_specs["gen_f"] = aposmm + if len(kwargs) > 0: # so user can specify aposmm-specific parameters as kwargs to constructor + gen_specs["user"] = kwargs + 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"]) + gen_specs["out"] = [ + ("x", float, n), + ("x_on_cube", float, 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({}, 4, seed=4321)[1] + persis_info["nworkers"] = 4 + super().__init__(gen_specs, History, persis_info, libE_info) + self.all_local_minima = [] + self.results_idx = 0 + self.last_ask = None + + def ask_numpy(self, num_points: int = 0) -> npt.NDArray: + """Request the next set of points to evaluate, as a NumPy array.""" + if (self.last_ask is None) or ( + self.results_idx >= len(self.last_ask) + ): # haven't been asked yet, or all previously enqueued points have been "asked" + self.results_idx = 0 + self.last_ask = super().ask_numpy(num_points) + if self.last_ask[ + "local_min" + ].any(): # filter out local minima rows, but they're cached in self.all_local_minima + min_idxs = self.last_ask["local_min"] + self.all_local_minima.append(self.last_ask[min_idxs]) + self.last_ask = self.last_ask[~min_idxs] + if num_points > 0: # we've been asked for a selection of the last ask + results = np.copy( + self.last_ask[self.results_idx : self.results_idx + num_points] + ) # if resetting last_ask later, results may point to "None" + self.results_idx += num_points + return results + results = np.copy(self.last_ask) + self.results = results + self.last_ask = None + return results + + def ask_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 diff --git a/libensemble/gen_classes/surmise.py b/libensemble/gen_classes/surmise.py new file mode 100644 index 000000000..3e1810f98 --- /dev/null +++ b/libensemble/gen_classes/surmise.py @@ -0,0 +1,60 @@ +import copy +import queue as thread_queue +from typing import List + +import numpy as np +from numpy import typing as npt + +from libensemble.generators import LibensembleGenThreadInterfacer + + +class Surmise(LibensembleGenThreadInterfacer): + """ + Standalone object-oriented Surmise generator + """ + + def __init__( + self, gen_specs: dict, History: npt.NDArray = [], persis_info: dict = {}, libE_info: dict = {} + ) -> None: + from libensemble.gen_funcs.persistent_surmise_calib import surmise_calib + + gen_specs["gen_f"] = surmise_calib + if ("sim_id", int) not in gen_specs["out"]: + gen_specs["out"].append(("sim_id", int)) + super().__init__(gen_specs, History, persis_info, libE_info) + self.sim_id_index = 0 + self.all_cancels = [] + + def _add_sim_ids(self, array: npt.NDArray) -> npt.NDArray: + array["sim_id"] = np.arange(self.sim_id_index, self.sim_id_index + len(array)) + self.sim_id_index += len(array) + return array + + def ready_to_be_asked(self) -> bool: + """Check if the generator has the next batch of points ready.""" + return not self.outbox.empty() + + def ask_numpy(self, *args) -> npt.NDArray: + """Request the next set of points to evaluate, as a NumPy array.""" + output = super().ask_numpy() + if "cancel_requested" in output.dtype.names: + cancels = output + got_cancels_first = True + self.all_cancels.append(cancels) + else: + self.results = self._add_sim_ids(output) + got_cancels_first = False + try: + _, additional = self.outbox.get(timeout=0.2) # either cancels or new points + if got_cancels_first: + return additional["calc_out"] + self.all_cancels.append(additional["calc_out"]) + return self.results + except thread_queue.Empty: + return self.results + + def ask_updates(self) -> List[npt.NDArray]: + """Request a list of NumPy arrays containing points that should be cancelled by the workflow.""" + cancels = copy.deepcopy(self.all_cancels) + self.all_cancels = [] + return cancels diff --git a/libensemble/generators.py b/libensemble/generators.py index 5005e5eb6..6440b3a7a 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -1,4 +1,3 @@ -import copy import queue as thread_queue from abc import ABC, abstractmethod from typing import List, Optional @@ -9,7 +8,6 @@ from libensemble.comms.comms import QComm, QCommThread from libensemble.executors import Executor from libensemble.message_numbers import EVAL_GEN_TAG, PERSIS_STOP -from libensemble.tools import add_unique_random_streams from libensemble.utils.misc import list_dicts_to_np, np_to_list_dicts """ @@ -104,9 +102,9 @@ def ask(self, num_points: Optional[int] = 0) -> List[dict]: """Request the next set of points to evaluate.""" return np_to_list_dicts(self.ask_numpy(num_points)) - def tell(self, calc_in: List[dict]) -> None: + def tell(self, results: List[dict]) -> None: """Send the results of evaluations to the generator.""" - self.tell_numpy(list_dicts_to_np(calc_in)) + self.tell_numpy(list_dicts_to_np(results)) class LibensembleGenThreadInterfacer(LibensembleGenerator): @@ -154,11 +152,11 @@ def _set_sim_ended(self, results: npt.NDArray) -> npt.NDArray: results = new_results return results - def tell(self, calc_in: List[dict], tag: int = EVAL_GEN_TAG) -> None: + 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(calc_in), tag) + self.tell_numpy(list_dicts_to_np(results), tag) - def ask_numpy(self, n_trials: int = 0) -> npt.NDArray: + 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: self.thread.run() @@ -184,117 +182,3 @@ def final_tell(self, results: npt.NDArray) -> (npt.NDArray, dict, int): """Send any last results to the generator, and it to close down.""" self.tell_numpy(results, PERSIS_STOP) # conversion happens in tell return self.thread.result() - - -class APOSMM(LibensembleGenThreadInterfacer): - """ - Standalone object-oriented APOSMM generator - """ - - def __init__( - self, gen_specs: dict = {}, History: npt.NDArray = [], persis_info: dict = {}, libE_info: dict = {}, **kwargs - ) -> None: - from libensemble.gen_funcs.persistent_aposmm import aposmm - - gen_specs["gen_f"] = aposmm - if len(kwargs) > 0: # so user can specify aposmm-specific parameters as kwargs to constructor - gen_specs["user"] = kwargs - 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"]) - gen_specs["out"] = [ - ("x", float, n), - ("x_on_cube", float, 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({}, 4, seed=4321)[1] - persis_info["nworkers"] = 4 - super().__init__(gen_specs, History, persis_info, libE_info) - self.all_local_minima = [] - self.results_idx = 0 - self.last_ask = None - - def ask_numpy(self, n_trials: int = 0) -> npt.NDArray: - """Request the next set of points to evaluate, as a NumPy array.""" - if (self.last_ask is None) or ( - self.results_idx >= len(self.last_ask) - ): # haven't been asked yet, or all previously enqueued points have been "asked" - self.results_idx = 0 - self.last_ask = super().ask_numpy(n_trials) - if self.last_ask[ - "local_min" - ].any(): # filter out local minima rows, but they're cached in self.all_local_minima - min_idxs = self.last_ask["local_min"] - self.all_local_minima.append(self.last_ask[min_idxs]) - self.last_ask = self.last_ask[~min_idxs] - if n_trials > 0: # we've been asked for a selection of the last ask - results = np.copy( - self.last_ask[self.results_idx : self.results_idx + n_trials] - ) # if resetting last_ask later, results may point to "None" - self.results_idx += n_trials - return results - results = np.copy(self.last_ask) - self.results = results - self.last_ask = None - return results - - def ask_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 - - -class Surmise(LibensembleGenThreadInterfacer): - """ - Standalone object-oriented Surmise generator - """ - - def __init__( - self, gen_specs: dict, History: npt.NDArray = [], persis_info: dict = {}, libE_info: dict = {} - ) -> None: - from libensemble.gen_funcs.persistent_surmise_calib import surmise_calib - - gen_specs["gen_f"] = surmise_calib - if ("sim_id", int) not in gen_specs["out"]: - gen_specs["out"].append(("sim_id", int)) - super().__init__(gen_specs, History, persis_info, libE_info) - self.sim_id_index = 0 - self.all_cancels = [] - - def _add_sim_ids(self, array: npt.NDArray) -> npt.NDArray: - array["sim_id"] = np.arange(self.sim_id_index, self.sim_id_index + len(array)) - self.sim_id_index += len(array) - return array - - def ready_to_be_asked(self) -> bool: - """Check if the generator has the next batch of points ready.""" - return not self.outbox.empty() - - def ask_numpy(self, *args) -> npt.NDArray: - """Request the next set of points to evaluate, as a NumPy array.""" - output = super().ask_numpy() - if "cancel_requested" in output.dtype.names: - cancels = output - got_cancels_first = True - self.all_cancels.append(cancels) - else: - self.results = self._add_sim_ids(output) - got_cancels_first = False - try: - _, additional = self.outbox.get(timeout=0.2) # either cancels or new points - if got_cancels_first: - return additional["calc_out"] - self.all_cancels.append(additional["calc_out"]) - return self.results - except thread_queue.Empty: - return self.results - - def ask_updates(self) -> List[npt.NDArray]: - """Request a list of NumPy arrays containing points that should be cancelled by the workflow.""" - cancels = copy.deepcopy(self.all_cancels) - self.all_cancels = [] - return cancels diff --git a/libensemble/tests/regression_tests/test_asktell_surmise.py b/libensemble/tests/regression_tests/test_asktell_surmise.py index 27e633441..3c424ea8b 100644 --- a/libensemble/tests/regression_tests/test_asktell_surmise.py +++ b/libensemble/tests/regression_tests/test_asktell_surmise.py @@ -12,7 +12,7 @@ if __name__ == "__main__": from libensemble.executors import Executor - from libensemble.generators import Surmise, list_dicts_to_np + from libensemble.gen_classes import Surmise, list_dicts_to_np # Import libEnsemble items for this test from libensemble.sim_funcs.borehole_kills import borehole diff --git a/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py b/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py index 22fcc62e2..684e015ec 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py @@ -30,7 +30,7 @@ from libensemble import Ensemble from libensemble.alloc_funcs.persistent_aposmm_alloc import persistent_aposmm_alloc as alloc_f -from libensemble.generators import APOSMM +from libensemble.gen_classes import APOSMM from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, SimSpecs from libensemble.tests.regression_tests.support import six_hump_camel_minima as minima from libensemble.tools import save_libE_output diff --git a/libensemble/tests/regression_tests/test_persistent_surmise_killsims_asktell.py b/libensemble/tests/regression_tests/test_persistent_surmise_killsims_asktell.py index 8d971fe91..842573de9 100644 --- a/libensemble/tests/regression_tests/test_persistent_surmise_killsims_asktell.py +++ b/libensemble/tests/regression_tests/test_persistent_surmise_killsims_asktell.py @@ -36,7 +36,7 @@ from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f from libensemble.executors.executor import Executor -from libensemble.generators import Surmise +from libensemble.gen_classes import Surmise # Import libEnsemble items for this test from libensemble.libE import libE diff --git a/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py b/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py index fccf1c26c..878833e36 100644 --- a/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py +++ b/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py @@ -173,7 +173,7 @@ def test_asktell_with_persistent_aposmm(): from math import gamma, pi, sqrt import libensemble.gen_funcs - from libensemble.generators import APOSMM + from libensemble.gen_classes import APOSMM from libensemble.message_numbers import FINISHED_PERSISTENT_GEN_TAG from libensemble.sim_funcs.six_hump_camel import six_hump_camel_func from libensemble.tests.regression_tests.support import six_hump_camel_minima as minima From a3c09a2888ddb59c83e005fc5ee43bfc9bd2c868 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 7 Aug 2024 13:08:15 -0500 Subject: [PATCH 174/288] tiny fixes and comments --- libensemble/utils/runners.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index d0cc85c1a..976b408b4 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -11,10 +11,8 @@ from libensemble.generators import LibensembleGenerator, LibensembleGenThreadInterfacer from libensemble.message_numbers import EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG from libensemble.tools.persistent_support import PersistentSupport - from libensemble.utils.misc import np_to_list_dicts - logger = logging.getLogger(__name__) @@ -122,7 +120,10 @@ def _to_array(self, x: list) -> npt.NDArray: return x def _get_points_updates(self, batch_size: int) -> (npt.NDArray, npt.NDArray): - return self._to_array(self.gen.ask(batch_size)), self._to_array(self.gen.ask_updates()) + return ( + self._to_array(self.gen.ask(batch_size)), + None, + ) # external ask/tell gens likely don't implement ask_updates def _convert_tell(self, x: npt.NDArray) -> list: self.gen.tell(np_to_list_dicts(x)) @@ -135,7 +136,7 @@ def _loop_over_gen(self, tag, Work): if updates is not None and len(updates): # returned "samples" and "updates". can combine if same dtype H_out = np.append(points, updates) else: - H_out = points + H_out = points # all external gens likely go here tag, Work, H_in = self.ps.send_recv(H_out) self._convert_tell(H_in) return H_in @@ -185,7 +186,7 @@ def _convert_tell(self, x: npt.NDArray) -> list: def _start_generator_loop(self, tag, Work, H_in) -> npt.NDArray: """Start the generator loop after choosing best way of giving initial results to gen""" self.gen.tell_numpy(H_in) - return self._loop_over_gen(tag, Work) + return self._loop_over_gen(tag, Work) # see parent class class LibensembleGenThreadRunner(AskTellGenRunner): From 4444a7174c3e2484d610cd3fb43f2947600fa902 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 7 Aug 2024 13:18:00 -0500 Subject: [PATCH 175/288] gen batch_size and initial_batch_size aren't used. lets remove them --- libensemble/utils/runners.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 976b408b4..9cec3bd9f 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -106,8 +106,6 @@ class AskTellGenRunner(Runner): def __init__(self, specs): super().__init__(specs) self.gen = specs.get("generator") - self.initial_batch = getattr(self.gen, "initial_batch_size", 0) - self.batch = getattr(self.gen, "batch_size", 0) def _to_array(self, x: list) -> npt.NDArray: """fast-cast list-of-dicts to NumPy array""" @@ -131,7 +129,7 @@ def _convert_tell(self, x: npt.NDArray) -> list: def _loop_over_gen(self, tag, Work): """Interact with ask/tell generator that *does not* contain a background thread""" while tag not in [PERSIS_STOP, STOP_TAG]: - batch_size = self.batch or Work["libE_info"]["batch_size"] + batch_size = Work["libE_info"]["batch_size"] points, updates = self._get_points_updates(batch_size) if updates is not None and len(updates): # returned "samples" and "updates". can combine if same dtype H_out = np.append(points, updates) @@ -143,7 +141,7 @@ def _loop_over_gen(self, tag, Work): def _get_initial_ask(self, libE_info) -> npt.NDArray: """Get initial batch from generator based on generator type""" - initial_batch = self.initial_batch or libE_info["batch_size"] + initial_batch = libE_info["batch_size"] H_out = self.gen.ask(initial_batch) return H_out @@ -174,7 +172,7 @@ def _result(self, calc_in: npt.NDArray, persis_info: dict, libE_info: dict) -> ( class LibensembleGenRunner(AskTellGenRunner): def _get_initial_ask(self, libE_info) -> npt.NDArray: """Get initial batch from generator based on generator type""" - H_out = self.gen.ask_numpy(self.initial_batch or libE_info["batch_size"]) + H_out = self.gen.ask_numpy(libE_info["batch_size"]) return H_out def _get_points_updates(self, batch_size: int) -> (npt.NDArray, npt.NDArray): From 4489d42f24098918df2b46d6f5325ad2ffd618d5 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 7 Aug 2024 15:21:30 -0500 Subject: [PATCH 176/288] dont import gpcam classes into gen_classes level --- libensemble/gen_classes/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/libensemble/gen_classes/__init__.py b/libensemble/gen_classes/__init__.py index 120ca1448..d5bfedd34 100644 --- a/libensemble/gen_classes/__init__.py +++ b/libensemble/gen_classes/__init__.py @@ -1,4 +1,3 @@ from .aposmm import APOSMM # noqa: F401 -from .gpCAM import GP_CAM, GP_CAM_Covar # noqa: F401 from .sampling import UniformSample, UniformSampleDicts # noqa: F401 from .surmise import Surmise # noqa: F401 From c9c467192e46d4e7226989108c86cee70df80800 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 7 Aug 2024 15:59:58 -0500 Subject: [PATCH 177/288] presumably fix surmise asktell test? --- libensemble/tests/regression_tests/test_asktell_surmise.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libensemble/tests/regression_tests/test_asktell_surmise.py b/libensemble/tests/regression_tests/test_asktell_surmise.py index 3c424ea8b..250aee20b 100644 --- a/libensemble/tests/regression_tests/test_asktell_surmise.py +++ b/libensemble/tests/regression_tests/test_asktell_surmise.py @@ -12,12 +12,13 @@ if __name__ == "__main__": from libensemble.executors import Executor - from libensemble.gen_classes import Surmise, list_dicts_to_np + from libensemble.gen_classes import Surmise # Import libEnsemble items for this test from libensemble.sim_funcs.borehole_kills import borehole from libensemble.tests.regression_tests.common import build_borehole # current location from libensemble.tools import add_unique_random_streams + from libensemble.utils.misc import list_dicts_to_np sim_app = os.path.join(os.getcwd(), "borehole.x") if not os.path.isfile(sim_app): From 601af44925b527d80b5e63156e0d9a0ef5078369 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 7 Aug 2024 16:44:59 -0500 Subject: [PATCH 178/288] actually fix surmise test. make sure that when passing around single points, they're singleton lists when necessary --- .../tests/regression_tests/test_asktell_surmise.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libensemble/tests/regression_tests/test_asktell_surmise.py b/libensemble/tests/regression_tests/test_asktell_surmise.py index 250aee20b..a4e5d9ae9 100644 --- a/libensemble/tests/regression_tests/test_asktell_surmise.py +++ b/libensemble/tests/regression_tests/test_asktell_surmise.py @@ -88,7 +88,7 @@ total_evals = 0 for point in initial_sample: - H_out, _a, _b = borehole(list_dicts_to_np(point), {}, sim_specs, {"H_rows": np.array([point["sim_id"]])}) + H_out, _a, _b = borehole(list_dicts_to_np([point]), {}, sim_specs, {"H_rows": np.array([point["sim_id"]])}) point["f"] = H_out["f"][0] # some "bugginess" with output shape of array in simf total_evals += 1 @@ -99,7 +99,7 @@ next_sample, cancels = surmise.ask(), surmise.ask_updates() for point in next_sample: - H_out, _a, _b = borehole(list_dicts_to_np(point), {}, sim_specs, {"H_rows": np.array([point["sim_id"]])}) + H_out, _a, _b = borehole(list_dicts_to_np([point]), {}, sim_specs, {"H_rows": np.array([point["sim_id"]])}) point["f"] = H_out["f"][0] total_evals += 1 @@ -109,10 +109,10 @@ while total_evals < max_evals: for point in sample: - H_out, _a, _b = borehole(list_dicts_to_np(point), {}, sim_specs, {"H_rows": np.array([point["sim_id"]])}) + H_out, _a, _b = borehole(list_dicts_to_np([point]), {}, sim_specs, {"H_rows": np.array([point["sim_id"]])}) point["f"] = H_out["f"][0] total_evals += 1 - surmise.tell(point) + surmise.tell([point]) if surmise.ready_to_be_asked(): new_sample, cancels = surmise.ask(), surmise.ask_updates() for m in cancels: From d454b5c1e3bd7ac99a0cc1d206d722a1b948a780 Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 8 Aug 2024 10:27:04 -0500 Subject: [PATCH 179/288] similarly exclude gpcam_class test from tests, for now --- .github/workflows/extra.yml | 1 + libensemble/tests/regression_tests/test_gpCAM_class.py | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/extra.yml b/.github/workflows/extra.yml index 13e15a7b2..80fd41b79 100644 --- a/.github/workflows/extra.yml +++ b/.github/workflows/extra.yml @@ -215,6 +215,7 @@ jobs: run: | rm ./libensemble/tests/regression_tests/test_ytopt_heffte.py # rm ./libensemble/tests/regression_tests/test_gpCAM.py + # rm ./libensemble/tests/regression_tests/test_gpCAM_class.py rm ./libensemble/tests/regression_tests/test_persistent_gp.py - name: Remove test for persistent Tasmanian on Python 3.12 diff --git a/libensemble/tests/regression_tests/test_gpCAM_class.py b/libensemble/tests/regression_tests/test_gpCAM_class.py index 1a609d525..f890c32ab 100644 --- a/libensemble/tests/regression_tests/test_gpCAM_class.py +++ b/libensemble/tests/regression_tests/test_gpCAM_class.py @@ -17,6 +17,7 @@ # TESTSUITE_COMMS: mpi local # TESTSUITE_NPROCS: 4 # TESTSUITE_EXTRA: true +# TESTSUITE_EXCLUDE: true import sys import warnings From 92e22e454a19bd2a1813d3b39dc8469f5330e7d3 Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 8 Aug 2024 16:59:55 -0500 Subject: [PATCH 180/288] experimenting with batch_size and initial_batch_size gen_specs options --- libensemble/specs.py | 18 ++++++++++++++++++ libensemble/utils/runners.py | 8 +++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/libensemble/specs.py b/libensemble/specs.py index e0d3b98d2..e19031586 100644 --- a/libensemble/specs.py +++ b/libensemble/specs.py @@ -110,6 +110,24 @@ class GenSpecs(BaseModel): calling them locally. """ + initial_batch_size: Optional[int] = 0 + """ + Number of initial points to request that the generator create. If zero, falls back to ``batch_size``. + If both options are zero, defaults to the number of workers. + + Note: Certain generators included with libEnsemble decide + batch sizes via ``gen_specs["user"]`` or other methods. + """ + + batch_size: Optional[int] = 0 + """ + Number of points to generate in each batch. If zero, falls back to ``initial_batch_size``. + If both options are zero, defaults to the number of workers. + + Note: Certain generators included with libEnsemble decide + batch sizes via ``gen_specs["user"]`` or other methods. + """ + threaded: Optional[bool] = False """ Instruct Worker process to launch user function to a thread. diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 9cec3bd9f..9084452b7 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -129,7 +129,9 @@ def _convert_tell(self, x: npt.NDArray) -> list: def _loop_over_gen(self, tag, Work): """Interact with ask/tell generator that *does not* contain a background thread""" while tag not in [PERSIS_STOP, STOP_TAG]: - batch_size = Work["libE_info"]["batch_size"] + batch_size = ( + self.specs.get("batch_size") or self.specs.get("initial_batch_size") or Work["libE_info"]["batch_size"] + ) # or len(Work["H_in"])? points, updates = self._get_points_updates(batch_size) if updates is not None and len(updates): # returned "samples" and "updates". can combine if same dtype H_out = np.append(points, updates) @@ -141,7 +143,7 @@ def _loop_over_gen(self, tag, Work): def _get_initial_ask(self, libE_info) -> npt.NDArray: """Get initial batch from generator based on generator type""" - initial_batch = libE_info["batch_size"] + initial_batch = self.specs.get("initial_batch_size") or self.specs.get("batch_size") or libE_info["batch_size"] H_out = self.gen.ask(initial_batch) return H_out @@ -172,7 +174,7 @@ def _result(self, calc_in: npt.NDArray, persis_info: dict, libE_info: dict) -> ( class LibensembleGenRunner(AskTellGenRunner): def _get_initial_ask(self, libE_info) -> npt.NDArray: """Get initial batch from generator based on generator type""" - H_out = self.gen.ask_numpy(libE_info["batch_size"]) + H_out = self.gen.ask_numpy(libE_info["batch_size"]) # OR GEN SPECS INITIAL BATCH SIZE return H_out def _get_points_updates(self, batch_size: int) -> (npt.NDArray, npt.NDArray): From d14f4d291b901f90ad8a58b70e209f7706aba0dc Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 9 Aug 2024 10:19:52 -0500 Subject: [PATCH 181/288] subsequent batch_sizes are either back_size or len(H_in) --- libensemble/specs.py | 4 ++-- libensemble/utils/runners.py | 15 +++++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/libensemble/specs.py b/libensemble/specs.py index e19031586..a1a5a718b 100644 --- a/libensemble/specs.py +++ b/libensemble/specs.py @@ -121,8 +121,8 @@ class GenSpecs(BaseModel): batch_size: Optional[int] = 0 """ - Number of points to generate in each batch. If zero, falls back to ``initial_batch_size``. - If both options are zero, defaults to the number of workers. + Number of points to generate in each batch. If zero, falls back to the number of + completed evaluations most recently told to the generator. Note: Certain generators included with libEnsemble decide batch sizes via ``gen_specs["user"]`` or other methods. diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 9084452b7..bfe2d16ae 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -118,20 +118,19 @@ def _to_array(self, x: list) -> npt.NDArray: return x def _get_points_updates(self, batch_size: int) -> (npt.NDArray, npt.NDArray): + # no ask_updates on external gens return ( self._to_array(self.gen.ask(batch_size)), None, - ) # external ask/tell gens likely don't implement ask_updates + ) def _convert_tell(self, x: npt.NDArray) -> list: self.gen.tell(np_to_list_dicts(x)) - def _loop_over_gen(self, tag, Work): + def _loop_over_gen(self, tag, Work, H_in): """Interact with ask/tell generator that *does not* contain a background thread""" while tag not in [PERSIS_STOP, STOP_TAG]: - batch_size = ( - self.specs.get("batch_size") or self.specs.get("initial_batch_size") or Work["libE_info"]["batch_size"] - ) # or len(Work["H_in"])? + batch_size = self.specs.get("batch_size") or len(H_in) points, updates = self._get_points_updates(batch_size) if updates is not None and len(updates): # returned "samples" and "updates". can combine if same dtype H_out = np.append(points, updates) @@ -150,7 +149,7 @@ def _get_initial_ask(self, libE_info) -> npt.NDArray: def _start_generator_loop(self, tag, Work, H_in): """Start the generator loop after choosing best way of giving initial results to gen""" self.gen.tell(np_to_list_dicts(H_in)) - return self._loop_over_gen(tag, Work) + return self._loop_over_gen(tag, Work, H_in) def _persistent_result(self, calc_in, persis_info, libE_info): """Setup comms with manager, setup gen, loop gen to completion, return gen's results""" @@ -186,7 +185,7 @@ def _convert_tell(self, x: npt.NDArray) -> list: def _start_generator_loop(self, tag, Work, H_in) -> npt.NDArray: """Start the generator loop after choosing best way of giving initial results to gen""" self.gen.tell_numpy(H_in) - return self._loop_over_gen(tag, Work) # see parent class + return self._loop_over_gen(tag, Work, H_in) # see parent class class LibensembleGenThreadRunner(AskTellGenRunner): @@ -205,7 +204,7 @@ def _ask_and_send(self): else: self.ps.send(points) - def _loop_over_gen(self, _, _2): + def _loop_over_gen(self, *args): """Cycle between moving all outbound / inbound messages between threaded gen and manager""" while True: time.sleep(0.0025) # dont need to ping the gen relentlessly. Let it calculate. 400hz From e27487dca35c3683833c68bfa303f276597c5887 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 9 Aug 2024 10:31:52 -0500 Subject: [PATCH 182/288] now test in test_sampling_asktell_gen --- libensemble/gen_classes/sampling.py | 5 ++--- .../tests/functionality_tests/test_sampling_asktell_gen.py | 4 +++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/libensemble/gen_classes/sampling.py b/libensemble/gen_classes/sampling.py index beaa7bf92..d2c21cae3 100644 --- a/libensemble/gen_classes/sampling.py +++ b/libensemble/gen_classes/sampling.py @@ -15,7 +15,6 @@ class SampleBase(LibensembleGenerator): 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 @@ -32,7 +31,7 @@ class UniformSample(SampleBase): mode by adjusting the allocation function. """ - def __init__(self, _, persis_info, gen_specs, libE_info=None) -> list: + def __init__(self, _, persis_info, gen_specs, libE_info=None): self.persis_info = persis_info self.gen_specs = gen_specs self.libE_info = libE_info @@ -63,7 +62,7 @@ class UniformSampleDicts(Generator): mode by adjusting the allocation function. """ - def __init__(self, _, persis_info, gen_specs, libE_info=None) -> list: + def __init__(self, _, persis_info, gen_specs, libE_info=None): self.persis_info = persis_info self.gen_specs = gen_specs self.libE_info = libE_info diff --git a/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py b/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py index 0cb35ecb4..57db0f5e4 100644 --- a/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py +++ b/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py @@ -42,8 +42,10 @@ def sim_f(In): gen_specs = { "persis_in": ["x", "f", "grad", "sim_id"], "out": [("x", float, (2,))], + "initial_batch_size": 20, + "batch_size": 10, "user": { - "initial_batch_size": 20, + "initial_batch_size": 20, # for wrapper "lb": np.array([-3, -2]), "ub": np.array([3, 2]), }, From a6feb77dd7eddc3570e92d967558ff836174b126 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 9 Aug 2024 16:10:44 -0500 Subject: [PATCH 183/288] cover asking aposmm for num points --- .../tests/unit_tests/RENAME_test_persistent_aposmm.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py b/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py index 878833e36..11cad7c63 100644 --- a/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py +++ b/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py @@ -14,6 +14,7 @@ import libensemble.tests.unit_tests.setup as setup from libensemble.sim_funcs.six_hump_camel import six_hump_camel_func, six_hump_camel_grad +from libensemble.utils.misc import list_dicts_to_np libE_info = {"comm": {}} @@ -204,7 +205,7 @@ def test_asktell_with_persistent_aposmm(): my_APOSMM = APOSMM(gen_specs) my_APOSMM.setup() - initial_sample = my_APOSMM.ask() + initial_sample = my_APOSMM.ask(100) total_evals = 0 eval_max = 2000 @@ -219,7 +220,7 @@ def test_asktell_with_persistent_aposmm(): while total_evals < eval_max: - sample, detected_minima = my_APOSMM.ask(), my_APOSMM.ask_updates() + sample, detected_minima = my_APOSMM.ask(6), my_APOSMM.ask_updates() if len(detected_minima): for m in detected_minima: potential_minima.append(m) @@ -227,7 +228,7 @@ def test_asktell_with_persistent_aposmm(): point["f"] = six_hump_camel_func(point["x"]) total_evals += 1 my_APOSMM.tell(sample) - H, persis_info, exit_code = my_APOSMM.final_tell(sample) + H, persis_info, exit_code = my_APOSMM.final_tell(list_dicts_to_np(sample)) # final_tell currently requires numpy assert exit_code == FINISHED_PERSISTENT_GEN_TAG, "Standalone persistent_aposmm didn't exit correctly" assert persis_info.get("run_order"), "Standalone persistent_aposmm didn't do any localopt runs" From 12a133bb457fda902fa0223e701d6a7b76f01bd6 Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 12 Aug 2024 13:22:15 -0500 Subject: [PATCH 184/288] various coverage adjustments and fixes --- .codecov.yml | 1 + libensemble/gen_classes/sampling.py | 5 ----- libensemble/generators.py | 24 ++++++++---------------- libensemble/utils/runners.py | 10 +++------- 4 files changed, 12 insertions(+), 28 deletions(-) diff --git a/.codecov.yml b/.codecov.yml index 18ef40801..f99839378 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -5,3 +5,4 @@ ignore: - "libensemble/sim_funcs/executor_hworld.py" - "libensemble/gen_funcs/persistent_ax_multitask.py" - "libensemble/gen_funcs/persistent_gpCAM.py" + - "libensemble/gen_classes/gpCAM.py" diff --git a/libensemble/gen_classes/sampling.py b/libensemble/gen_classes/sampling.py index d2c21cae3..275624bb9 100644 --- a/libensemble/gen_classes/sampling.py +++ b/libensemble/gen_classes/sampling.py @@ -40,11 +40,6 @@ def __init__(self, _, persis_info, gen_specs, libE_info=None): 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)) - - if "obj_component" in H_o.dtype.fields: # needs H_o - needs to be created in here. - H_o["obj_component"] = self.persis_info["rand_stream"].integers( - low=0, high=self.gen_specs["user"]["num_components"], size=n_trials - ) return H_o def tell_numpy(self, calc_in): diff --git a/libensemble/generators.py b/libensemble/generators.py index 6440b3a7a..1ee243954 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -65,7 +65,7 @@ def ask(self, num_points: Optional[int]) -> List[dict]: Request the next set of points to evaluate. """ - def ask_updates(self) -> npt.NDArray: + def ask_updates(self) -> List[npt.NDArray]: """ Request any updates to previous points, e.g. minima discovered, points to cancel. """ @@ -92,11 +92,11 @@ class LibensembleGenerator(Generator): @abstractmethod def ask_numpy(self, num_points: Optional[int] = 0) -> npt.NDArray: - pass + """Request the next set of points to evaluate, as a NumPy array.""" @abstractmethod def tell_numpy(self, results: npt.NDArray) -> None: - pass + """Send the results, as a NumPy array, of evaluations to the generator.""" def ask(self, num_points: Optional[int] = 0) -> List[dict]: """Request the next set of points to evaluate.""" @@ -142,15 +142,11 @@ def setup(self) -> None: ) # note that self.thread's inbox/outbox are unused by the underlying gen def _set_sim_ended(self, results: npt.NDArray) -> npt.NDArray: - if "sim_ended" in results.dtype.names: - results["sim_ended"] = True - else: - new_results = np.zeros(len(results), dtype=self.gen_specs["out"] + [("sim_ended", bool), ("f", float)]) - for field in results.dtype.names: - new_results[field] = results[field] - new_results["sim_ended"] = True - results = new_results - return results + new_results = np.zeros(len(results), dtype=self.gen_specs["out"] + [("sim_ended", bool), ("f", float)]) + for field in results.dtype.names: + new_results[field] = results[field] + new_results["sim_ended"] = True + return new_results def tell(self, results: List[dict], tag: int = EVAL_GEN_TAG) -> None: """Send the results of evaluations to the generator.""" @@ -163,10 +159,6 @@ def ask_numpy(self, num_points: int = 0) -> npt.NDArray: _, ask_full = self.outbox.get() return ask_full["calc_out"] - def ask_updates(self) -> npt.NDArray: - """Request any updates to previous points, e.g. minima discovered, points to cancel.""" - return self.ask_numpy() - def tell_numpy(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: """Send the results of evaluations to the generator, as a NumPy array.""" if results is not None: diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index bfe2d16ae..d688a427e 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -131,11 +131,7 @@ def _loop_over_gen(self, tag, Work, H_in): """Interact with ask/tell generator that *does not* contain a background thread""" while tag not in [PERSIS_STOP, STOP_TAG]: batch_size = self.specs.get("batch_size") or len(H_in) - points, updates = self._get_points_updates(batch_size) - if updates is not None and len(updates): # returned "samples" and "updates". can combine if same dtype - H_out = np.append(points, updates) - else: - H_out = points # all external gens likely go here + H_out, _ = self._get_points_updates(batch_size) tag, Work, H_in = self.ps.send_recv(H_out) self._convert_tell(H_in) return H_in @@ -167,7 +163,7 @@ def _persistent_result(self, calc_in, persis_info, libE_info): def _result(self, calc_in: npt.NDArray, persis_info: dict, libE_info: dict) -> (npt.NDArray, dict, Optional[int]): if libE_info.get("persistent"): return self._persistent_result(calc_in, persis_info, libE_info) - return self._to_array(self.gen.ask(getattr(self.gen, "batch_size", 0) or libE_info["batch_size"])) + raise ValueError("ask/tell generators must run in persistent mode. This may be the default in the future.") class LibensembleGenRunner(AskTellGenRunner): @@ -176,7 +172,7 @@ def _get_initial_ask(self, libE_info) -> npt.NDArray: H_out = self.gen.ask_numpy(libE_info["batch_size"]) # OR GEN SPECS INITIAL BATCH SIZE return H_out - def _get_points_updates(self, batch_size: int) -> (npt.NDArray, npt.NDArray): + def _get_points_updates(self, batch_size: int) -> (npt.NDArray, list): return self.gen.ask_numpy(batch_size), self.gen.ask_updates() def _convert_tell(self, x: npt.NDArray) -> list: From ee2508e46779a831ef774cf0257e44109a076683 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 16 Aug 2024 14:21:15 -0500 Subject: [PATCH 185/288] initial commit, creating ask/tell gen unit test, base LibensembleGenerator class can set gen_specs.user via kwargs --- libensemble/gen_classes/aposmm.py | 2 -- libensemble/gen_classes/sampling.py | 14 ++++++-------- libensemble/generators.py | 15 ++++++++++++++- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index 8e8fb47f0..36a2bc390 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -19,8 +19,6 @@ def __init__( from libensemble.gen_funcs.persistent_aposmm import aposmm gen_specs["gen_f"] = aposmm - if len(kwargs) > 0: # so user can specify aposmm-specific parameters as kwargs to constructor - gen_specs["user"] = kwargs 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"]) gen_specs["out"] = [ diff --git a/libensemble/gen_classes/sampling.py b/libensemble/gen_classes/sampling.py index 275624bb9..e7cbc808a 100644 --- a/libensemble/gen_classes/sampling.py +++ b/libensemble/gen_classes/sampling.py @@ -31,11 +31,10 @@ class UniformSample(SampleBase): mode by adjusting the allocation function. """ - def __init__(self, _, persis_info, gen_specs, libE_info=None): - self.persis_info = persis_info - self.gen_specs = gen_specs - self.libE_info = libE_info + def __init__(self, _=[], persis_info={}, gen_specs={}, libE_info=None, **kwargs): + super().__init__(gen_specs, _, persis_info, libE_info, **kwargs) self._get_user_params(self.gen_specs["user"]) + self.gen_specs["out"] = [("x", float, (self.n,))] def ask_numpy(self, n_trials): H_o = np.zeros(n_trials, dtype=self.gen_specs["out"]) @@ -57,11 +56,10 @@ class UniformSampleDicts(Generator): mode by adjusting the allocation function. """ - def __init__(self, _, persis_info, gen_specs, libE_info=None): - self.persis_info = persis_info - self.gen_specs = gen_specs - self.libE_info = libE_info + def __init__(self, _, persis_info, gen_specs, libE_info=None, **kwargs): + super().__init__(_, persis_info, gen_specs, libE_info) self._get_user_params(self.gen_specs["user"]) + self.gen_specs["out"] = [("x", float, (self.n,))] def ask(self, n_trials): H_o = [] diff --git a/libensemble/generators.py b/libensemble/generators.py index 1ee243954..2aee4bacb 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -8,6 +8,7 @@ from libensemble.comms.comms import QComm, QCommThread from libensemble.executors import Executor from libensemble.message_numbers import EVAL_GEN_TAG, PERSIS_STOP +from libensemble.tools.tools import add_unique_random_streams from libensemble.utils.misc import list_dicts_to_np, np_to_list_dicts """ @@ -90,6 +91,18 @@ class LibensembleGenerator(Generator): ``ask_numpy/tell_numpy`` methods communicate numpy arrays containing the same data. """ + def __init__( + self, gen_specs: dict = {}, History: npt.NDArray = [], persis_info: dict = {}, libE_info: dict = {}, **kwargs + ): + self.gen_specs = gen_specs + if len(kwargs) > 0: # so user can specify gen-specific parameters as kwargs to constructor + self.gen_specs["user"] = kwargs + if not persis_info: + self.persis_info = add_unique_random_streams({}, 4, seed=4321)[1] + self.persis_info["nworkers"] = 4 + else: + self.persis_info = persis_info + @abstractmethod def ask_numpy(self, num_points: Optional[int] = 0) -> npt.NDArray: """Request the next set of points to evaluate, as a NumPy array.""" @@ -115,8 +128,8 @@ class LibensembleGenThreadInterfacer(LibensembleGenerator): def __init__( self, gen_specs: dict, History: npt.NDArray = [], persis_info: dict = {}, libE_info: dict = {} ) -> None: + super().__init__(gen_specs, History, persis_info, libE_info) self.gen_f = gen_specs["gen_f"] - self.gen_specs = gen_specs self.History = History self.persis_info = persis_info self.libE_info = libE_info From 070fc6f9f76b5fa25b3d6d84704e55e7389c788e Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 16 Aug 2024 15:12:30 -0500 Subject: [PATCH 186/288] add test, arrays become flattened dicts in np_to_list_dicts --- libensemble/tests/unit_tests/test_asktell.py | 36 ++++++++++++++++++++ libensemble/utils/misc.py | 6 +++- 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 libensemble/tests/unit_tests/test_asktell.py diff --git a/libensemble/tests/unit_tests/test_asktell.py b/libensemble/tests/unit_tests/test_asktell.py new file mode 100644 index 000000000..0adef408f --- /dev/null +++ b/libensemble/tests/unit_tests/test_asktell.py @@ -0,0 +1,36 @@ +import numpy as np + +from libensemble.tools.tools import add_unique_random_streams + + +def test_asktell_sampling(): + from libensemble.gen_classes.sampling import UniformSample + + persis_info = add_unique_random_streams({}, 5, seed=1234) + gen_specs = { + "out": [("x", float, (2,))], + "user": { + "lb": np.array([-3, -2]), + "ub": np.array([3, 2]), + }, + } + + # Test initialization with libensembley parameters + gen = UniformSample(None, persis_info[1], gen_specs, None) + assert len(gen.ask(10)) == 10 + + # Test initialization gen-specific keyword args + gen = UniformSample(lb=np.array([-3, -2]), ub=np.array([3, 2])) + assert len(gen.ask(10)) == 10 + + import ipdb + + ipdb.set_trace() + + out = gen.ask_numpy(3) # should get numpy arrays, non-flattened + out = gen.ask(3) # needs to get dicts, 2d+ arrays need to be flattened + assert all([len(x) == 2 for x in out]) # np_to_list_dicts is now tested + + +if __name__ == "__main__": + test_asktell_sampling() diff --git a/libensemble/utils/misc.py b/libensemble/utils/misc.py index db73ccf91..7a7704183 100644 --- a/libensemble/utils/misc.py +++ b/libensemble/utils/misc.py @@ -117,6 +117,10 @@ def np_to_list_dicts(array: npt.NDArray) -> List[dict]: for row in array: new_dict = {} for field in row.dtype.names: - new_dict[field] = row[field] + if len(row[field]) > 1: + for i, x in enumerate(row[field]): + new_dict[field + str(i)] = x + else: + new_dict[field] = row[field] out.append(new_dict) return out From a969f500f58f52609ba954829477cef8041702d9 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 16 Aug 2024 15:16:36 -0500 Subject: [PATCH 187/288] remove debug statement --- libensemble/tests/unit_tests/test_asktell.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/libensemble/tests/unit_tests/test_asktell.py b/libensemble/tests/unit_tests/test_asktell.py index 0adef408f..6b79060ab 100644 --- a/libensemble/tests/unit_tests/test_asktell.py +++ b/libensemble/tests/unit_tests/test_asktell.py @@ -23,10 +23,6 @@ def test_asktell_sampling(): gen = UniformSample(lb=np.array([-3, -2]), ub=np.array([3, 2])) assert len(gen.ask(10)) == 10 - import ipdb - - ipdb.set_trace() - out = gen.ask_numpy(3) # should get numpy arrays, non-flattened out = gen.ask(3) # needs to get dicts, 2d+ arrays need to be flattened assert all([len(x) == 2 for x in out]) # np_to_list_dicts is now tested From 6eb5fe86d4edd942fa474c106e9a29eca526cdcf Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 16 Aug 2024 17:42:25 -0500 Subject: [PATCH 188/288] additional attempts to unflatten the input dict... --- libensemble/tests/unit_tests/test_asktell.py | 4 ++ libensemble/utils/misc.py | 49 ++++++++++++++------ 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/libensemble/tests/unit_tests/test_asktell.py b/libensemble/tests/unit_tests/test_asktell.py index 6b79060ab..c7b43bc02 100644 --- a/libensemble/tests/unit_tests/test_asktell.py +++ b/libensemble/tests/unit_tests/test_asktell.py @@ -1,6 +1,7 @@ import numpy as np from libensemble.tools.tools import add_unique_random_streams +from libensemble.utils.misc import list_dicts_to_np def test_asktell_sampling(): @@ -27,6 +28,9 @@ def test_asktell_sampling(): out = gen.ask(3) # needs to get dicts, 2d+ arrays need to be flattened assert all([len(x) == 2 for x in out]) # np_to_list_dicts is now tested + # now we test list_dicts_to_np directly + out = list_dicts_to_np(out) + if __name__ == "__main__": test_asktell_sampling() diff --git a/libensemble/utils/misc.py b/libensemble/utils/misc.py index 7a7704183..e8c2e235b 100644 --- a/libensemble/utils/misc.py +++ b/libensemble/utils/misc.py @@ -81,18 +81,15 @@ def specs_checker_setattr(obj, key, value): obj.__dict__[key] = value -def _copy_data(array, list_dicts): - for i, entry in enumerate(list_dicts): - for field in entry.keys(): - array[field][i] = entry[field] - return array +def _decide_dtype(name, entry, size): + if size == 1: + return (name, type(entry)) + else: + return (name, type(entry), (size,)) -def _decide_dtype(name, entry): - if hasattr(entry, "shape") and len(entry.shape): # numpy type - return (name, entry.dtype, entry.shape) - else: - return (name, type(entry)) +def _combine_names(names): + return list(set(i[:-1] if i[-1].isdigit() else i for i in names)) def list_dicts_to_np(list_dicts: list) -> npt.NDArray: @@ -100,14 +97,38 @@ def list_dicts_to_np(list_dicts: list) -> npt.NDArray: return None first = list_dicts[0] - new_dtype_names = [i for i in first.keys()] + new_dtype_names = _combine_names([i for i in first.keys()]) new_dtype = [] - for i, entry in enumerate(first.values()): # must inspect values to get presumptive types + combinable_names = [] + for name in new_dtype_names: + combinable_names.append([i for i in first.keys() if i.startswith(name)]) + + for i, entry in enumerate(combinable_names): # must inspect values to get presumptive types name = new_dtype_names[i] - new_dtype.append(_decide_dtype(name, entry)) + size = len(combinable_names[i]) + new_dtype.append(_decide_dtype(name, first[entry[0]], size)) out = np.zeros(len(list_dicts), dtype=new_dtype) - return _copy_data(out, list_dicts) + + # good lord, this is ugly + # for names_group_idx, entry in enumerate(combinable_names): + # for input_dict in list_dicts: + # for l in range(len(input_dict)): + # for name_idx, src_key in enumerate(entry): + # out[new_dtype_names[names_group_idx]][name_idx][l] = input_dict[src_key] + + for name in new_dtype_names: + for i, input_dict in enumerate(list_dicts): + for j, value in enumerate(input_dict.values()): + out[name][j][i] = value + + [ + {"x0": -1.3315287487797274, "x1": -1.1102419596798931}, + {"x0": 2.2035749254093417, "x1": -0.04551905560134939}, + {"x0": -1.043550345357007, "x1": -0.853671651707665}, + ] + + return out def np_to_list_dicts(array: npt.NDArray) -> List[dict]: From 12612744cf1633dc8be36bb8ac183fc54d75d1f2 Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 19 Aug 2024 10:54:19 -0500 Subject: [PATCH 189/288] fix index ordering, cleanup/complete tentatively unit test --- libensemble/tests/unit_tests/test_asktell.py | 14 ++++++--- libensemble/utils/misc.py | 33 +++++++------------- 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/libensemble/tests/unit_tests/test_asktell.py b/libensemble/tests/unit_tests/test_asktell.py index c7b43bc02..660e19ae8 100644 --- a/libensemble/tests/unit_tests/test_asktell.py +++ b/libensemble/tests/unit_tests/test_asktell.py @@ -4,7 +4,7 @@ from libensemble.utils.misc import list_dicts_to_np -def test_asktell_sampling(): +def test_asktell_sampling_and_utils(): from libensemble.gen_classes.sampling import UniformSample persis_info = add_unique_random_streams({}, 5, seed=1234) @@ -24,13 +24,19 @@ def test_asktell_sampling(): gen = UniformSample(lb=np.array([-3, -2]), ub=np.array([3, 2])) assert len(gen.ask(10)) == 10 - out = gen.ask_numpy(3) # should get numpy arrays, non-flattened + out_np = gen.ask_numpy(3) # should get numpy arrays, non-flattened out = gen.ask(3) # needs to get dicts, 2d+ arrays need to be flattened assert all([len(x) == 2 for x in out]) # np_to_list_dicts is now tested # now we test list_dicts_to_np directly - out = list_dicts_to_np(out) + out_np = list_dicts_to_np(out) + + # check combined values resemble flattened list-of-dicts values + assert out_np.dtype.names == ("x",) + for i, entry in enumerate(out): + for j, value in enumerate(entry.values()): + assert value == out_np["x"][i][j] if __name__ == "__main__": - test_asktell_sampling() + test_asktell_sampling_and_utils() diff --git a/libensemble/utils/misc.py b/libensemble/utils/misc.py index e8c2e235b..a5de08695 100644 --- a/libensemble/utils/misc.py +++ b/libensemble/utils/misc.py @@ -81,14 +81,17 @@ def specs_checker_setattr(obj, key, value): obj.__dict__[key] = value -def _decide_dtype(name, entry, size): +def _decide_dtype(name: str, entry, size: int) -> tuple: if size == 1: return (name, type(entry)) else: return (name, type(entry), (size,)) -def _combine_names(names): +def _combine_names(names: list) -> list: + """combine fields with same name *except* for final digit""" + # how many final digits could possibly be in each name? + # do we have to iterate through negative-indexes until we reach a non-digit? return list(set(i[:-1] if i[-1].isdigit() else i for i in names)) @@ -96,37 +99,25 @@ def list_dicts_to_np(list_dicts: list) -> npt.NDArray: if list_dicts is None: return None - first = list_dicts[0] - new_dtype_names = _combine_names([i for i in first.keys()]) - new_dtype = [] - combinable_names = [] + first = list_dicts[0] # for determining dtype of output np array + new_dtype_names = _combine_names([i for i in first.keys()]) # -> ['x', 'y'] + combinable_names = [] # [['x0', 'x1'], ['y0', 'y1', 'y2']] for name in new_dtype_names: combinable_names.append([i for i in first.keys() if i.startswith(name)]) - for i, entry in enumerate(combinable_names): # must inspect values to get presumptive types + new_dtype = [] + + for i, entry in enumerate(combinable_names): name = new_dtype_names[i] size = len(combinable_names[i]) new_dtype.append(_decide_dtype(name, first[entry[0]], size)) out = np.zeros(len(list_dicts), dtype=new_dtype) - # good lord, this is ugly - # for names_group_idx, entry in enumerate(combinable_names): - # for input_dict in list_dicts: - # for l in range(len(input_dict)): - # for name_idx, src_key in enumerate(entry): - # out[new_dtype_names[names_group_idx]][name_idx][l] = input_dict[src_key] - for name in new_dtype_names: for i, input_dict in enumerate(list_dicts): for j, value in enumerate(input_dict.values()): - out[name][j][i] = value - - [ - {"x0": -1.3315287487797274, "x1": -1.1102419596798931}, - {"x0": 2.2035749254093417, "x1": -0.04551905560134939}, - {"x0": -1.043550345357007, "x1": -0.853671651707665}, - ] + out[name][i][j] = value return out From d960b960bf70b11c11e8f1a203b0a7c1f0a62320 Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 19 Aug 2024 12:49:44 -0500 Subject: [PATCH 190/288] passthrough kwargs to superclasses, try to handle empty lists for single-dim fields --- libensemble/gen_classes/aposmm.py | 2 +- libensemble/generators.py | 4 ++-- libensemble/utils/misc.py | 13 ++++++++----- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index 36a2bc390..17caa6f4c 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -32,7 +32,7 @@ def __init__( if not persis_info: persis_info = add_unique_random_streams({}, 4, seed=4321)[1] persis_info["nworkers"] = 4 - super().__init__(gen_specs, History, persis_info, libE_info) + super().__init__(gen_specs, History, persis_info, libE_info, **kwargs) self.all_local_minima = [] self.results_idx = 0 self.last_ask = None diff --git a/libensemble/generators.py b/libensemble/generators.py index 2aee4bacb..b61ba1099 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -126,9 +126,9 @@ class LibensembleGenThreadInterfacer(LibensembleGenerator): """ def __init__( - self, gen_specs: dict, History: npt.NDArray = [], persis_info: dict = {}, libE_info: dict = {} + self, gen_specs: dict, History: npt.NDArray = [], persis_info: dict = {}, libE_info: dict = {}, **kwargs ) -> None: - super().__init__(gen_specs, History, persis_info, libE_info) + super().__init__(gen_specs, History, persis_info, libE_info, **kwargs) self.gen_f = gen_specs["gen_f"] self.History = History self.persis_info = persis_info diff --git a/libensemble/utils/misc.py b/libensemble/utils/misc.py index a5de08695..2de1c841b 100644 --- a/libensemble/utils/misc.py +++ b/libensemble/utils/misc.py @@ -82,7 +82,7 @@ def specs_checker_setattr(obj, key, value): def _decide_dtype(name: str, entry, size: int) -> tuple: - if size == 1: + if size == 1 or not size: return (name, type(entry)) else: return (name, type(entry), (size,)) @@ -101,16 +101,19 @@ def list_dicts_to_np(list_dicts: list) -> npt.NDArray: first = list_dicts[0] # for determining dtype of output np array new_dtype_names = _combine_names([i for i in first.keys()]) # -> ['x', 'y'] - combinable_names = [] # [['x0', 'x1'], ['y0', 'y1', 'y2']] + combinable_names = [] # [['x0', 'x1'], ['y0', 'y1', 'y2'], []] for name in new_dtype_names: - combinable_names.append([i for i in first.keys() if i.startswith(name)]) + combinable_names.append([i for i in first.keys() if i[:-1] == name]) new_dtype = [] for i, entry in enumerate(combinable_names): name = new_dtype_names[i] size = len(combinable_names[i]) - new_dtype.append(_decide_dtype(name, first[entry[0]], size)) + if len(entry): # combinable names detected, e.g. x0, x1 + new_dtype.append(_decide_dtype(name, first[entry[0]], size)) + else: # only a single name, e.g. local_pt + new_dtype.append(_decide_dtype(name, first[name], size)) out = np.zeros(len(list_dicts), dtype=new_dtype) @@ -129,7 +132,7 @@ def np_to_list_dicts(array: npt.NDArray) -> List[dict]: for row in array: new_dict = {} for field in row.dtype.names: - if len(row[field]) > 1: + if hasattr(row[field], "__len__") and len(row[field]) > 1: for i, x in enumerate(row[field]): new_dict[field + str(i)] = x else: From 3ce0ca2997a793a42f4062baa6c44a76483de221 Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 19 Aug 2024 13:19:06 -0500 Subject: [PATCH 191/288] better handling of multi-dim and single-dim output-array item assignment from input list of dicts --- libensemble/utils/misc.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/libensemble/utils/misc.py b/libensemble/utils/misc.py index 2de1c841b..e6b810ce0 100644 --- a/libensemble/utils/misc.py +++ b/libensemble/utils/misc.py @@ -117,10 +117,13 @@ def list_dicts_to_np(list_dicts: list) -> npt.NDArray: out = np.zeros(len(list_dicts), dtype=new_dtype) - for name in new_dtype_names: - for i, input_dict in enumerate(list_dicts): - for j, value in enumerate(input_dict.values()): - out[name][i][j] = value + for i, group in enumerate(combinable_names): + new_dtype_name = new_dtype_names[i] + for j, input_dict in enumerate(list_dicts): + if not len(group): + out[new_dtype_name][j] = input_dict[new_dtype_name] + else: + out[new_dtype_name][j] = tuple([input_dict[name] for name in group]) return out From 09cb4a68d2d9624a35d582ed5c14132d51e27792 Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 19 Aug 2024 13:21:39 -0500 Subject: [PATCH 192/288] comments --- libensemble/utils/misc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libensemble/utils/misc.py b/libensemble/utils/misc.py index e6b810ce0..878bc1dff 100644 --- a/libensemble/utils/misc.py +++ b/libensemble/utils/misc.py @@ -120,9 +120,9 @@ def list_dicts_to_np(list_dicts: list) -> npt.NDArray: for i, group in enumerate(combinable_names): new_dtype_name = new_dtype_names[i] for j, input_dict in enumerate(list_dicts): - if not len(group): + if not len(group): # only a single name, e.g. local_pt out[new_dtype_name][j] = input_dict[new_dtype_name] - else: + else: # combinable names detected, e.g. x0, x1 out[new_dtype_name][j] = tuple([input_dict[name] for name in group]) return out From 601f02c2463629a2a4d88abc0f4a707d4f438122 Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 19 Aug 2024 13:58:49 -0500 Subject: [PATCH 193/288] adjust persistent_gen_wrapper, fix UniformSampleDicts --- libensemble/gen_classes/sampling.py | 3 ++- libensemble/gen_funcs/persistent_gen_wrapper.py | 10 ++-------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/libensemble/gen_classes/sampling.py b/libensemble/gen_classes/sampling.py index e7cbc808a..d11998e11 100644 --- a/libensemble/gen_classes/sampling.py +++ b/libensemble/gen_classes/sampling.py @@ -57,7 +57,8 @@ class UniformSampleDicts(Generator): """ def __init__(self, _, persis_info, gen_specs, libE_info=None, **kwargs): - super().__init__(_, persis_info, gen_specs, libE_info) + self.gen_specs = gen_specs + self.persis_info = persis_info self._get_user_params(self.gen_specs["user"]) self.gen_specs["out"] = [("x", float, (self.n,))] diff --git a/libensemble/gen_funcs/persistent_gen_wrapper.py b/libensemble/gen_funcs/persistent_gen_wrapper.py index 2ad862864..7fd01ec4d 100644 --- a/libensemble/gen_funcs/persistent_gen_wrapper.py +++ b/libensemble/gen_funcs/persistent_gen_wrapper.py @@ -1,10 +1,8 @@ import inspect -import numpy as np - from libensemble.message_numbers import EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG from libensemble.tools.persistent_support import PersistentSupport -from libensemble.utils.misc import np_to_list_dicts +from libensemble.utils.misc import list_dicts_to_np, np_to_list_dicts def persistent_gen_f(H, persis_info, gen_specs, libE_info): @@ -24,11 +22,7 @@ def persistent_gen_f(H, persis_info, gen_specs, libE_info): while tag not in [STOP_TAG, PERSIS_STOP]: H_o = gen.ask(b) if isinstance(H_o, list): - H_o_arr = np.zeros(len(H_o), dtype=gen_specs["out"]) - for i in range(len(H_o)): - for key in H_o[0].keys(): - H_o_arr[i][key] = H_o[i][key] - H_o = H_o_arr + H_o = list_dicts_to_np(H_o) tag, Work, calc_in = ps.send_recv(H_o) gen.tell(np_to_list_dicts(calc_in)) From 6733fe5cd30676c8d713b536429292e1cbaf8a61 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 21 Aug 2024 10:28:54 -0500 Subject: [PATCH 194/288] fix ordering of parameters in implemented ask/tell classes and parent classes, fix aposmm unit test --- libensemble/gen_classes/aposmm.py | 4 ++-- libensemble/gen_classes/sampling.py | 2 +- libensemble/gen_classes/surmise.py | 4 ++-- libensemble/generators.py | 6 +++--- .../tests/unit_tests/RENAME_test_persistent_aposmm.py | 6 +++--- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index 17caa6f4c..d49832730 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -14,7 +14,7 @@ class APOSMM(LibensembleGenThreadInterfacer): """ def __init__( - self, gen_specs: dict = {}, History: npt.NDArray = [], persis_info: dict = {}, libE_info: dict = {}, **kwargs + self, History: npt.NDArray = [], persis_info: dict = {}, gen_specs: dict = {}, libE_info: dict = {}, **kwargs ) -> None: from libensemble.gen_funcs.persistent_aposmm import aposmm @@ -32,7 +32,7 @@ def __init__( if not persis_info: persis_info = add_unique_random_streams({}, 4, seed=4321)[1] persis_info["nworkers"] = 4 - super().__init__(gen_specs, History, persis_info, libE_info, **kwargs) + super().__init__(History, persis_info, gen_specs, libE_info, **kwargs) self.all_local_minima = [] self.results_idx = 0 self.last_ask = None diff --git a/libensemble/gen_classes/sampling.py b/libensemble/gen_classes/sampling.py index d11998e11..dd347db51 100644 --- a/libensemble/gen_classes/sampling.py +++ b/libensemble/gen_classes/sampling.py @@ -32,7 +32,7 @@ class UniformSample(SampleBase): """ def __init__(self, _=[], persis_info={}, gen_specs={}, libE_info=None, **kwargs): - super().__init__(gen_specs, _, persis_info, libE_info, **kwargs) + super().__init__(_, persis_info, gen_specs, libE_info, **kwargs) self._get_user_params(self.gen_specs["user"]) self.gen_specs["out"] = [("x", float, (self.n,))] diff --git a/libensemble/gen_classes/surmise.py b/libensemble/gen_classes/surmise.py index 3e1810f98..b62cd20dc 100644 --- a/libensemble/gen_classes/surmise.py +++ b/libensemble/gen_classes/surmise.py @@ -14,14 +14,14 @@ class Surmise(LibensembleGenThreadInterfacer): """ def __init__( - self, gen_specs: dict, History: npt.NDArray = [], persis_info: dict = {}, libE_info: dict = {} + self, History: npt.NDArray = [], persis_info: dict = {}, gen_specs: dict = {}, libE_info: dict = {} ) -> None: from libensemble.gen_funcs.persistent_surmise_calib import surmise_calib gen_specs["gen_f"] = surmise_calib if ("sim_id", int) not in gen_specs["out"]: gen_specs["out"].append(("sim_id", int)) - super().__init__(gen_specs, History, persis_info, libE_info) + super().__init__(History, persis_info, gen_specs, libE_info) self.sim_id_index = 0 self.all_cancels = [] diff --git a/libensemble/generators.py b/libensemble/generators.py index b61ba1099..5e9d957b4 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -92,7 +92,7 @@ class LibensembleGenerator(Generator): """ def __init__( - self, gen_specs: dict = {}, History: npt.NDArray = [], persis_info: dict = {}, libE_info: dict = {}, **kwargs + self, History: npt.NDArray = [], persis_info: dict = {}, gen_specs: dict = {}, libE_info: dict = {}, **kwargs ): self.gen_specs = gen_specs if len(kwargs) > 0: # so user can specify gen-specific parameters as kwargs to constructor @@ -126,9 +126,9 @@ class LibensembleGenThreadInterfacer(LibensembleGenerator): """ def __init__( - self, gen_specs: dict, History: npt.NDArray = [], persis_info: dict = {}, libE_info: dict = {}, **kwargs + self, History: npt.NDArray = [], persis_info: dict = {}, gen_specs: dict = {}, libE_info: dict = {}, **kwargs ) -> None: - super().__init__(gen_specs, History, persis_info, libE_info, **kwargs) + super().__init__(History, persis_info, gen_specs, libE_info, **kwargs) self.gen_f = gen_specs["gen_f"] self.History = History self.persis_info = persis_info diff --git a/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py b/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py index 11cad7c63..9bc097a18 100644 --- a/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py +++ b/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py @@ -203,7 +203,7 @@ def test_asktell_with_persistent_aposmm(): }, } - my_APOSMM = APOSMM(gen_specs) + my_APOSMM = APOSMM(gen_specs=gen_specs) my_APOSMM.setup() initial_sample = my_APOSMM.ask(100) @@ -211,7 +211,7 @@ def test_asktell_with_persistent_aposmm(): eval_max = 2000 for point in initial_sample: - point["f"] = six_hump_camel_func(point["x"]) + point["f"] = six_hump_camel_func(np.array([point["x0"], point["x1"]])) total_evals += 1 my_APOSMM.tell(initial_sample) @@ -225,7 +225,7 @@ def test_asktell_with_persistent_aposmm(): for m in detected_minima: potential_minima.append(m) for point in sample: - point["f"] = six_hump_camel_func(point["x"]) + point["f"] = six_hump_camel_func(np.array([point["x0"], point["x1"]])) total_evals += 1 my_APOSMM.tell(sample) H, persis_info, exit_code = my_APOSMM.final_tell(list_dicts_to_np(sample)) # final_tell currently requires numpy From 74661007e1a271f6a427c747ab9ac2164cf2be77 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 21 Aug 2024 14:55:30 -0500 Subject: [PATCH 195/288] better detecting of combinable names, by stripping out the numeric suffix, instead of just checking if last char is digit. better decide output numpy array type for strings --- libensemble/tests/unit_tests/test_asktell.py | 29 ++++++++++++++++++++ libensemble/utils/misc.py | 17 ++++++++---- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/libensemble/tests/unit_tests/test_asktell.py b/libensemble/tests/unit_tests/test_asktell.py index 660e19ae8..6ff789356 100644 --- a/libensemble/tests/unit_tests/test_asktell.py +++ b/libensemble/tests/unit_tests/test_asktell.py @@ -38,5 +38,34 @@ def test_asktell_sampling_and_utils(): assert value == out_np["x"][i][j] +def test_additional_converts(): + from libensemble.utils.misc import list_dicts_to_np + + # test list_dicts_to_np on a weirdly formatted dictionary + out_np = list_dicts_to_np( + [ + { + "x0": "abcd", + "x1": "efgh", + "y": 56, + "z0": 1, + "z1": 2, + "z2": 3, + "z3": 4, + "z4": 5, + "z5": 6, + "z6": 7, + "z7": 8, + "z8": 9, + "z9": 10, + "z10": 11, + } + ] + ) + + assert all([i in ("x", "y", "z") for i in out_np.dtype.names]) + + if __name__ == "__main__": test_asktell_sampling_and_utils() + test_additional_converts() diff --git a/libensemble/utils/misc.py b/libensemble/utils/misc.py index 878bc1dff..f7b2b3737 100644 --- a/libensemble/utils/misc.py +++ b/libensemble/utils/misc.py @@ -82,17 +82,21 @@ def specs_checker_setattr(obj, key, value): def _decide_dtype(name: str, entry, size: int) -> tuple: + if isinstance(entry, str): + output_type = "U" + str(len(entry) + 1) + else: + output_type = type(entry) if size == 1 or not size: - return (name, type(entry)) + return (name, output_type) else: - return (name, type(entry), (size,)) + return (name, output_type, (size,)) def _combine_names(names: list) -> list: - """combine fields with same name *except* for final digit""" + """combine fields with same name *except* for final digits""" # how many final digits could possibly be in each name? # do we have to iterate through negative-indexes until we reach a non-digit? - return list(set(i[:-1] if i[-1].isdigit() else i for i in names)) + return list(set(i.rstrip("0123456789") for i in names)) def list_dicts_to_np(list_dicts: list) -> npt.NDArray: @@ -103,7 +107,8 @@ def list_dicts_to_np(list_dicts: list) -> npt.NDArray: new_dtype_names = _combine_names([i for i in first.keys()]) # -> ['x', 'y'] combinable_names = [] # [['x0', 'x1'], ['y0', 'y1', 'y2'], []] for name in new_dtype_names: - combinable_names.append([i for i in first.keys() if i[:-1] == name]) + combinable_group = [i for i in first.keys() if i.rstrip("0123456789") == name] + combinable_names.append(combinable_group) new_dtype = [] @@ -120,7 +125,7 @@ def list_dicts_to_np(list_dicts: list) -> npt.NDArray: for i, group in enumerate(combinable_names): new_dtype_name = new_dtype_names[i] for j, input_dict in enumerate(list_dicts): - if not len(group): # only a single name, e.g. local_pt + if len(group) == 1: # only a single name, e.g. local_pt out[new_dtype_name][j] = input_dict[new_dtype_name] else: # combinable names detected, e.g. x0, x1 out[new_dtype_name][j] = tuple([input_dict[name] for name in group]) From 751de5e8c2c1849f585ed38ccc9c65313b9260ba Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 21 Aug 2024 15:51:52 -0500 Subject: [PATCH 196/288] deal with keys that end with integers, but aren't similar to any other keys. e.g. {"co2": 12} --- libensemble/tests/unit_tests/test_asktell.py | 3 +- libensemble/utils/misc.py | 30 +++++++++++++------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/libensemble/tests/unit_tests/test_asktell.py b/libensemble/tests/unit_tests/test_asktell.py index 6ff789356..dbdc4148d 100644 --- a/libensemble/tests/unit_tests/test_asktell.py +++ b/libensemble/tests/unit_tests/test_asktell.py @@ -59,11 +59,12 @@ def test_additional_converts(): "z8": 9, "z9": 10, "z10": 11, + "a0": "B", } ] ) - assert all([i in ("x", "y", "z") for i in out_np.dtype.names]) + assert all([i in ("x", "y", "z", "a0") for i in out_np.dtype.names]) if __name__ == "__main__": diff --git a/libensemble/utils/misc.py b/libensemble/utils/misc.py index f7b2b3737..659a97440 100644 --- a/libensemble/utils/misc.py +++ b/libensemble/utils/misc.py @@ -94,9 +94,18 @@ def _decide_dtype(name: str, entry, size: int) -> tuple: def _combine_names(names: list) -> list: """combine fields with same name *except* for final digits""" - # how many final digits could possibly be in each name? - # do we have to iterate through negative-indexes until we reach a non-digit? - return list(set(i.rstrip("0123456789") for i in names)) + + out_names = [] + stripped = list(i.rstrip("0123456789") for i in names) # ['x', 'x', y', 'z', 'a'] + for name in names: + stripped_name = name.rstrip("0123456789") + if stripped.count(stripped_name) > 1: # if name appears >= 1, will combine, don't keep int suffix + out_names.append(stripped_name) + else: + out_names.append(name) # name appears once, keep integer suffix, e.g. "co2" + + # intending [x, y, z, a0] from [x0, x1, y, z0, z1, z2, z3, a0] + return list(set(out_names)) def list_dicts_to_np(list_dicts: list) -> npt.NDArray: @@ -105,20 +114,21 @@ def list_dicts_to_np(list_dicts: list) -> npt.NDArray: first = list_dicts[0] # for determining dtype of output np array new_dtype_names = _combine_names([i for i in first.keys()]) # -> ['x', 'y'] - combinable_names = [] # [['x0', 'x1'], ['y0', 'y1', 'y2'], []] - for name in new_dtype_names: + combinable_names = [] # [['x0', 'x1'], ['y0', 'y1', 'y2'], ['z']] + for name in new_dtype_names: # is this a necessary search over the keys again? we did it earlier... combinable_group = [i for i in first.keys() if i.rstrip("0123456789") == name] - combinable_names.append(combinable_group) + if len(combinable_group) > 1: # multiple similar names, e.g. x0, x1 + combinable_names.append(combinable_group) + else: # single name, e.g. local_pt, a0 *AS LONG AS THERE ISNT AN A1* + combinable_names.append([name]) new_dtype = [] + # another loop over names, there's probably a more elegant way, but my brain is fried for i, entry in enumerate(combinable_names): name = new_dtype_names[i] size = len(combinable_names[i]) - if len(entry): # combinable names detected, e.g. x0, x1 - new_dtype.append(_decide_dtype(name, first[entry[0]], size)) - else: # only a single name, e.g. local_pt - new_dtype.append(_decide_dtype(name, first[name], size)) + new_dtype.append(_decide_dtype(name, first[entry[0]], size)) out = np.zeros(len(list_dicts), dtype=new_dtype) From 18e70794cfd24bd6f90c3ee47029651881833003 Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 22 Aug 2024 14:49:21 -0500 Subject: [PATCH 197/288] keyword assignment of gen_specs to Surmise --- .../test_persistent_surmise_killsims_asktell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libensemble/tests/regression_tests/test_persistent_surmise_killsims_asktell.py b/libensemble/tests/regression_tests/test_persistent_surmise_killsims_asktell.py index 842573de9..9071e80d4 100644 --- a/libensemble/tests/regression_tests/test_persistent_surmise_killsims_asktell.py +++ b/libensemble/tests/regression_tests/test_persistent_surmise_killsims_asktell.py @@ -126,7 +126,7 @@ } persis_info = add_unique_random_streams({}, nworkers + 1) - gen_specs["generator"] = Surmise(gen_specs, persis_info=persis_info) + gen_specs["generator"] = Surmise(gen_specs=gen_specs, persis_info=persis_info) exit_criteria = {"sim_max": max_evals} From a34d589e363cd36541abda1d60fe1cf6f4ae9b00 Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 22 Aug 2024 15:44:40 -0500 Subject: [PATCH 198/288] forgot another keyword surmise assignment --- libensemble/tests/regression_tests/test_asktell_surmise.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libensemble/tests/regression_tests/test_asktell_surmise.py b/libensemble/tests/regression_tests/test_asktell_surmise.py index a4e5d9ae9..d0aa5310c 100644 --- a/libensemble/tests/regression_tests/test_asktell_surmise.py +++ b/libensemble/tests/regression_tests/test_asktell_surmise.py @@ -80,7 +80,7 @@ } persis_info = add_unique_random_streams({}, 5) - surmise = Surmise(gen_specs, persis_info=persis_info[1]) # we add sim_id as a field to gen_specs["out"] + surmise = Surmise(gen_specs=gen_specs, persis_info=persis_info[1]) # we add sim_id as a field to gen_specs["out"] surmise.setup() initial_sample = surmise.ask() From 5f33724ecf6ae5f2a86d39e48dd4f61d0cafaa32 Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 26 Aug 2024 11:36:30 -0500 Subject: [PATCH 199/288] add unit test for awkward H and checking routine from shuds, add case for np_to_list_dicts to unpack length-1 arrays/lists, into scalars --- libensemble/tests/unit_tests/test_asktell.py | 38 ++++++++++++++++++-- libensemble/utils/misc.py | 5 ++- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/libensemble/tests/unit_tests/test_asktell.py b/libensemble/tests/unit_tests/test_asktell.py index dbdc4148d..ed25ac7bb 100644 --- a/libensemble/tests/unit_tests/test_asktell.py +++ b/libensemble/tests/unit_tests/test_asktell.py @@ -4,6 +4,24 @@ from libensemble.utils.misc import list_dicts_to_np +def _check_conversion(H, npp): + + for field in H.dtype.names: + print(f"Comparing {field}: {H[field]} {npp[field]}") + + if isinstance(H[field], np.ndarray): + assert np.array_equal(H[field], npp[field]), f"Mismatch found in field {field}" + + elif isinstance(H[field], str) and isinstance(npp[field], str): + assert H[field] == npp[field], f"Mismatch found in field {field}" + + elif np.isscalar(H[field]) and np.isscalar(npp[field]): + assert np.isclose(H[field], npp[field]), f"Mismatch found in field {field}" + + else: + raise TypeError(f"Unhandled or mismatched types in field {field}: {type(H[field])} vs {type(npp[field])}") + + def test_asktell_sampling_and_utils(): from libensemble.gen_classes.sampling import UniformSample @@ -38,10 +56,12 @@ def test_asktell_sampling_and_utils(): assert value == out_np["x"][i][j] -def test_additional_converts(): +def test_awkward_list_dict(): from libensemble.utils.misc import list_dicts_to_np # test list_dicts_to_np on a weirdly formatted dictionary + # Unfortunately, we're not really checking against some original + # libE-styled source of truth, like H. out_np = list_dicts_to_np( [ { @@ -67,6 +87,20 @@ def test_additional_converts(): assert all([i in ("x", "y", "z", "a0") for i in out_np.dtype.names]) +def test_awkward_H(): + from libensemble.utils.misc import list_dicts_to_np, np_to_list_dicts + + dtype = [("a", "i4"), ("x", "f4", (3,)), ("y", "f4", (1,)), ("z", "f4", (12,)), ("greeting", "U10"), ("co2", "f8")] + H = np.zeros(2, dtype=dtype) + H[0] = (1, [1.1, 2.2, 3.3], [10.1], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], "hello", "1.23") + H[1] = (2, [4.4, 5.5, 6.6], [11.1], [51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62], "goodbye", "2.23") + + list_dicts = np_to_list_dicts(H) + npp = list_dicts_to_np(list_dicts) + _check_conversion(H, npp) + + if __name__ == "__main__": test_asktell_sampling_and_utils() - test_additional_converts() + test_awkward_list_dict() + test_awkward_H() diff --git a/libensemble/utils/misc.py b/libensemble/utils/misc.py index 659a97440..1e03beab6 100644 --- a/libensemble/utils/misc.py +++ b/libensemble/utils/misc.py @@ -150,9 +150,12 @@ def np_to_list_dicts(array: npt.NDArray) -> List[dict]: for row in array: new_dict = {} for field in row.dtype.names: - if hasattr(row[field], "__len__") and len(row[field]) > 1: + # non-string arrays, lists, etc. + if hasattr(row[field], "__len__") and len(row[field]) > 1 and not isinstance(row[field], str): for i, x in enumerate(row[field]): new_dict[field + str(i)] = x + elif hasattr(row[field], "__len__") and len(row[field]) == 1: # single-entry arrays, lists, etc. + new_dict[field] = row[field][0] # will still work on single-char strings else: new_dict[field] = row[field] out.append(new_dict) From 41c16b7c79d34ecd159f36c17da7da23255b6dee Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 26 Aug 2024 12:17:48 -0500 Subject: [PATCH 200/288] add optional dtype argument for list_dicts_to_np to preempt "dtype discovery" routine. formatting --- libensemble/tests/unit_tests/test_asktell.py | 45 ++++++++++---------- libensemble/utils/misc.py | 18 ++++---- 2 files changed, 33 insertions(+), 30 deletions(-) diff --git a/libensemble/tests/unit_tests/test_asktell.py b/libensemble/tests/unit_tests/test_asktell.py index ed25ac7bb..9e60550e8 100644 --- a/libensemble/tests/unit_tests/test_asktell.py +++ b/libensemble/tests/unit_tests/test_asktell.py @@ -62,27 +62,28 @@ def test_awkward_list_dict(): # test list_dicts_to_np on a weirdly formatted dictionary # Unfortunately, we're not really checking against some original # libE-styled source of truth, like H. - out_np = list_dicts_to_np( - [ - { - "x0": "abcd", - "x1": "efgh", - "y": 56, - "z0": 1, - "z1": 2, - "z2": 3, - "z3": 4, - "z4": 5, - "z5": 6, - "z6": 7, - "z7": 8, - "z8": 9, - "z9": 10, - "z10": 11, - "a0": "B", - } - ] - ) + + weird_list_dict = [ + { + "x0": "abcd", + "x1": "efgh", + "y": 56, + "z0": 1, + "z1": 2, + "z2": 3, + "z3": 4, + "z4": 5, + "z5": 6, + "z6": 7, + "z7": 8, + "z8": 9, + "z9": 10, + "z10": 11, + "a0": "B", + } + ] + + out_np = list_dicts_to_np(weird_list_dict) assert all([i in ("x", "y", "z", "a0") for i in out_np.dtype.names]) @@ -96,7 +97,7 @@ def test_awkward_H(): H[1] = (2, [4.4, 5.5, 6.6], [11.1], [51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62], "goodbye", "2.23") list_dicts = np_to_list_dicts(H) - npp = list_dicts_to_np(list_dicts) + npp = list_dicts_to_np(list_dicts, dtype=dtype) _check_conversion(H, npp) diff --git a/libensemble/utils/misc.py b/libensemble/utils/misc.py index 1e03beab6..d242edf65 100644 --- a/libensemble/utils/misc.py +++ b/libensemble/utils/misc.py @@ -108,7 +108,7 @@ def _combine_names(names: list) -> list: return list(set(out_names)) -def list_dicts_to_np(list_dicts: list) -> npt.NDArray: +def list_dicts_to_np(list_dicts: list, dtype: list = None) -> npt.NDArray: if list_dicts is None: return None @@ -122,15 +122,17 @@ def list_dicts_to_np(list_dicts: list) -> npt.NDArray: else: # single name, e.g. local_pt, a0 *AS LONG AS THERE ISNT AN A1* combinable_names.append([name]) - new_dtype = [] + if dtype is None: + dtype = [] - # another loop over names, there's probably a more elegant way, but my brain is fried - for i, entry in enumerate(combinable_names): - name = new_dtype_names[i] - size = len(combinable_names[i]) - new_dtype.append(_decide_dtype(name, first[entry[0]], size)) + if not len(dtype): + # another loop over names, there's probably a more elegant way, but my brain is fried + for i, entry in enumerate(combinable_names): + name = new_dtype_names[i] + size = len(combinable_names[i]) + dtype.append(_decide_dtype(name, first[entry[0]], size)) - out = np.zeros(len(list_dicts), dtype=new_dtype) + out = np.zeros(len(list_dicts), dtype=dtype) for i, group in enumerate(combinable_names): new_dtype_name = new_dtype_names[i] From 48604287b99c207a9c8dca012598abf3d9ec2a80 Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 26 Aug 2024 12:51:23 -0500 Subject: [PATCH 201/288] replace _to_array with list_dicts_to_np with dtype parameter. list_dicts_to_np passes through input as-is if its not a list (already numpy, no conversion necessary. _to_array did this previously) --- libensemble/utils/misc.py | 3 +++ libensemble/utils/runners.py | 21 ++++----------------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/libensemble/utils/misc.py b/libensemble/utils/misc.py index d242edf65..34b7a0931 100644 --- a/libensemble/utils/misc.py +++ b/libensemble/utils/misc.py @@ -112,6 +112,9 @@ def list_dicts_to_np(list_dicts: list, dtype: list = None) -> npt.NDArray: if list_dicts is None: return None + if not isinstance(list_dicts, list): # presumably already a numpy array, conversion not necessary + return list_dicts + first = list_dicts[0] # for determining dtype of output np array new_dtype_names = _combine_names([i for i in first.keys()]) # -> ['x', 'y'] combinable_names = [] # [['x0', 'x1'], ['y0', 'y1', 'y2'], ['z']] diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index d688a427e..fe9a9fa2a 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -4,14 +4,13 @@ import time from typing import Optional -import numpy as np import numpy.typing as npt from libensemble.comms.comms import QCommThread from libensemble.generators import LibensembleGenerator, LibensembleGenThreadInterfacer from libensemble.message_numbers import EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG from libensemble.tools.persistent_support import PersistentSupport -from libensemble.utils.misc import np_to_list_dicts +from libensemble.utils.misc import list_dicts_to_np, np_to_list_dicts logger = logging.getLogger(__name__) @@ -107,22 +106,9 @@ def __init__(self, specs): super().__init__(specs) self.gen = specs.get("generator") - def _to_array(self, x: list) -> npt.NDArray: - """fast-cast list-of-dicts to NumPy array""" - if isinstance(x, list) and len(x) and isinstance(x[0], dict): - arr = np.zeros(len(x), dtype=self.specs["out"]) - for i in range(len(x)): - for key in x[0].keys(): - arr[i][key] = x[i][key] - return arr - return x - def _get_points_updates(self, batch_size: int) -> (npt.NDArray, npt.NDArray): # no ask_updates on external gens - return ( - self._to_array(self.gen.ask(batch_size)), - None, - ) + return (list_dicts_to_np(self.gen.ask(batch_size), dtype=self.gen_specs["out"]), None) def _convert_tell(self, x: npt.NDArray) -> list: self.gen.tell(np_to_list_dicts(x)) @@ -155,7 +141,8 @@ def _persistent_result(self, calc_in, persis_info, libE_info): self.gen.libE_info = libE_info if self.gen.thread is None: self.gen.setup() # maybe we're reusing a live gen from a previous run - H_out = self._to_array(self._get_initial_ask(libE_info)) + # libE gens will hit the following line, but list_dicts_to_np will passthrough if the output is a numpy array + H_out = list_dicts_to_np(self._get_initial_ask(libE_info), dtype=self.specs["out"]) tag, Work, H_in = self.ps.send_recv(H_out) # evaluate the initial sample final_H_in = self._start_generator_loop(tag, Work, H_in) return self.gen.final_tell(final_H_in), FINISHED_PERSISTENT_GEN_TAG From ced8992b3bd8bbd93d8351a1c5fad7f0e1918911 Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 26 Aug 2024 13:10:59 -0500 Subject: [PATCH 202/288] fix --- libensemble/utils/runners.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index fe9a9fa2a..1d94fa097 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -108,7 +108,7 @@ def __init__(self, specs): def _get_points_updates(self, batch_size: int) -> (npt.NDArray, npt.NDArray): # no ask_updates on external gens - return (list_dicts_to_np(self.gen.ask(batch_size), dtype=self.gen_specs["out"]), None) + return (list_dicts_to_np(self.gen.ask(batch_size), dtype=self.specs["out"]), None) def _convert_tell(self, x: npt.NDArray) -> list: self.gen.tell(np_to_list_dicts(x)) From 4261ca889ad99d5c1aaa723f81ec9d62ecaef4ed Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 29 Aug 2024 14:33:05 -0500 Subject: [PATCH 203/288] LibensembleGenerator can provide matching dtype for list_dicts_to_np, but its only necessary within the ask() --- libensemble/generators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 5e9d957b4..9fa450123 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -117,7 +117,7 @@ def ask(self, num_points: Optional[int] = 0) -> List[dict]: def tell(self, results: List[dict]) -> None: """Send the results of evaluations to the generator.""" - self.tell_numpy(list_dicts_to_np(results)) + self.tell_numpy(list_dicts_to_np(results), dtype=self.gen_specs.get("out")) class LibensembleGenThreadInterfacer(LibensembleGenerator): From 460bbe346dc0f9530275b3a3a47f3b88a318853c Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 29 Aug 2024 15:51:52 -0500 Subject: [PATCH 204/288] fix --- libensemble/generators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 9fa450123..74c8682e1 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -117,7 +117,7 @@ def ask(self, num_points: Optional[int] = 0) -> List[dict]: def tell(self, results: List[dict]) -> None: """Send the results of evaluations to the generator.""" - self.tell_numpy(list_dicts_to_np(results), dtype=self.gen_specs.get("out")) + self.tell_numpy(list_dicts_to_np(results, dtype=self.gen_specs.get("out"))) class LibensembleGenThreadInterfacer(LibensembleGenerator): From 7fdd8a662845900636c9390b4c0040f7092e3e64 Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 29 Aug 2024 15:55:25 -0500 Subject: [PATCH 205/288] ahhhh, just gen_specs['out']'s dtype isn't sufficient. persis_in, describing the names of the fields, decides what fields are passed in, but their "actual datatypes" come from the sim / sim_specs --- libensemble/generators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 74c8682e1..70eac32e1 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -117,7 +117,7 @@ def ask(self, num_points: Optional[int] = 0) -> List[dict]: def tell(self, results: List[dict]) -> None: """Send the results of evaluations to the generator.""" - self.tell_numpy(list_dicts_to_np(results, dtype=self.gen_specs.get("out"))) + self.tell_numpy(list_dicts_to_np(results)) # OH, we need the union of sim_specs.out and gen_specs.out class LibensembleGenThreadInterfacer(LibensembleGenerator): From 69b0584cc9282ca28cbb147a4a8f3e9912f6029f Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 12 Sep 2024 12:33:04 -0500 Subject: [PATCH 206/288] removing hardcoded gen_specs.out, removing hardcoded persis_info.nworkers, use gen_specs.get("out") so if it isnt provided, the dtype discovery process commences --- libensemble/gen_classes/aposmm.py | 3 +-- libensemble/gen_classes/sampling.py | 2 -- libensemble/generators.py | 1 - libensemble/utils/runners.py | 4 ++-- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index d49832730..108282e07 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -30,8 +30,7 @@ def __init__( ] 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({}, 4, seed=4321)[1] - persis_info["nworkers"] = 4 + persis_info = add_unique_random_streams({}, 2, seed=4321)[1] super().__init__(History, persis_info, gen_specs, libE_info, **kwargs) self.all_local_minima = [] self.results_idx = 0 diff --git a/libensemble/gen_classes/sampling.py b/libensemble/gen_classes/sampling.py index dd347db51..166286482 100644 --- a/libensemble/gen_classes/sampling.py +++ b/libensemble/gen_classes/sampling.py @@ -34,7 +34,6 @@ class UniformSample(SampleBase): def __init__(self, _=[], persis_info={}, gen_specs={}, libE_info=None, **kwargs): super().__init__(_, persis_info, gen_specs, libE_info, **kwargs) self._get_user_params(self.gen_specs["user"]) - self.gen_specs["out"] = [("x", float, (self.n,))] def ask_numpy(self, n_trials): H_o = np.zeros(n_trials, dtype=self.gen_specs["out"]) @@ -60,7 +59,6 @@ def __init__(self, _, persis_info, gen_specs, libE_info=None, **kwargs): self.gen_specs = gen_specs self.persis_info = persis_info self._get_user_params(self.gen_specs["user"]) - self.gen_specs["out"] = [("x", float, (self.n,))] def ask(self, n_trials): H_o = [] diff --git a/libensemble/generators.py b/libensemble/generators.py index 70eac32e1..37b974139 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -99,7 +99,6 @@ def __init__( self.gen_specs["user"] = kwargs if not persis_info: self.persis_info = add_unique_random_streams({}, 4, seed=4321)[1] - self.persis_info["nworkers"] = 4 else: self.persis_info = persis_info diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 1d94fa097..08d52a27e 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -108,7 +108,7 @@ def __init__(self, specs): def _get_points_updates(self, batch_size: int) -> (npt.NDArray, npt.NDArray): # no ask_updates on external gens - return (list_dicts_to_np(self.gen.ask(batch_size), dtype=self.specs["out"]), None) + return (list_dicts_to_np(self.gen.ask(batch_size), dtype=self.specs.get("out")), None) def _convert_tell(self, x: npt.NDArray) -> list: self.gen.tell(np_to_list_dicts(x)) @@ -142,7 +142,7 @@ def _persistent_result(self, calc_in, persis_info, libE_info): if self.gen.thread is None: self.gen.setup() # maybe we're reusing a live gen from a previous run # libE gens will hit the following line, but list_dicts_to_np will passthrough if the output is a numpy array - H_out = list_dicts_to_np(self._get_initial_ask(libE_info), dtype=self.specs["out"]) + H_out = list_dicts_to_np(self._get_initial_ask(libE_info), dtype=self.specs.get("out")) tag, Work, H_in = self.ps.send_recv(H_out) # evaluate the initial sample final_H_in = self._start_generator_loop(tag, Work, H_in) return self.gen.final_tell(final_H_in), FINISHED_PERSISTENT_GEN_TAG From 8c01ca95f76d1f9d1edb3c59333bcdb0c92c448d Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 12 Sep 2024 12:35:59 -0500 Subject: [PATCH 207/288] clarify a comment --- libensemble/generators.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 37b974139..b13bae31c 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -116,7 +116,9 @@ def ask(self, num_points: Optional[int] = 0) -> List[dict]: def tell(self, results: List[dict]) -> None: """Send the results of evaluations to the generator.""" - self.tell_numpy(list_dicts_to_np(results)) # OH, we need the union of sim_specs.out and gen_specs.out + 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. class LibensembleGenThreadInterfacer(LibensembleGenerator): From 4541d8afbdf45b3132fa881035b91ad6d7a200d2 Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 12 Sep 2024 14:15:44 -0500 Subject: [PATCH 208/288] as discussed, currently gen_specs['out'] must be provided to a gen instead of it deciding it for itself internally --- libensemble/tests/unit_tests/test_asktell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libensemble/tests/unit_tests/test_asktell.py b/libensemble/tests/unit_tests/test_asktell.py index 9e60550e8..fd80b8829 100644 --- a/libensemble/tests/unit_tests/test_asktell.py +++ b/libensemble/tests/unit_tests/test_asktell.py @@ -39,7 +39,7 @@ def test_asktell_sampling_and_utils(): assert len(gen.ask(10)) == 10 # Test initialization gen-specific keyword args - gen = UniformSample(lb=np.array([-3, -2]), ub=np.array([3, 2])) + gen = UniformSample(gen_specs=gen_specs, lb=np.array([-3, -2]), ub=np.array([3, 2])) assert len(gen.ask(10)) == 10 out_np = gen.ask_numpy(3) # should get numpy arrays, non-flattened From 0d7e1a372a8b5a86f53c21dad8da60e2d1be4203 Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 12 Sep 2024 17:37:31 -0500 Subject: [PATCH 209/288] specify gen_specs.out dtype to conversion in independent borehole-call --- .../regression_tests/test_asktell_surmise.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/libensemble/tests/regression_tests/test_asktell_surmise.py b/libensemble/tests/regression_tests/test_asktell_surmise.py index d0aa5310c..b8672b185 100644 --- a/libensemble/tests/regression_tests/test_asktell_surmise.py +++ b/libensemble/tests/regression_tests/test_asktell_surmise.py @@ -88,7 +88,9 @@ total_evals = 0 for point in initial_sample: - H_out, _a, _b = borehole(list_dicts_to_np([point]), {}, sim_specs, {"H_rows": np.array([point["sim_id"]])}) + H_out, _a, _b = borehole( + list_dicts_to_np([point], dtype=gen_specs["out"]), {}, sim_specs, {"H_rows": np.array([point["sim_id"]])} + ) point["f"] = H_out["f"][0] # some "bugginess" with output shape of array in simf total_evals += 1 @@ -99,7 +101,9 @@ next_sample, cancels = surmise.ask(), surmise.ask_updates() for point in next_sample: - H_out, _a, _b = borehole(list_dicts_to_np([point]), {}, sim_specs, {"H_rows": np.array([point["sim_id"]])}) + H_out, _a, _b = borehole( + list_dicts_to_np([point], dtype=gen_specs["out"]), {}, sim_specs, {"H_rows": np.array([point["sim_id"]])} + ) point["f"] = H_out["f"][0] total_evals += 1 @@ -109,7 +113,12 @@ while total_evals < max_evals: for point in sample: - H_out, _a, _b = borehole(list_dicts_to_np([point]), {}, sim_specs, {"H_rows": np.array([point["sim_id"]])}) + H_out, _a, _b = borehole( + list_dicts_to_np([point], dtype=gen_specs["out"]), + {}, + sim_specs, + {"H_rows": np.array([point["sim_id"]])}, + ) point["f"] = H_out["f"][0] total_evals += 1 surmise.tell([point]) From c7d1cb1595e865364c539eef1f8cc4e53b89e433 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 13 Sep 2024 07:37:42 -0500 Subject: [PATCH 210/288] dont assert cancelled sims in asktell surmise test (at this time) --- libensemble/tests/regression_tests/test_asktell_surmise.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libensemble/tests/regression_tests/test_asktell_surmise.py b/libensemble/tests/regression_tests/test_asktell_surmise.py index b8672b185..1afad75c3 100644 --- a/libensemble/tests/regression_tests/test_asktell_surmise.py +++ b/libensemble/tests/regression_tests/test_asktell_surmise.py @@ -133,4 +133,4 @@ H, persis_info, exit_code = surmise.final_tell(None) assert exit_code == FINISHED_PERSISTENT_GEN_TAG, "Standalone persistent_aposmm didn't exit correctly" - assert len(requested_canceled_sim_ids), "No cancellations sent by Surmise" + # assert len(requested_canceled_sim_ids), "No cancellations sent by Surmise" From 94de46f399ac1109494b871da990da96c9121952 Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 19 Sep 2024 14:08:37 -0500 Subject: [PATCH 211/288] slotting in variables/objectives into Generator abc. changes to subclasses coming in future --- libensemble/generators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index b13bae31c..88b80bb7a 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -50,7 +50,7 @@ def final_tell(self, results): """ @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. From 80df25fd814cc385b7b425cd5d157babf577f785 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 20 Sep 2024 15:45:14 -0500 Subject: [PATCH 212/288] try an indexing fix --- libensemble/gen_classes/gpCAM.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libensemble/gen_classes/gpCAM.py b/libensemble/gen_classes/gpCAM.py index 00e53c915..7894d2bd6 100644 --- a/libensemble/gen_classes/gpCAM.py +++ b/libensemble/gen_classes/gpCAM.py @@ -85,7 +85,7 @@ def ask_numpy(self, n_trials: int) -> npt.NDArray: def tell_numpy(self, calc_in: npt.NDArray) -> None: if calc_in is not None: 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)] + 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) From b5d8bcf515e348e52904c544102bed845b018226 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 20 Sep 2024 16:39:02 -0500 Subject: [PATCH 213/288] dont require an explicit "None" to shut down a threaded generator --- libensemble/generators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 88b80bb7a..5ba79dfcb 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -184,7 +184,7 @@ def tell_numpy(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: self.inbox.put((tag, None)) self.inbox.put((0, np.copy(results))) - def final_tell(self, results: npt.NDArray) -> (npt.NDArray, dict, int): + def final_tell(self, results: npt.NDArray = None) -> (npt.NDArray, dict, int): """Send any last results to the generator, and it to close down.""" self.tell_numpy(results, PERSIS_STOP) # conversion happens in tell return self.thread.result() From f52bf922b74d1816750f875660c41d5b0e396d08 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 25 Sep 2024 15:28:49 -0500 Subject: [PATCH 214/288] various internal logics and routines for buffering results passed back to APOSMM until either the entire initial sample is complete, or the subequent sample is --- libensemble/gen_classes/aposmm.py | 55 +++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index 108282e07..6a911cacf 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -5,6 +5,7 @@ from numpy import typing as npt from libensemble.generators import LibensembleGenThreadInterfacer +from libensemble.message_numbers import PERSIS_STOP from libensemble.tools import add_unique_random_streams @@ -28,21 +29,47 @@ def __init__( ("local_min", bool), ("local_pt", bool), ] - gen_specs["persis_in"] = ["x", "f", "local_pt", "sim_id", "sim_ended", "x_on_cube", "local_min"] + gen_specs["persis_in"] = ["x", "x_on_cube", "f", "local_pt", "sim_id", "sim_ended", "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) self.all_local_minima = [] - self.results_idx = 0 + self.ask_idx = 0 self.last_ask = None + self.last_ask_len = 0 + self.tell_buf = None + self.num_evals = 0 + self._told_initial_sample = False + + def _slot_in_data(self, results): + """Slot in libE_calc_in and trial data into corresponding array fields.""" + for field in ["f", "x", "x_on_cube", "sim_id", "local_pt"]: + self.tell_buf[field] = results[field] + + @property + def _array_size(self): + """Output array size must match either initial sample or N points to evaluate in parallel.""" + user = self.gen_specs["user"] + return user["initial_sample_size"] if not self._told_initial_sample else user["max_active_runs"] + + @property + def _enough_initial_sample(self): + """We're typically happy with at least 90% of the initial sample.""" + return self.num_evals > int(0.9 * self.gen_specs["user"]["initial_sample_size"]) + + @property + def _enough_subsequent_points(self): + """But we need to evaluate at least N points, for the N local-optimization processes.""" + return self.num_evals >= self.gen_specs["user"]["max_active_runs"] def ask_numpy(self, num_points: int = 0) -> npt.NDArray: """Request the next set of points to evaluate, as a NumPy array.""" if (self.last_ask is None) or ( - self.results_idx >= len(self.last_ask) + self.ask_idx >= len(self.last_ask) ): # haven't been asked yet, or all previously enqueued points have been "asked" - self.results_idx = 0 + self.ask_idx = 0 self.last_ask = super().ask_numpy(num_points) + self.last_ask_len = len(self.last_ask) if self.last_ask[ "local_min" ].any(): # filter out local minima rows, but they're cached in self.all_local_minima @@ -51,15 +78,31 @@ def ask_numpy(self, num_points: int = 0) -> npt.NDArray: self.last_ask = self.last_ask[~min_idxs] if num_points > 0: # we've been asked for a selection of the last ask results = np.copy( - self.last_ask[self.results_idx : self.results_idx + num_points] + self.last_ask[self.ask_idx : self.ask_idx + num_points] ) # if resetting last_ask later, results may point to "None" - self.results_idx += num_points + self.ask_idx += num_points return results results = np.copy(self.last_ask) self.results = results self.last_ask = None return results + def tell_numpy(self, results: npt.NDArray, tag) -> None: + if tag == PERSIS_STOP: + super().tell_numpy(results, tag) + return + if self.num_evals == 0: + self.tell_buf = np.zeros(self.last_ask_len, dtype=self.gen_specs["out"] + [("f", float)]) + self._slot_in_data(results) + self.num_evals += len(results) + if not self._told_initial_sample and self._enough_initial_sample: + super().tell_numpy(self.tell_buf, tag) + self._told_initial_sample = True + self.num_evals = 0 + elif self._told_initial_sample and self._enough_subsequent_points: + super().tell_numpy(self.tell_buf, tag) + self.num_evals = 0 + def ask_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) From 5434dfa7f1058e1d774d55578315623f07f2735b Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 25 Sep 2024 15:37:02 -0500 Subject: [PATCH 215/288] fixes --- libensemble/gen_classes/aposmm.py | 10 ++++------ .../test_persistent_aposmm_nlopt_asktell.py | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index 6a911cacf..0f9daf45b 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -5,7 +5,7 @@ from numpy import typing as npt from libensemble.generators import LibensembleGenThreadInterfacer -from libensemble.message_numbers import PERSIS_STOP +from libensemble.message_numbers import EVAL_GEN_TAG, PERSIS_STOP from libensemble.tools import add_unique_random_streams @@ -36,7 +36,6 @@ def __init__( self.all_local_minima = [] self.ask_idx = 0 self.last_ask = None - self.last_ask_len = 0 self.tell_buf = None self.num_evals = 0 self._told_initial_sample = False @@ -69,7 +68,6 @@ def ask_numpy(self, num_points: int = 0) -> npt.NDArray: ): # haven't been asked yet, or all previously enqueued points have been "asked" self.ask_idx = 0 self.last_ask = super().ask_numpy(num_points) - self.last_ask_len = len(self.last_ask) if self.last_ask[ "local_min" ].any(): # filter out local minima rows, but they're cached in self.all_local_minima @@ -87,12 +85,12 @@ def ask_numpy(self, num_points: int = 0) -> npt.NDArray: self.last_ask = None return results - def tell_numpy(self, results: npt.NDArray, tag) -> None: + def tell_numpy(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: if tag == PERSIS_STOP: - super().tell_numpy(results, tag) + super().tell_numpy(None, tag) return if self.num_evals == 0: - self.tell_buf = np.zeros(self.last_ask_len, dtype=self.gen_specs["out"] + [("f", float)]) + self.tell_buf = np.zeros(self._array_size, dtype=self.gen_specs["out"] + [("f", float)]) self._slot_in_data(results) self.num_evals += len(results) if not self._told_initial_sample and self._enough_initial_sample: diff --git a/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py b/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py index 684e015ec..dc44d820c 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py @@ -58,7 +58,7 @@ rk_const=0.5 * ((gamma(1 + (n / 2)) * 5) ** (1 / n)) / sqrt(pi), xtol_abs=1e-6, ftol_abs=1e-6, - max_active_runs=6, + max_active_runs=4, # should this match nworkers always? practically? lb=np.array([-3, -2]), ub=np.array([3, 2]), ) From 0ab048d4298c194236f1abd002e3c3d239ab89a0 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 25 Sep 2024 15:38:37 -0500 Subject: [PATCH 216/288] given that persis_info available to the aposmm thread needs nworkers...? do we assume thats the same as max_active_runs? --- libensemble/gen_classes/aposmm.py | 1 + 1 file changed, 1 insertion(+) diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index 0f9daf45b..7eca6b201 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -32,6 +32,7 @@ def __init__( gen_specs["persis_in"] = ["x", "x_on_cube", "f", "local_pt", "sim_id", "sim_ended", "local_min"] if not persis_info: persis_info = add_unique_random_streams({}, 2, seed=4321)[1] + persis_info["nworkers"] = gen_specs["user"]["max_active_runs"] # ?????????? super().__init__(History, persis_info, gen_specs, libE_info, **kwargs) self.all_local_minima = [] self.ask_idx = 0 From 7a9a2d869137ea0892b279ce9088e8fc72e08d24 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 25 Sep 2024 16:16:48 -0500 Subject: [PATCH 217/288] fix --- libensemble/gen_classes/aposmm.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index 7eca6b201..ef09e6780 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -32,8 +32,9 @@ def __init__( gen_specs["persis_in"] = ["x", "x_on_cube", "f", "local_pt", "sim_id", "sim_ended", "local_min"] if not persis_info: persis_info = add_unique_random_streams({}, 2, seed=4321)[1] - persis_info["nworkers"] = gen_specs["user"]["max_active_runs"] # ?????????? super().__init__(History, persis_info, gen_specs, libE_info, **kwargs) + if not self.persis_info.get("nworkers"): + self.persis_info["nworkers"] = gen_specs["user"]["max_active_runs"] # ?????????? self.all_local_minima = [] self.ask_idx = 0 self.last_ask = None From a68ffb8874145b9768ea275eed2273aed30c268e Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 27 Sep 2024 09:41:14 -0500 Subject: [PATCH 218/288] tiny fix --- libensemble/gen_classes/aposmm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index ef09e6780..3aac8863e 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -89,7 +89,7 @@ def ask_numpy(self, num_points: int = 0) -> npt.NDArray: def tell_numpy(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: if tag == PERSIS_STOP: - super().tell_numpy(None, tag) + super().tell_numpy(results, tag) return if self.num_evals == 0: self.tell_buf = np.zeros(self._array_size, dtype=self.gen_specs["out"] + [("f", float)]) From 5228711437cb8e9fe51c9ccddc531ae3ee6847b5 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 27 Sep 2024 10:03:17 -0500 Subject: [PATCH 219/288] tiny fix --- .../regression_tests/test_persistent_aposmm_nlopt_asktell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py b/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py index dc44d820c..5cbce5290 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py @@ -58,7 +58,7 @@ rk_const=0.5 * ((gamma(1 + (n / 2)) * 5) ** (1 / n)) / sqrt(pi), xtol_abs=1e-6, ftol_abs=1e-6, - max_active_runs=4, # should this match nworkers always? practically? + max_active_runs=workflow.nworkers, # should this match nworkers always? practically? lb=np.array([-3, -2]), ub=np.array([3, 2]), ) From 1ef58980d9e3d3b6d32f22bd9fcc7c3f056b5a2b Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 27 Sep 2024 12:15:00 -0500 Subject: [PATCH 220/288] undo some unneeded changes --- libensemble/gen_classes/aposmm.py | 2 +- libensemble/generators.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index 3aac8863e..ffea69323 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -29,7 +29,7 @@ def __init__( ("local_min", bool), ("local_pt", bool), ] - gen_specs["persis_in"] = ["x", "x_on_cube", "f", "local_pt", "sim_id", "sim_ended", "local_min"] + 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) diff --git a/libensemble/generators.py b/libensemble/generators.py index 5ba79dfcb..bd197f84d 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -50,7 +50,7 @@ def final_tell(self, results): """ @abstractmethod - def __init__(self, variables: dict[str, List[float]], objectives: dict[str, str], *args, **kwargs): + def __init__(self, *args, **kwargs): """ Initialize the Generator object on the user-side. Constants, class-attributes, and preparation goes here. From 8371d97e585bc2695e96c6a2d29d1a6484f0ddbf Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 27 Sep 2024 16:25:53 -0500 Subject: [PATCH 221/288] enormously ugly iterating over the buffering, tell_numpy process. gotta deal with getting a variable number of responses --- libensemble/gen_classes/aposmm.py | 34 ++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index ffea69323..b1a5df3fb 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -39,13 +39,23 @@ def __init__( self.ask_idx = 0 self.last_ask = None self.tell_buf = None - self.num_evals = 0 + 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.""" - for field in ["f", "x", "x_on_cube", "sim_id", "local_pt"]: - self.tell_buf[field] = results[field] + indexes = results["sim_id"] + fields = results.dtype.names + for j, ind in enumerate(indexes): + for field in fields: + if np.isscalar(results[field][j]) or results.dtype[field].hasobject: + self.tell_buf[field][ind] = results[field][j] + else: + field_size = len(results[field][j]) + if field_size == len(self.tell_buf[field][ind]): + self.tell_buf[field][ind] = results[field][j] + else: + self.tell_buf[field][ind][:field_size] = results[field][j] @property def _array_size(self): @@ -56,12 +66,12 @@ def _array_size(self): @property def _enough_initial_sample(self): """We're typically happy with at least 90% of the initial sample.""" - return self.num_evals > int(0.9 * self.gen_specs["user"]["initial_sample_size"]) + return self.n_buffd_results > int(0.9 * self.gen_specs["user"]["initial_sample_size"]) @property def _enough_subsequent_points(self): """But we need to evaluate at least N points, for the N local-optimization processes.""" - return self.num_evals >= self.gen_specs["user"]["max_active_runs"] + return self.n_buffd_results >= self.gen_specs["user"]["max_active_runs"] def ask_numpy(self, num_points: int = 0) -> npt.NDArray: """Request the next set of points to evaluate, as a NumPy array.""" @@ -88,20 +98,24 @@ def ask_numpy(self, num_points: int = 0) -> npt.NDArray: return results def tell_numpy(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: - if tag == PERSIS_STOP: + if results is None and tag == PERSIS_STOP: super().tell_numpy(results, tag) return - if self.num_evals == 0: + if len(results) == self._array_size: # DONT NEED TO COPY OVER IF THE INPUT ARRAY IS THE CORRECT SIZE + self._told_initial_sample = True # we definitely got an initial sample already if one matches + super().tell_numpy(results, tag) + return + if self.n_buffd_results == 0: self.tell_buf = np.zeros(self._array_size, dtype=self.gen_specs["out"] + [("f", float)]) self._slot_in_data(results) - self.num_evals += len(results) + self.n_buffd_results += len(results) if not self._told_initial_sample and self._enough_initial_sample: super().tell_numpy(self.tell_buf, tag) self._told_initial_sample = True - self.num_evals = 0 + self.n_buffd_results = 0 elif self._told_initial_sample and self._enough_subsequent_points: super().tell_numpy(self.tell_buf, tag) - self.num_evals = 0 + self.n_buffd_results = 0 def ask_updates(self) -> List[npt.NDArray]: """Request a list of NumPy arrays containing entries that have been identified as minima.""" From 3ebc467f0c16b73597aa2da72e2240ce8ecc9f5e Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 4 Oct 2024 12:50:58 -0500 Subject: [PATCH 222/288] making some attributes private --- libensemble/gen_classes/aposmm.py | 60 +++++++++++++++---------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index b1a5df3fb..151d29d87 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -36,10 +36,10 @@ def __init__( if not self.persis_info.get("nworkers"): self.persis_info["nworkers"] = gen_specs["user"]["max_active_runs"] # ?????????? self.all_local_minima = [] - self.ask_idx = 0 - self.last_ask = None - self.tell_buf = None - self.n_buffd_results = 0 + self._ask_idx = 0 + self._last_ask = None + self._tell_buf = None + self._n_buffd_results = 0 self._told_initial_sample = False def _slot_in_data(self, results): @@ -49,13 +49,13 @@ def _slot_in_data(self, results): for j, ind in enumerate(indexes): for field in fields: if np.isscalar(results[field][j]) or results.dtype[field].hasobject: - self.tell_buf[field][ind] = results[field][j] + self._tell_buf[field][ind] = results[field][j] else: field_size = len(results[field][j]) - if field_size == len(self.tell_buf[field][ind]): - self.tell_buf[field][ind] = results[field][j] + if field_size == len(self._tell_buf[field][ind]): + self._tell_buf[field][ind] = results[field][j] else: - self.tell_buf[field][ind][:field_size] = results[field][j] + self._tell_buf[field][ind][:field_size] = results[field][j] @property def _array_size(self): @@ -66,35 +66,35 @@ def _array_size(self): @property def _enough_initial_sample(self): """We're typically happy with at least 90% of the initial sample.""" - return self.n_buffd_results > int(0.9 * self.gen_specs["user"]["initial_sample_size"]) + return self._n_buffd_results > int(0.9 * self.gen_specs["user"]["initial_sample_size"]) @property def _enough_subsequent_points(self): """But we need to evaluate at least N points, for the N local-optimization processes.""" - return self.n_buffd_results >= self.gen_specs["user"]["max_active_runs"] + return self._n_buffd_results >= self.gen_specs["user"]["max_active_runs"] def ask_numpy(self, num_points: int = 0) -> npt.NDArray: """Request the next set of points to evaluate, as a NumPy array.""" - if (self.last_ask is None) or ( - self.ask_idx >= len(self.last_ask) + if (self._last_ask is None) or ( + self._ask_idx >= len(self._last_ask) ): # haven't been asked yet, or all previously enqueued points have been "asked" - self.ask_idx = 0 - self.last_ask = super().ask_numpy(num_points) - if self.last_ask[ + self._ask_idx = 0 + self._last_ask = super().ask_numpy(num_points) + if self._last_ask[ "local_min" ].any(): # filter out local minima rows, but they're cached in self.all_local_minima - min_idxs = self.last_ask["local_min"] - self.all_local_minima.append(self.last_ask[min_idxs]) - self.last_ask = self.last_ask[~min_idxs] + min_idxs = self._last_ask["local_min"] + self.all_local_minima.append(self._last_ask[min_idxs]) + self._last_ask = self._last_ask[~min_idxs] if num_points > 0: # we've been asked for a selection of the last ask results = np.copy( - self.last_ask[self.ask_idx : self.ask_idx + num_points] - ) # if resetting last_ask later, results may point to "None" - self.ask_idx += num_points + self._last_ask[self._ask_idx : self._ask_idx + num_points] + ) # if resetting _last_ask later, results may point to "None" + self._ask_idx += num_points return results - results = np.copy(self.last_ask) + results = np.copy(self._last_ask) self.results = results - self.last_ask = None + self._last_ask = None return results def tell_numpy(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: @@ -105,17 +105,17 @@ def tell_numpy(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: self._told_initial_sample = True # we definitely got an initial sample already if one matches super().tell_numpy(results, tag) return - if self.n_buffd_results == 0: - self.tell_buf = np.zeros(self._array_size, dtype=self.gen_specs["out"] + [("f", float)]) + if self._n_buffd_results == 0: + self._tell_buf = np.zeros(self._array_size, dtype=self.gen_specs["out"] + [("f", float)]) self._slot_in_data(results) - self.n_buffd_results += len(results) + self._n_buffd_results += len(results) if not self._told_initial_sample and self._enough_initial_sample: - super().tell_numpy(self.tell_buf, tag) + super().tell_numpy(self._tell_buf, tag) self._told_initial_sample = True - self.n_buffd_results = 0 + self._n_buffd_results = 0 elif self._told_initial_sample and self._enough_subsequent_points: - super().tell_numpy(self.tell_buf, tag) - self.n_buffd_results = 0 + super().tell_numpy(self._tell_buf, tag) + self._n_buffd_results = 0 def ask_updates(self) -> List[npt.NDArray]: """Request a list of NumPy arrays containing entries that have been identified as minima.""" From c2a2802d845ee8357580c5a753205b76cbc342fa Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 7 Oct 2024 10:40:39 -0500 Subject: [PATCH 223/288] comments, reorganizing tell_numpy as usual --- libensemble/gen_classes/aposmm.py | 47 ++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index 151d29d87..757af9fe2 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -48,14 +48,22 @@ def _slot_in_data(self, results): fields = results.dtype.names for j, ind in enumerate(indexes): for field in fields: - if np.isscalar(results[field][j]) or results.dtype[field].hasobject: - self._tell_buf[field][ind] = results[field][j] - else: - field_size = len(results[field][j]) - if field_size == len(self._tell_buf[field][ind]): + if not ind > len( + self._tell_buf[field] + ): # we got back an index e.g. 715, but our buffer is length e.g. 2 + if np.isscalar(results[field][j]) or results.dtype[field].hasobject: self._tell_buf[field][ind] = results[field][j] else: - self._tell_buf[field][ind][:field_size] = results[field][j] + field_size = len(results[field][j]) + if not ind > len( + self._tell_buf[field] + ): # we got back an index e.g. 715, but our buffer is length e.g. 2 + if field_size == len(self._tell_buf[field][ind]): + self._tell_buf[field][ind] = results[field][j] + else: + self._tell_buf[field][ind][:field_size] = results[field][j] + else: # we slot it back by enumeration, not sim_id + self._tell_buf[field][j] = results[field][j] @property def _array_size(self): @@ -65,8 +73,11 @@ def _array_size(self): @property def _enough_initial_sample(self): - """We're typically happy with at least 90% of the initial sample.""" - return self._n_buffd_results > int(0.9 * self.gen_specs["user"]["initial_sample_size"]) + """We're typically happy with at least 90% of the initial sample, or we've already told the initial sample""" + return ( + self._n_buffd_results > int(0.9 * self.gen_specs["user"]["initial_sample_size"]) + or self._told_initial_sample + ) @property def _enough_subsequent_points(self): @@ -98,24 +109,34 @@ def ask_numpy(self, num_points: int = 0) -> npt.NDArray: return results def tell_numpy(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: - if results is None and tag == PERSIS_STOP: - super().tell_numpy(results, tag) - return - if len(results) == self._array_size: # DONT NEED TO COPY OVER IF THE INPUT ARRAY IS THE CORRECT SIZE + if (results is None and tag == PERSIS_STOP) or len( + results + ) == self._array_size: # told to stop, by final_tell or libE self._told_initial_sample = True # we definitely got an initial sample already if one matches super().tell_numpy(results, tag) return - if self._n_buffd_results == 0: + + if ( + self._n_buffd_results == 0 + ): # now in Optimas; which prefers to give back chunks of initial_sample. So we buffer them self._tell_buf = np.zeros(self._array_size, dtype=self.gen_specs["out"] + [("f", float)]) + self._slot_in_data(results) self._n_buffd_results += len(results) + if not self._told_initial_sample and self._enough_initial_sample: super().tell_numpy(self._tell_buf, tag) self._told_initial_sample = True self._n_buffd_results = 0 + return + elif self._told_initial_sample and self._enough_subsequent_points: super().tell_numpy(self._tell_buf, tag) self._n_buffd_results = 0 + return + + else: # probably libE: given back smaller selection. but from alloc, so its ok? + super().tell_numpy(results, tag) def ask_updates(self) -> List[npt.NDArray]: """Request a list of NumPy arrays containing entries that have been identified as minima.""" From 5a2eb09062ff2fc21c130c6ac66f46149bfbb4b5 Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 7 Oct 2024 13:16:55 -0500 Subject: [PATCH 224/288] using gen_specs.batch_size and gen_specs.initial_batch_size to try covering for similar-to-optimas asks and tells --- libensemble/gen_classes/aposmm.py | 11 ++++------- .../test_persistent_aposmm_nlopt_asktell.py | 2 ++ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index 757af9fe2..435d70612 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -55,13 +55,10 @@ def _slot_in_data(self, results): self._tell_buf[field][ind] = results[field][j] else: field_size = len(results[field][j]) - if not ind > len( - self._tell_buf[field] - ): # we got back an index e.g. 715, but our buffer is length e.g. 2 - if field_size == len(self._tell_buf[field][ind]): - self._tell_buf[field][ind] = results[field][j] - else: - self._tell_buf[field][ind][:field_size] = results[field][j] + if field_size == len(self._tell_buf[field][ind]): + self._tell_buf[field][ind] = results[field][j] + else: + self._tell_buf[field][ind][:field_size] = results[field][j] else: # we slot it back by enumeration, not sim_id self._tell_buf[field][j] = results[field][j] diff --git a/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py b/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py index 5cbce5290..101759966 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py @@ -74,6 +74,8 @@ ("f", float), ], generator=aposmm, + batch_size=5, + initial_batch_size=10, user={"initial_sample_size": 100}, ) From aa8ad57000c6c661e45a8c96e9e953fc73636f98 Mon Sep 17 00:00:00 2001 From: jlnav Date: Tue, 8 Oct 2024 14:25:22 -0500 Subject: [PATCH 225/288] use base MPIRunner if detection fails, so KeyError doesnt occur? --- libensemble/executors/mpi_runner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libensemble/executors/mpi_runner.py b/libensemble/executors/mpi_runner.py index 1568ec343..654c447bf 100644 --- a/libensemble/executors/mpi_runner.py +++ b/libensemble/executors/mpi_runner.py @@ -21,7 +21,7 @@ def get_runner(mpi_runner_type, runner_name=None, platform_info=None): "msmpi": MSMPI_MPIRunner, "custom": MPIRunner, } - mpi_runner = mpi_runners[mpi_runner_type] + mpi_runner = mpi_runners.get(mpi_runner_type, MPIRunner) if runner_name is not None: runner = mpi_runner(run_command=runner_name, platform_info=platform_info) else: From 1eec392aca7f0a9ce8ff8cd5b47fb7339ac4aabb Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 10 Oct 2024 13:45:58 -0500 Subject: [PATCH 226/288] various fixes as usual, plus experimenting with running gen-on-process instead of Thread, to potentially prevent data mangling --- libensemble/gen_classes/aposmm.py | 19 +++++++------- .../gen_funcs/aposmm_localopt_support.py | 2 +- libensemble/generators.py | 26 +++++++++++++++---- 3 files changed, 31 insertions(+), 16 deletions(-) diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index 435d70612..507a25d67 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -60,7 +60,7 @@ def _slot_in_data(self, results): else: self._tell_buf[field][ind][:field_size] = results[field][j] else: # we slot it back by enumeration, not sim_id - self._tell_buf[field][j] = results[field][j] + self._tell_buf[field][self._n_buffd_results] = results[field][j] @property def _array_size(self): @@ -71,10 +71,7 @@ def _array_size(self): @property def _enough_initial_sample(self): """We're typically happy with at least 90% of the initial sample, or we've already told the initial sample""" - return ( - self._n_buffd_results > int(0.9 * self.gen_specs["user"]["initial_sample_size"]) - or self._told_initial_sample - ) + return (self._n_buffd_results > self.gen_specs["user"]["initial_sample_size"] - 1) or self._told_initial_sample @property def _enough_subsequent_points(self): @@ -118,19 +115,21 @@ def tell_numpy(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: ): # now in Optimas; which prefers to give back chunks of initial_sample. So we buffer them self._tell_buf = np.zeros(self._array_size, dtype=self.gen_specs["out"] + [("f", float)]) - self._slot_in_data(results) + self._slot_in_data(np.copy(results)) self._n_buffd_results += len(results) if not self._told_initial_sample and self._enough_initial_sample: - super().tell_numpy(self._tell_buf, tag) + self._tell_buf.sort(order="sim_id") + print(self._tell_buf) + super().tell_numpy(np.copy(self._tell_buf), tag) self._told_initial_sample = True self._n_buffd_results = 0 - return elif self._told_initial_sample and self._enough_subsequent_points: - super().tell_numpy(self._tell_buf, tag) + self._tell_buf.sort(order="sim_id") + print(self._tell_buf) + super().tell_numpy(np.copy(self._tell_buf), tag) self._n_buffd_results = 0 - return else: # probably libE: given back smaller selection. but from alloc, so its ok? super().tell_numpy(results, tag) diff --git a/libensemble/gen_funcs/aposmm_localopt_support.py b/libensemble/gen_funcs/aposmm_localopt_support.py index 0bd1b9f3c..499bc38d5 100644 --- a/libensemble/gen_funcs/aposmm_localopt_support.py +++ b/libensemble/gen_funcs/aposmm_localopt_support.py @@ -683,7 +683,7 @@ def put_set_wait_get(x, comm_queue, parent_can_read, child_can_read, user_specs) if user_specs.get("periodic"): assert np.allclose(x % 1, values[0] % 1, rtol=1e-15, atol=1e-15), "The point I gave is not the point I got back" else: - assert np.allclose(x, values[0], rtol=1e-15, atol=1e-15), "The point I gave is not the point I got back" + assert np.allclose(x, values[0], rtol=1e-8, atol=1e-8), "The point I gave is not the point I got back" return values diff --git a/libensemble/generators.py b/libensemble/generators.py index bd197f84d..f971f46d5 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -1,11 +1,14 @@ -import queue as thread_queue +# import queue as thread_queue from abc import ABC, abstractmethod +from multiprocessing import Manager + +# from multiprocessing import Queue as process_queue from typing import List, Optional import numpy as np from numpy import typing as npt -from libensemble.comms.comms import QComm, QCommThread +from libensemble.comms.comms import QComm, QCommProcess # , QCommThread from libensemble.executors import Executor from libensemble.message_numbers import EVAL_GEN_TAG, PERSIS_STOP from libensemble.tools.tools import add_unique_random_streams @@ -138,14 +141,27 @@ def __init__( 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.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() comm = QComm(self.inbox, self.outbox) self.libE_info["comm"] = comm # replacing comm so gen sends HERE instead of manager self.libE_info["executor"] = Executor.executor - self.thread = QCommThread( + # 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, self.History, From 2b8e537106822c2b394db57d25ff90c9db81c180 Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 14 Oct 2024 12:52:55 -0500 Subject: [PATCH 227/288] initial commit - adding variables/objectives to initializer signatures in several gens --- libensemble/gen_classes/aposmm.py | 9 ++++++++- libensemble/gen_classes/sampling.py | 2 +- libensemble/generators.py | 20 +++++++++++++++++--- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index 108282e07..964346359 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -14,7 +14,14 @@ 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 diff --git a/libensemble/gen_classes/sampling.py b/libensemble/gen_classes/sampling.py index 166286482..f15a0f412 100644 --- a/libensemble/gen_classes/sampling.py +++ b/libensemble/gen_classes/sampling.py @@ -31,7 +31,7 @@ class UniformSample(SampleBase): mode by adjusting the allocation function. """ - 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): super().__init__(_, persis_info, gen_specs, libE_info, **kwargs) self._get_user_params(self.gen_specs["user"]) diff --git a/libensemble/generators.py b/libensemble/generators.py index b13bae31c..1303b9571 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -50,7 +50,7 @@ def final_tell(self, results): """ @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. @@ -92,7 +92,14 @@ 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.gen_specs = gen_specs if len(kwargs) > 0: # so user can specify gen-specific parameters as kwargs to constructor @@ -127,7 +134,14 @@ 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) self.gen_f = gen_specs["gen_f"] From 70dde7b224a512712ab2db07c82b393151d83839 Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 14 Oct 2024 13:29:41 -0500 Subject: [PATCH 228/288] recreate the buffer after the results' final opportunity to send onto the persistent_gen - otherwise since it was slotted in the same point will get sent back again --- libensemble/gen_classes/aposmm.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index 507a25d67..d7d870945 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -120,19 +120,18 @@ def tell_numpy(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: if not self._told_initial_sample and self._enough_initial_sample: self._tell_buf.sort(order="sim_id") - print(self._tell_buf) super().tell_numpy(np.copy(self._tell_buf), tag) self._told_initial_sample = True self._n_buffd_results = 0 elif self._told_initial_sample and self._enough_subsequent_points: self._tell_buf.sort(order="sim_id") - print(self._tell_buf) super().tell_numpy(np.copy(self._tell_buf), tag) self._n_buffd_results = 0 else: # probably libE: given back smaller selection. but from alloc, so its ok? super().tell_numpy(results, tag) + self._n_buffd_results = 0 # dont want to send the same point more than once. slotted in earlier def ask_updates(self) -> List[npt.NDArray]: """Request a list of NumPy arrays containing entries that have been identified as minima.""" From 484304b89d210e15bfd45e2f040f10249c0b99f7 Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 14 Oct 2024 13:31:29 -0500 Subject: [PATCH 229/288] dont need sim_id sorting --- libensemble/gen_classes/aposmm.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index d7d870945..f5ad0f8e6 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -119,13 +119,11 @@ def tell_numpy(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: self._n_buffd_results += len(results) if not self._told_initial_sample and self._enough_initial_sample: - self._tell_buf.sort(order="sim_id") super().tell_numpy(np.copy(self._tell_buf), tag) self._told_initial_sample = True self._n_buffd_results = 0 elif self._told_initial_sample and self._enough_subsequent_points: - self._tell_buf.sort(order="sim_id") super().tell_numpy(np.copy(self._tell_buf), tag) self._n_buffd_results = 0 From b0897d0fef954c188871bd5d08b15def96b4c243 Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 14 Oct 2024 13:43:07 -0500 Subject: [PATCH 230/288] the initial sample being done is determined by the total number of results, not just the number we've buffered... --- libensemble/gen_classes/aposmm.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index f5ad0f8e6..81c8a497d 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -40,6 +40,7 @@ def __init__( self._last_ask = None self._tell_buf = None self._n_buffd_results = 0 + self._n_total_results = 0 self._told_initial_sample = False def _slot_in_data(self, results): @@ -117,6 +118,7 @@ def tell_numpy(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: self._slot_in_data(np.copy(results)) self._n_buffd_results += len(results) + self._n_total_results += len(results) if not self._told_initial_sample and self._enough_initial_sample: super().tell_numpy(np.copy(self._tell_buf), tag) From 57bbfb1cb3760a4fe820512f64e0e376181db802 Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 17 Oct 2024 09:45:42 -0500 Subject: [PATCH 231/288] it was long-past time to give up on the super-complicated slot-in-data routine for subsequent runs that dont need slotting in anyway!! --- libensemble/gen_classes/aposmm.py | 45 ++++++++++--------------------- 1 file changed, 14 insertions(+), 31 deletions(-) diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index 81c8a497d..881709509 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -44,24 +44,12 @@ def __init__( self._told_initial_sample = False def _slot_in_data(self, results): - """Slot in libE_calc_in and trial data into corresponding array fields.""" - indexes = results["sim_id"] - fields = results.dtype.names - for j, ind in enumerate(indexes): - for field in fields: - if not ind > len( - self._tell_buf[field] - ): # we got back an index e.g. 715, but our buffer is length e.g. 2 - if np.isscalar(results[field][j]) or results.dtype[field].hasobject: - self._tell_buf[field][ind] = results[field][j] - else: - field_size = len(results[field][j]) - if field_size == len(self._tell_buf[field][ind]): - self._tell_buf[field][ind] = results[field][j] - else: - self._tell_buf[field][ind][:field_size] = results[field][j] - else: # we slot it back by enumeration, not sim_id - self._tell_buf[field][self._n_buffd_results] = results[field][j] + """Slot in libE_calc_in and trial data into corresponding array fields. *Initial sample only!!*""" + self._tell_buf["f"][self._n_buffd_results] = results["f"] + self._tell_buf["x"][self._n_buffd_results] = results["x"] + self._tell_buf["sim_id"][self._n_buffd_results] = results["sim_id"] + self._tell_buf["x_on_cube"][self._n_buffd_results] = results["x_on_cube"] + self._tell_buf["local_pt"][self._n_buffd_results] = results["local_pt"] @property def _array_size(self): @@ -72,12 +60,9 @@ def _array_size(self): @property def _enough_initial_sample(self): """We're typically happy with at least 90% of the initial sample, or we've already told the initial sample""" - return (self._n_buffd_results > self.gen_specs["user"]["initial_sample_size"] - 1) or self._told_initial_sample - - @property - def _enough_subsequent_points(self): - """But we need to evaluate at least N points, for the N local-optimization processes.""" - return self._n_buffd_results >= self.gen_specs["user"]["max_active_runs"] + return ( + self._n_buffd_results >= self.gen_specs["user"]["initial_sample_size"] - 10 + ) or self._told_initial_sample def ask_numpy(self, num_points: int = 0) -> npt.NDArray: """Request the next set of points to evaluate, as a NumPy array.""" @@ -112,23 +97,21 @@ def tell_numpy(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: return if ( - self._n_buffd_results == 0 + self._n_buffd_results == 0 # ONLY NEED TO BUFFER RESULTS FOR INITIAL SAMPLE???? ): # now in Optimas; which prefers to give back chunks of initial_sample. So we buffer them self._tell_buf = np.zeros(self._array_size, dtype=self.gen_specs["out"] + [("f", float)]) - self._slot_in_data(np.copy(results)) - self._n_buffd_results += len(results) + if not self._enough_initial_sample: + self._slot_in_data(np.copy(results)) + self._n_buffd_results += len(results) self._n_total_results += len(results) if not self._told_initial_sample and self._enough_initial_sample: + self._tell_buf = self._tell_buf[self._tell_buf["sim_id"] != 0] super().tell_numpy(np.copy(self._tell_buf), tag) self._told_initial_sample = True self._n_buffd_results = 0 - elif self._told_initial_sample and self._enough_subsequent_points: - super().tell_numpy(np.copy(self._tell_buf), tag) - self._n_buffd_results = 0 - else: # probably libE: given back smaller selection. but from alloc, so its ok? super().tell_numpy(results, tag) self._n_buffd_results = 0 # dont want to send the same point more than once. slotted in earlier From 994b6529a055e4f1e0a8f5747a310dd8b8bf4b57 Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 17 Oct 2024 13:57:44 -0500 Subject: [PATCH 232/288] enormously critical bugfix; optimas workflow now finds minima --- libensemble/gen_classes/aposmm.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index 881709509..4bcf795f6 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -98,7 +98,7 @@ def tell_numpy(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: if ( self._n_buffd_results == 0 # ONLY NEED TO BUFFER RESULTS FOR INITIAL SAMPLE???? - ): # now in Optimas; which prefers to give back chunks of initial_sample. So we buffer them + ): # Optimas prefers to give back chunks of initial_sample. So we buffer them self._tell_buf = np.zeros(self._array_size, dtype=self.gen_specs["out"] + [("f", float)]) if not self._enough_initial_sample: @@ -108,11 +108,11 @@ def tell_numpy(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: if not self._told_initial_sample and self._enough_initial_sample: self._tell_buf = self._tell_buf[self._tell_buf["sim_id"] != 0] - super().tell_numpy(np.copy(self._tell_buf), tag) + super().tell_numpy(self._tell_buf, tag) self._told_initial_sample = True self._n_buffd_results = 0 - else: # probably libE: given back smaller selection. but from alloc, so its ok? + elif self._told_initial_sample: # probably libE: given back smaller selection. but from alloc, so its ok? super().tell_numpy(results, tag) self._n_buffd_results = 0 # dont want to send the same point more than once. slotted in earlier From 31042400dd720e54defe3275eea1a85c5ca36d91 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 18 Oct 2024 16:09:35 -0500 Subject: [PATCH 233/288] experiment with UniformSampleDicts using variables/objectiveso --- libensemble/gen_classes/sampling.py | 26 +++++++------------ libensemble/generators.py | 9 +------ .../test_sampling_asktell_gen.py | 6 ++++- 3 files changed, 16 insertions(+), 25 deletions(-) diff --git a/libensemble/gen_classes/sampling.py b/libensemble/gen_classes/sampling.py index f15a0f412..0ec5d6f0f 100644 --- a/libensemble/gen_classes/sampling.py +++ b/libensemble/gen_classes/sampling.py @@ -29,9 +29,11 @@ class UniformSample(SampleBase): 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 *probably* won't implement variables/objectives, for now. """ - def __init__(self, variables: dict, objectives: dict, _=[], persis_info={}, gen_specs={}, libE_info=None, **kwargs): + def __init__(self, _=[], persis_info={}, gen_specs={}, libE_info=None, **kwargs): super().__init__(_, persis_info, gen_specs, libE_info, **kwargs) self._get_user_params(self.gen_specs["user"]) @@ -53,31 +55,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" diff --git a/libensemble/generators.py b/libensemble/generators.py index eb97023a6..606e9e882 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -95,14 +95,7 @@ class LibensembleGenerator(Generator): """ def __init__( - self, - variables: dict, - objectives: dict, - History: npt.NDArray = [], - persis_info: dict = {}, - gen_specs: dict = {}, - libE_info: dict = {}, - **kwargs + self, History: npt.NDArray = [], persis_info: dict = {}, gen_specs: dict = {}, libE_info: dict = {}, **kwargs ): self.gen_specs = gen_specs if len(kwargs) > 0: # so user can specify gen-specific parameters as kwargs to constructor diff --git a/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py b/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py index 57db0f5e4..2efc314f2 100644 --- a/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py +++ b/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py @@ -51,6 +51,10 @@ def sim_f(In): }, } + variables = {"x0": [-3, 3], "x1": [-2, 2]} + + objectives = {"f": "EXPLORE"} + alloc_specs = {"alloc_f": alloc_f} exit_criteria = {"gen_max": 201} @@ -76,7 +80,7 @@ def sim_f(In): elif inst == 3: # Using asktell runner - pass object - with standardized interface. gen_specs.pop("gen_f", None) - generator = UniformSampleDicts(None, persis_info[1], gen_specs, None) + generator = UniformSampleDicts(variables, objectives, None, persis_info[1], gen_specs, None) gen_specs["generator"] = generator H, persis_info, flag = libE( From 3d262fb5b81bb8c172ddf8cf496232fd2e4c8c08 Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 21 Oct 2024 15:41:22 -0500 Subject: [PATCH 234/288] i wonder if we can determine lb, ub, and n based on the contents of standard-variables --- libensemble/gen_classes/aposmm.py | 25 ++++++++++++++++--- .../test_persistent_aposmm_nlopt_asktell.py | 2 ++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index 38e7cefb9..1032845ff 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -26,12 +26,29 @@ def __init__( ) -> 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 self.variables: + self.n = len(self.variables) # we'll unpack output x's to correspond with variables + if not kwargs: + lb = [] + ub = [] + for v in self.variables.values(): + if isinstance(v, list) and (isinstance(v[0], int) or isinstance(v[0], float)): + # we got a range, append to lb and ub + lb.append(v[0]) + ub.append(v[1]) + kwargs["lb"] = np.array(lb) + kwargs["ub"] = np.array(ub) + + elif not gen_specs.get("out"): # gen_specs never especially changes for aposmm even as the problem varies + self.n = len(kwargs["lb"]) or len(kwargs["ub"]) 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), diff --git a/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py b/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py index 101759966..833f46bb5 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py @@ -52,6 +52,8 @@ workflow.exit_criteria = ExitCriteria(sim_max=2000) aposmm = APOSMM( + variables={"a": [-3, 3], "b": [-2, 2]}, + objectives={"f": "MINIMIZE"}, initial_sample_size=100, sample_points=minima, localopt_method="LN_BOBYQA", From 847a617f14c2d7b436212e0262a7f577d15c6279 Mon Sep 17 00:00:00 2001 From: jlnav Date: Tue, 22 Oct 2024 11:34:33 -0500 Subject: [PATCH 235/288] APOSMM can now accept variables and objectives instead of needing ub, lb and gen_specs.out --- libensemble/gen_classes/aposmm.py | 6 +-- libensemble/generators.py | 17 +++++-- .../test_persistent_aposmm_nlopt_asktell.py | 18 +------ libensemble/utils/pydantic_bindings.py | 7 +-- libensemble/utils/specs_checkers.py | 51 +++++++++++-------- libensemble/utils/validators.py | 20 +++++--- 6 files changed, 66 insertions(+), 53 deletions(-) diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index 1032845ff..7f2710e49 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -33,7 +33,7 @@ def __init__( if self.variables: self.n = len(self.variables) # we'll unpack output x's to correspond with variables - if not kwargs: + if "lb" not in kwargs and "ub" not in kwargs: lb = [] ub = [] for v in self.variables.values(): @@ -44,7 +44,7 @@ def __init__( kwargs["lb"] = np.array(lb) kwargs["ub"] = np.array(ub) - elif not gen_specs.get("out"): # gen_specs never especially changes for aposmm even as the problem varies + if not gen_specs.get("out"): # gen_specs never especially changes for aposmm even as the problem varies self.n = len(kwargs["lb"]) or len(kwargs["ub"]) gen_specs["out"] = [ ("x", float, self.n), @@ -56,7 +56,7 @@ def __init__( 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"] = gen_specs["user"]["max_active_runs"] # ?????????? self.all_local_minima = [] diff --git a/libensemble/generators.py b/libensemble/generators.py index 606e9e882..96687e216 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -23,6 +23,10 @@ """ +class GeneratorNotStartedException(Exception): + """Exception raised by a threaded/multiprocessed generator upon being asked without having been started""" + + class Generator(ABC): """ @@ -95,7 +99,14 @@ class LibensembleGenerator(Generator): """ def __init__( - self, History: npt.NDArray = [], persis_info: dict = {}, gen_specs: dict = {}, libE_info: dict = {}, **kwargs + self, + variables, + objectives, + History: npt.NDArray = [], + persis_info: dict = {}, + gen_specs: dict = {}, + libE_info: dict = {}, + **kwargs ): self.gen_specs = gen_specs if len(kwargs) > 0: # so user can specify gen-specific parameters as kwargs to constructor @@ -139,7 +150,7 @@ def __init__( 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 @@ -191,7 +202,7 @@ def tell(self, results: List[dict], tag: int = EVAL_GEN_TAG) -> None: 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"] diff --git a/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py b/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py index 833f46bb5..805dd9c67 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py @@ -33,7 +33,6 @@ from libensemble.gen_classes import APOSMM from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, SimSpecs from libensemble.tests.regression_tests.support import six_hump_camel_minima as minima -from libensemble.tools import save_libE_output # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": @@ -52,7 +51,7 @@ workflow.exit_criteria = ExitCriteria(sim_max=2000) aposmm = APOSMM( - variables={"a": [-3, 3], "b": [-2, 2]}, + variables={"x0": [-3, 3], "x1": [-2, 2]}, # we hope to combine these objectives={"f": "MINIMIZE"}, initial_sample_size=100, sample_points=minima, @@ -61,20 +60,10 @@ xtol_abs=1e-6, ftol_abs=1e-6, max_active_runs=workflow.nworkers, # should this match nworkers always? practically? - lb=np.array([-3, -2]), - ub=np.array([3, 2]), ) workflow.gen_specs = GenSpecs( persis_in=["x", "x_on_cube", "sim_id", "local_min", "local_pt", "f"], - outputs=[ - ("x", float, n), - ("x_on_cube", float, n), - ("sim_id", int), - ("local_min", bool), - ("local_pt", bool), - ("f", float), - ], generator=aposmm, batch_size=5, initial_batch_size=10, @@ -84,7 +73,7 @@ workflow.libE_specs.gen_on_manager = True workflow.add_random_streams() - H, persis_info, _ = workflow.run() + H, _, _ = workflow.run() # Perform the run @@ -98,6 +87,3 @@ # We use their values to test APOSMM has identified all minima print(np.min(np.sum((H[H["local_min"]]["x"] - m) ** 2, 1)), flush=True) assert np.min(np.sum((H[H["local_min"]]["x"] - m) ** 2, 1)) < tol - - persis_info[0]["comm"] = None - save_libE_output(H, persis_info, __file__, workflow.nworkers) diff --git a/libensemble/utils/pydantic_bindings.py b/libensemble/utils/pydantic_bindings.py index 7ceca9615..3226f98b2 100644 --- a/libensemble/utils/pydantic_bindings.py +++ b/libensemble/utils/pydantic_bindings.py @@ -5,7 +5,7 @@ from libensemble import specs from libensemble.resources import platforms from libensemble.utils.misc import pydanticV1 -from libensemble.utils.validators import ( +from libensemble.utils.validators import ( # check_output_fields, _UFUNC_INVALID_ERR, _UNRECOGNIZED_ERR, check_any_workers_and_disable_rm_if_tcp, @@ -16,8 +16,8 @@ check_inputs_exist, check_logical_cores, check_mpi_runner_type, - check_output_fields, check_provided_ufuncs, + check_set_gen_specs_from_variables, check_valid_comms_type, check_valid_in, check_valid_out, @@ -104,6 +104,7 @@ class Config: __validators__={ "check_valid_out": check_valid_out, "check_valid_in": check_valid_in, + "check_set_gen_specs_from_variables": check_set_gen_specs_from_variables, "genf_set_in_out_from_attrs": genf_set_in_out_from_attrs, }, ) @@ -129,7 +130,7 @@ class Config: __base__=specs._EnsembleSpecs, __validators__={ "check_exit_criteria": check_exit_criteria, - "check_output_fields": check_output_fields, + # "check_output_fields": check_output_fields, "check_H0": check_H0, "check_provided_ufuncs": check_provided_ufuncs, }, diff --git a/libensemble/utils/specs_checkers.py b/libensemble/utils/specs_checkers.py index cf33d359f..2e4a80d68 100644 --- a/libensemble/utils/specs_checkers.py +++ b/libensemble/utils/specs_checkers.py @@ -25,28 +25,35 @@ def _check_exit_criteria(values): return values -def _check_output_fields(values): - out_names = [e[0] for e in libE_fields] - if scg(values, "H0") is not None and scg(values, "H0").dtype.names is not None: - out_names += list(scg(values, "H0").dtype.names) - out_names += [e[0] for e in scg(values, "sim_specs").outputs] - if scg(values, "gen_specs"): - out_names += [e[0] for e in scg(values, "gen_specs").outputs] - if scg(values, "alloc_specs"): - out_names += [e[0] for e in scg(values, "alloc_specs").outputs] - - for name in scg(values, "sim_specs").inputs: - assert name in out_names, ( - name + " in sim_specs['in'] is not in sim_specs['out'], " - "gen_specs['out'], alloc_specs['out'], H0, or libE_fields." - ) - - if scg(values, "gen_specs"): - for name in scg(values, "gen_specs").inputs: - assert name in out_names, ( - name + " in gen_specs['in'] is not in sim_specs['out'], " - "gen_specs['out'], alloc_specs['out'], H0, or libE_fields." - ) +# def _check_output_fields(values): +# out_names = [e[0] for e in libE_fields] +# if scg(values, "H0") is not None and scg(values, "H0").dtype.names is not None: +# out_names += list(scg(values, "H0").dtype.names) +# out_names += [e[0] for e in scg(values, "sim_specs").outputs] +# if scg(values, "gen_specs"): +# out_names += [e[0] for e in scg(values, "gen_specs").outputs] +# if scg(values, "alloc_specs"): +# out_names += [e[0] for e in scg(values, "alloc_specs").outputs] + +# for name in scg(values, "sim_specs").inputs: +# assert name in out_names, ( +# name + " in sim_specs['in'] is not in sim_specs['out'], " +# "gen_specs['out'], alloc_specs['out'], H0, or libE_fields." +# ) + +# if scg(values, "gen_specs"): +# for name in scg(values, "gen_specs").inputs: +# assert name in out_names, ( +# name + " in gen_specs['in'] is not in sim_specs['out'], " +# "gen_specs['out'], alloc_specs['out'], H0, or libE_fields." +# ) +# return values + + +def _check_set_gen_specs_from_variables(values): + if not len(scg(values, "outputs")): + if scg(values, "generator") and len(scg(values, "generator").gen_specs["out"]): + scs(values, "outputs", scg(values, "generator").gen_specs["out"]) return values diff --git a/libensemble/utils/validators.py b/libensemble/utils/validators.py index 80abfa9a3..7db02656a 100644 --- a/libensemble/utils/validators.py +++ b/libensemble/utils/validators.py @@ -6,13 +6,13 @@ from libensemble.resources.platforms import Platform from libensemble.utils.misc import pydanticV1 -from libensemble.utils.specs_checkers import ( +from libensemble.utils.specs_checkers import ( # _check_output_fields, _check_any_workers_and_disable_rm_if_tcp, _check_exit_criteria, _check_H0, _check_logical_cores, - _check_output_fields, _check_set_calc_dirs_on_input_dir, + _check_set_gen_specs_from_variables, _check_set_workflow_dir, ) @@ -147,9 +147,13 @@ def set_calc_dirs_on_input_dir(cls, values): def check_exit_criteria(cls, values): return _check_exit_criteria(values) + # @root_validator + # def check_output_fields(cls, values): + # return _check_output_fields(values) + @root_validator - def check_output_fields(cls, values): - return _check_output_fields(values) + def check_set_gen_specs_from_variables(cls, values): + return _check_set_gen_specs_from_variables(values) @root_validator def check_H0(cls, values): @@ -245,9 +249,13 @@ def set_calc_dirs_on_input_dir(self): def check_exit_criteria(self): return _check_exit_criteria(self) + # @model_validator(mode="after") + # def check_output_fields(self): + # return _check_output_fields(self) + @model_validator(mode="after") - def check_output_fields(self): - return _check_output_fields(self) + def check_set_gen_specs_from_variables(self): + return _check_set_gen_specs_from_variables(self) @model_validator(mode="after") def check_H0(self): From f45ddbedda90585abcb0d399ae247617feb42f5e Mon Sep 17 00:00:00 2001 From: jlnav Date: Tue, 22 Oct 2024 11:36:01 -0500 Subject: [PATCH 236/288] cleanup the removed validator; since gen_specs['out'] can be absent --- libensemble/utils/pydantic_bindings.py | 1 - libensemble/utils/specs_checkers.py | 25 ------------------------- libensemble/utils/validators.py | 8 -------- 3 files changed, 34 deletions(-) diff --git a/libensemble/utils/pydantic_bindings.py b/libensemble/utils/pydantic_bindings.py index 3226f98b2..5c1f6e17d 100644 --- a/libensemble/utils/pydantic_bindings.py +++ b/libensemble/utils/pydantic_bindings.py @@ -130,7 +130,6 @@ class Config: __base__=specs._EnsembleSpecs, __validators__={ "check_exit_criteria": check_exit_criteria, - # "check_output_fields": check_output_fields, "check_H0": check_H0, "check_provided_ufuncs": check_provided_ufuncs, }, diff --git a/libensemble/utils/specs_checkers.py b/libensemble/utils/specs_checkers.py index 2e4a80d68..b8e793fa5 100644 --- a/libensemble/utils/specs_checkers.py +++ b/libensemble/utils/specs_checkers.py @@ -25,31 +25,6 @@ def _check_exit_criteria(values): return values -# def _check_output_fields(values): -# out_names = [e[0] for e in libE_fields] -# if scg(values, "H0") is not None and scg(values, "H0").dtype.names is not None: -# out_names += list(scg(values, "H0").dtype.names) -# out_names += [e[0] for e in scg(values, "sim_specs").outputs] -# if scg(values, "gen_specs"): -# out_names += [e[0] for e in scg(values, "gen_specs").outputs] -# if scg(values, "alloc_specs"): -# out_names += [e[0] for e in scg(values, "alloc_specs").outputs] - -# for name in scg(values, "sim_specs").inputs: -# assert name in out_names, ( -# name + " in sim_specs['in'] is not in sim_specs['out'], " -# "gen_specs['out'], alloc_specs['out'], H0, or libE_fields." -# ) - -# if scg(values, "gen_specs"): -# for name in scg(values, "gen_specs").inputs: -# assert name in out_names, ( -# name + " in gen_specs['in'] is not in sim_specs['out'], " -# "gen_specs['out'], alloc_specs['out'], H0, or libE_fields." -# ) -# return values - - def _check_set_gen_specs_from_variables(values): if not len(scg(values, "outputs")): if scg(values, "generator") and len(scg(values, "generator").gen_specs["out"]): diff --git a/libensemble/utils/validators.py b/libensemble/utils/validators.py index 7db02656a..6cd100f4d 100644 --- a/libensemble/utils/validators.py +++ b/libensemble/utils/validators.py @@ -147,10 +147,6 @@ def set_calc_dirs_on_input_dir(cls, values): def check_exit_criteria(cls, values): return _check_exit_criteria(values) - # @root_validator - # def check_output_fields(cls, values): - # return _check_output_fields(values) - @root_validator def check_set_gen_specs_from_variables(cls, values): return _check_set_gen_specs_from_variables(values) @@ -249,10 +245,6 @@ def set_calc_dirs_on_input_dir(self): def check_exit_criteria(self): return _check_exit_criteria(self) - # @model_validator(mode="after") - # def check_output_fields(self): - # return _check_output_fields(self) - @model_validator(mode="after") def check_set_gen_specs_from_variables(self): return _check_set_gen_specs_from_variables(self) From 26f1d7330c05434da19f97c54d39a708a33e3e04 Mon Sep 17 00:00:00 2001 From: jlnav Date: Tue, 22 Oct 2024 13:42:29 -0500 Subject: [PATCH 237/288] cleanup/fixes --- libensemble/gen_classes/sampling.py | 4 +--- libensemble/generators.py | 18 +++--------------- .../test_sampling_asktell_gen.py | 8 ++++---- 3 files changed, 8 insertions(+), 22 deletions(-) diff --git a/libensemble/gen_classes/sampling.py b/libensemble/gen_classes/sampling.py index 0ec5d6f0f..571246de2 100644 --- a/libensemble/gen_classes/sampling.py +++ b/libensemble/gen_classes/sampling.py @@ -29,11 +29,9 @@ class UniformSample(SampleBase): 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 *probably* won't implement variables/objectives, for now. """ - 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): super().__init__(_, persis_info, gen_specs, libE_info, **kwargs) self._get_user_params(self.gen_specs["user"]) diff --git a/libensemble/generators.py b/libensemble/generators.py index 96687e216..50060a7da 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -100,8 +100,8 @@ class LibensembleGenerator(Generator): def __init__( self, - variables, - objectives, + variables: dict, + objectives: dict = {}, History: npt.NDArray = [], persis_info: dict = {}, gen_specs: dict = {}, @@ -143,7 +143,7 @@ class LibensembleGenThreadInterfacer(LibensembleGenerator): def __init__( self, variables: dict, - objectives: dict, + objectives: dict = {}, History: npt.NDArray = [], persis_info: dict = {}, gen_specs: dict = {}, @@ -159,8 +159,6 @@ def __init__( 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() @@ -169,16 +167,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, diff --git a/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py b/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py index 2efc314f2..4d1ac40e9 100644 --- a/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py +++ b/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py @@ -70,14 +70,14 @@ def sim_f(In): if inst == 1: # Using wrapper - pass object gen_specs["gen_f"] = gen_f - generator = UniformSample(None, persis_info[1], gen_specs, None) + generator = UniformSample(variables, objectives, None, persis_info[1], gen_specs, None) gen_specs["user"]["generator"] = generator - elif inst == 2: + if inst == 2: # Using asktell runner - pass object gen_specs.pop("gen_f", None) - generator = UniformSample(None, persis_info[1], gen_specs, None) + generator = UniformSample(variables, objectives, None, persis_info[1], gen_specs, None) gen_specs["generator"] = generator - elif inst == 3: + if inst == 3: # Using asktell runner - pass object - with standardized interface. gen_specs.pop("gen_f", None) generator = UniformSampleDicts(variables, objectives, None, persis_info[1], gen_specs, None) From 10e96d80d22753dc87cb9d83725cb71b9665aa51 Mon Sep 17 00:00:00 2001 From: jlnav Date: Tue, 22 Oct 2024 15:59:12 -0500 Subject: [PATCH 238/288] stop kwargs from replacing entire gen_specs.user; try out vars/objs with aposmm in unit test --- libensemble/generators.py | 2 +- libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 50060a7da..9d139596b 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -110,7 +110,7 @@ def __init__( ): self.gen_specs = gen_specs if len(kwargs) > 0: # so user can specify gen-specific parameters as kwargs to constructor - self.gen_specs["user"] = kwargs + self.gen_specs["user"].update(kwargs) if not persis_info: self.persis_info = add_unique_random_streams({}, 4, seed=4321)[1] else: diff --git a/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py b/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py index 9bc097a18..c8934cf3c 100644 --- a/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py +++ b/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py @@ -203,7 +203,8 @@ def test_asktell_with_persistent_aposmm(): }, } - my_APOSMM = APOSMM(gen_specs=gen_specs) + my_APOSMM = APOSMM(variables={"x0": [-3, 3], "x1": [-2, 2]}, objectives={"f": "MINIMIZE"}, gen_specs=gen_specs) + my_APOSMM.setup() initial_sample = my_APOSMM.ask(100) From e01e87b2246170bb53a828f3d7fc6220eca8f4a8 Mon Sep 17 00:00:00 2001 From: jlnav Date: Tue, 22 Oct 2024 16:36:19 -0500 Subject: [PATCH 239/288] removing ask/tell generator wrapper user function; removing from sampling_asktell_gen --- .../gen_funcs/persistent_gen_wrapper.py | 32 ---------- .../test_sampling_asktell_gen.py | 58 ++++++++----------- 2 files changed, 24 insertions(+), 66 deletions(-) delete mode 100644 libensemble/gen_funcs/persistent_gen_wrapper.py diff --git a/libensemble/gen_funcs/persistent_gen_wrapper.py b/libensemble/gen_funcs/persistent_gen_wrapper.py deleted file mode 100644 index 7fd01ec4d..000000000 --- a/libensemble/gen_funcs/persistent_gen_wrapper.py +++ /dev/null @@ -1,32 +0,0 @@ -import inspect - -from libensemble.message_numbers import EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG -from libensemble.tools.persistent_support import PersistentSupport -from libensemble.utils.misc import list_dicts_to_np, np_to_list_dicts - - -def persistent_gen_f(H, persis_info, gen_specs, libE_info): - - ps = PersistentSupport(libE_info, EVAL_GEN_TAG) - U = gen_specs["user"] - b = U.get("initial_batch_size") or U.get("batch_size") - - generator = U["generator"] - if inspect.isclass(generator): - gen = generator(H, persis_info, gen_specs, libE_info) - else: - gen = generator - - tag = None - calc_in = None - while tag not in [STOP_TAG, PERSIS_STOP]: - H_o = gen.ask(b) - if isinstance(H_o, list): - H_o = list_dicts_to_np(H_o) - tag, Work, calc_in = ps.send_recv(H_o) - gen.tell(np_to_list_dicts(calc_in)) - - if hasattr(calc_in, "__len__"): - b = len(calc_in) - - return H_o, persis_info, FINISHED_PERSISTENT_GEN_TAG diff --git a/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py b/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py index 57db0f5e4..8de6f60b7 100644 --- a/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py +++ b/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py @@ -18,7 +18,6 @@ # Import libEnsemble items for this test from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f from libensemble.gen_classes.sampling import UniformSample, UniformSampleDicts -from libensemble.gen_funcs.persistent_gen_wrapper import persistent_gen_f as gen_f from libensemble.libE import libE from libensemble.tools import add_unique_random_streams, parse_args @@ -54,36 +53,27 @@ def sim_f(In): alloc_specs = {"alloc_f": alloc_f} exit_criteria = {"gen_max": 201} - for inst in range(4): - persis_info = add_unique_random_streams({}, nworkers + 1, seed=1234) - - if inst == 0: - # Using wrapper - pass class - generator = UniformSample - gen_specs["gen_f"] = gen_f - gen_specs["user"]["generator"] = generator - - if inst == 1: - # Using wrapper - pass object - gen_specs["gen_f"] = gen_f - generator = UniformSample(None, persis_info[1], gen_specs, None) - gen_specs["user"]["generator"] = generator - elif inst == 2: - # Using asktell runner - pass object - gen_specs.pop("gen_f", None) - generator = UniformSample(None, persis_info[1], gen_specs, None) - gen_specs["generator"] = generator - elif inst == 3: - # Using asktell runner - pass object - with standardized interface. - gen_specs.pop("gen_f", None) - generator = UniformSampleDicts(None, persis_info[1], gen_specs, None) - gen_specs["generator"] = generator - - H, persis_info, flag = libE( - sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs=libE_specs - ) - - if is_manager: - print(H[["sim_id", "x", "f"]][:10]) - assert len(H) >= 201, f"H has length {len(H)}" - assert np.isclose(H["f"][9], 1.96760289) + persis_info = add_unique_random_streams({}, nworkers + 1, seed=1234) + + # Test mostly-libE version + generator = UniformSample(None, persis_info[1], gen_specs, None) + gen_specs["generator"] = generator + + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs=libE_specs) + + if is_manager: + print(H[["sim_id", "x", "f"]][:10]) + assert len(H) >= 201, f"H has length {len(H)}" + assert np.isclose(H["f"][9], 1.96760289) + + # Using UniformSample that doesn't have ask_numpy/tell_numpy + persis_info = add_unique_random_streams({}, nworkers + 1, seed=1234) + generator = UniformSampleDicts(None, persis_info[1], gen_specs, None) + gen_specs["generator"] = generator + + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs=libE_specs) + + if is_manager: + print(H[["sim_id", "x", "f"]][:10]) + assert len(H) >= 201, f"H has length {len(H)}" + assert np.isclose(H["f"][9], 1.96760289) From a1eb450f4cd8ca3fd272ba3b49b1a725a3120ae7 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 23 Oct 2024 14:48:02 -0500 Subject: [PATCH 240/288] adjust ask/tell gpcam test --- .../tests/regression_tests/test_gpCAM_class.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/libensemble/tests/regression_tests/test_gpCAM_class.py b/libensemble/tests/regression_tests/test_gpCAM_class.py index f890c32ab..1c8e2559c 100644 --- a/libensemble/tests/regression_tests/test_gpCAM_class.py +++ b/libensemble/tests/regression_tests/test_gpCAM_class.py @@ -26,7 +26,6 @@ from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f from libensemble.gen_classes.gpCAM import GP_CAM, GP_CAM_Covar -from libensemble.gen_funcs.persistent_gen_wrapper import persistent_gen_f as gen_f # Import libEnsemble items for this test from libensemble.libE import libE @@ -66,10 +65,13 @@ alloc_specs = {"alloc_f": alloc_f} + persis_info = add_unique_random_streams({}, nworkers + 1) + + gen = GP_CAM_Covar(None, persis_info[1], gen_specs, None) + for inst in range(3): if inst == 0: - gen_specs["gen_f"] = gen_f - gen_specs["user"]["generator"] = GP_CAM_Covar + gen_specs["generator"] = gen num_batches = 10 exit_criteria = {"sim_max": num_batches * batch_size, "wallclock_max": 300} libE_specs["save_every_k_gens"] = 150 @@ -81,13 +83,12 @@ del libE_specs["H_file_prefix"] del libE_specs["save_every_k_gens"] elif inst == 2: - gen_specs["user"]["generator"] = GP_CAM + persis_info = add_unique_random_streams({}, nworkers + 1) + gen_specs["generator"] = GP_CAM(None, persis_info[1], gen_specs, None) num_batches = 3 # Few because the ask_tell gen can be slow gen_specs["user"]["ask_max_iter"] = 1 # For quicker test exit_criteria = {"sim_max": num_batches * batch_size, "wallclock_max": 300} - persis_info = add_unique_random_streams({}, nworkers + 1) - # Perform the run H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) From 1b4c2c6351cd629b084b89f1ec3e07a85abf5334 Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 24 Oct 2024 10:29:04 -0500 Subject: [PATCH 241/288] we dont need to run multiple tests for asktell surmise --- .../test_persistent_surmise_killsims_asktell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libensemble/tests/regression_tests/test_persistent_surmise_killsims_asktell.py b/libensemble/tests/regression_tests/test_persistent_surmise_killsims_asktell.py index 9071e80d4..4e35966ee 100644 --- a/libensemble/tests/regression_tests/test_persistent_surmise_killsims_asktell.py +++ b/libensemble/tests/regression_tests/test_persistent_surmise_killsims_asktell.py @@ -23,7 +23,7 @@ # Do not change these lines - they are parsed by run-tests.sh # TESTSUITE_COMMS: mpi local -# TESTSUITE_NPROCS: 3 4 +# TESTSUITE_NPROCS: 4 # TESTSUITE_EXTRA: true # TESTSUITE_OS_SKIP: OSX From 5f777c2a6a36aac82078f7760679e6027450a7a1 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 25 Oct 2024 11:06:51 -0500 Subject: [PATCH 242/288] additional experiments with vars/objs, including seeing if we can append objective keys to the internal dtype --- libensemble/generators.py | 20 +++++++++++++++---- .../RENAME_test_persistent_aposmm.py | 5 ++++- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 9d139596b..9a07bd6f7 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -37,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) @@ -52,7 +52,10 @@ 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, ...) """ @@ -108,8 +111,12 @@ def __init__( libE_info: dict = {}, **kwargs ): + self.variables = variables + self.objectives = objectives self.gen_specs = gen_specs if len(kwargs) > 0: # so user can specify gen-specific parameters as kwargs to constructor + if not self.gen_specs.get("user"): + self.gen_specs["user"] = {} self.gen_specs["user"].update(kwargs) if not persis_info: self.persis_info = add_unique_random_streams({}, 4, seed=4321)[1] @@ -178,7 +185,12 @@ def setup(self) -> None: ) # note that self.thread's inbox/outbox are unused by the underlying gen def _set_sim_ended(self, results: npt.NDArray) -> npt.NDArray: - new_results = np.zeros(len(results), dtype=self.gen_specs["out"] + [("sim_ended", bool), ("f", float)]) + new_results = np.zeros( + len(results), + dtype=self.gen_specs["out"] + + [("sim_ended", bool), ("f", float)] + + [(i, float) for i in self.objectives.keys()], + ) for field in results.dtype.names: new_results[field] = results[field] new_results["sim_ended"] = True diff --git a/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py b/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py index c8934cf3c..42eb29602 100644 --- a/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py +++ b/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py @@ -203,7 +203,10 @@ def test_asktell_with_persistent_aposmm(): }, } - my_APOSMM = APOSMM(variables={"x0": [-3, 3], "x1": [-2, 2]}, objectives={"f": "MINIMIZE"}, gen_specs=gen_specs) + variables = {"x0": [-3, 3], "x1": [-2, 2]} + objectives = {"f": "MINIMIZE"} + + my_APOSMM = APOSMM(variables=variables, objectives=objectives, gen_specs=gen_specs) my_APOSMM.setup() initial_sample = my_APOSMM.ask(100) From a165cdda0c64f24943103cb10e2d359db28315e5 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 25 Oct 2024 15:57:20 -0500 Subject: [PATCH 243/288] tiny changes for slotting in data back from the waket/optimas workflow --- libensemble/gen_classes/aposmm.py | 2 +- libensemble/generators.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index 4bcf795f6..9b1e22cc0 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -48,7 +48,7 @@ def _slot_in_data(self, results): self._tell_buf["f"][self._n_buffd_results] = results["f"] self._tell_buf["x"][self._n_buffd_results] = results["x"] self._tell_buf["sim_id"][self._n_buffd_results] = results["sim_id"] - self._tell_buf["x_on_cube"][self._n_buffd_results] = results["x_on_cube"] + # self._tell_buf["x_on_cube"][self._n_buffd_results] = results["x_on_cube"] self._tell_buf["local_pt"][self._n_buffd_results] = results["local_pt"] @property diff --git a/libensemble/generators.py b/libensemble/generators.py index f971f46d5..4367bd92f 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -174,7 +174,10 @@ def setup(self) -> None: def _set_sim_ended(self, results: npt.NDArray) -> npt.NDArray: new_results = np.zeros(len(results), dtype=self.gen_specs["out"] + [("sim_ended", bool), ("f", float)]) for field in results.dtype.names: - new_results[field] = results[field] + try: + new_results[field] = results[field] + except ValueError: # lets not slot in data that the gen doesnt need? + continue new_results["sim_ended"] = True return new_results From ac5467ba3bfd5a1a127cf174234a4de047a7365f Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 28 Oct 2024 11:34:26 -0500 Subject: [PATCH 244/288] moving logic for determining lb and ub from variables into parent class; setting up unit test to eventually map user-specififed variables into internal xs --- .gitignore | 1 + libensemble/gen_classes/aposmm.py | 16 ---------------- libensemble/generators.py | 14 ++++++++++++++ .../unit_tests/RENAME_test_persistent_aposmm.py | 14 +++++++------- 4 files changed, 22 insertions(+), 23 deletions(-) diff --git a/.gitignore b/.gitignore index 828a6fff6..c6bd3c0dd 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ dist/ .spyproject/ .hypothesis +.pixi diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index 7f2710e49..41720c2f3 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -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): @@ -31,19 +30,6 @@ def __init__( gen_specs["gen_f"] = aposmm - if self.variables: - self.n = len(self.variables) # we'll unpack output x's to correspond with variables - if "lb" not in kwargs and "ub" not in kwargs: - lb = [] - ub = [] - for v in self.variables.values(): - if isinstance(v, list) and (isinstance(v[0], int) or isinstance(v[0], float)): - # we got a range, append to lb and ub - lb.append(v[0]) - ub.append(v[1]) - kwargs["lb"] = np.array(lb) - kwargs["ub"] = np.array(ub) - if not gen_specs.get("out"): # gen_specs never especially changes for aposmm even as the problem varies self.n = len(kwargs["lb"]) or len(kwargs["ub"]) gen_specs["out"] = [ @@ -54,8 +40,6 @@ def __init__( ("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__(variables, objectives, History, persis_info, gen_specs, libE_info, **kwargs) if not self.persis_info.get("nworkers"): self.persis_info["nworkers"] = gen_specs["user"]["max_active_runs"] # ?????????? diff --git a/libensemble/generators.py b/libensemble/generators.py index 9a07bd6f7..fc426c3fe 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -122,6 +122,20 @@ def __init__( self.persis_info = add_unique_random_streams({}, 4, seed=4321)[1] else: self.persis_info = persis_info + if self.variables: + self._vars_x_mapping = {i: k for i, k in enumerate(self.variables.keys())} + self._vars_f_mapping = {i: k for i, k, in enumerate(self.objectives.keys())} + self.n = len(self.variables) # we'll unpack output x's to correspond with variables + if "lb" not in kwargs and "ub" not in kwargs: + lb = [] + ub = [] + for v in self.variables.values(): + if isinstance(v, list) and (isinstance(v[0], int) or isinstance(v[0], float)): + # we got a range, append to lb and ub + lb.append(v[0]) + ub.append(v[1]) + kwargs["lb"] = np.array(lb) + kwargs["ub"] = np.array(ub) @abstractmethod def ask_numpy(self, num_points: Optional[int] = 0) -> npt.NDArray: diff --git a/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py b/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py index 42eb29602..ea4595c4e 100644 --- a/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py +++ b/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py @@ -184,11 +184,11 @@ def test_asktell_with_persistent_aposmm(): n = 2 eval_max = 2000 - gen_out = [("x", float, n), ("x_on_cube", float, n), ("sim_id", int), ("local_min", bool), ("local_pt", bool)] + # gen_out = [("x", float, n), ("x_on_cube", float, n), ("sim_id", int), ("local_min", bool), ("local_pt", bool)] gen_specs = { - "in": ["x", "f", "local_pt", "sim_id", "sim_ended", "x_on_cube", "local_min"], - "out": gen_out, + # "in": ["x", "f", "local_pt", "sim_id", "sim_ended", "x_on_cube", "local_min"], + # "out": gen_out, "user": { "initial_sample_size": 100, "sample_points": np.round(minima, 1), @@ -203,8 +203,8 @@ def test_asktell_with_persistent_aposmm(): }, } - variables = {"x0": [-3, 3], "x1": [-2, 2]} - objectives = {"f": "MINIMIZE"} + variables = {"core": [-3, 3], "edge": [-2, 2]} + objectives = {"energy": "MINIMIZE"} my_APOSMM = APOSMM(variables=variables, objectives=objectives, gen_specs=gen_specs) @@ -215,7 +215,7 @@ def test_asktell_with_persistent_aposmm(): eval_max = 2000 for point in initial_sample: - point["f"] = six_hump_camel_func(np.array([point["x0"], point["x1"]])) + point["energy"] = six_hump_camel_func(np.array([point["core"], point["edge"]])) total_evals += 1 my_APOSMM.tell(initial_sample) @@ -229,7 +229,7 @@ def test_asktell_with_persistent_aposmm(): for m in detected_minima: potential_minima.append(m) for point in sample: - point["f"] = six_hump_camel_func(np.array([point["x0"], point["x1"]])) + point["energy"] = six_hump_camel_func(np.array([point["core"], point["edge"]])) total_evals += 1 my_APOSMM.tell(sample) H, persis_info, exit_code = my_APOSMM.final_tell(list_dicts_to_np(sample)) # final_tell currently requires numpy From 85507a42e0beda201426cb23ad7e987b0c070061 Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 28 Oct 2024 15:07:00 -0500 Subject: [PATCH 245/288] init pair of functions for mapping, slot in where they'll be called --- libensemble/gen_classes/sampling.py | 11 ++++--- libensemble/gen_funcs/persistent_aposmm.py | 1 + libensemble/generators.py | 34 +++++++++++++--------- 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/libensemble/gen_classes/sampling.py b/libensemble/gen_classes/sampling.py index 571246de2..a750f4a1a 100644 --- a/libensemble/gen_classes/sampling.py +++ b/libensemble/gen_classes/sampling.py @@ -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", @@ -32,13 +33,15 @@ class UniformSample(SampleBase): """ def __init__(self, variables: dict, objectives: dict, _=[], persis_info={}, gen_specs={}, libE_info=None, **kwargs): - super().__init__(_, persis_info, gen_specs, libE_info, **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.qlibE_info + ).ask(n_trials) + ) def tell_numpy(self, calc_in): pass # random sample so nothing to tell diff --git a/libensemble/gen_funcs/persistent_aposmm.py b/libensemble/gen_funcs/persistent_aposmm.py index c5c3aa5e6..2659d9b99 100644 --- a/libensemble/gen_funcs/persistent_aposmm.py +++ b/libensemble/gen_funcs/persistent_aposmm.py @@ -539,6 +539,7 @@ def decide_where_to_start_localopt(H, n, n_s, rk_const, ld=0, mu=0, nu=0): .. seealso:: `start_persistent_local_opt_gens.py `_ """ + print(H["x_on_cube"]) r_k = calc_rk(n, n_s, rk_const, ld) diff --git a/libensemble/generators.py b/libensemble/generators.py index fc426c3fe..00c0cd9a5 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -114,29 +114,39 @@ def __init__( self.variables = variables self.objectives = objectives self.gen_specs = gen_specs - if len(kwargs) > 0: # so user can specify gen-specific parameters as kwargs to constructor - if not self.gen_specs.get("user"): - self.gen_specs["user"] = {} - self.gen_specs["user"].update(kwargs) - if not persis_info: - self.persis_info = add_unique_random_streams({}, 4, seed=4321)[1] - else: - self.persis_info = persis_info + if self.variables: self._vars_x_mapping = {i: k for i, k in enumerate(self.variables.keys())} self._vars_f_mapping = {i: k for i, k, in enumerate(self.objectives.keys())} + self._numeric_vars = [] self.n = len(self.variables) # we'll unpack output x's to correspond with variables if "lb" not in kwargs and "ub" not in kwargs: lb = [] ub = [] - for v in self.variables.values(): + for i, v in enumerate(self.variables.values()): if isinstance(v, list) and (isinstance(v[0], int) or isinstance(v[0], float)): # we got a range, append to lb and ub + self._numeric_vars.append(self.variables.keys()[i]) 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 + if not self.gen_specs.get("user"): + self.gen_specs["user"] = {} + self.gen_specs["user"].update(kwargs) + if not persis_info: + self.persis_info = add_unique_random_streams({}, 4, seed=4321)[1] + else: + self.persis_info = persis_info + + def _gen_out_to_vars(self, results: dict) -> dict: + pass + + def _objs_to_gen_in(self, results: dict) -> dict: + pass + @abstractmethod def ask_numpy(self, num_points: Optional[int] = 0) -> npt.NDArray: """Request the next set of points to evaluate, as a NumPy array.""" @@ -147,13 +157,11 @@ def tell_numpy(self, results: npt.NDArray) -> None: def ask(self, num_points: Optional[int] = 0) -> List[dict]: """Request the next set of points to evaluate.""" - return np_to_list_dicts(self.ask_numpy(num_points)) + return self._gen_out_to_vars(np_to_list_dicts(self.ask_numpy(num_points))) 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(self._objs_to_gen_in(results))) class LibensembleGenThreadInterfacer(LibensembleGenerator): From fc3028447565cda01aeaa06fff7c4ff9b6cfa27d Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 28 Oct 2024 16:43:10 -0500 Subject: [PATCH 246/288] remove a debugging print --- libensemble/gen_funcs/persistent_aposmm.py | 1 - 1 file changed, 1 deletion(-) diff --git a/libensemble/gen_funcs/persistent_aposmm.py b/libensemble/gen_funcs/persistent_aposmm.py index 2659d9b99..c5c3aa5e6 100644 --- a/libensemble/gen_funcs/persistent_aposmm.py +++ b/libensemble/gen_funcs/persistent_aposmm.py @@ -539,7 +539,6 @@ def decide_where_to_start_localopt(H, n, n_s, rk_const, ld=0, mu=0, nu=0): .. seealso:: `start_persistent_local_opt_gens.py `_ """ - print(H["x_on_cube"]) r_k = calc_rk(n, n_s, rk_const, ld) From 98267e39ea9426274c486c61783c2caa25712564 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 30 Oct 2024 08:50:47 -0500 Subject: [PATCH 247/288] small fixes, including slotting-in x-on-cube, removing hardcoded -10 in initial_sample_size check, initing/fixing sim_ids to be -1, and can specify nworkers as kwarg to aposmm class --- libensemble/gen_classes/aposmm.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index 9b1e22cc0..ca8455d21 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -34,7 +34,7 @@ def __init__( persis_info = add_unique_random_streams({}, 2, seed=4321)[1] super().__init__(History, persis_info, gen_specs, libE_info, **kwargs) if not self.persis_info.get("nworkers"): - self.persis_info["nworkers"] = gen_specs["user"]["max_active_runs"] # ?????????? + self.persis_info["nworkers"] = kwargs.get("nworkers", gen_specs["user"]["max_active_runs"]) self.all_local_minima = [] self._ask_idx = 0 self._last_ask = None @@ -48,7 +48,7 @@ def _slot_in_data(self, results): self._tell_buf["f"][self._n_buffd_results] = results["f"] self._tell_buf["x"][self._n_buffd_results] = results["x"] self._tell_buf["sim_id"][self._n_buffd_results] = results["sim_id"] - # self._tell_buf["x_on_cube"][self._n_buffd_results] = results["x_on_cube"] + self._tell_buf["x_on_cube"][self._n_buffd_results] = results["x_on_cube"] self._tell_buf["local_pt"][self._n_buffd_results] = results["local_pt"] @property @@ -60,9 +60,7 @@ def _array_size(self): @property def _enough_initial_sample(self): """We're typically happy with at least 90% of the initial sample, or we've already told the initial sample""" - return ( - self._n_buffd_results >= self.gen_specs["user"]["initial_sample_size"] - 10 - ) or self._told_initial_sample + return (self._n_buffd_results >= self.gen_specs["user"]["initial_sample_size"]) or self._told_initial_sample def ask_numpy(self, num_points: int = 0) -> npt.NDArray: """Request the next set of points to evaluate, as a NumPy array.""" @@ -100,6 +98,7 @@ def tell_numpy(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: self._n_buffd_results == 0 # ONLY NEED TO BUFFER RESULTS FOR INITIAL SAMPLE???? ): # Optimas prefers to give back chunks of initial_sample. So we buffer them self._tell_buf = np.zeros(self._array_size, dtype=self.gen_specs["out"] + [("f", float)]) + self._tell_buf["sim_id"] = -1 if not self._enough_initial_sample: self._slot_in_data(np.copy(results)) @@ -107,7 +106,7 @@ def tell_numpy(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: self._n_total_results += len(results) if not self._told_initial_sample and self._enough_initial_sample: - self._tell_buf = self._tell_buf[self._tell_buf["sim_id"] != 0] + self._tell_buf = self._tell_buf[self._tell_buf["sim_id"] != -1] super().tell_numpy(self._tell_buf, tag) self._told_initial_sample = True self._n_buffd_results = 0 From 74579d562881780e5afe4a5ba96a33b375910f5d Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 30 Oct 2024 11:28:24 -0500 Subject: [PATCH 248/288] simplifications from code-review; need to determine reason for hang in optimas when roughly enough initial sample points have been slotted in --- libensemble/gen_classes/aposmm.py | 52 +++++++++++++------------------ libensemble/generators.py | 2 +- 2 files changed, 22 insertions(+), 32 deletions(-) diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index ca8455d21..118d243a5 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -45,22 +45,20 @@ def __init__( def _slot_in_data(self, results): """Slot in libE_calc_in and trial data into corresponding array fields. *Initial sample only!!*""" - self._tell_buf["f"][self._n_buffd_results] = results["f"] - self._tell_buf["x"][self._n_buffd_results] = results["x"] - self._tell_buf["sim_id"][self._n_buffd_results] = results["sim_id"] - self._tell_buf["x_on_cube"][self._n_buffd_results] = results["x_on_cube"] - self._tell_buf["local_pt"][self._n_buffd_results] = results["local_pt"] - - @property - def _array_size(self): - """Output array size must match either initial sample or N points to evaluate in parallel.""" - user = self.gen_specs["user"] - return user["initial_sample_size"] if not self._told_initial_sample else user["max_active_runs"] - - @property + for field in results.dtype.names: + self._tell_buf[field][self._n_buffd_results] = results[field] + + # @property + # def _array_size(self): + # """Output array size must match either initial sample or N points to evaluate in parallel.""" + # user = self.gen_specs["user"] # SHOULD NOT BE MAX ACTIVE RUNS. NWORKERS OR LEN LAST TELL + # # return user["initial_sample_size"] if not self._told_initial_sample else user["max_active_runs"] + # return user["initial_sample_size"] if not self._told_initial_sample else len(self._last_ask) + def _enough_initial_sample(self): - """We're typically happy with at least 90% of the initial sample, or we've already told the initial sample""" - return (self._n_buffd_results >= self.gen_specs["user"]["initial_sample_size"]) or self._told_initial_sample + return ( + self._n_buffd_results >= int(self.gen_specs["user"]["initial_sample_size"]) + ) or self._told_initial_sample def ask_numpy(self, num_points: int = 0) -> npt.NDArray: """Request the next set of points to evaluate, as a NumPy array.""" @@ -87,34 +85,26 @@ def ask_numpy(self, num_points: int = 0) -> npt.NDArray: return results def tell_numpy(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: - if (results is None and tag == PERSIS_STOP) or len( - results - ) == self._array_size: # told to stop, by final_tell or libE - self._told_initial_sample = True # we definitely got an initial sample already if one matches + if (results is None and tag == PERSIS_STOP) or self._told_initial_sample: # told to stop, by final_tell or libE super().tell_numpy(results, tag) + self._n_buffd_results = 0 return - if ( - self._n_buffd_results == 0 # ONLY NEED TO BUFFER RESULTS FOR INITIAL SAMPLE???? - ): # Optimas prefers to give back chunks of initial_sample. So we buffer them - self._tell_buf = np.zeros(self._array_size, dtype=self.gen_specs["out"] + [("f", float)]) + # Initial sample buffering here: + + if self._n_buffd_results == 0: + self._tell_buf = np.zeros(self.gen_specs["user"]["initial_sample_size"], dtype=results.dtype) self._tell_buf["sim_id"] = -1 - if not self._enough_initial_sample: + if not self._enough_initial_sample(): self._slot_in_data(np.copy(results)) self._n_buffd_results += len(results) - self._n_total_results += len(results) - if not self._told_initial_sample and self._enough_initial_sample: - self._tell_buf = self._tell_buf[self._tell_buf["sim_id"] != -1] + if self._enough_initial_sample(): super().tell_numpy(self._tell_buf, tag) self._told_initial_sample = True self._n_buffd_results = 0 - elif self._told_initial_sample: # probably libE: given back smaller selection. but from alloc, so its ok? - super().tell_numpy(results, tag) - self._n_buffd_results = 0 # dont want to send the same point more than once. slotted in earlier - def ask_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) diff --git a/libensemble/generators.py b/libensemble/generators.py index 4367bd92f..904aba930 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -199,9 +199,9 @@ def tell_numpy(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: self.inbox.put( (tag, {"libE_info": {"H_rows": np.copy(results["sim_id"]), "persistent": True, "executor": None}}) ) + self.inbox.put((0, np.copy(results))) else: self.inbox.put((tag, None)) - self.inbox.put((0, np.copy(results))) def final_tell(self, results: npt.NDArray = None) -> (npt.NDArray, dict, int): """Send any last results to the generator, and it to close down.""" From 63ef323ac371f3fc13e4869e4b0e694cb3dafdb2 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 30 Oct 2024 15:38:03 -0500 Subject: [PATCH 249/288] i determined that besides having asked for at least as many points as the last ask, another important indicator is that the last point produced has been returned to the gen. this gets us past the initial sample now, but now aposmm seems to return empty arrays? --- libensemble/gen_classes/aposmm.py | 49 ++++++++++++++++++------------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index 118d243a5..2f93b09ac 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -48,44 +48,51 @@ def _slot_in_data(self, results): for field in results.dtype.names: self._tell_buf[field][self._n_buffd_results] = results[field] - # @property - # def _array_size(self): - # """Output array size must match either initial sample or N points to evaluate in parallel.""" - # user = self.gen_specs["user"] # SHOULD NOT BE MAX ACTIVE RUNS. NWORKERS OR LEN LAST TELL - # # return user["initial_sample_size"] if not self._told_initial_sample else user["max_active_runs"] - # return user["initial_sample_size"] if not self._told_initial_sample else len(self._last_ask) - 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_ask_genf(self): + """We're presumably ready to be asked IF: + - We have no _last_ask cached + - the last point given out has returned AND we've been asked *at least* as many points as we cached + """ + return ( + self._last_ask is None + or (self._last_ask["sim_id"][-1] in self._tell_buf["sim_id"]) + and (self._ask_idx >= len(self._last_ask)) + ) + def ask_numpy(self, num_points: int = 0) -> npt.NDArray: """Request the next set of points to evaluate, as a NumPy array.""" - if (self._last_ask is None) or ( - self._ask_idx >= len(self._last_ask) - ): # haven't been asked yet, or all previously enqueued points have been "asked" + if self._ready_to_ask_genf(): self._ask_idx = 0 self._last_ask = super().ask_numpy(num_points) - if self._last_ask[ - "local_min" - ].any(): # filter out local minima rows, but they're cached in self.all_local_minima + + if self._last_ask["local_min"].any(): # filter out local minima rows min_idxs = self._last_ask["local_min"] self.all_local_minima.append(self._last_ask[min_idxs]) self._last_ask = self._last_ask[~min_idxs] + if num_points > 0: # we've been asked for a selection of the last ask - results = np.copy( - self._last_ask[self._ask_idx : self._ask_idx + num_points] - ) # if resetting _last_ask later, results may point to "None" + results = np.copy(self._last_ask[self._ask_idx : self._ask_idx + num_points]) self._ask_idx += num_points - return results - results = np.copy(self._last_ask) - self.results = results - self._last_ask = None + if self._ask_idx >= len(self._last_ask): # now given out everything; need to reset + pass # DEBUGGING WILL CONTINUE HERE + + else: + results = np.copy(self._last_ask) + self._last_ask = None + return results def tell_numpy(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: - if (results is None and tag == PERSIS_STOP) or self._told_initial_sample: # told to stop, by final_tell or libE + if (results is None and tag == PERSIS_STOP) or self._told_initial_sample: + if results["sim_id"] >= 99: + import ipdb + + ipdb.set_trace() super().tell_numpy(results, tag) self._n_buffd_results = 0 return From 1b1cd59201ba859fa11257019ad75362018d4fb9 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 30 Oct 2024 15:49:29 -0500 Subject: [PATCH 250/288] better check: all generated sim_ids have returned to the buffer --- libensemble/gen_classes/aposmm.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index 2f93b09ac..a5b967bf6 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -60,7 +60,7 @@ def _ready_to_ask_genf(self): """ return ( self._last_ask is None - or (self._last_ask["sim_id"][-1] in self._tell_buf["sim_id"]) + or all([i in self._tell_buf["sim_id"] for i in self._last_ask["sim_id"]]) and (self._ask_idx >= len(self._last_ask)) ) @@ -89,10 +89,6 @@ def ask_numpy(self, num_points: int = 0) -> npt.NDArray: def tell_numpy(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: if (results is None and tag == PERSIS_STOP) or self._told_initial_sample: - if results["sim_id"] >= 99: - import ipdb - - ipdb.set_trace() super().tell_numpy(results, tag) self._n_buffd_results = 0 return From dcb3486b1936fce5042225b9aa57eb5aa6aeeb88 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 30 Oct 2024 16:27:03 -0500 Subject: [PATCH 251/288] fix LibensembleGenThreadInterfacer._set_sim_ended to use results' dtype + [("sim_ended", bool)] --- libensemble/generators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 904aba930..4277187d8 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -172,7 +172,7 @@ def setup(self) -> None: ) # note that self.thread's inbox/outbox are unused by the underlying gen def _set_sim_ended(self, results: npt.NDArray) -> npt.NDArray: - new_results = np.zeros(len(results), dtype=self.gen_specs["out"] + [("sim_ended", bool), ("f", float)]) + new_results = np.zeros(len(results), dtype=results.dtype + [("sim_ended", bool)]) for field in results.dtype.names: try: new_results[field] = results[field] From 6828fe09d87076d11010f6eee8ca5e86930b48ac Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 30 Oct 2024 16:34:34 -0500 Subject: [PATCH 252/288] whoops, fix dtype definition in set_sim_ended --- libensemble/generators.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 4277187d8..84cc81d27 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -172,7 +172,8 @@ def setup(self) -> None: ) # note that self.thread's inbox/outbox are unused by the underlying gen def _set_sim_ended(self, results: npt.NDArray) -> npt.NDArray: - new_results = np.zeros(len(results), dtype=results.dtype + [("sim_ended", bool)]) + new_dtype = results.dtype.descr + [("sim_ended", bool)] + new_results = np.zeros(len(results), dtype=new_dtype) for field in results.dtype.names: try: new_results[field] = results[field] From e443af910c9b986c4078726789d575bb15562781 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 30 Oct 2024 17:00:05 -0500 Subject: [PATCH 253/288] cleanup unused attributes --- libensemble/gen_classes/aposmm.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index a5b967bf6..b17abc5f4 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -40,7 +40,6 @@ def __init__( self._last_ask = None self._tell_buf = None self._n_buffd_results = 0 - self._n_total_results = 0 self._told_initial_sample = False def _slot_in_data(self, results): @@ -90,7 +89,6 @@ def ask_numpy(self, num_points: int = 0) -> npt.NDArray: def tell_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().tell_numpy(results, tag) - self._n_buffd_results = 0 return # Initial sample buffering here: From a1937a91f7edfebad2bb32b6bf0ed764d6fb3b0e Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 31 Oct 2024 09:05:55 -0500 Subject: [PATCH 254/288] better buffer updating suggestion from shuds --- libensemble/gen_classes/aposmm.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index b17abc5f4..0de02fffd 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -44,8 +44,7 @@ def __init__( def _slot_in_data(self, results): """Slot in libE_calc_in and trial data into corresponding array fields. *Initial sample only!!*""" - for field in results.dtype.names: - self._tell_buf[field][self._n_buffd_results] = results[field] + self._tell_buf[self._n_buffd_results : self._n_buffd_results + len(results)] = results def _enough_initial_sample(self): return ( From dedef4c7a3db05d306ce8b59f06e4780c9f492fc Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 31 Oct 2024 10:05:51 -0500 Subject: [PATCH 255/288] fix ask-the-genf condition to accomodate after initial sample has completed --- libensemble/gen_classes/aposmm.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index 0de02fffd..d3f068577 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -52,15 +52,19 @@ def _enough_initial_sample(self): ) or self._told_initial_sample def _ready_to_ask_genf(self): - """We're presumably ready to be asked IF: - - We have no _last_ask cached - - the last point given out has returned AND we've been asked *at least* as many points as we cached """ - return ( - self._last_ask is None - or all([i in self._tell_buf["sim_id"] for i in self._last_ask["sim_id"]]) - and (self._ask_idx >= len(self._last_ask)) - ) + We're presumably ready to be asked IF: + - When we're working on the initial sample: + - We have no _last_ask cached + - all points given out have returned AND we've been asked *at least* as many points as we cached + - When we're done with the initial sample: + - we've been asked *at least* as many points as we cached + """ + if not self._told_initial_sample and self._last_ask is not None: + cond = all([i in self._tell_buf["sim_id"] for i in self._last_ask["sim_id"]]) + else: + cond = True + return self._last_ask is None or (cond and (self._ask_idx >= len(self._last_ask))) def ask_numpy(self, num_points: int = 0) -> npt.NDArray: """Request the next set of points to evaluate, as a NumPy array.""" @@ -76,8 +80,6 @@ def ask_numpy(self, num_points: int = 0) -> npt.NDArray: if num_points > 0: # we've been asked for a selection of the last ask results = np.copy(self._last_ask[self._ask_idx : self._ask_idx + num_points]) self._ask_idx += num_points - if self._ask_idx >= len(self._last_ask): # now given out everything; need to reset - pass # DEBUGGING WILL CONTINUE HERE else: results = np.copy(self._last_ask) From 4b812d6b5cfca84d3702dac6b2415d550516ad1b Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 31 Oct 2024 10:40:07 -0500 Subject: [PATCH 256/288] fix set_sim_ended new array dtype specification --- libensemble/generators.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 84cc81d27..904aba930 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -172,8 +172,7 @@ def setup(self) -> None: ) # note that self.thread's inbox/outbox are unused by the underlying gen def _set_sim_ended(self, results: npt.NDArray) -> npt.NDArray: - new_dtype = results.dtype.descr + [("sim_ended", bool)] - new_results = np.zeros(len(results), dtype=new_dtype) + new_results = np.zeros(len(results), dtype=self.gen_specs["out"] + [("sim_ended", bool), ("f", float)]) for field in results.dtype.names: try: new_results[field] = results[field] From f9e3cba1b5b835049324e80a23dfdf155ab8d1b7 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 1 Nov 2024 10:42:55 -0500 Subject: [PATCH 257/288] small fixes, and first tentative implementation of converter for xs to variables --- libensemble/gen_classes/aposmm.py | 5 +- libensemble/generators.py | 53 +++++++++++++++++-- .../RENAME_test_persistent_aposmm.py | 2 - 3 files changed, 52 insertions(+), 8 deletions(-) diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index 41720c2f3..0dc4a3ea2 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -31,7 +31,10 @@ def __init__( gen_specs["gen_f"] = aposmm if not gen_specs.get("out"): # gen_specs never especially changes for aposmm even as the problem varies - self.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, self.n), ("x_on_cube", float, self.n), diff --git a/libensemble/generators.py b/libensemble/generators.py index 00c0cd9a5..520311e0d 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -115,9 +115,14 @@ def __init__( self.objectives = objectives self.gen_specs = gen_specs + self._var_to_replace = "x" # need to figure this out dynamically + if self.variables: self._vars_x_mapping = {i: k for i, k in enumerate(self.variables.keys())} self._vars_f_mapping = {i: k for i, k, in enumerate(self.objectives.keys())} + + self._determined_x_mapping = {} + self._numeric_vars = [] self.n = len(self.variables) # we'll unpack output x's to correspond with variables if "lb" not in kwargs and "ub" not in kwargs: @@ -126,7 +131,7 @@ def __init__( for i, v in enumerate(self.variables.values()): if isinstance(v, list) and (isinstance(v[0], int) or isinstance(v[0], float)): # we got a range, append to lb and ub - self._numeric_vars.append(self.variables.keys()[i]) + self._numeric_vars.append(list(self.variables.keys())[i]) lb.append(v[0]) ub.append(v[1]) kwargs["lb"] = np.array(lb) @@ -136,13 +141,52 @@ def __init__( if not self.gen_specs.get("user"): self.gen_specs["user"] = {} self.gen_specs["user"].update(kwargs) - if not persis_info: + if not persis_info.get("rand_stream"): self.persis_info = add_unique_random_streams({}, 4, seed=4321)[1] else: self.persis_info = persis_info - def _gen_out_to_vars(self, results: dict) -> dict: - pass + def _gen_out_to_vars(self, gen_out: dict) -> dict: + + """ + We must replace internal, enumerated "x"s with the variables the user requested to sample over. + + Basically, for the following example, if the user requested the following variables: + + ``{'core': [-3, 3], 'edge': [-2, 2]}`` + + Then for the following directly-from-aposmm point: + + ``{'x0': -0.1, 'x1': 0.7, 'x_on_cube0': 0.4833, + 'x_on_cube1': 0.675, 'sim_id': 0...}`` + + We need to replace (for aposmm, for example) "x0" with "core", "x1" with "edge", + "x_on_cube0" with "core_on_cube", and "x_on_cube1" with "edge_on_cube". + + + """ + new_out = [] + for entry in gen_out: # get a dict + + new_entry = {} + for map_key in self._vars_x_mapping.keys(): # get 0, 1 + + for out_key in entry.keys(): # get x0, x1, x_on_cube0, etc. + + if out_key.endswith(str(map_key)): # found key that ends with 0, 1 + new_name = str(out_key).replace( + self._var_to_replace, self._vars_x_mapping[map_key] + ) # replace 'x' with 'core' + new_name = new_name.rstrip("0123456789") # now remove trailing integer + new_entry[new_name] = entry[out_key] + + elif not out_key[-1].isnumeric(): # found key that is not enumerated + new_entry[out_key] = entry[out_key] + + # we now naturally continue over cases where e.g. the map_key may be 0 but we're looking at x1 + new_out.append(new_entry) + + return new_out def _objs_to_gen_in(self, results: dict) -> dict: pass @@ -182,7 +226,6 @@ def __init__( 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 diff --git a/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py b/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py index ea4595c4e..518da104e 100644 --- a/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py +++ b/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py @@ -198,8 +198,6 @@ def test_asktell_with_persistent_aposmm(): "ftol_abs": 1e-6, "dist_to_bound_multiple": 0.5, "max_active_runs": 6, - "lb": np.array([-3, -2]), - "ub": np.array([3, 2]), }, } From 14c36fae5f253c0b00e6c85f82c2b4e32ba39538 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 1 Nov 2024 12:24:00 -0500 Subject: [PATCH 258/288] perhaps the input conversion will be easier on a numpy array? --- libensemble/generators.py | 9 +++++---- .../tests/unit_tests/RENAME_test_persistent_aposmm.py | 4 ---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 520311e0d..519868725 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -115,7 +115,8 @@ def __init__( self.objectives = objectives self.gen_specs = gen_specs - self._var_to_replace = "x" # need to figure this out dynamically + self._var_to_replace = "x" # need to figure these out dynamically + self._obj_to_replace = "f" if self.variables: self._vars_x_mapping = {i: k for i, k in enumerate(self.variables.keys())} @@ -188,7 +189,7 @@ def _gen_out_to_vars(self, gen_out: dict) -> dict: return new_out - def _objs_to_gen_in(self, results: dict) -> dict: + def _objs_and_vars_to_gen_in(self, results: npt.NDArray) -> npt.NDArray: pass @abstractmethod @@ -205,7 +206,7 @@ def ask(self, num_points: Optional[int] = 0) -> List[dict]: def tell(self, results: List[dict]) -> None: """Send the results of evaluations to the generator.""" - self.tell_numpy(list_dicts_to_np(self._objs_to_gen_in(results))) + self.tell_numpy(self._objs_and_vars_to_gen_in(list_dicts_to_np(results))) class LibensembleGenThreadInterfacer(LibensembleGenerator): @@ -263,7 +264,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(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.""" diff --git a/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py b/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py index 518da104e..cbfdf230b 100644 --- a/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py +++ b/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py @@ -184,11 +184,7 @@ def test_asktell_with_persistent_aposmm(): n = 2 eval_max = 2000 - # gen_out = [("x", float, n), ("x_on_cube", float, n), ("sim_id", int), ("local_min", bool), ("local_pt", bool)] - gen_specs = { - # "in": ["x", "f", "local_pt", "sim_id", "sim_ended", "x_on_cube", "local_min"], - # "out": gen_out, "user": { "initial_sample_size": 100, "sample_points": np.round(minima, 1), From 25299e72d8da4b41060a38214bee5f6e0461cee3 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 1 Nov 2024 15:04:41 -0500 Subject: [PATCH 259/288] tentatively complete converter for vars/objs -> x/f. but those xs and fs need to be figured out reasonably, somehow, still --- libensemble/generators.py | 48 +++++++++++++++---- .../RENAME_test_persistent_aposmm.py | 3 +- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 519868725..872487a64 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -120,7 +120,6 @@ def __init__( if self.variables: self._vars_x_mapping = {i: k for i, k in enumerate(self.variables.keys())} - self._vars_f_mapping = {i: k for i, k, in enumerate(self.objectives.keys())} self._determined_x_mapping = {} @@ -189,8 +188,42 @@ def _gen_out_to_vars(self, gen_out: dict) -> dict: return new_out - def _objs_and_vars_to_gen_in(self, results: npt.NDArray) -> npt.NDArray: - pass + def _objs_and_vars_to_gen_in(self, results: dict) -> dict: + """We now need to do the inverse of _gen_out_to_vars, plus replace + the objective name with the internal gen's expected name, .e.g "energy" -> "f". + + So given: + + {'core': -0.1, 'core_on_cube': 0.483, 'sim_id': 0, 'local_min': False, + 'local_pt': False, 'edge': 0.7, 'edge_on_cube': 0.675, 'energy': -1.02} + + We need the following again: + + {'x0': -0.1, 'x_on_cube0': 0.483, 'sim_id': 0, 'local_min': False, + 'local_pt': False, 'x1': 0.7, 'x_on_cube1': 0.675, 'f': -1.02} + + """ + new_results = [] + for entry in results: # get a dict + new_entry = {} + for map_key in self._vars_x_mapping.keys(): # get 0, 1 + for out_key in entry.keys(): # get core, core_on_cube, energy, sim_id, etc. + if self._vars_x_mapping[map_key] == out_key: # found core + new_name = self._var_to_replace + str(map_key) # create x0, x1, etc. + elif out_key.startswith(self._vars_x_mapping[map_key]): # found core_on_cube + new_name = out_key.replace(self._vars_x_mapping[map_key], self._var_to_replace) + str( + map_key + ) # create x_on_cube0 + elif out_key in list(self.objectives.keys()): # found energy + new_name = self._obj_to_replace # create f + elif out_key in self.gen_specs["persis_in"]: # found everything else, sim_id, local_pt, etc. + new_name = out_key + else: # continue over cases where e.g. the map_key may be 0 but we're looking at x1 + continue + new_entry[new_name] = entry[out_key] + new_results.append(new_entry) + + return new_results @abstractmethod def ask_numpy(self, num_points: Optional[int] = 0) -> npt.NDArray: @@ -206,7 +239,7 @@ def ask(self, num_points: Optional[int] = 0) -> List[dict]: def tell(self, results: List[dict]) -> None: """Send the results of evaluations to the generator.""" - self.tell_numpy(self._objs_and_vars_to_gen_in(list_dicts_to_np(results))) + self.tell_numpy(list_dicts_to_np(self._objs_and_vars_to_gen_in(results))) class LibensembleGenThreadInterfacer(LibensembleGenerator): @@ -251,12 +284,7 @@ def setup(self) -> None: ) # note that self.thread's inbox/outbox are unused by the underlying gen def _set_sim_ended(self, results: npt.NDArray) -> npt.NDArray: - new_results = np.zeros( - len(results), - dtype=self.gen_specs["out"] - + [("sim_ended", bool), ("f", float)] - + [(i, float) for i in self.objectives.keys()], - ) + new_results = np.zeros(len(results), dtype=self.gen_specs["out"] + [("sim_ended", bool), ("f", float)]) for field in results.dtype.names: new_results[field] = results[field] new_results["sim_ended"] = True diff --git a/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py b/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py index cbfdf230b..669bdeb03 100644 --- a/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py +++ b/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py @@ -14,7 +14,6 @@ import libensemble.tests.unit_tests.setup as setup from libensemble.sim_funcs.six_hump_camel import six_hump_camel_func, six_hump_camel_grad -from libensemble.utils.misc import list_dicts_to_np libE_info = {"comm": {}} @@ -226,7 +225,7 @@ def test_asktell_with_persistent_aposmm(): point["energy"] = six_hump_camel_func(np.array([point["core"], point["edge"]])) total_evals += 1 my_APOSMM.tell(sample) - H, persis_info, exit_code = my_APOSMM.final_tell(list_dicts_to_np(sample)) # final_tell currently requires numpy + H, persis_info, exit_code = my_APOSMM.final_tell() assert exit_code == FINISHED_PERSISTENT_GEN_TAG, "Standalone persistent_aposmm didn't exit correctly" assert persis_info.get("run_order"), "Standalone persistent_aposmm didn't do any localopt runs" From f0736fbc3c3be049b8fcb34339cf333a5238a7ae Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 1 Nov 2024 15:12:54 -0500 Subject: [PATCH 260/288] some cleanup --- libensemble/generators.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 872487a64..16fd4529c 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -115,23 +115,19 @@ def __init__( self.objectives = objectives self.gen_specs = gen_specs - self._var_to_replace = "x" # need to figure these out dynamically - self._obj_to_replace = "f" + self._internal_variable = "x" # need to figure these out dynamically + self._internal_objective = "f" if self.variables: self._vars_x_mapping = {i: k for i, k in enumerate(self.variables.keys())} - self._determined_x_mapping = {} - - self._numeric_vars = [] - self.n = len(self.variables) # we'll unpack output x's to correspond with 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)): - # we got a range, append to lb and ub - self._numeric_vars.append(list(self.variables.keys())[i]) lb.append(v[0]) ub.append(v[1]) kwargs["lb"] = np.array(lb) @@ -175,7 +171,7 @@ def _gen_out_to_vars(self, gen_out: dict) -> dict: if out_key.endswith(str(map_key)): # found key that ends with 0, 1 new_name = str(out_key).replace( - self._var_to_replace, self._vars_x_mapping[map_key] + self._internal_variable, self._vars_x_mapping[map_key] ) # replace 'x' with 'core' new_name = new_name.rstrip("0123456789") # now remove trailing integer new_entry[new_name] = entry[out_key] @@ -205,21 +201,29 @@ def _objs_and_vars_to_gen_in(self, results: dict) -> dict: """ new_results = [] for entry in results: # get a dict + new_entry = {} for map_key in self._vars_x_mapping.keys(): # get 0, 1 + for out_key in entry.keys(): # get core, core_on_cube, energy, sim_id, etc. + if self._vars_x_mapping[map_key] == out_key: # found core - new_name = self._var_to_replace + str(map_key) # create x0, x1, etc. + new_name = self._internal_variable + str(map_key) # create x0, x1, etc. + elif out_key.startswith(self._vars_x_mapping[map_key]): # found core_on_cube - new_name = out_key.replace(self._vars_x_mapping[map_key], self._var_to_replace) + str( + new_name = out_key.replace(self._vars_x_mapping[map_key], self._internal_variable) + str( map_key ) # create x_on_cube0 + elif out_key in list(self.objectives.keys()): # found energy - new_name = self._obj_to_replace # create f + new_name = self._internal_objective # create f + elif out_key in self.gen_specs["persis_in"]: # found everything else, sim_id, local_pt, etc. new_name = out_key + else: # continue over cases where e.g. the map_key may be 0 but we're looking at x1 continue + new_entry[new_name] = entry[out_key] new_results.append(new_entry) From 7fa4d1ebb9a660e89b89b822d510f201e0ece40f Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 4 Nov 2024 09:09:26 -0600 Subject: [PATCH 261/288] fix continue-condition to occur earlier if we're looking at keys we don't want to convert. fix key-that-starts-with-variable condition, plus append the distinguishing integer --- libensemble/generators.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 16fd4529c..b5e1db4f5 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -207,13 +207,22 @@ def _objs_and_vars_to_gen_in(self, results: dict) -> dict: for out_key in entry.keys(): # get core, core_on_cube, energy, sim_id, etc. + # continue over cases where e.g. the map_key may be 0 but we're looking at x1 + if out_key[-1].isnumeric() and not out_key.endswith(str(map_key)): + continue + if self._vars_x_mapping[map_key] == out_key: # found core new_name = self._internal_variable + str(map_key) # create x0, x1, etc. - elif out_key.startswith(self._vars_x_mapping[map_key]): # found core_on_cube - new_name = out_key.replace(self._vars_x_mapping[map_key], self._internal_variable) + str( - map_key - ) # create x_on_cube0 + # we need to strip trailing ints for this condition in case vars were formatted: x0, x1 + # avoid the "x0_on_cube0" naming scheme + elif out_key.startswith(self._vars_x_mapping[map_key].rstrip("0123456789")): # found core_on_cube + new_name = out_key.replace( + self._vars_x_mapping[map_key].rstrip("0123456789"), self._internal_variable + ) + # presumably multi-dim key; preserve that trailing int on the end of new key + if not new_name[-1].isnumeric(): + new_name += str(map_key) # create x_on_cube0 elif out_key in list(self.objectives.keys()): # found energy new_name = self._internal_objective # create f @@ -221,9 +230,6 @@ def _objs_and_vars_to_gen_in(self, results: dict) -> dict: elif out_key in self.gen_specs["persis_in"]: # found everything else, sim_id, local_pt, etc. new_name = out_key - else: # continue over cases where e.g. the map_key may be 0 but we're looking at x1 - continue - new_entry[new_name] = entry[out_key] new_results.append(new_entry) From 14daf3caebbf961c499387b26f04f8649ac7503a Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 4 Nov 2024 09:24:31 -0600 Subject: [PATCH 262/288] test fixes, plus if our gen naturally returns the requested variables, honor that --- libensemble/gen_classes/sampling.py | 2 +- libensemble/generators.py | 9 +++++++++ libensemble/tests/unit_tests/test_asktell.py | 18 ++++-------------- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/libensemble/gen_classes/sampling.py b/libensemble/gen_classes/sampling.py index a750f4a1a..35a075e22 100644 --- a/libensemble/gen_classes/sampling.py +++ b/libensemble/gen_classes/sampling.py @@ -39,7 +39,7 @@ def __init__(self, variables: dict, objectives: dict, _=[], persis_info={}, gen_ def ask_numpy(self, n_trials): return list_dicts_to_np( UniformSampleDicts( - self.variables, self.objectives, self.History, self.persis_info, self.gen_specs, self.qlibE_info + self.variables, self.objectives, self.History, self.persis_info, self.gen_specs, self.libE_info ).ask(n_trials) ) diff --git a/libensemble/generators.py b/libensemble/generators.py index b5e1db4f5..381a04b1d 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -113,7 +113,9 @@ def __init__( ): self.variables = variables self.objectives = objectives + self.History = History self.gen_specs = gen_specs + self.libE_info = libE_info self._internal_variable = "x" # need to figure these out dynamically self._internal_objective = "f" @@ -159,8 +161,15 @@ def _gen_out_to_vars(self, gen_out: dict) -> dict: We need to replace (for aposmm, for example) "x0" with "core", "x1" with "edge", "x_on_cube0" with "core_on_cube", and "x_on_cube1" with "edge_on_cube". + ... + + BUT: if we're given "x0" and "x1" as our variables, we need to honor that """ + + if all([i in list(self.variables.keys()) for i in list(gen_out[0].keys())]): + return gen_out + new_out = [] for entry in gen_out: # get a dict diff --git a/libensemble/tests/unit_tests/test_asktell.py b/libensemble/tests/unit_tests/test_asktell.py index fd80b8829..5a4bd9565 100644 --- a/libensemble/tests/unit_tests/test_asktell.py +++ b/libensemble/tests/unit_tests/test_asktell.py @@ -1,6 +1,5 @@ import numpy as np -from libensemble.tools.tools import add_unique_random_streams from libensemble.utils.misc import list_dicts_to_np @@ -25,25 +24,16 @@ def _check_conversion(H, npp): def test_asktell_sampling_and_utils(): from libensemble.gen_classes.sampling import UniformSample - persis_info = add_unique_random_streams({}, 5, seed=1234) - gen_specs = { - "out": [("x", float, (2,))], - "user": { - "lb": np.array([-3, -2]), - "ub": np.array([3, 2]), - }, - } + variables = {"x0": [-3, 3], "x1": [-2, 2]} + objectives = {"f": "EXPLORE"} # Test initialization with libensembley parameters - gen = UniformSample(None, persis_info[1], gen_specs, None) - assert len(gen.ask(10)) == 10 - - # Test initialization gen-specific keyword args - gen = UniformSample(gen_specs=gen_specs, lb=np.array([-3, -2]), ub=np.array([3, 2])) + gen = UniformSample(variables, objectives) assert len(gen.ask(10)) == 10 out_np = gen.ask_numpy(3) # should get numpy arrays, non-flattened out = gen.ask(3) # needs to get dicts, 2d+ arrays need to be flattened + assert all([len(x) == 2 for x in out]) # np_to_list_dicts is now tested # now we test list_dicts_to_np directly From 114c7a4957c9fef88df2709738aea272cdb3c70d Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 4 Nov 2024 09:42:40 -0600 Subject: [PATCH 263/288] fix asktell_gen functionality test - including removing wrapper tests, since variables/objectives probably wont be passed in. remove exact H-entry test, since the gen does its own internal persis_info --- .../test_sampling_asktell_gen.py | 23 ++++--------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py b/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py index 4d1ac40e9..ade86b7a5 100644 --- a/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py +++ b/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py @@ -18,7 +18,6 @@ # Import libEnsemble items for this test from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f from libensemble.gen_classes.sampling import UniformSample, UniformSampleDicts -from libensemble.gen_funcs.persistent_gen_wrapper import persistent_gen_f as gen_f from libensemble.libE import libE from libensemble.tools import add_unique_random_streams, parse_args @@ -58,29 +57,16 @@ def sim_f(In): alloc_specs = {"alloc_f": alloc_f} exit_criteria = {"gen_max": 201} - for inst in range(4): + for inst in range(2): persis_info = add_unique_random_streams({}, nworkers + 1, seed=1234) if inst == 0: - # Using wrapper - pass class - generator = UniformSample - gen_specs["gen_f"] = gen_f - gen_specs["user"]["generator"] = generator - - if inst == 1: - # Using wrapper - pass object - gen_specs["gen_f"] = gen_f - generator = UniformSample(variables, objectives, None, persis_info[1], gen_specs, None) - gen_specs["user"]["generator"] = generator - if inst == 2: # Using asktell runner - pass object - gen_specs.pop("gen_f", None) - generator = UniformSample(variables, objectives, None, persis_info[1], gen_specs, None) + generator = UniformSample(variables, objectives) gen_specs["generator"] = generator - if inst == 3: + if inst == 1: # Using asktell runner - pass object - with standardized interface. - gen_specs.pop("gen_f", None) - generator = UniformSampleDicts(variables, objectives, None, persis_info[1], gen_specs, None) + generator = UniformSampleDicts(variables, objectives) gen_specs["generator"] = generator H, persis_info, flag = libE( @@ -90,4 +76,3 @@ def sim_f(In): if is_manager: print(H[["sim_id", "x", "f"]][:10]) assert len(H) >= 201, f"H has length {len(H)}" - assert np.isclose(H["f"][9], 1.96760289) From 507bc0a15b81f5c1f0349cffb9537acb4041097e Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 4 Nov 2024 10:01:51 -0600 Subject: [PATCH 264/288] just use UniformSample class --- .../test_sampling_asktell_gen.py | 32 +++++++------------ 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py b/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py index ade86b7a5..506118d5c 100644 --- a/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py +++ b/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py @@ -17,7 +17,7 @@ # Import libEnsemble items for this test from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f -from libensemble.gen_classes.sampling import UniformSample, UniformSampleDicts +from libensemble.gen_classes.sampling import UniformSample from libensemble.libE import libE from libensemble.tools import add_unique_random_streams, parse_args @@ -57,22 +57,14 @@ def sim_f(In): alloc_specs = {"alloc_f": alloc_f} exit_criteria = {"gen_max": 201} - for inst in range(2): - persis_info = add_unique_random_streams({}, nworkers + 1, seed=1234) - - if inst == 0: - # Using asktell runner - pass object - generator = UniformSample(variables, objectives) - gen_specs["generator"] = generator - if inst == 1: - # Using asktell runner - pass object - with standardized interface. - generator = UniformSampleDicts(variables, objectives) - gen_specs["generator"] = generator - - H, persis_info, flag = libE( - sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs=libE_specs - ) - - if is_manager: - print(H[["sim_id", "x", "f"]][:10]) - assert len(H) >= 201, f"H has length {len(H)}" + persis_info = add_unique_random_streams({}, nworkers + 1, seed=1234) + + # Using asktell runner - pass object + generator = UniformSample(variables, objectives) + gen_specs["generator"] = generator + + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs=libE_specs) + + if is_manager: + print(H[["sim_id", "x", "f"]][:10]) + assert len(H) >= 201, f"H has length {len(H)}" From 18a52c92915d7e0f867694f12452af19b5786545 Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 4 Nov 2024 12:48:54 -0600 Subject: [PATCH 265/288] remove ask/tell surmise and ask/tell surmise test - they were proof-of-concepts from before we became dedicated to ask/tell, plus currently it's gen_out is rather "largely dimensioned" for defining via variables/objectives --- libensemble/gen_classes/surmise.py | 60 -------- ...est_persistent_surmise_killsims_asktell.py | 144 ------------------ 2 files changed, 204 deletions(-) delete mode 100644 libensemble/gen_classes/surmise.py delete mode 100644 libensemble/tests/regression_tests/test_persistent_surmise_killsims_asktell.py diff --git a/libensemble/gen_classes/surmise.py b/libensemble/gen_classes/surmise.py deleted file mode 100644 index b62cd20dc..000000000 --- a/libensemble/gen_classes/surmise.py +++ /dev/null @@ -1,60 +0,0 @@ -import copy -import queue as thread_queue -from typing import List - -import numpy as np -from numpy import typing as npt - -from libensemble.generators import LibensembleGenThreadInterfacer - - -class Surmise(LibensembleGenThreadInterfacer): - """ - Standalone object-oriented Surmise generator - """ - - def __init__( - self, History: npt.NDArray = [], persis_info: dict = {}, gen_specs: dict = {}, libE_info: dict = {} - ) -> None: - from libensemble.gen_funcs.persistent_surmise_calib import surmise_calib - - gen_specs["gen_f"] = surmise_calib - if ("sim_id", int) not in gen_specs["out"]: - gen_specs["out"].append(("sim_id", int)) - super().__init__(History, persis_info, gen_specs, libE_info) - self.sim_id_index = 0 - self.all_cancels = [] - - def _add_sim_ids(self, array: npt.NDArray) -> npt.NDArray: - array["sim_id"] = np.arange(self.sim_id_index, self.sim_id_index + len(array)) - self.sim_id_index += len(array) - return array - - def ready_to_be_asked(self) -> bool: - """Check if the generator has the next batch of points ready.""" - return not self.outbox.empty() - - def ask_numpy(self, *args) -> npt.NDArray: - """Request the next set of points to evaluate, as a NumPy array.""" - output = super().ask_numpy() - if "cancel_requested" in output.dtype.names: - cancels = output - got_cancels_first = True - self.all_cancels.append(cancels) - else: - self.results = self._add_sim_ids(output) - got_cancels_first = False - try: - _, additional = self.outbox.get(timeout=0.2) # either cancels or new points - if got_cancels_first: - return additional["calc_out"] - self.all_cancels.append(additional["calc_out"]) - return self.results - except thread_queue.Empty: - return self.results - - def ask_updates(self) -> List[npt.NDArray]: - """Request a list of NumPy arrays containing points that should be cancelled by the workflow.""" - cancels = copy.deepcopy(self.all_cancels) - self.all_cancels = [] - return cancels diff --git a/libensemble/tests/regression_tests/test_persistent_surmise_killsims_asktell.py b/libensemble/tests/regression_tests/test_persistent_surmise_killsims_asktell.py deleted file mode 100644 index 9071e80d4..000000000 --- a/libensemble/tests/regression_tests/test_persistent_surmise_killsims_asktell.py +++ /dev/null @@ -1,144 +0,0 @@ -""" -Tests libEnsemble's capability to kill/cancel simulations that are in progress. - -Execute via one of the following commands (e.g. 3 workers): - mpiexec -np 4 python test_persistent_surmise_killsims.py - python test_persistent_surmise_killsims.py --nworkers 3 --comms local - python test_persistent_surmise_killsims.py --nworkers 3 --comms tcp - -When running with the above commands, the number of concurrent evaluations of -the objective function will be 2, as one of the three workers will be the -persistent generator. - -This test is a smaller variant of test_persistent_surmise_calib.py, but which -subprocesses a compiled version of the borehole simulation. A delay is -added to simulations after the initial batch, so that the killing of running -simulations can be tested. This will only affect simulations that have already -been issued to a worker when the cancel request is registesred by the manager. - -See more information, see tutorial: -"Borehole Calibration with Selective Simulation Cancellation" -in the libEnsemble documentation. -""" - -# Do not change these lines - they are parsed by run-tests.sh -# TESTSUITE_COMMS: mpi local -# TESTSUITE_NPROCS: 3 4 -# TESTSUITE_EXTRA: true -# TESTSUITE_OS_SKIP: OSX - -# Requires: -# Install Surmise package - -import os - -import numpy as np - -from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f -from libensemble.executors.executor import Executor -from libensemble.gen_classes import Surmise - -# Import libEnsemble items for this test -from libensemble.libE import libE -from libensemble.sim_funcs.borehole_kills import borehole as sim_f -from libensemble.tests.regression_tests.common import build_borehole # current location -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output - -# from libensemble import logger -# logger.set_level("DEBUG") # To get debug logging in ensemble.log - -if __name__ == "__main__": - nworkers, is_manager, libE_specs, _ = parse_args() - - n_init_thetas = 15 # Initial batch of thetas - n_x = 5 # No. of x values - nparams = 4 # No. of theta params - ndims = 3 # No. of x coordinates. - max_add_thetas = 20 # Max no. of thetas added for evaluation - step_add_theta = 10 # No. of thetas to generate per step, before emulator is rebuilt - n_explore_theta = 200 # No. of thetas to explore while selecting the next theta - obsvar = 10 ** (-1) # Constant for generating noise in obs - - # Batch mode until after init_sample_size (add one theta to batch for observations) - init_sample_size = (n_init_thetas + 1) * n_x - - # Stop after max_emul_runs runs of the emulator - max_evals = init_sample_size + max_add_thetas * n_x - - sim_app = os.path.join(os.getcwd(), "borehole.x") - if not os.path.isfile(sim_app): - build_borehole() - - exctr = Executor() # Run serial sub-process in place - exctr.register_app(full_path=sim_app, app_name="borehole") - - # Subprocess variant creates input and output files for each sim - libE_specs["sim_dirs_make"] = True # To keep all - make sim dirs - # libE_specs["use_worker_dirs"] = True # To overwrite - make worker dirs only - - # Rename ensemble dir for non-interference with other regression tests - libE_specs["ensemble_dir_path"] = "ensemble_calib_kills_asktell" - libE_specs["gen_on_manager"] = True - - sim_specs = { - "sim_f": sim_f, - "in": ["x", "thetas"], - "out": [ - ("f", float), - ("sim_killed", bool), # "sim_killed" is used only for display at the end of this test - ], - "user": { - "num_obs": n_x, - "init_sample_size": init_sample_size, - }, - } - - gen_out = [ - ("x", float, ndims), - ("thetas", float, nparams), - ("priority", int), - ("obs", float, n_x), - ("obsvar", float, n_x), - ] - - gen_specs = { - "persis_in": [o[0] for o in gen_out] + ["f", "sim_ended", "sim_id"], - "out": gen_out, - "user": { - "n_init_thetas": n_init_thetas, # Num thetas in initial batch - "num_x_vals": n_x, # Num x points to create - "step_add_theta": step_add_theta, # No. of thetas to generate per step - "n_explore_theta": n_explore_theta, # No. of thetas to explore each step - "obsvar": obsvar, # Variance for generating noise in obs - "init_sample_size": init_sample_size, # Initial batch size inc. observations - "priorloc": 1, # Prior location in the unit cube. - "priorscale": 0.2, # Standard deviation of prior - }, - } - - alloc_specs = { - "alloc_f": alloc_f, - "user": { - "init_sample_size": init_sample_size, - "async_return": True, # True = Return results to gen as they come in (after sample) - "active_recv_gen": True, # Persistent gen can handle irregular communications - }, - } - - persis_info = add_unique_random_streams({}, nworkers + 1) - gen_specs["generator"] = Surmise(gen_specs=gen_specs, persis_info=persis_info) - - exit_criteria = {"sim_max": max_evals} - - # Perform the run - H, persis_info, flag = libE( - sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs=alloc_specs, libE_specs=libE_specs - ) - - if is_manager: - print("Cancelled sims", H["sim_id"][H["cancel_requested"]]) - print("Kills sent by manager to running simulations", H["sim_id"][H["kill_sent"]]) - print("Killed sims", H["sim_id"][H["sim_killed"]]) - sims_done = np.count_nonzero(H["sim_ended"]) - save_libE_output(H, persis_info, __file__, nworkers) - assert sims_done == max_evals, f"Num of completed simulations should be {max_evals}. Is {sims_done}" From 231e6f0416291d7b1dc4c07a4eb16d824ca693d8 Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 4 Nov 2024 13:08:33 -0600 Subject: [PATCH 266/288] fix import --- libensemble/gen_classes/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/libensemble/gen_classes/__init__.py b/libensemble/gen_classes/__init__.py index d5bfedd34..f33c2ebc0 100644 --- a/libensemble/gen_classes/__init__.py +++ b/libensemble/gen_classes/__init__.py @@ -1,3 +1,2 @@ from .aposmm import APOSMM # noqa: F401 from .sampling import UniformSample, UniformSampleDicts # noqa: F401 -from .surmise import Surmise # noqa: F401 From eaebbff92568d9182ab40ac4a9db5679b30a5df0 Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 4 Nov 2024 13:32:20 -0600 Subject: [PATCH 267/288] remove the other ask/tell surmise test --- .../regression_tests/test_asktell_surmise.py | 136 ------------------ 1 file changed, 136 deletions(-) delete mode 100644 libensemble/tests/regression_tests/test_asktell_surmise.py diff --git a/libensemble/tests/regression_tests/test_asktell_surmise.py b/libensemble/tests/regression_tests/test_asktell_surmise.py deleted file mode 100644 index 1afad75c3..000000000 --- a/libensemble/tests/regression_tests/test_asktell_surmise.py +++ /dev/null @@ -1,136 +0,0 @@ -# TESTSUITE_COMMS: local -# TESTSUITE_NPROCS: 4 -# TESTSUITE_EXTRA: true -# TESTSUITE_OS_SKIP: OSX - -import os - -import numpy as np - -from libensemble.message_numbers import FINISHED_PERSISTENT_GEN_TAG - -if __name__ == "__main__": - - from libensemble.executors import Executor - from libensemble.gen_classes import Surmise - - # Import libEnsemble items for this test - from libensemble.sim_funcs.borehole_kills import borehole - from libensemble.tests.regression_tests.common import build_borehole # current location - from libensemble.tools import add_unique_random_streams - from libensemble.utils.misc import list_dicts_to_np - - sim_app = os.path.join(os.getcwd(), "borehole.x") - if not os.path.isfile(sim_app): - build_borehole() - - exctr = Executor() # Run serial sub-process in place - exctr.register_app(full_path=sim_app, app_name="borehole") - - n_init_thetas = 15 # Initial batch of thetas - n_x = 5 # No. of x values - nparams = 4 # No. of theta params - ndims = 3 # No. of x coordinates. - max_add_thetas = 20 # Max no. of thetas added for evaluation - step_add_theta = 10 # No. of thetas to generate per step, before emulator is rebuilt - n_explore_theta = 200 # No. of thetas to explore while selecting the next theta - obsvar = 10 ** (-1) # Constant for generating noise in obs - - # Batch mode until after init_sample_size (add one theta to batch for observations) - init_sample_size = (n_init_thetas + 1) * n_x - - # Stop after max_emul_runs runs of the emulator - max_evals = init_sample_size + max_add_thetas * n_x - - # Rename ensemble dir for non-interference with other regression tests - sim_specs = { - "in": ["x", "thetas"], - "out": [ - ("f", float), - ("sim_killed", bool), - ], - "user": { - "num_obs": n_x, - "init_sample_size": init_sample_size, - "poll_manager": False, - }, - } - - gen_out = [ - ("x", float, ndims), - ("thetas", float, nparams), - ("priority", int), - ("obs", float, n_x), - ("obsvar", float, n_x), - ] - - gen_specs = { - "persis_in": [o[0] for o in gen_out] + ["f", "sim_ended", "sim_id"], - "out": gen_out, - "user": { - "n_init_thetas": n_init_thetas, # Num thetas in initial batch - "num_x_vals": n_x, # Num x points to create - "step_add_theta": step_add_theta, # No. of thetas to generate per step - "n_explore_theta": n_explore_theta, # No. of thetas to explore each step - "obsvar": obsvar, # Variance for generating noise in obs - "init_sample_size": init_sample_size, # Initial batch size inc. observations - "priorloc": 1, # Prior location in the unit cube. - "priorscale": 0.2, # Standard deviation of prior - }, - } - - persis_info = add_unique_random_streams({}, 5) - surmise = Surmise(gen_specs=gen_specs, persis_info=persis_info[1]) # we add sim_id as a field to gen_specs["out"] - surmise.setup() - - initial_sample = surmise.ask() - - total_evals = 0 - - for point in initial_sample: - H_out, _a, _b = borehole( - list_dicts_to_np([point], dtype=gen_specs["out"]), {}, sim_specs, {"H_rows": np.array([point["sim_id"]])} - ) - point["f"] = H_out["f"][0] # some "bugginess" with output shape of array in simf - total_evals += 1 - - surmise.tell(initial_sample) - - requested_canceled_sim_ids = [] - - next_sample, cancels = surmise.ask(), surmise.ask_updates() - - for point in next_sample: - H_out, _a, _b = borehole( - list_dicts_to_np([point], dtype=gen_specs["out"]), {}, sim_specs, {"H_rows": np.array([point["sim_id"]])} - ) - point["f"] = H_out["f"][0] - total_evals += 1 - - surmise.tell(next_sample) - sample, cancels = surmise.ask(), surmise.ask_updates() - - while total_evals < max_evals: - - for point in sample: - H_out, _a, _b = borehole( - list_dicts_to_np([point], dtype=gen_specs["out"]), - {}, - sim_specs, - {"H_rows": np.array([point["sim_id"]])}, - ) - point["f"] = H_out["f"][0] - total_evals += 1 - surmise.tell([point]) - if surmise.ready_to_be_asked(): - new_sample, cancels = surmise.ask(), surmise.ask_updates() - for m in cancels: - requested_canceled_sim_ids.append(m) - if len(new_sample): - sample = new_sample - break - - H, persis_info, exit_code = surmise.final_tell(None) - - assert exit_code == FINISHED_PERSISTENT_GEN_TAG, "Standalone persistent_aposmm didn't exit correctly" - # assert len(requested_canceled_sim_ids), "No cancellations sent by Surmise" From 043feeb711705a49352411be08b8d3aa7c62edb6 Mon Sep 17 00:00:00 2001 From: jlnav Date: Tue, 5 Nov 2024 14:40:13 -0600 Subject: [PATCH 268/288] renable persistent_aposmm unit test --- ...RENAME_test_persistent_aposmm.py => test_persistent_aposmm.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename libensemble/tests/unit_tests/{RENAME_test_persistent_aposmm.py => test_persistent_aposmm.py} (100%) diff --git a/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py b/libensemble/tests/unit_tests/test_persistent_aposmm.py similarity index 100% rename from libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py rename to libensemble/tests/unit_tests/test_persistent_aposmm.py From c66f10b9c1d1500a58521eaa054a01846627210e Mon Sep 17 00:00:00 2001 From: shudson Date: Tue, 5 Nov 2024 17:28:08 -0600 Subject: [PATCH 269/288] gpCAM class uses returned x --- libensemble/gen_classes/gpCAM.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libensemble/gen_classes/gpCAM.py b/libensemble/gen_classes/gpCAM.py index 7894d2bd6..884832980 100644 --- a/libensemble/gen_classes/gpCAM.py +++ b/libensemble/gen_classes/gpCAM.py @@ -84,6 +84,8 @@ def ask_numpy(self, n_trials: int) -> npt.NDArray: def tell_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) From 3d7981b9ec3eebbb74edb978014bf6414e23be9f Mon Sep 17 00:00:00 2001 From: shudson Date: Tue, 5 Nov 2024 17:37:46 -0600 Subject: [PATCH 270/288] Convert numpy scalar types --- libensemble/generators.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index f971f46d5..e4e8fe5bd 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -113,9 +113,16 @@ def ask_numpy(self, num_points: Optional[int] = 0) -> npt.NDArray: def tell_numpy(self, results: npt.NDArray) -> None: """Send the results, as a NumPy array, of evaluations to the generator.""" + @staticmethod + def convert_np_types(dict_list): + return [ + {key: (value.item() if isinstance(value, np.generic) else value) for key, value in item.items()} + for item in dict_list + ] + def ask(self, num_points: Optional[int] = 0) -> List[dict]: """Request the next set of points to evaluate.""" - return np_to_list_dicts(self.ask_numpy(num_points)) + return LibensembleGenerator.convert_np_types(np_to_list_dicts(self.ask_numpy(num_points))) def tell(self, results: List[dict]) -> None: """Send the results of evaluations to the generator.""" From c3805956b5e79924320f2bf049a48df90e992329 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 6 Nov 2024 16:04:32 -0600 Subject: [PATCH 271/288] preparing to add variables_mapping to LibensembleGenerator parent class; so we know which variables refer to which internal 'x'-like fields --- libensemble/generators.py | 139 +++++++++--------- .../unit_tests/test_persistent_aposmm.py | 5 +- 2 files changed, 75 insertions(+), 69 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 381a04b1d..c875c2b75 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -117,11 +117,14 @@ def __init__( 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._vars_x_mapping = {i: k for i, k in enumerate(self.variables.keys())} + assert len(self.variables_mapping), "Must specify a variable mapping for libEnsemble generators." + # self._vars_x_mapping = {i: k for i, k in enumerate(self.variables.keys())} self.n = len(self.variables) # build our own lb and ub @@ -144,105 +147,105 @@ def __init__( else: self.persis_info = persis_info - def _gen_out_to_vars(self, gen_out: dict) -> dict: + # def _gen_out_to_vars(self, gen_out: dict) -> dict: - """ - We must replace internal, enumerated "x"s with the variables the user requested to sample over. + # """ + # We must replace internal, enumerated "x"s with the variables the user requested to sample over. - Basically, for the following example, if the user requested the following variables: + # Basically, for the following example, if the user requested the following variables: - ``{'core': [-3, 3], 'edge': [-2, 2]}`` + # ``{'core': [-3, 3], 'edge': [-2, 2]}`` - Then for the following directly-from-aposmm point: + # Then for the following directly-from-aposmm point: - ``{'x0': -0.1, 'x1': 0.7, 'x_on_cube0': 0.4833, - 'x_on_cube1': 0.675, 'sim_id': 0...}`` + # ``{'x0': -0.1, 'x1': 0.7, 'x_on_cube0': 0.4833, + # 'x_on_cube1': 0.675, 'sim_id': 0...}`` - We need to replace (for aposmm, for example) "x0" with "core", "x1" with "edge", - "x_on_cube0" with "core_on_cube", and "x_on_cube1" with "edge_on_cube". + # We need to replace (for aposmm, for example) "x0" with "core", "x1" with "edge", + # "x_on_cube0" with "core_on_cube", and "x_on_cube1" with "edge_on_cube". - ... + # ... - BUT: if we're given "x0" and "x1" as our variables, we need to honor that + # BUT: if we're given "x0" and "x1" as our variables, we need to honor that - """ + # """ - if all([i in list(self.variables.keys()) for i in list(gen_out[0].keys())]): - return gen_out + # if all([i in list(self.variables.keys()) for i in list(gen_out[0].keys())]): + # return gen_out - new_out = [] - for entry in gen_out: # get a dict + # new_out = [] + # for entry in gen_out: # get a dict - new_entry = {} - for map_key in self._vars_x_mapping.keys(): # get 0, 1 + # new_entry = {} + # for map_key in self._vars_x_mapping.keys(): # get 0, 1 - for out_key in entry.keys(): # get x0, x1, x_on_cube0, etc. + # for out_key in entry.keys(): # get x0, x1, x_on_cube0, etc. - if out_key.endswith(str(map_key)): # found key that ends with 0, 1 - new_name = str(out_key).replace( - self._internal_variable, self._vars_x_mapping[map_key] - ) # replace 'x' with 'core' - new_name = new_name.rstrip("0123456789") # now remove trailing integer - new_entry[new_name] = entry[out_key] + # if out_key.endswith(str(map_key)): # found key that ends with 0, 1 + # new_name = str(out_key).replace( + # self._internal_variable, self._vars_x_mapping[map_key] + # ) # replace 'x' with 'core' + # new_name = new_name.rstrip("0123456789") # now remove trailing integer + # new_entry[new_name] = entry[out_key] - elif not out_key[-1].isnumeric(): # found key that is not enumerated - new_entry[out_key] = entry[out_key] + # elif not out_key[-1].isnumeric(): # found key that is not enumerated + # new_entry[out_key] = entry[out_key] - # we now naturally continue over cases where e.g. the map_key may be 0 but we're looking at x1 - new_out.append(new_entry) + # # we now naturally continue over cases where e.g. the map_key may be 0 but we're looking at x1 + # new_out.append(new_entry) - return new_out + # return new_out - def _objs_and_vars_to_gen_in(self, results: dict) -> dict: - """We now need to do the inverse of _gen_out_to_vars, plus replace - the objective name with the internal gen's expected name, .e.g "energy" -> "f". + # def _objs_and_vars_to_gen_in(self, results: dict) -> dict: + # """We now need to do the inverse of _gen_out_to_vars, plus replace + # the objective name with the internal gen's expected name, .e.g "energy" -> "f". - So given: + # So given: - {'core': -0.1, 'core_on_cube': 0.483, 'sim_id': 0, 'local_min': False, - 'local_pt': False, 'edge': 0.7, 'edge_on_cube': 0.675, 'energy': -1.02} + # {'core': -0.1, 'core_on_cube': 0.483, 'sim_id': 0, 'local_min': False, + # 'local_pt': False, 'edge': 0.7, 'edge_on_cube': 0.675, 'energy': -1.02} - We need the following again: + # We need the following again: - {'x0': -0.1, 'x_on_cube0': 0.483, 'sim_id': 0, 'local_min': False, - 'local_pt': False, 'x1': 0.7, 'x_on_cube1': 0.675, 'f': -1.02} + # {'x0': -0.1, 'x_on_cube0': 0.483, 'sim_id': 0, 'local_min': False, + # 'local_pt': False, 'x1': 0.7, 'x_on_cube1': 0.675, 'f': -1.02} - """ - new_results = [] - for entry in results: # get a dict + # """ + # new_results = [] + # for entry in results: # get a dict - new_entry = {} - for map_key in self._vars_x_mapping.keys(): # get 0, 1 + # new_entry = {} + # for map_key in self._vars_x_mapping.keys(): # get 0, 1 - for out_key in entry.keys(): # get core, core_on_cube, energy, sim_id, etc. + # for out_key in entry.keys(): # get core, core_on_cube, energy, sim_id, etc. - # continue over cases where e.g. the map_key may be 0 but we're looking at x1 - if out_key[-1].isnumeric() and not out_key.endswith(str(map_key)): - continue + # # continue over cases where e.g. the map_key may be 0 but we're looking at x1 + # if out_key[-1].isnumeric() and not out_key.endswith(str(map_key)): + # continue - if self._vars_x_mapping[map_key] == out_key: # found core - new_name = self._internal_variable + str(map_key) # create x0, x1, etc. + # if self._vars_x_mapping[map_key] == out_key: # found core + # new_name = self._internal_variable + str(map_key) # create x0, x1, etc. - # we need to strip trailing ints for this condition in case vars were formatted: x0, x1 - # avoid the "x0_on_cube0" naming scheme - elif out_key.startswith(self._vars_x_mapping[map_key].rstrip("0123456789")): # found core_on_cube - new_name = out_key.replace( - self._vars_x_mapping[map_key].rstrip("0123456789"), self._internal_variable - ) - # presumably multi-dim key; preserve that trailing int on the end of new key - if not new_name[-1].isnumeric(): - new_name += str(map_key) # create x_on_cube0 + # # we need to strip trailing ints for this condition in case vars were formatted: x0, x1 + # # avoid the "x0_on_cube0" naming scheme + # elif out_key.startswith(self._vars_x_mapping[map_key].rstrip("0123456789")): # found core_on_cube + # new_name = out_key.replace( + # self._vars_x_mapping[map_key].rstrip("0123456789"), self._internal_variable + # ) + # # presumably multi-dim key; preserve that trailing int on the end of new key + # if not new_name[-1].isnumeric(): + # new_name += str(map_key) # create x_on_cube0 - elif out_key in list(self.objectives.keys()): # found energy - new_name = self._internal_objective # create f + # elif out_key in list(self.objectives.keys()): # found energy + # new_name = self._internal_objective # create f - elif out_key in self.gen_specs["persis_in"]: # found everything else, sim_id, local_pt, etc. - new_name = out_key + # elif out_key in self.gen_specs["persis_in"]: # found everything else, sim_id, local_pt, etc. + # new_name = out_key - new_entry[new_name] = entry[out_key] - new_results.append(new_entry) + # new_entry[new_name] = entry[out_key] + # new_results.append(new_entry) - return new_results + # return new_results @abstractmethod def ask_numpy(self, num_points: Optional[int] = 0) -> npt.NDArray: diff --git a/libensemble/tests/unit_tests/test_persistent_aposmm.py b/libensemble/tests/unit_tests/test_persistent_aposmm.py index 669bdeb03..a49d5be39 100644 --- a/libensemble/tests/unit_tests/test_persistent_aposmm.py +++ b/libensemble/tests/unit_tests/test_persistent_aposmm.py @@ -198,8 +198,11 @@ def test_asktell_with_persistent_aposmm(): variables = {"core": [-3, 3], "edge": [-2, 2]} objectives = {"energy": "MINIMIZE"} + variables_mapping = {"x": ["core", "edge"]} - my_APOSMM = APOSMM(variables=variables, objectives=objectives, gen_specs=gen_specs) + my_APOSMM = APOSMM( + variables=variables, objectives=objectives, gen_specs=gen_specs, variables_mapping=variables_mapping + ) my_APOSMM.setup() initial_sample = my_APOSMM.ask(100) From c7ea54bf329d1ece20a5fc8362a6d8749a87811c Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 7 Nov 2024 15:03:08 -0600 Subject: [PATCH 272/288] intermediate work on passing mapping into np_to_list_dicts. need to put into list_dicts_to_np now, to unpack the opposite direction --- libensemble/generators.py | 6 ++---- libensemble/tests/unit_tests/test_asktell.py | 4 +++- libensemble/utils/misc.py | 21 ++++++++++++-------- libensemble/utils/runners.py | 9 +++++++-- 4 files changed, 25 insertions(+), 15 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 4d998b297..0566949c1 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -109,7 +109,7 @@ def __init__( persis_info: dict = {}, gen_specs: dict = {}, libE_info: dict = {}, - **kwargs + **kwargs, ): self.variables = variables self.objectives = objectives @@ -123,8 +123,6 @@ def __init__( self._internal_objective = "f" if self.variables: - assert len(self.variables_mapping), "Must specify a variable mapping for libEnsemble generators." - # self._vars_x_mapping = {i: k for i, k in enumerate(self.variables.keys())} self.n = len(self.variables) # build our own lb and ub @@ -284,7 +282,7 @@ def __init__( persis_info: dict = {}, gen_specs: dict = {}, libE_info: dict = {}, - **kwargs + **kwargs, ) -> None: super().__init__(variables, objectives, History, persis_info, gen_specs, libE_info, **kwargs) self.gen_f = gen_specs["gen_f"] diff --git a/libensemble/tests/unit_tests/test_asktell.py b/libensemble/tests/unit_tests/test_asktell.py index 5a4bd9565..8d593bc4a 100644 --- a/libensemble/tests/unit_tests/test_asktell.py +++ b/libensemble/tests/unit_tests/test_asktell.py @@ -86,7 +86,9 @@ def test_awkward_H(): H[0] = (1, [1.1, 2.2, 3.3], [10.1], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], "hello", "1.23") H[1] = (2, [4.4, 5.5, 6.6], [11.1], [51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62], "goodbye", "2.23") - list_dicts = np_to_list_dicts(H) + mapping = {"x": ["core", "beam", "edge"]} + + list_dicts = np_to_list_dicts(H, mapping) npp = list_dicts_to_np(list_dicts, dtype=dtype) _check_conversion(H, npp) diff --git a/libensemble/utils/misc.py b/libensemble/utils/misc.py index 34b7a0931..d346cea11 100644 --- a/libensemble/utils/misc.py +++ b/libensemble/utils/misc.py @@ -108,7 +108,7 @@ def _combine_names(names: list) -> list: return list(set(out_names)) -def list_dicts_to_np(list_dicts: list, dtype: list = None) -> npt.NDArray: +def list_dicts_to_np(list_dicts: list, dtype: list = None, mapping: dict = {}) -> npt.NDArray: if list_dicts is None: return None @@ -148,7 +148,7 @@ def list_dicts_to_np(list_dicts: list, dtype: list = None) -> npt.NDArray: return out -def np_to_list_dicts(array: npt.NDArray) -> List[dict]: +def np_to_list_dicts(array: npt.NDArray, mapping: dict = {}) -> List[dict]: if array is None: return None out = [] @@ -156,12 +156,17 @@ def np_to_list_dicts(array: npt.NDArray) -> List[dict]: new_dict = {} for field in row.dtype.names: # non-string arrays, lists, etc. - if hasattr(row[field], "__len__") and len(row[field]) > 1 and not isinstance(row[field], str): - for i, x in enumerate(row[field]): - new_dict[field + str(i)] = x - elif hasattr(row[field], "__len__") and len(row[field]) == 1: # single-entry arrays, lists, etc. - new_dict[field] = row[field][0] # will still work on single-char strings + if field not in list(mapping.keys()): + if hasattr(row[field], "__len__") and len(row[field]) > 1 and not isinstance(row[field], str): + for i, x in enumerate(row[field]): + new_dict[field + str(i)] = x + elif hasattr(row[field], "__len__") and len(row[field]) == 1: # single-entry arrays, lists, etc. + new_dict[field] = row[field][0] # will still work on single-char strings + else: + new_dict[field] = row[field] else: - new_dict[field] = row[field] + assert array.dtype[field].shape[0] == len(mapping[field]), "unable to unpack multidimensional array" + for i, name in enumerate(mapping[field]): + new_dict[name] = row[field][i] out.append(new_dict) return out diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 08d52a27e..c7db42600 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -108,7 +108,10 @@ def __init__(self, specs): def _get_points_updates(self, batch_size: int) -> (npt.NDArray, npt.NDArray): # no ask_updates on external gens - return (list_dicts_to_np(self.gen.ask(batch_size), dtype=self.specs.get("out")), None) + return ( + list_dicts_to_np(self.gen.ask(batch_size), dtype=self.specs.get("out"), mapping=self.gen.variables_mapping), + None, + ) def _convert_tell(self, x: npt.NDArray) -> list: self.gen.tell(np_to_list_dicts(x)) @@ -142,7 +145,9 @@ def _persistent_result(self, calc_in, persis_info, libE_info): if self.gen.thread is None: self.gen.setup() # maybe we're reusing a live gen from a previous run # libE gens will hit the following line, but list_dicts_to_np will passthrough if the output is a numpy array - H_out = list_dicts_to_np(self._get_initial_ask(libE_info), dtype=self.specs.get("out")) + H_out = list_dicts_to_np( + self._get_initial_ask(libE_info), dtype=self.specs.get("out"), mapping=self.gen.variables_mapping + ) tag, Work, H_in = self.ps.send_recv(H_out) # evaluate the initial sample final_H_in = self._start_generator_loop(tag, Work, H_in) return self.gen.final_tell(final_H_in), FINISHED_PERSISTENT_GEN_TAG From c111afd57cdc18e6ac0a34ed81feb3fcae9fc8d4 Mon Sep 17 00:00:00 2001 From: shudson Date: Fri, 8 Nov 2024 10:05:11 -0600 Subject: [PATCH 273/288] Call setup on first ask --- libensemble/generators.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 9021977d1..eb9dfe462 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -194,7 +194,8 @@ def tell(self, results: List[dict], tag: int = EVAL_GEN_TAG) -> None: 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: + self.setup() self.thread.run() _, ask_full = self.outbox.get() return ask_full["calc_out"] From 0ee448c40eee4252914fd117edff3e365aa5e63e Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 8 Nov 2024 12:10:21 -0600 Subject: [PATCH 274/288] use mapping to construct list_dicts_to_np dtype when provided --- libensemble/tests/unit_tests/test_asktell.py | 19 ++++-- libensemble/utils/misc.py | 71 +++++++++++--------- 2 files changed, 56 insertions(+), 34 deletions(-) diff --git a/libensemble/tests/unit_tests/test_asktell.py b/libensemble/tests/unit_tests/test_asktell.py index 8d593bc4a..95b4fc485 100644 --- a/libensemble/tests/unit_tests/test_asktell.py +++ b/libensemble/tests/unit_tests/test_asktell.py @@ -3,7 +3,7 @@ from libensemble.utils.misc import list_dicts_to_np -def _check_conversion(H, npp): +def _check_conversion(H, npp, mapping={}): for field in H.dtype.names: print(f"Comparing {field}: {H[field]} {npp[field]}") @@ -45,6 +45,19 @@ def test_asktell_sampling_and_utils(): for j, value in enumerate(entry.values()): assert value == out_np["x"][i][j] + variables = {"core": [-3, 3], "edge": [-2, 2]} + objectives = {"energy": "EXPLORE"} + mapping = {"x": ["core", "edge"]} + + gen = UniformSample(variables, objectives, mapping) + out = gen.ask(1) + assert len(out) == 1 + assert out[0].get("core") + assert out[0].get("edge") + + out_np = list_dicts_to_np(out, mapping=mapping) + assert out_np.dtype.names == ("x") + def test_awkward_list_dict(): from libensemble.utils.misc import list_dicts_to_np @@ -86,9 +99,7 @@ def test_awkward_H(): H[0] = (1, [1.1, 2.2, 3.3], [10.1], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], "hello", "1.23") H[1] = (2, [4.4, 5.5, 6.6], [11.1], [51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62], "goodbye", "2.23") - mapping = {"x": ["core", "beam", "edge"]} - - list_dicts = np_to_list_dicts(H, mapping) + list_dicts = np_to_list_dicts(H) npp = list_dicts_to_np(list_dicts, dtype=dtype) _check_conversion(H, npp) diff --git a/libensemble/utils/misc.py b/libensemble/utils/misc.py index d346cea11..56b495c10 100644 --- a/libensemble/utils/misc.py +++ b/libensemble/utils/misc.py @@ -115,36 +115,47 @@ def list_dicts_to_np(list_dicts: list, dtype: list = None, mapping: dict = {}) - if not isinstance(list_dicts, list): # presumably already a numpy array, conversion not necessary return list_dicts - first = list_dicts[0] # for determining dtype of output np array - new_dtype_names = _combine_names([i for i in first.keys()]) # -> ['x', 'y'] - combinable_names = [] # [['x0', 'x1'], ['y0', 'y1', 'y2'], ['z']] - for name in new_dtype_names: # is this a necessary search over the keys again? we did it earlier... - combinable_group = [i for i in first.keys() if i.rstrip("0123456789") == name] - if len(combinable_group) > 1: # multiple similar names, e.g. x0, x1 - combinable_names.append(combinable_group) - else: # single name, e.g. local_pt, a0 *AS LONG AS THERE ISNT AN A1* - combinable_names.append([name]) - - if dtype is None: - dtype = [] - - if not len(dtype): - # another loop over names, there's probably a more elegant way, but my brain is fried - for i, entry in enumerate(combinable_names): - name = new_dtype_names[i] - size = len(combinable_names[i]) - dtype.append(_decide_dtype(name, first[entry[0]], size)) - - out = np.zeros(len(list_dicts), dtype=dtype) - - for i, group in enumerate(combinable_names): - new_dtype_name = new_dtype_names[i] - for j, input_dict in enumerate(list_dicts): - if len(group) == 1: # only a single name, e.g. local_pt - out[new_dtype_name][j] = input_dict[new_dtype_name] - else: # combinable names detected, e.g. x0, x1 - out[new_dtype_name][j] = tuple([input_dict[name] for name in group]) - + # build a presumptive dtype + if not len(mapping): + + first = list_dicts[0] # for determining dtype of output np array + new_dtype_names = _combine_names([i for i in first.keys()]) # -> ['x', 'y'] + combinable_names = [] # [['x0', 'x1'], ['y0', 'y1', 'y2'], ['z']] + for name in new_dtype_names: # is this a necessary search over the keys again? we did it earlier... + combinable_group = [i for i in first.keys() if i.rstrip("0123456789") == name] + if len(combinable_group) > 1: # multiple similar names, e.g. x0, x1 + combinable_names.append(combinable_group) + else: # single name, e.g. local_pt, a0 *AS LONG AS THERE ISNT AN A1* + combinable_names.append([name]) + + if dtype is None: + dtype = [] + + if not len(dtype): + # another loop over names, there's probably a more elegant way, but my brain is fried + for i, entry in enumerate(combinable_names): + name = new_dtype_names[i] + size = len(combinable_names[i]) + dtype.append(_decide_dtype(name, first[entry[0]], size)) + + out = np.zeros(len(list_dicts), dtype=dtype) + + # dont need dtype, assume x-mapping for floats + if len(mapping): + dtype = [(name, float, (len(mapping[name]),)) for name in mapping] + out = np.zeros(len(list_dicts), dtype=dtype) + for name in mapping: + for i, entry in enumerate(list_dicts): + for j, value in enumerate(entry.values()): + out[name][i][j] = value + else: + for i, group in enumerate(combinable_names): + new_dtype_name = new_dtype_names[i] + for j, input_dict in enumerate(list_dicts): + if len(group) == 1: # only a single name, e.g. local_pt + out[new_dtype_name][j] = input_dict[new_dtype_name] + else: # combinable names detected, e.g. x0, x1 + out[new_dtype_name][j] = tuple([input_dict[name] for name in group]) return out From bb37f4b6198243153471ffdf8d3bd71350056fea Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 8 Nov 2024 13:30:20 -0600 Subject: [PATCH 275/288] additional work on replacing dict keys with xs and fs --- libensemble/tests/unit_tests/test_asktell.py | 31 +++++++- libensemble/utils/misc.py | 80 ++++++++++---------- 2 files changed, 71 insertions(+), 40 deletions(-) diff --git a/libensemble/tests/unit_tests/test_asktell.py b/libensemble/tests/unit_tests/test_asktell.py index 95b4fc485..aaa895ea6 100644 --- a/libensemble/tests/unit_tests/test_asktell.py +++ b/libensemble/tests/unit_tests/test_asktell.py @@ -56,7 +56,7 @@ def test_asktell_sampling_and_utils(): assert out[0].get("edge") out_np = list_dicts_to_np(out, mapping=mapping) - assert out_np.dtype.names == ("x") + assert out_np.dtype.names[0] == "x" def test_awkward_list_dict(): @@ -90,6 +90,35 @@ def test_awkward_list_dict(): assert all([i in ("x", "y", "z", "a0") for i in out_np.dtype.names]) + weird_list_dict = [ + { + "sim_id": 77, + "core": 89, + "edge": 10.1, + "beam": 76.5, + "energy": 12.34, + "local_pt": True, + "local_min": False, + }, + { + "sim_id": 10, + "core": 32.8, + "edge": 16.2, + "beam": 33.5, + "energy": 99.34, + "local_pt": False, + "local_min": False, + }, + ] + + # target dtype: [("sim_id", int), ("x, float, (3,)), ("f", float), ("local_pt", bool), ("local_min", bool)] + + mapping = {"x": ["core", "edge", "beam"], "f": ["energy"]} + out_np = list_dicts_to_np(weird_list_dict, mapping=mapping) + + # we need to map the x-values to a len-3 x field, map energy to a len-1 f field + # then preserve the other fields + def test_awkward_H(): from libensemble.utils.misc import list_dicts_to_np, np_to_list_dicts diff --git a/libensemble/utils/misc.py b/libensemble/utils/misc.py index 56b495c10..86f6d843a 100644 --- a/libensemble/utils/misc.py +++ b/libensemble/utils/misc.py @@ -2,7 +2,7 @@ Misc internal functions """ -from itertools import groupby +from itertools import chain, groupby from operator import itemgetter from typing import List @@ -116,46 +116,48 @@ def list_dicts_to_np(list_dicts: list, dtype: list = None, mapping: dict = {}) - return list_dicts # build a presumptive dtype - if not len(mapping): - - first = list_dicts[0] # for determining dtype of output np array - new_dtype_names = _combine_names([i for i in first.keys()]) # -> ['x', 'y'] - combinable_names = [] # [['x0', 'x1'], ['y0', 'y1', 'y2'], ['z']] - for name in new_dtype_names: # is this a necessary search over the keys again? we did it earlier... - combinable_group = [i for i in first.keys() if i.rstrip("0123456789") == name] - if len(combinable_group) > 1: # multiple similar names, e.g. x0, x1 - combinable_names.append(combinable_group) - else: # single name, e.g. local_pt, a0 *AS LONG AS THERE ISNT AN A1* - combinable_names.append([name]) - - if dtype is None: - dtype = [] - - if not len(dtype): - # another loop over names, there's probably a more elegant way, but my brain is fried - for i, entry in enumerate(combinable_names): - name = new_dtype_names[i] - size = len(combinable_names[i]) - dtype.append(_decide_dtype(name, first[entry[0]], size)) - - out = np.zeros(len(list_dicts), dtype=dtype) - # dont need dtype, assume x-mapping for floats + first = list_dicts[0] # for determining dtype of output np array + new_dtype_names = _combine_names([i for i in first.keys()]) # -> ['x', 'y'] + fields_to_convert = list(chain.from_iterable(list(mapping.values()))) + new_dtype_names = [i for i in new_dtype_names if i not in fields_to_convert] + list(mapping.keys()) + combinable_names = [] # [['x0', 'x1'], ['y0', 'y1', 'y2'], ['z']] + for name in new_dtype_names: # is this a necessary search over the keys again? we did it earlier... + combinable_group = [i for i in first.keys() if i.rstrip("0123456789") == name] + if len(combinable_group) > 1: # multiple similar names, e.g. x0, x1 + combinable_names.append(combinable_group) + else: # single name, e.g. local_pt, a0 *AS LONG AS THERE ISNT AN A1* + combinable_names.append([name]) + + if dtype is None: + dtype = [] + + if not len(dtype): + # another loop over names, there's probably a more elegant way, but my brain is fried + for i, entry in enumerate(combinable_names): + name = new_dtype_names[i] + size = len(combinable_names[i]) + dtype.append(_decide_dtype(name, first[entry[0]], size)) + if len(mapping): - dtype = [(name, float, (len(mapping[name]),)) for name in mapping] - out = np.zeros(len(list_dicts), dtype=dtype) - for name in mapping: - for i, entry in enumerate(list_dicts): - for j, value in enumerate(entry.values()): - out[name][i][j] = value - else: - for i, group in enumerate(combinable_names): - new_dtype_name = new_dtype_names[i] - for j, input_dict in enumerate(list_dicts): - if len(group) == 1: # only a single name, e.g. local_pt - out[new_dtype_name][j] = input_dict[new_dtype_name] - else: # combinable names detected, e.g. x0, x1 - out[new_dtype_name][j] = tuple([input_dict[name] for name in group]) + map_dtype = [(name, float, (len(mapping[name]),)) for name in mapping] + dtype.append(map_dtype) + + out = np.zeros(len(list_dicts), dtype=dtype) + + # dont need dtype, assume x-mapping for floats + for name in mapping: + for i, entry in enumerate(list_dicts): + for j, value in enumerate(entry.values()): + out[name][i][j] = value + + for i, group in enumerate(combinable_names): + new_dtype_name = new_dtype_names[i] + for j, input_dict in enumerate(list_dicts): + if len(group) == 1: # only a single name, e.g. local_pt + out[new_dtype_name][j] = input_dict[new_dtype_name] + else: # combinable names detected, e.g. x0, x1 + out[new_dtype_name][j] = tuple([input_dict[name] for name in group]) return out From 38b39671c08999689d4372fcb8b11b57b7a8fe59 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 8 Nov 2024 13:32:03 -0600 Subject: [PATCH 276/288] some cleanup of generators.py in anticipation of the changes to the dict->np converters --- libensemble/generators.py | 106 ++------------------------------------ 1 file changed, 4 insertions(+), 102 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 0566949c1..f7be79ec1 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -145,106 +145,6 @@ def __init__( else: self.persis_info = persis_info - # def _gen_out_to_vars(self, gen_out: dict) -> dict: - - # """ - # We must replace internal, enumerated "x"s with the variables the user requested to sample over. - - # Basically, for the following example, if the user requested the following variables: - - # ``{'core': [-3, 3], 'edge': [-2, 2]}`` - - # Then for the following directly-from-aposmm point: - - # ``{'x0': -0.1, 'x1': 0.7, 'x_on_cube0': 0.4833, - # 'x_on_cube1': 0.675, 'sim_id': 0...}`` - - # We need to replace (for aposmm, for example) "x0" with "core", "x1" with "edge", - # "x_on_cube0" with "core_on_cube", and "x_on_cube1" with "edge_on_cube". - - # ... - - # BUT: if we're given "x0" and "x1" as our variables, we need to honor that - - # """ - - # if all([i in list(self.variables.keys()) for i in list(gen_out[0].keys())]): - # return gen_out - - # new_out = [] - # for entry in gen_out: # get a dict - - # new_entry = {} - # for map_key in self._vars_x_mapping.keys(): # get 0, 1 - - # for out_key in entry.keys(): # get x0, x1, x_on_cube0, etc. - - # if out_key.endswith(str(map_key)): # found key that ends with 0, 1 - # new_name = str(out_key).replace( - # self._internal_variable, self._vars_x_mapping[map_key] - # ) # replace 'x' with 'core' - # new_name = new_name.rstrip("0123456789") # now remove trailing integer - # new_entry[new_name] = entry[out_key] - - # elif not out_key[-1].isnumeric(): # found key that is not enumerated - # new_entry[out_key] = entry[out_key] - - # # we now naturally continue over cases where e.g. the map_key may be 0 but we're looking at x1 - # new_out.append(new_entry) - - # return new_out - - # def _objs_and_vars_to_gen_in(self, results: dict) -> dict: - # """We now need to do the inverse of _gen_out_to_vars, plus replace - # the objective name with the internal gen's expected name, .e.g "energy" -> "f". - - # So given: - - # {'core': -0.1, 'core_on_cube': 0.483, 'sim_id': 0, 'local_min': False, - # 'local_pt': False, 'edge': 0.7, 'edge_on_cube': 0.675, 'energy': -1.02} - - # We need the following again: - - # {'x0': -0.1, 'x_on_cube0': 0.483, 'sim_id': 0, 'local_min': False, - # 'local_pt': False, 'x1': 0.7, 'x_on_cube1': 0.675, 'f': -1.02} - - # """ - # new_results = [] - # for entry in results: # get a dict - - # new_entry = {} - # for map_key in self._vars_x_mapping.keys(): # get 0, 1 - - # for out_key in entry.keys(): # get core, core_on_cube, energy, sim_id, etc. - - # # continue over cases where e.g. the map_key may be 0 but we're looking at x1 - # if out_key[-1].isnumeric() and not out_key.endswith(str(map_key)): - # continue - - # if self._vars_x_mapping[map_key] == out_key: # found core - # new_name = self._internal_variable + str(map_key) # create x0, x1, etc. - - # # we need to strip trailing ints for this condition in case vars were formatted: x0, x1 - # # avoid the "x0_on_cube0" naming scheme - # elif out_key.startswith(self._vars_x_mapping[map_key].rstrip("0123456789")): # found core_on_cube - # new_name = out_key.replace( - # self._vars_x_mapping[map_key].rstrip("0123456789"), self._internal_variable - # ) - # # presumably multi-dim key; preserve that trailing int on the end of new key - # if not new_name[-1].isnumeric(): - # new_name += str(map_key) # create x_on_cube0 - - # elif out_key in list(self.objectives.keys()): # found energy - # new_name = self._internal_objective # create f - - # elif out_key in self.gen_specs["persis_in"]: # found everything else, sim_id, local_pt, etc. - # new_name = out_key - - # new_entry[new_name] = entry[out_key] - # new_results.append(new_entry) - - # return new_results - @abstractmethod def ask_numpy(self, num_points: Optional[int] = 0) -> npt.NDArray: """Request the next set of points to evaluate, as a NumPy array.""" @@ -262,11 +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(self._objs_and_vars_to_gen_in(results))) + self.tell_numpy(list_dicts_to_np(results, mapping=self.variables_mapping)) class LibensembleGenThreadInterfacer(LibensembleGenerator): From 1d213efae98191d2cb94df0fd4650fb00d0d422f Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 11 Nov 2024 09:33:39 -0600 Subject: [PATCH 277/288] dont try to determine dtype for fields that aren't actually in the input list --- libensemble/utils/misc.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libensemble/utils/misc.py b/libensemble/utils/misc.py index 86f6d843a..0534655b1 100644 --- a/libensemble/utils/misc.py +++ b/libensemble/utils/misc.py @@ -132,13 +132,16 @@ def list_dicts_to_np(list_dicts: list, dtype: list = None, mapping: dict = {}) - if dtype is None: dtype = [] + # build dtype of non-mapped fields if not len(dtype): # another loop over names, there's probably a more elegant way, but my brain is fried for i, entry in enumerate(combinable_names): name = new_dtype_names[i] size = len(combinable_names[i]) - dtype.append(_decide_dtype(name, first[entry[0]], size)) + if name not in mapping: + dtype.append(_decide_dtype(name, first[entry[0]], size)) + # append dtype of mapped float fields if len(mapping): map_dtype = [(name, float, (len(mapping[name]),)) for name in mapping] dtype.append(map_dtype) From f8c5eaf9162f417af789fde0a1b0950fc10f386a Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 11 Nov 2024 13:09:16 -0600 Subject: [PATCH 278/288] finalize mapping support within list_dicts_to_np, now need to refactor/cleanup --- libensemble/tests/unit_tests/test_asktell.py | 3 +- libensemble/utils/misc.py | 31 ++++++++++++-------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/libensemble/tests/unit_tests/test_asktell.py b/libensemble/tests/unit_tests/test_asktell.py index aaa895ea6..1364b7031 100644 --- a/libensemble/tests/unit_tests/test_asktell.py +++ b/libensemble/tests/unit_tests/test_asktell.py @@ -116,8 +116,7 @@ def test_awkward_list_dict(): mapping = {"x": ["core", "edge", "beam"], "f": ["energy"]} out_np = list_dicts_to_np(weird_list_dict, mapping=mapping) - # we need to map the x-values to a len-3 x field, map energy to a len-1 f field - # then preserve the other fields + assert all([i in ("sim_id", "x", "f", "local_pt", "local_min") for i in out_np.dtype.names]) def test_awkward_H(): diff --git a/libensemble/utils/misc.py b/libensemble/utils/misc.py index 0534655b1..91c84d7ee 100644 --- a/libensemble/utils/misc.py +++ b/libensemble/utils/misc.py @@ -143,24 +143,29 @@ def list_dicts_to_np(list_dicts: list, dtype: list = None, mapping: dict = {}) - # append dtype of mapped float fields if len(mapping): - map_dtype = [(name, float, (len(mapping[name]),)) for name in mapping] - dtype.append(map_dtype) + for name in mapping: + if len(mapping[name]) == 1: + dtype.append((name, float)) + else: + dtype.append((name, float, (len(mapping[name]),))) out = np.zeros(len(list_dicts), dtype=dtype) - # dont need dtype, assume x-mapping for floats - for name in mapping: - for i, entry in enumerate(list_dicts): - for j, value in enumerate(entry.values()): - out[name][i][j] = value - for i, group in enumerate(combinable_names): new_dtype_name = new_dtype_names[i] - for j, input_dict in enumerate(list_dicts): - if len(group) == 1: # only a single name, e.g. local_pt - out[new_dtype_name][j] = input_dict[new_dtype_name] - else: # combinable names detected, e.g. x0, x1 - out[new_dtype_name][j] = tuple([input_dict[name] for name in group]) + if new_dtype_name not in mapping: + for j, input_dict in enumerate(list_dicts): + if len(group) == 1: # only a single name, e.g. local_pt + out[new_dtype_name][j] = input_dict[new_dtype_name] + else: # combinable names detected, e.g. x0, x1 + out[new_dtype_name][j] = tuple([input_dict[name] for name in group]) + else: + for j, input_dict in enumerate(list_dicts): + combined = tuple([input_dict[name] for name in mapping[new_dtype_name]]) + if len(combined) == 1: + out[new_dtype_name][j] = combined[0] + else: + out[new_dtype_name][j] = combined return out From dff6bada12e9ea324d2cde5bbd0dff2820da21f6 Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 11 Nov 2024 13:53:33 -0600 Subject: [PATCH 279/288] refactoring --- libensemble/utils/misc.py | 45 ++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/libensemble/utils/misc.py b/libensemble/utils/misc.py index 91c84d7ee..2a91394fd 100644 --- a/libensemble/utils/misc.py +++ b/libensemble/utils/misc.py @@ -115,6 +115,9 @@ def list_dicts_to_np(list_dicts: list, dtype: list = None, mapping: dict = {}) - if not isinstance(list_dicts, list): # presumably already a numpy array, conversion not necessary return list_dicts + if dtype is None: + dtype = [] + # build a presumptive dtype first = list_dicts[0] # for determining dtype of output np array @@ -122,19 +125,15 @@ def list_dicts_to_np(list_dicts: list, dtype: list = None, mapping: dict = {}) - fields_to_convert = list(chain.from_iterable(list(mapping.values()))) new_dtype_names = [i for i in new_dtype_names if i not in fields_to_convert] + list(mapping.keys()) combinable_names = [] # [['x0', 'x1'], ['y0', 'y1', 'y2'], ['z']] - for name in new_dtype_names: # is this a necessary search over the keys again? we did it earlier... + for name in new_dtype_names: combinable_group = [i for i in first.keys() if i.rstrip("0123456789") == name] if len(combinable_group) > 1: # multiple similar names, e.g. x0, x1 combinable_names.append(combinable_group) else: # single name, e.g. local_pt, a0 *AS LONG AS THERE ISNT AN A1* combinable_names.append([name]) - if dtype is None: - dtype = [] - # build dtype of non-mapped fields if not len(dtype): - # another loop over names, there's probably a more elegant way, but my brain is fried for i, entry in enumerate(combinable_names): name = new_dtype_names[i] size = len(combinable_names[i]) @@ -144,28 +143,26 @@ def list_dicts_to_np(list_dicts: list, dtype: list = None, mapping: dict = {}) - # append dtype of mapped float fields if len(mapping): for name in mapping: - if len(mapping[name]) == 1: - dtype.append((name, float)) - else: - dtype.append((name, float, (len(mapping[name]),))) + size = len(mapping[name]) + dtype.append(_decide_dtype(name, 0.0, size)) # float out = np.zeros(len(list_dicts), dtype=dtype) - for i, group in enumerate(combinable_names): - new_dtype_name = new_dtype_names[i] - if new_dtype_name not in mapping: - for j, input_dict in enumerate(list_dicts): - if len(group) == 1: # only a single name, e.g. local_pt - out[new_dtype_name][j] = input_dict[new_dtype_name] - else: # combinable names detected, e.g. x0, x1 - out[new_dtype_name][j] = tuple([input_dict[name] for name in group]) - else: - for j, input_dict in enumerate(list_dicts): - combined = tuple([input_dict[name] for name in mapping[new_dtype_name]]) - if len(combined) == 1: - out[new_dtype_name][j] = combined[0] - else: - out[new_dtype_name][j] = combined + for j, input_dict in enumerate(list_dicts): + for output_name, field_names in zip(new_dtype_names, combinable_names): + if output_name not in mapping: + out[output_name][j] = ( + tuple(input_dict[name] for name in field_names) + if len(field_names) > 1 + else input_dict[field_names[0]] + ) + else: + out[output_name][j] = ( + tuple(input_dict[name] for name in mapping[output_name]) + if len(mapping[output_name]) > 1 + else input_dict[mapping[output_name][0]] + ) + return out From c1ec7f6b4c1fb30ecd937f98fe4a5cd4c613c2a1 Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 11 Nov 2024 14:49:05 -0600 Subject: [PATCH 280/288] tiny fixes; need to figure out why aposmm_nlopt reg test is hanging --- libensemble/generators.py | 4 +++- .../regression_tests/test_persistent_aposmm_nlopt_asktell.py | 1 + libensemble/tests/unit_tests/test_persistent_aposmm.py | 3 +-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index cd8414f5a..9c6bf4293 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -194,6 +194,8 @@ def __init__( def setup(self) -> None: """Must be called once before calling ask/tell. Initializes the background thread.""" + if self.thread is not None: + return self.m = Manager() self.inbox = self.m.Queue() self.outbox = self.m.Queue() @@ -224,7 +226,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(self._objs_and_vars_to_gen_in(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.""" diff --git a/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py b/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py index 805dd9c67..25fbc6afb 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py @@ -60,6 +60,7 @@ xtol_abs=1e-6, ftol_abs=1e-6, max_active_runs=workflow.nworkers, # should this match nworkers always? practically? + variables_mapping={"x": ["x0", "x1"]}, ) workflow.gen_specs = GenSpecs( diff --git a/libensemble/tests/unit_tests/test_persistent_aposmm.py b/libensemble/tests/unit_tests/test_persistent_aposmm.py index a49d5be39..25ecdfd46 100644 --- a/libensemble/tests/unit_tests/test_persistent_aposmm.py +++ b/libensemble/tests/unit_tests/test_persistent_aposmm.py @@ -198,13 +198,12 @@ def test_asktell_with_persistent_aposmm(): variables = {"core": [-3, 3], "edge": [-2, 2]} objectives = {"energy": "MINIMIZE"} - variables_mapping = {"x": ["core", "edge"]} + variables_mapping = {"x": ["core", "edge"], "f": ["energy"]} my_APOSMM = APOSMM( variables=variables, objectives=objectives, gen_specs=gen_specs, variables_mapping=variables_mapping ) - my_APOSMM.setup() initial_sample = my_APOSMM.ask(100) total_evals = 0 From a5133b98fee32d4644581aa2db9c86895563378e Mon Sep 17 00:00:00 2001 From: jlnav Date: Mon, 11 Nov 2024 14:50:23 -0600 Subject: [PATCH 281/288] runners.py no longer calls setup() on gen --- libensemble/utils/runners.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index c7db42600..769e1a214 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -139,11 +139,6 @@ def _start_generator_loop(self, tag, Work, H_in): def _persistent_result(self, calc_in, persis_info, libE_info): """Setup comms with manager, setup gen, loop gen to completion, return gen's results""" self.ps = PersistentSupport(libE_info, EVAL_GEN_TAG) - if hasattr(self.gen, "setup"): - self.gen.persis_info = persis_info # passthrough, setup() uses the gen attributes - self.gen.libE_info = libE_info - if self.gen.thread is None: - self.gen.setup() # maybe we're reusing a live gen from a previous run # libE gens will hit the following line, but list_dicts_to_np will passthrough if the output is a numpy array H_out = list_dicts_to_np( self._get_initial_ask(libE_info), dtype=self.specs.get("out"), mapping=self.gen.variables_mapping From 682daa81340ebd658f8af483c476d2ced8200dd2 Mon Sep 17 00:00:00 2001 From: jlnav Date: Tue, 12 Nov 2024 08:43:06 -0600 Subject: [PATCH 282/288] rename a handful of asktell tests to have a test_asktell prefix --- .flake8 | 1 + .../{test_sampling_asktell_gen.py => test_asktell_sampling.py} | 0 ...tent_aposmm_nlopt_asktell.py => test_asktell_aposmm_nlopt.py} | 0 .../{test_gpCAM_class.py => test_asktell_gpCAM.py} | 0 ...mise_killsims_asktell.py => test_asktell_surmise_killsims.py} | 0 5 files changed, 1 insertion(+) rename libensemble/tests/functionality_tests/{test_sampling_asktell_gen.py => test_asktell_sampling.py} (100%) rename libensemble/tests/regression_tests/{test_persistent_aposmm_nlopt_asktell.py => test_asktell_aposmm_nlopt.py} (100%) rename libensemble/tests/regression_tests/{test_gpCAM_class.py => test_asktell_gpCAM.py} (100%) rename libensemble/tests/regression_tests/{test_persistent_surmise_killsims_asktell.py => test_asktell_surmise_killsims.py} (100%) diff --git a/.flake8 b/.flake8 index d49bc0d3b..c21368b65 100644 --- a/.flake8 +++ b/.flake8 @@ -40,6 +40,7 @@ per-file-ignores = libensemble/tests/scaling_tests/warpx/run_libensemble_on_warpx.py:E402 examples/calling_scripts/run_libensemble_on_warpx.py:E402 libensemble/tests/regression_tests/test_persistent_aposmm*:E402 + libensemble/tests/regression_tests/test_asktell_aposmm_nlopt.py:E402 libensemble/tests/regression_tests/test_persistent_gp_multitask_ax.py:E402 libensemble/tests/functionality_tests/test_uniform_sampling_then_persistent_localopt_runs.py:E402 libensemble/tests/functionality_tests/test_stats_output.py:E402 diff --git a/libensemble/tests/functionality_tests/test_sampling_asktell_gen.py b/libensemble/tests/functionality_tests/test_asktell_sampling.py similarity index 100% rename from libensemble/tests/functionality_tests/test_sampling_asktell_gen.py rename to libensemble/tests/functionality_tests/test_asktell_sampling.py diff --git a/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py b/libensemble/tests/regression_tests/test_asktell_aposmm_nlopt.py similarity index 100% rename from libensemble/tests/regression_tests/test_persistent_aposmm_nlopt_asktell.py rename to libensemble/tests/regression_tests/test_asktell_aposmm_nlopt.py diff --git a/libensemble/tests/regression_tests/test_gpCAM_class.py b/libensemble/tests/regression_tests/test_asktell_gpCAM.py similarity index 100% rename from libensemble/tests/regression_tests/test_gpCAM_class.py rename to libensemble/tests/regression_tests/test_asktell_gpCAM.py diff --git a/libensemble/tests/regression_tests/test_persistent_surmise_killsims_asktell.py b/libensemble/tests/regression_tests/test_asktell_surmise_killsims.py similarity index 100% rename from libensemble/tests/regression_tests/test_persistent_surmise_killsims_asktell.py rename to libensemble/tests/regression_tests/test_asktell_surmise_killsims.py From 09ebdbc4404d4dd31e87e53d152820908d283886 Mon Sep 17 00:00:00 2001 From: jlnav Date: Tue, 12 Nov 2024 09:49:30 -0600 Subject: [PATCH 283/288] remove redundant .setup calls that also cause hangs --- .../tests/unit_tests/RENAME_test_persistent_aposmm.py | 1 - libensemble/utils/runners.py | 5 ----- 2 files changed, 6 deletions(-) diff --git a/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py b/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py index 9bc097a18..f1959e789 100644 --- a/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py +++ b/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py @@ -204,7 +204,6 @@ def test_asktell_with_persistent_aposmm(): } my_APOSMM = APOSMM(gen_specs=gen_specs) - my_APOSMM.setup() initial_sample = my_APOSMM.ask(100) total_evals = 0 diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 08d52a27e..5a11f7e09 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -136,11 +136,6 @@ def _start_generator_loop(self, tag, Work, H_in): def _persistent_result(self, calc_in, persis_info, libE_info): """Setup comms with manager, setup gen, loop gen to completion, return gen's results""" self.ps = PersistentSupport(libE_info, EVAL_GEN_TAG) - if hasattr(self.gen, "setup"): - self.gen.persis_info = persis_info # passthrough, setup() uses the gen attributes - self.gen.libE_info = libE_info - if self.gen.thread is None: - self.gen.setup() # maybe we're reusing a live gen from a previous run # libE gens will hit the following line, but list_dicts_to_np will passthrough if the output is a numpy array H_out = list_dicts_to_np(self._get_initial_ask(libE_info), dtype=self.specs.get("out")) tag, Work, H_in = self.ps.send_recv(H_out) # evaluate the initial sample From 2c6a9c4431e2991ce6c76695a7a60b44a1fb8f78 Mon Sep 17 00:00:00 2001 From: jlnav Date: Wed, 13 Nov 2024 08:03:13 -0600 Subject: [PATCH 284/288] lock nlopt to 2.8.0? --- .github/workflows/basic.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/basic.yml b/.github/workflows/basic.yml index 92c46aee4..0cbf77a50 100644 --- a/.github/workflows/basic.yml +++ b/.github/workflows/basic.yml @@ -92,7 +92,7 @@ jobs: run: | python -m pip install --upgrade pip pip install mpmath matplotlib - conda install numpy nlopt scipy + conda install numpy nlopt==2.8.0 scipy - name: Install libEnsemble, flake8 run: | From e8b7052bd7613d81f92028993ea3519b6ec5ed10 Mon Sep 17 00:00:00 2001 From: Stephen Hudson Date: Thu, 14 Nov 2024 12:39:40 -0600 Subject: [PATCH 285/288] Feature/spawn with interfacer (#1464) * Use QComm in QCommProcess for comms * Remove thread locked comm in executor * Add conditional code for executor forwarding * Remove extra setup() call * Use correct outbox queue --- libensemble/comms/comms.py | 5 +++-- libensemble/generators.py | 28 ++++++++++++++-------------- libensemble/utils/runners.py | 2 +- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/libensemble/comms/comms.py b/libensemble/comms/comms.py index 51042c463..d8d892319 100644 --- a/libensemble/comms/comms.py +++ b/libensemble/comms/comms.py @@ -226,6 +226,7 @@ def _qcomm_main(comm, main, *args, **kwargs): if not kwargs.get("user_function"): _result = main(comm, *args, **kwargs) else: + # SH - could we insert comm into libE_info["comm"] here if it exists _result = main(*args) comm.send(CommResult(_result)) except Exception as e: @@ -264,8 +265,8 @@ def __init__(self, main, nworkers, *args, **kwargs): self.inbox = Queue() self.outbox = Queue() super().__init__(self, main, *args, **kwargs) - comm = QComm(self.inbox, self.outbox, nworkers) - self.handle = Process(target=_qcomm_main, args=(comm, main) + args, kwargs=kwargs) + self.comm = QComm(self.inbox, self.outbox, nworkers) + self.handle = Process(target=_qcomm_main, args=(self.comm, main) + args, kwargs=kwargs) def terminate(self, timeout=None): """Terminate the process.""" diff --git a/libensemble/generators.py b/libensemble/generators.py index eb9dfe462..cae1f109e 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -1,6 +1,5 @@ # import queue as thread_queue from abc import ABC, abstractmethod -from multiprocessing import Manager # from multiprocessing import Queue as process_queue from typing import List, Optional @@ -8,7 +7,7 @@ import numpy as np from numpy import typing as npt -from libensemble.comms.comms import QComm, QCommProcess # , QCommThread +from libensemble.comms.comms import QCommProcess # , QCommThread from libensemble.executors import Executor from libensemble.message_numbers import EVAL_GEN_TAG, PERSIS_STOP from libensemble.tools.tools import add_unique_random_streams @@ -150,14 +149,13 @@ 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() - comm = QComm(self.inbox, self.outbox) - self.libE_info["comm"] = comm # replacing comm so gen sends HERE instead of manager + # SH this contains the thread lock - removing.... wrong comm to pass on anyway. + if hasattr(Executor.executor, "comm"): + del Executor.executor.comm 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, @@ -176,7 +174,10 @@ def setup(self) -> None: self.gen_specs, self.libE_info, user_function=True, - ) # note that self.thread's inbox/outbox are unused by the underlying gen + ) + + # SH this is a bit hacky - maybe it can be done inside comms (in _qcomm_main)? + self.libE_info["comm"] = self.thread.comm def _set_sim_ended(self, results: npt.NDArray) -> npt.NDArray: new_results = np.zeros(len(results), dtype=self.gen_specs["out"] + [("sim_ended", bool), ("f", float)]) @@ -197,19 +198,18 @@ def ask_numpy(self, num_points: int = 0) -> npt.NDArray: if self.thread is None: self.setup() self.thread.run() - _, ask_full = self.outbox.get() + _, ask_full = self.thread.recv() return ask_full["calc_out"] def tell_numpy(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: """Send the results of evaluations to the generator, as a NumPy array.""" if results is not None: results = self._set_sim_ended(results) - self.inbox.put( - (tag, {"libE_info": {"H_rows": np.copy(results["sim_id"]), "persistent": True, "executor": None}}) - ) - self.inbox.put((0, np.copy(results))) + Work = {"libE_info": {"H_rows": np.copy(results["sim_id"]), "persistent": True, "executor": None}} + self.thread.send(tag, Work) + self.thread.send(tag, np.copy(results)) # SH for threads check - might need deepcopy due to dtype=object else: - self.inbox.put((tag, None)) + self.thread.send(tag, None) def final_tell(self, results: npt.NDArray = None) -> (npt.NDArray, dict, int): """Send any last results to the generator, and it to close down.""" diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 5a11f7e09..3adab746a 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -173,7 +173,7 @@ def _get_initial_ask(self, libE_info) -> npt.NDArray: def _ask_and_send(self): """Loop over generator's outbox contents, send to manager""" - while self.gen.outbox.qsize(): # recv/send any outstanding messages + while self.gen.thread.outbox.qsize(): # recv/send any outstanding messages points, updates = self.gen.ask_numpy(), self.gen.ask_updates() if updates is not None and len(updates): self.ps.send(points) From 23e5164227dadb228192668e557023fd996715be Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 14 Nov 2024 14:09:35 -0600 Subject: [PATCH 286/288] use macOS-supported condition to check if gen_f has enqueued any outbound messages --- libensemble/utils/runners.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 3adab746a..d74ea89d8 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -173,7 +173,7 @@ def _get_initial_ask(self, libE_info) -> npt.NDArray: def _ask_and_send(self): """Loop over generator's outbox contents, send to manager""" - while self.gen.thread.outbox.qsize(): # recv/send any outstanding messages + while not self.gen.thread.outbox.empty(): # recv/send any outstanding messages points, updates = self.gen.ask_numpy(), self.gen.ask_updates() if updates is not None and len(updates): self.ps.send(points) From 5c2308da68b7d538b6a7468eac4dbdfa998f3c9f Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 14 Nov 2024 16:04:06 -0600 Subject: [PATCH 287/288] avoid redundant install of nlopt? --- install/gen_deps_environment.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/install/gen_deps_environment.yml b/install/gen_deps_environment.yml index a69146f3e..9c5492663 100644 --- a/install/gen_deps_environment.yml +++ b/install/gen_deps_environment.yml @@ -6,7 +6,6 @@ channels: dependencies: - pip - numpy>=2 - - nlopt==2.7.1 - scipy - superlu_dist - hypre From 64b64017fc5a76a17893b3c3bb68f09cff0a0585 Mon Sep 17 00:00:00 2001 From: jlnav Date: Tue, 3 Dec 2024 14:54:28 -0600 Subject: [PATCH 288/288] swap sim_id with _id when data goes out from gen. swap _id with sim_id when data goes into gen. --- libensemble/utils/misc.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/libensemble/utils/misc.py b/libensemble/utils/misc.py index 34b7a0931..87786b832 100644 --- a/libensemble/utils/misc.py +++ b/libensemble/utils/misc.py @@ -115,6 +115,10 @@ def list_dicts_to_np(list_dicts: list, dtype: list = None) -> npt.NDArray: if not isinstance(list_dicts, list): # presumably already a numpy array, conversion not necessary return list_dicts + for entry in list_dicts: + if "_id" in entry: + entry["sim_id"] = entry.pop("_id") + first = list_dicts[0] # for determining dtype of output np array new_dtype_names = _combine_names([i for i in first.keys()]) # -> ['x', 'y'] combinable_names = [] # [['x0', 'x1'], ['y0', 'y1', 'y2'], ['z']] @@ -164,4 +168,9 @@ def np_to_list_dicts(array: npt.NDArray) -> List[dict]: else: new_dict[field] = row[field] out.append(new_dict) + + for entry in out: + if "sim_id" in entry: + entry["_id"] = entry.pop("sim_id") + return out