diff --git a/docs/platforms/frontier.rst b/docs/platforms/frontier.rst index a57ffadd9..ef2251b32 100644 --- a/docs/platforms/frontier.rst +++ b/docs/platforms/frontier.rst @@ -64,10 +64,9 @@ Now grab an interactive session on one node:: Then in the session run:: - python run_libe_forces.py --nworkers 9 + python run_libe_forces.py --nworkers 8 -This places the generator on the first worker and runs simulations on the -others (each simulation using one GPU). +The workers will each run simulations with one GPU each. To see GPU usage, ssh into the node you are on in another window and run:: diff --git a/docs/platforms/perlmutter.rst b/docs/platforms/perlmutter.rst index a1c79703f..7f6098657 100644 --- a/docs/platforms/perlmutter.rst +++ b/docs/platforms/perlmutter.rst @@ -96,10 +96,9 @@ Now grab an interactive session on one node:: Then in the session run:: export LIBE_PLATFORM="perlmutter_g" - python run_libe_forces.py -n 5 + python run_libe_forces.py -n 4 -This places the generator on the first worker and runs simulations on the -others (each simulation using one GPU). +The workers will each run simualations with one GPU each. To see GPU usage, ssh into the node you are on in another window and run:: diff --git a/docs/tutorials/xopt_bayesian_gen.rst b/docs/tutorials/xopt_bayesian_gen.rst index 9227ac8ce..14e3b50b9 100644 --- a/docs/tutorials/xopt_bayesian_gen.rst +++ b/docs/tutorials/xopt_bayesian_gen.rst @@ -52,7 +52,7 @@ Define the VOCS specification and set up the generator. .. code-block:: python - libE_specs = LibeSpecs(gen_on_manager=True, nworkers=4) + libE_specs = LibeSpecs(nworkers=4) vocs = VOCS( variables={"x1": [0, 1.0], "x2": [0, 10.0]}, diff --git a/examples/tutorials/gpcam_surrogate_model/gpcam.ipynb b/examples/tutorials/gpcam_surrogate_model/gpcam.ipynb index 29616f582..53fdfd4a0 100644 --- a/examples/tutorials/gpcam_surrogate_model/gpcam.ipynb +++ b/examples/tutorials/gpcam_surrogate_model/gpcam.ipynb @@ -285,9 +285,9 @@ "\n", "nworkers = 4\n", "\n", - "# When using gen_on_manager, nworkers is number of concurrent sims.\n", + "# nworkers is number of concurrent sims.\n", "# final_gen_send means the last evaluated points are returned to the generator to update the model.\n", - "libE_specs = LibeSpecs(nworkers=nworkers, gen_on_manager=True, final_gen_send=True)\n", + "libE_specs = LibeSpecs(nworkers=nworkers, final_gen_send=True)\n", "\n", "n = 2 # Input dimensions\n", "batch_size = 4\n", @@ -400,7 +400,7 @@ "import matplotlib\n", "import matplotlib.pyplot as plt\n", "\n", - "# Get \"mean_squared_error\" from generators return (worker 0 as we ran gen_on_manager)\n", + "# Get \"mean_squared_error\" from generators return\n", "mse = persis_info[0][\"mean_squared_error\"]\n", "niter = len(mse)\n", "num_sims = list(range(batch_size, (niter * batch_size) + 1, batch_size))\n", diff --git a/examples/tutorials/xopt_bayesian_gen/xopt_EI_example.ipynb b/examples/tutorials/xopt_bayesian_gen/xopt_EI_example.ipynb index bc27f41a2..140b961a4 100644 --- a/examples/tutorials/xopt_bayesian_gen/xopt_EI_example.ipynb +++ b/examples/tutorials/xopt_bayesian_gen/xopt_EI_example.ipynb @@ -98,7 +98,7 @@ "metadata": {}, "outputs": [], "source": [ - "libE_specs = LibeSpecs(gen_on_manager=True, nworkers=4)\n", + "libE_specs = LibeSpecs(nworkers=4)\n", "\n", "vocs = VOCS(\n", " variables={\"x1\": [0, 1.0], \"x2\": [0, 10.0]},\n", diff --git a/libensemble/specs.py b/libensemble/specs.py index 9ee04baa3..c95bdd8f8 100644 --- a/libensemble/specs.py +++ b/libensemble/specs.py @@ -2,6 +2,7 @@ import warnings from pathlib import Path +import numpy as np import pydantic from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator @@ -354,6 +355,24 @@ def set_fields_from_vocs(self): if "_id" not in self.persis_in: self.persis_in.append("_id") + # Set user["lb"]/["ub"] from VOCS continuous variables (for legacy generators + # that read bounds from gen_specs["user"]). Skip variables without a ``.domain`` + # attribute (e.g., DiscreteVariable). Do not overwrite user-provided values. + if self.user is None: + self.user = {} + if "lb" not in self.user or "ub" not in self.user: + lbs, ubs = [], [] + for _name, var in (getattr(self.vocs, "variables", None) or {}).items(): + domain = getattr(var, "domain", None) + if domain is not None and len(domain) == 2: + lbs.append(domain[0]) + ubs.append(domain[1]) + if lbs: + if "lb" not in self.user: + self.user["lb"] = np.array(lbs, dtype=float) + if "ub" not in self.user: + self.user["ub"] = np.array(ubs, dtype=float) + return self @model_validator(mode="after") diff --git a/libensemble/tests/functionality_tests/test_1d_super_simple.py b/libensemble/tests/functionality_tests/test_1d_super_simple.py index 1a178c2cf..ff167ae3d 100644 --- a/libensemble/tests/functionality_tests/test_1d_super_simple.py +++ b/libensemble/tests/functionality_tests/test_1d_super_simple.py @@ -14,6 +14,7 @@ # TESTSUITE_NPROCS: 2 4 import numpy as np +from gest_api.vocs import VOCS from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first from libensemble.gen_funcs.sampling import latin_hypercube_sample as gen_f @@ -38,14 +39,13 @@ def sim_f(In): "out": [("f", float)], } + vocs = VOCS(variables={"x0": [-3, 3]}, objectives={"f": "EXPLORE"}) + gen_specs = { "gen_f": gen_f, "out": [("x", float, (1,))], "batch_size": 500, - "user": { - "lb": np.array([-3]), - "ub": np.array([3]), - }, + "vocs": vocs, } exit_criteria = {"gen_max": 501} @@ -54,7 +54,13 @@ def sim_f(In): "alloc_f": give_sim_work_first, } - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) + H, persis_info, flag = libE( + sim_specs, + gen_specs, + exit_criteria, + alloc_specs=alloc_specs, + libE_specs=libE_specs, + ) if is_manager: assert len(H) >= 501 diff --git a/libensemble/tests/functionality_tests/test_asktell_sampling.py b/libensemble/tests/functionality_tests/test_asktell_sampling.py index 49d86f317..300109aa1 100644 --- a/libensemble/tests/functionality_tests/test_asktell_sampling.py +++ b/libensemble/tests/functionality_tests/test_asktell_sampling.py @@ -43,12 +43,8 @@ def sim_f(In): gen_specs = { "persis_in": ["x", "f", "sim_id"], "out": [("x", float, (2,))], - "initial_batch_size": 2, - "batch_size": 1, - "user": { - "lb": np.array([-3, -2]), - "ub": np.array([3, 2]), - }, + "initial_batch_size": 20, + "batch_size": 10, } variables = {"x0": [-3, 3], "x1": [-2, 2]} diff --git a/libensemble/tests/functionality_tests/test_calc_exception.py b/libensemble/tests/functionality_tests/test_calc_exception.py index 55d99ca63..81c0b791a 100644 --- a/libensemble/tests/functionality_tests/test_calc_exception.py +++ b/libensemble/tests/functionality_tests/test_calc_exception.py @@ -11,7 +11,7 @@ # TESTSUITE_COMMS: mpi local tcp # TESTSUITE_NPROCS: 2 4 -import numpy as np +from gest_api.vocs import VOCS from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first from libensemble.gen_funcs.sampling import uniform_random_sample as gen_f @@ -35,15 +35,14 @@ def six_hump_camel_err(H, persis_info, sim_specs, _): "out": [("f", float)], } + vocs = VOCS(variables={"x0": [-3, 3], "x1": [-2, 2]}, objectives={"f": "EXPLORE"}) + gen_specs = { "gen_f": gen_f, "in": ["sim_id"], "out": [("x", float, 2)], "batch_size": 10, - "user": { - "lb": np.array([-3, -2]), - "ub": np.array([3, 2]), - }, + "vocs": vocs, } alloc_specs = { @@ -57,7 +56,13 @@ def six_hump_camel_err(H, persis_info, sim_specs, _): # Perform the run return_flag = 1 try: - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) + H, persis_info, flag = libE( + sim_specs, + gen_specs, + exit_criteria, + alloc_specs=alloc_specs, + libE_specs=libE_specs, + ) except LoggedException as e: print(f"Caught deliberate exception: {e}") return_flag = 0 diff --git a/libensemble/tests/functionality_tests/test_cancel_in_alloc.py b/libensemble/tests/functionality_tests/test_cancel_in_alloc.py index 0098ba93f..72953cdfd 100644 --- a/libensemble/tests/functionality_tests/test_cancel_in_alloc.py +++ b/libensemble/tests/functionality_tests/test_cancel_in_alloc.py @@ -18,6 +18,7 @@ # TESTSUITE_NPROCS: 4 import numpy as np +from gest_api.vocs import VOCS from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first from libensemble.gen_funcs.sampling import uniform_random_sample as gen_f @@ -39,16 +40,15 @@ "user": {"uniform_random_pause_ub": 10}, # long sleep ensures sims are still running when cancel fires } + vocs = VOCS(variables={"x0": [-3, 3], "x1": [-2, 2]}, objectives={"f": "EXPLORE"}) + gen_specs = { "gen_f": gen_f, "in": ["sim_id"], "out": [("x", float, (2,))], "batch_size": nworkers, "num_active_gens": 1, - "user": { - "lb": np.array([-3, -2]), - "ub": np.array([3, 2]), - }, + "vocs": vocs, } alloc_specs = { diff --git a/libensemble/tests/functionality_tests/test_comms.py b/libensemble/tests/functionality_tests/test_comms.py index daa9e564c..de46c20f8 100644 --- a/libensemble/tests/functionality_tests/test_comms.py +++ b/libensemble/tests/functionality_tests/test_comms.py @@ -15,6 +15,7 @@ # TESTSUITE_NPROCS: 2 4 import numpy as np +from gest_api.vocs import VOCS from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first from libensemble.executors.mpi_executor import MPIExecutor # Only used to get workerID in float_x1000 @@ -41,15 +42,14 @@ "out": [("arr_vals", float, array_size), ("scal_val", float)], } + vocs = VOCS(variables={"x0": [-3, 3], "x1": [-2, 2]}, objectives={"f": "EXPLORE"}) + gen_specs = { "gen_f": gen_f, "in": ["sim_id"], "out": [("x", float, (2,))], "batch_size": sim_max, - "user": { - "lb": np.array([-3, -2]), - "ub": np.array([3, 2]), - }, + "vocs": vocs, } exit_criteria = {"sim_max": sim_max, "wallclock_max": 300} @@ -59,7 +59,13 @@ } # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) + H, persis_info, flag = libE( + sim_specs, + gen_specs, + exit_criteria, + alloc_specs=alloc_specs, + libE_specs=libE_specs, + ) if is_manager: assert flag == 0 diff --git a/libensemble/tests/functionality_tests/test_elapsed_time_abort.py b/libensemble/tests/functionality_tests/test_elapsed_time_abort.py index 1396da50f..419f85c98 100644 --- a/libensemble/tests/functionality_tests/test_elapsed_time_abort.py +++ b/libensemble/tests/functionality_tests/test_elapsed_time_abort.py @@ -13,7 +13,7 @@ # TESTSUITE_COMMS: mpi local tcp # TESTSUITE_NPROCS: 2 4 -import numpy as np +from gest_api.vocs import VOCS from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first from libensemble.gen_funcs.sampling import uniform_random_sample as gen_f @@ -34,16 +34,15 @@ "user": {"pause_time": 2}, } + vocs = VOCS(variables={"x0": [-3, 3], "x1": [-2, 2]}, objectives={"f": "EXPLORE"}) + gen_specs = { "gen_f": gen_f, "in": ["sim_id"], "out": [("x", float, (2,))], "batch_size": 5, "num_active_gens": 2, - "user": { - "lb": np.array([-3, -2]), - "ub": np.array([3, 2]), - }, + "vocs": vocs, } alloc_specs = { @@ -56,7 +55,13 @@ exit_criteria = {"wallclock_max": 1} # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, libE_specs=libE_specs, alloc_specs=alloc_specs) + H, persis_info, flag = libE( + sim_specs, + gen_specs, + exit_criteria, + libE_specs=libE_specs, + alloc_specs=alloc_specs, + ) if is_manager: eprint(flag) diff --git a/libensemble/tests/functionality_tests/test_evaluate_existing_plus_gen.py b/libensemble/tests/functionality_tests/test_evaluate_existing_plus_gen.py index 3e37bc86d..bfe30eae9 100644 --- a/libensemble/tests/functionality_tests/test_evaluate_existing_plus_gen.py +++ b/libensemble/tests/functionality_tests/test_evaluate_existing_plus_gen.py @@ -15,6 +15,7 @@ # TESTSUITE_NPROCS: 2 4 import numpy as np +from gest_api.vocs import VOCS # Import libEnsemble items for this test from libensemble import Ensemble @@ -24,11 +25,8 @@ from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, SimSpecs -def create_H0(gen_specs, H0_size): +def create_H0(lb, ub, H0_size): """Create an H0 for give_pregenerated_sim_work""" - # Manually creating H0 - ub = gen_specs["user"]["ub"] - lb = gen_specs["user"]["lb"] n = len(lb) b = H0_size @@ -41,22 +39,22 @@ def create_H0(gen_specs, H0_size): # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": - sampling = Ensemble(parse_args=True) sampling.sim_specs = SimSpecs(sim_f=sim_f, inputs=["x"], out=[("f", float)]) - gen_specs = { - "gen_f": gen_f, - "outputs": [("x", float, (2,))], - "batch_size": 50, - "user": { - "lb": np.array([-3, -3]), - "ub": np.array([3, 3]), - }, - } - sampling.gen_specs = GenSpecs(**gen_specs) + vocs = VOCS(variables={"x0": [-3, 3], "x1": [-3, 3]}, objectives={"f": "EXPLORE"}) + lb = np.array([-3, -3]) + ub = np.array([3, 3]) + + sampling.gen_specs = GenSpecs( + gen_f=gen_f, + persis_in=["f"], + outputs=[("x", float, (2,))], + batch_size=50, + vocs=vocs, + ) sampling.exit_criteria = ExitCriteria(sim_max=100) - sampling.H0 = create_H0(gen_specs, 50) + sampling.H0 = create_H0(lb, ub, 50) sampling.alloc_specs = AllocSpecs(alloc_f=give_sim_work_first) sampling.run() diff --git a/libensemble/tests/functionality_tests/test_executor_forces_tutorial.py b/libensemble/tests/functionality_tests/test_executor_forces_tutorial.py index d6b368b93..78393db20 100644 --- a/libensemble/tests/functionality_tests/test_executor_forces_tutorial.py +++ b/libensemble/tests/functionality_tests/test_executor_forces_tutorial.py @@ -1,8 +1,8 @@ import os import sys -import numpy as np from forces_simf import run_forces # Sim func from current dir +from gest_api.vocs import VOCS from libensemble import Ensemble from libensemble.executors import MPIExecutor @@ -37,6 +37,8 @@ outputs=[("energy", float)], ) + vocs = VOCS(variables={"nparticles": [1000, 3000]}, objectives={"energy": "MINIMIZE"}) + ensemble.gen_specs = GenSpecs( gen_f=gen_f, inputs=[], # No input when starting persistent generator @@ -44,10 +46,7 @@ outputs=[("x", float, (1,))], initial_batch_size=nsim_workers, async_return=False, - user={ - "lb": np.array([1000]), # min particles - "ub": np.array([3000]), # max particles - }, + vocs=vocs, ) # gen_specs_end_tag # Starts one persistent generator. Simulated values are returned in batch. diff --git a/libensemble/tests/functionality_tests/test_executor_forces_tutorial_2.py b/libensemble/tests/functionality_tests/test_executor_forces_tutorial_2.py index 2a6cda15b..a19bac4bb 100644 --- a/libensemble/tests/functionality_tests/test_executor_forces_tutorial_2.py +++ b/libensemble/tests/functionality_tests/test_executor_forces_tutorial_2.py @@ -1,8 +1,8 @@ import os import sys -import numpy as np from forces_simf import run_forces # Sim func from current dir +from gest_api.vocs import VOCS from libensemble import Ensemble, logger from libensemble.executors import MPIExecutor @@ -39,6 +39,8 @@ outputs=[("energy", float)], ) + vocs = VOCS(variables={"nparticles": [1000, 3000]}, objectives={"energy": "MINIMIZE"}) + ensemble.gen_specs = GenSpecs( gen_f=gen_f, inputs=[], # No input when starting persistent generator @@ -46,10 +48,7 @@ outputs=[("x", float, (1,))], initial_batch_size=nsim_workers, async_return=True, - user={ - "lb": np.array([1000]), # min particles - "ub": np.array([3000]), # max particles - }, + vocs=vocs, ) # gen_specs_end_tag # Starts one persistent generator. Simulated values are returned in batch. diff --git a/libensemble/tests/functionality_tests/test_executor_hworld_pass_fail.py b/libensemble/tests/functionality_tests/test_executor_hworld_pass_fail.py index a65462e0d..c138ea846 100644 --- a/libensemble/tests/functionality_tests/test_executor_hworld_pass_fail.py +++ b/libensemble/tests/functionality_tests/test_executor_hworld_pass_fail.py @@ -13,6 +13,7 @@ import os import numpy as np +from gest_api.vocs import VOCS import libensemble.sim_funcs.six_hump_camel as six_hump_camel from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first @@ -21,7 +22,12 @@ from libensemble.libE import libE # Import libEnsemble items for this test -from libensemble.message_numbers import TASK_FAILED, WORKER_DONE, WORKER_KILL_ON_ERR, WORKER_KILL_ON_TIMEOUT +from libensemble.message_numbers import ( + TASK_FAILED, + WORKER_DONE, + WORKER_KILL_ON_ERR, + WORKER_KILL_ON_TIMEOUT, +) from libensemble.sim_funcs.executor_hworld import executor_hworld as sim_f from libensemble.tests.regression_tests.common import build_simfunc from libensemble.tools import parse_args @@ -73,15 +79,14 @@ "user": {"cores": cores_per_task}, } + vocs = VOCS(variables={"x0": [-3, 3], "x1": [-2, 2]}, objectives={"f": "EXPLORE"}) + gen_specs = { "gen_f": gen_f, "in": ["sim_id"], "out": [("x", float, (2,))], "batch_size": nworkers, - "user": { - "lb": np.array([-3, -2]), - "ub": np.array([3, 2]), - }, + "vocs": vocs, } # num sim_ended_count conditions in executor_hworld @@ -92,13 +97,25 @@ } # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) + H, persis_info, flag = libE( + sim_specs, + gen_specs, + exit_criteria, + alloc_specs=alloc_specs, + libE_specs=libE_specs, + ) if is_manager: print("\nChecking expected task status against Workers ...\n") calc_status_list_in = np.asarray( - [WORKER_DONE, WORKER_KILL_ON_ERR, WORKER_DONE, WORKER_KILL_ON_TIMEOUT, TASK_FAILED] + [ + WORKER_DONE, + WORKER_KILL_ON_ERR, + WORKER_DONE, + WORKER_KILL_ON_TIMEOUT, + TASK_FAILED, + ] ) calc_status_list = np.repeat(calc_status_list_in, nworkers) diff --git a/libensemble/tests/functionality_tests/test_executor_hworld_timeout.py b/libensemble/tests/functionality_tests/test_executor_hworld_timeout.py index 2a604007e..edab131de 100644 --- a/libensemble/tests/functionality_tests/test_executor_hworld_timeout.py +++ b/libensemble/tests/functionality_tests/test_executor_hworld_timeout.py @@ -13,6 +13,7 @@ import os import numpy as np +from gest_api.vocs import VOCS import libensemble.sim_funcs.six_hump_camel as six_hump_camel from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first @@ -75,15 +76,14 @@ }, } + vocs = VOCS(variables={"x0": [-3, 3], "x1": [-2, 2]}, objectives={"f": "EXPLORE"}) + gen_specs = { "gen_f": gen_f, "in": ["sim_id"], "out": [("x", float, (2,))], "batch_size": nworkers, - "user": { - "lb": np.array([-3, -2]), - "ub": np.array([3, 2]), - }, + "vocs": vocs, } alloc_specs = { @@ -100,7 +100,13 @@ for i in range(iterations): # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) + H, persis_info, flag = libE( + sim_specs, + gen_specs, + exit_criteria, + alloc_specs=alloc_specs, + libE_specs=libE_specs, + ) if is_manager: print("\nChecking expected task status against Workers ...\n") diff --git a/libensemble/tests/functionality_tests/test_mpi_runners_subnode_uneven.py b/libensemble/tests/functionality_tests/test_mpi_runners_subnode_uneven.py index c179028ab..643a41563 100644 --- a/libensemble/tests/functionality_tests/test_mpi_runners_subnode_uneven.py +++ b/libensemble/tests/functionality_tests/test_mpi_runners_subnode_uneven.py @@ -11,7 +11,7 @@ import sys -import numpy as np +from gest_api.vocs import VOCS from libensemble import logger from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first @@ -82,15 +82,14 @@ "out": [("f", float)], } + vocs = VOCS(variables={"x0": [-3, 3], "x1": [-2, 2]}, objectives={"f": "EXPLORE"}) + gen_specs = { "gen_f": gen_f, "in": ["sim_id"], "out": [("x", float, (n,))], "batch_size": 20, - "user": { - "lb": np.array([-3, -2]), - "ub": np.array([3, 2]), - }, + "vocs": vocs, } alloc_specs = {"alloc_f": give_sim_work_first} @@ -139,6 +138,12 @@ } # Perform the run - H, _, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) + H, _, flag = libE( + sim_specs, + gen_specs, + exit_criteria, + alloc_specs=alloc_specs, + libE_specs=libE_specs, + ) # All asserts are in sim func diff --git a/libensemble/tests/functionality_tests/test_mpi_runners_supernode_uneven.py b/libensemble/tests/functionality_tests/test_mpi_runners_supernode_uneven.py index 428297d5d..be826d91e 100644 --- a/libensemble/tests/functionality_tests/test_mpi_runners_supernode_uneven.py +++ b/libensemble/tests/functionality_tests/test_mpi_runners_supernode_uneven.py @@ -8,7 +8,7 @@ python test_mpi_runners_supernode_uneven.py --nworkers 5 """ -import numpy as np +from gest_api.vocs import VOCS from libensemble import logger from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first @@ -72,15 +72,14 @@ "out": [("f", float)], } + vocs = VOCS(variables={"x0": [-3, 3], "x1": [-2, 2]}, objectives={"f": "EXPLORE"}) + gen_specs = { "gen_f": gen_f, "in": [], "out": [("x", float, (n,))], "batch_size": 20, - "user": { - "lb": np.array([-3, -2]), - "ub": np.array([3, 2]), - }, + "vocs": vocs, } exit_criteria = {"sim_max": (nsim_workers) * rounds} @@ -134,6 +133,12 @@ alloc_specs = {"alloc_f": give_sim_work_first} # Perform the run - H, _, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) + H, _, flag = libE( + sim_specs, + gen_specs, + exit_criteria, + alloc_specs=alloc_specs, + libE_specs=libE_specs, + ) # All asserts are in sim func diff --git a/libensemble/tests/functionality_tests/test_mpi_warning.py b/libensemble/tests/functionality_tests/test_mpi_warning.py index f58620b90..257297a6c 100644 --- a/libensemble/tests/functionality_tests/test_mpi_warning.py +++ b/libensemble/tests/functionality_tests/test_mpi_warning.py @@ -14,7 +14,7 @@ import os import time -import numpy as np +from gest_api.vocs import VOCS from libensemble import Ensemble, logger from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first @@ -33,14 +33,13 @@ sampling = Ensemble() sampling.libE_specs.save_every_k_sims = 100 sampling.sim_specs = SimSpecs(sim_f=sim_f, inputs=["x"], outputs=[("f", float)]) + vocs = VOCS(variables={"x0": [-3, 3], "x1": [-2, 2]}, objectives={"f": "EXPLORE"}) + sampling.gen_specs = GenSpecs( gen_f=gen_f, outputs=[("x", float, 2)], batch_size=100, - user={ - "lb": np.array([-3, -2]), - "ub": np.array([3, 2]), - }, + vocs=vocs, ) sampling.alloc_specs = AllocSpecs(alloc_f=give_sim_work_first) diff --git a/libensemble/tests/functionality_tests/test_persistent_uniform_sampling_async.py b/libensemble/tests/functionality_tests/test_persistent_uniform_sampling_async.py index fc9632a58..4b36678bc 100644 --- a/libensemble/tests/functionality_tests/test_persistent_uniform_sampling_async.py +++ b/libensemble/tests/functionality_tests/test_persistent_uniform_sampling_async.py @@ -19,6 +19,7 @@ import sys import numpy as np +from gest_api.vocs import VOCS from libensemble.gen_funcs.persistent_sampling import persistent_uniform as gen_f @@ -44,16 +45,15 @@ "user": {"uniform_random_pause_ub": 0.5}, } + vocs = VOCS(variables={"x0": [-3, 3], "x1": [-2, 2]}, objectives={"f": "EXPLORE"}) + gen_specs = { "gen_f": gen_f, "persis_in": ["f", "x", "sim_id"], "out": [("x", float, (n,))], "initial_batch_size": nworkers, "async_return": True, - "user": { - "lb": np.array([-3, -2]), - "ub": np.array([3, 2]), - }, + "vocs": vocs, } exit_criteria = {"gen_max": 10, "wallclock_max": 300} diff --git a/libensemble/tests/functionality_tests/test_sim_dirs_per_calc.py b/libensemble/tests/functionality_tests/test_sim_dirs_per_calc.py index baa34b838..7e49ab997 100644 --- a/libensemble/tests/functionality_tests/test_sim_dirs_per_calc.py +++ b/libensemble/tests/functionality_tests/test_sim_dirs_per_calc.py @@ -16,7 +16,7 @@ import os -import numpy as np +from gest_api.vocs import VOCS from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first from libensemble.gen_funcs.sampling import uniform_random_sample as gen_f @@ -52,14 +52,13 @@ "out": [("f", float)], } + vocs = VOCS(variables={"x0": [-3, 3]}, objectives={"f": "EXPLORE"}) + gen_specs = { "gen_f": gen_f, "out": [("x", float, (1,))], "batch_size": 20, - "user": { - "lb": np.array([-3]), - "ub": np.array([3]), - }, + "vocs": vocs, } alloc_specs = { @@ -68,7 +67,13 @@ exit_criteria = {"sim_max": 21} - H, _, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) + H, _, flag = libE( + sim_specs, + gen_specs, + exit_criteria, + alloc_specs=alloc_specs, + libE_specs=libE_specs, + ) if is_manager: assert os.path.isdir(c_ensemble), f"Ensemble directory {c_ensemble} not created." diff --git a/libensemble/tests/functionality_tests/test_sim_dirs_per_worker.py b/libensemble/tests/functionality_tests/test_sim_dirs_per_worker.py index 74c77252c..3d041b237 100644 --- a/libensemble/tests/functionality_tests/test_sim_dirs_per_worker.py +++ b/libensemble/tests/functionality_tests/test_sim_dirs_per_worker.py @@ -16,7 +16,7 @@ import os -import numpy as np +from gest_api.vocs import VOCS from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first from libensemble.gen_funcs.sampling import uniform_random_sample as gen_f @@ -51,21 +51,26 @@ "out": [("f", float)], } + vocs = VOCS(variables={"x0": [-3, 3]}, objectives={"f": "EXPLORE"}) + gen_specs = { "gen_f": gen_f, "out": [("x", float, (1,))], "batch_size": 20, - "user": { - "lb": np.array([-3]), - "ub": np.array([3]), - }, + "vocs": vocs, } alloc_specs = {"alloc_f": give_sim_work_first} exit_criteria = {"sim_max": 21} - H, _, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) + H, _, flag = libE( + sim_specs, + gen_specs, + exit_criteria, + alloc_specs=alloc_specs, + libE_specs=libE_specs, + ) if is_manager: assert os.path.isdir(w_ensemble), f"Ensemble directory {w_ensemble} not created." diff --git a/libensemble/tests/functionality_tests/test_sim_dirs_with_exception.py b/libensemble/tests/functionality_tests/test_sim_dirs_with_exception.py index b3189dc1a..9fc0a3a17 100644 --- a/libensemble/tests/functionality_tests/test_sim_dirs_with_exception.py +++ b/libensemble/tests/functionality_tests/test_sim_dirs_with_exception.py @@ -16,7 +16,7 @@ import os -import numpy as np +from gest_api.vocs import VOCS from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first from libensemble.gen_funcs.sampling import uniform_random_sample as gen_f @@ -44,14 +44,13 @@ "out": [("f", float)], } + vocs = VOCS(variables={"x0": [-3, 3]}, objectives={"f": "EXPLORE"}) + gen_specs = { "gen_f": gen_f, "out": [("x", float, (1,))], "batch_size": 20, - "user": { - "lb": np.array([-3]), - "ub": np.array([3]), - }, + "vocs": vocs, } alloc_specs = { @@ -62,7 +61,13 @@ return_flag = 1 try: - H, _, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) + H, _, flag = libE( + sim_specs, + gen_specs, + exit_criteria, + alloc_specs=alloc_specs, + libE_specs=libE_specs, + ) except LoggedException as e: print(f"Caught deliberate exception: {e}") return_flag = 0 diff --git a/libensemble/tests/functionality_tests/test_sim_input_dir_option.py b/libensemble/tests/functionality_tests/test_sim_input_dir_option.py index 4e58d27d6..b3453428f 100644 --- a/libensemble/tests/functionality_tests/test_sim_input_dir_option.py +++ b/libensemble/tests/functionality_tests/test_sim_input_dir_option.py @@ -16,7 +16,7 @@ import os -import numpy as np +from gest_api.vocs import VOCS from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first from libensemble.gen_funcs.sampling import uniform_random_sample as gen_f @@ -47,14 +47,13 @@ "out": [("f", float)], } + vocs = VOCS(variables={"x0": [-3, 3]}, objectives={"f": "EXPLORE"}) + gen_specs = { "gen_f": gen_f, "out": [("x", float, (1,))], "batch_size": 20, - "user": { - "lb": np.array([-3]), - "ub": np.array([3]), - }, + "vocs": vocs, } exit_criteria = {"sim_max": 21} @@ -63,7 +62,13 @@ "alloc_f": give_sim_work_first, } - H, _, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) + H, _, flag = libE( + sim_specs, + gen_specs, + exit_criteria, + alloc_specs=alloc_specs, + libE_specs=libE_specs, + ) if is_manager: assert os.path.isdir(o_ensemble), f"Ensemble directory {o_ensemble} not created." diff --git a/libensemble/tests/functionality_tests/test_uniform_sampling.py b/libensemble/tests/functionality_tests/test_uniform_sampling.py index 4d8a6baa7..a2da65af9 100644 --- a/libensemble/tests/functionality_tests/test_uniform_sampling.py +++ b/libensemble/tests/functionality_tests/test_uniform_sampling.py @@ -18,6 +18,7 @@ import os import numpy as np +from gest_api.vocs import VOCS from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first from libensemble.gen_funcs.sampling import uniform_random_sample @@ -45,14 +46,13 @@ } # end_sim_specs_rst_tag + vocs = VOCS(variables={"x0": [-3, 3], "x1": [-2, 2]}, objectives={"f": "EXPLORE"}) + gen_specs = { "gen_f": uniform_random_sample, # Function generating sim_f input "out": [("x", float, (2,))], # Tell libE gen_f output, type, size "batch_size": 500, - "user": { - "lb": np.array([-3, -2]), # Used by this specific gen_f - "ub": np.array([3, 2]), # Used by this specific gen_f - }, + "vocs": vocs, } # end_gen_specs_rst_tag @@ -70,7 +70,13 @@ sim_specs["user"] = {"history_file": hfile} # Perform the run - H, _, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) + H, _, flag = libE( + sim_specs, + gen_specs, + exit_criteria, + alloc_specs=alloc_specs, + libE_specs=libE_specs, + ) if is_manager: assert flag == 0 diff --git a/libensemble/tests/functionality_tests/test_worker_exceptions.py b/libensemble/tests/functionality_tests/test_worker_exceptions.py index efdba0ec4..ddfec68be 100644 --- a/libensemble/tests/functionality_tests/test_worker_exceptions.py +++ b/libensemble/tests/functionality_tests/test_worker_exceptions.py @@ -14,7 +14,7 @@ # TESTSUITE_COMMS: mpi local tcp # TESTSUITE_NPROCS: 2 4 -import numpy as np +from gest_api.vocs import VOCS from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first from libensemble.gen_funcs.sampling import uniform_random_sample as gen_f @@ -34,15 +34,14 @@ "out": [("f", float)], } + vocs = VOCS(variables={"x0": [-3, 3], "x1": [-2, 2]}, objectives={"f": "EXPLORE"}) + gen_specs = { "gen_f": gen_f, "in": [], "out": [("x", float, 2)], - "user": { - "lb": np.array([-3, -2]), - "ub": np.array([3, 2]), - "initial_sample": 100, - }, + "vocs": vocs, + "user": {"initial_sample": 100}, } libE_specs["abort_on_exception"] = False @@ -58,7 +57,13 @@ # Perform the run return_flag = 1 try: - H, _, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) + H, _, flag = libE( + sim_specs, + gen_specs, + exit_criteria, + alloc_specs=alloc_specs, + libE_specs=libE_specs, + ) except LoggedException as e: print(f"Caught deliberate exception: {e}") return_flag = 0 diff --git a/libensemble/tests/functionality_tests/test_workflow_dir.py b/libensemble/tests/functionality_tests/test_workflow_dir.py index 6502b78ed..a16796b72 100644 --- a/libensemble/tests/functionality_tests/test_workflow_dir.py +++ b/libensemble/tests/functionality_tests/test_workflow_dir.py @@ -16,7 +16,7 @@ import os -import numpy as np +from gest_api.vocs import VOCS from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first from libensemble.gen_funcs.sampling import uniform_random_sample as gen_f @@ -49,14 +49,13 @@ "out": [("f", float)], } + vocs = VOCS(variables={"x0": [-3, 3]}, objectives={"f": "EXPLORE"}) + gen_specs = { "gen_f": gen_f, "out": [("x", float, (1,))], "batch_size": 20, - "user": { - "lb": np.array([-3]), - "ub": np.array([3]), - }, + "vocs": vocs, } alloc_specs = { @@ -73,7 +72,13 @@ "./test_workflow" + str(i) + "_nworkers" + str(nworkers) + "_comms-" + libE_specs["comms"] ) - H, _, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) + H, _, flag = libE( + sim_specs, + gen_specs, + exit_criteria, + alloc_specs=alloc_specs, + libE_specs=libE_specs, + ) assert os.path.isdir(libE_specs["workflow_dir_path"]), "workflow_dir not created" assert all( diff --git a/libensemble/tests/regression_tests/test_1d_sampling.py b/libensemble/tests/regression_tests/test_1d_sampling.py index 456b3ca60..21d3aaced 100644 --- a/libensemble/tests/regression_tests/test_1d_sampling.py +++ b/libensemble/tests/regression_tests/test_1d_sampling.py @@ -13,7 +13,7 @@ # TESTSUITE_COMMS: mpi local threads tcp # TESTSUITE_NPROCS: 3 4 -import numpy as np +from gest_api.vocs import VOCS from libensemble import Ensemble from libensemble.gen_funcs.persistent_sampling import persistent_uniform @@ -26,15 +26,14 @@ sampling = Ensemble(parse_args=True) sampling.libE_specs = LibeSpecs(save_every_k_gens=300, safe_mode=False, disable_log_files=True) sampling.sim_specs = SimSpecs(sim_f=sim_f, inputs=["x"], outputs=[("f", float)]) + vocs = VOCS(variables={"x0": [-3, 3]}, objectives={"f": "EXPLORE"}) + sampling.gen_specs = GenSpecs( gen_f=persistent_uniform, persis_in=["f"], outputs=[("x", float, (1,))], initial_batch_size=100, - user={ - "lb": np.array([-3]), - "ub": np.array([3]), - }, + vocs=vocs, ) sampling.exit_criteria = ExitCriteria(sim_max=500) diff --git a/libensemble/tests/regression_tests/test_2d_sampling.py b/libensemble/tests/regression_tests/test_2d_sampling.py index c26a66201..b6b237eae 100644 --- a/libensemble/tests/regression_tests/test_2d_sampling.py +++ b/libensemble/tests/regression_tests/test_2d_sampling.py @@ -14,6 +14,7 @@ # TESTSUITE_NPROCS: 2 4 import numpy as np +from gest_api.vocs import VOCS from libensemble import Ensemble from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first @@ -28,14 +29,13 @@ sampling = Ensemble(parse_args=True) sampling.libE_specs = LibeSpecs(save_every_k_sims=100) sampling.sim_specs = SimSpecs(sim_f=sim_f, inputs=["x"], outputs=[("f", float)]) + vocs = VOCS(variables={"x0": [-3, 3], "x1": [-2, 2]}, objectives={"f": "EXPLORE"}) + sampling.gen_specs = GenSpecs( gen_f=gen_f, outputs=[("x", float, 2)], batch_size=100, - user={ - "lb": np.array([-3, -2]), - "ub": np.array([3, 2]), - }, + vocs=vocs, ) sampling.alloc_specs = AllocSpecs(alloc_f=give_sim_work_first) diff --git a/libensemble/tests/regression_tests/test_2d_sampling_vocs.py b/libensemble/tests/regression_tests/test_2d_sampling_vocs.py index f535e1709..94740420a 100644 --- a/libensemble/tests/regression_tests/test_2d_sampling_vocs.py +++ b/libensemble/tests/regression_tests/test_2d_sampling_vocs.py @@ -33,7 +33,7 @@ def sim_f(In, persis_info, sim_specs, _): vocs = VOCS( variables={"x0": [-3.0, 3.0], "x1": [-2.0, 2.0]}, - objectives={"f": "MINIMIZE"}, + objectives={"f": "EXPLORE"}, ) generator = LatinHypercubeSample(vocs, random_seed=1) @@ -53,6 +53,6 @@ def sim_f(In, persis_info, sim_specs, _): x0 = sampling.H["x0"] x1 = sampling.H["x1"] f = sampling.H["f"] - assert np.all(np.isclose(f, np.sqrt(x0 ** 2 + x1 ** 2))) + assert np.all(np.isclose(f, np.sqrt(x0**2 + x1**2))) print("\nlibEnsemble has calculated the 2D vector norm of all points") sampling.save_output(__file__) diff --git a/libensemble/tests/regression_tests/test_asktell_gpCAM.py b/libensemble/tests/regression_tests/test_asktell_gpCAM.py index f59fc135d..ca4ca3ef9 100644 --- a/libensemble/tests/regression_tests/test_asktell_gpCAM.py +++ b/libensemble/tests/regression_tests/test_asktell_gpCAM.py @@ -56,10 +56,6 @@ "persis_in": ["x", "f", "sim_id"], "out": [("x", float, (n,))], "batch_size": batch_size, - "user": { - "lb": np.array([-3, -2, -1, -1]), - "ub": np.array([3, 2, 1, 1]), - }, } vocs = VOCS(variables={"x0": [-3, 3], "x1": [-2, 2], "x2": [-1, 1], "x3": [-1, 1]}, objectives={"f": "MINIMIZE"}) diff --git a/libensemble/tests/regression_tests/test_xopt_EI_initial_sample.py b/libensemble/tests/regression_tests/test_xopt_EI_initial_sample.py index d89de9449..116ffeb13 100644 --- a/libensemble/tests/regression_tests/test_xopt_EI_initial_sample.py +++ b/libensemble/tests/regression_tests/test_xopt_EI_initial_sample.py @@ -39,10 +39,9 @@ def xtest_sim(H, persis_info, sim_specs, _): if __name__ == "__main__": - batch_size = 4 - libE_specs = LibeSpecs(gen_on_manager=True, nworkers=batch_size) + libE_specs = LibeSpecs(nworkers=batch_size) libE_specs.reuse_output_dir = True vocs = VOCS( diff --git a/libensemble/tests/regression_tests/test_xopt_EI_initial_sample_instance.py b/libensemble/tests/regression_tests/test_xopt_EI_initial_sample_instance.py index c7db6d363..28a66b076 100644 --- a/libensemble/tests/regression_tests/test_xopt_EI_initial_sample_instance.py +++ b/libensemble/tests/regression_tests/test_xopt_EI_initial_sample_instance.py @@ -41,10 +41,9 @@ def xtest_sim(H, persis_info, sim_specs, _): if __name__ == "__main__": - batch_size = 4 - libE_specs = LibeSpecs(gen_on_manager=True, nworkers=batch_size) + libE_specs = LibeSpecs(nworkers=batch_size) libE_specs.reuse_output_dir = True vocs = VOCS( diff --git a/libensemble/tests/unit_tests/test_ensemble.py b/libensemble/tests/unit_tests/test_ensemble.py index 0e5de3223..5e5e9314f 100644 --- a/libensemble/tests/unit_tests/test_ensemble.py +++ b/libensemble/tests/unit_tests/test_ensemble.py @@ -42,7 +42,13 @@ def test_full_workflow(): from libensemble.ensemble import Ensemble from libensemble.gen_funcs.sampling import latin_hypercube_sample from libensemble.sim_funcs.simple_sim import norm_eval - from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, LibeSpecs, SimSpecs + from libensemble.specs import ( + AllocSpecs, + ExitCriteria, + GenSpecs, + LibeSpecs, + SimSpecs, + ) LS = LibeSpecs(comms="local", nworkers=4) @@ -270,6 +276,97 @@ def test_ready_happy_path(): assert issues == [], f"Issues should be empty but got: {issues}" +def test_gen_specs_vocs_populates_user_bounds(): + """GenSpecs should populate user['lb']/['ub'] from VOCS continuous variables.""" + from gest_api.vocs import VOCS + + from libensemble.specs import GenSpecs + + vocs = VOCS( + variables={"x0": [-3, 3], "x1": [-2, 2], "x2": [-1, 1], "x3": [-1, 1]}, + objectives={"f": "EXPLORE"}, + ) + gs = GenSpecs(vocs=vocs) + assert "lb" in gs.user, "lb should be populated in user from VOCS" + assert "ub" in gs.user, "ub should be populated in user from VOCS" + assert isinstance(gs.user["lb"], np.ndarray), "lb should be a numpy array" + assert isinstance(gs.user["ub"], np.ndarray), "ub should be a numpy array" + assert np.array_equal(gs.user["lb"], np.array([-3, -2, -1, -1])) + assert np.array_equal(gs.user["ub"], np.array([3, 2, 1, 1])) + + +def test_gen_specs_vocs_does_not_overwrite_user_bounds(): + """GenSpecs should not overwrite user-provided lb/ub when vocs is also given.""" + from gest_api.vocs import VOCS + + from libensemble.specs import GenSpecs + + vocs = VOCS(variables={"x0": [-3, 3], "x1": [-2, 2]}, objectives={"f": "EXPLORE"}) + explicit_lb = np.array([0.0, 0.0]) + explicit_ub = np.array([1.0, 1.0]) + gs = GenSpecs(vocs=vocs, user={"lb": explicit_lb, "ub": explicit_ub}) + assert np.array_equal(gs.user["lb"], explicit_lb), "Explicit lb should be preserved" + assert np.array_equal(gs.user["ub"], explicit_ub), "Explicit ub should be preserved" + + +def test_gen_specs_vocs_partial_user_bounds(): + """GenSpecs should fill in only the missing one of lb/ub if user supplies just one.""" + from gest_api.vocs import VOCS + + from libensemble.specs import GenSpecs + + vocs = VOCS(variables={"x0": [-3, 3], "x1": [-2, 2]}, objectives={"f": "EXPLORE"}) + explicit_lb = np.array([0.0, 0.0]) + gs = GenSpecs(vocs=vocs, user={"lb": explicit_lb}) + assert np.array_equal(gs.user["lb"], explicit_lb), "Explicit lb should be preserved" + assert "ub" in gs.user, "ub should be populated from VOCS" + assert np.array_equal(gs.user["ub"], np.array([3, 2])) + + +def test_gen_specs_no_vocs_leaves_user_empty(): + """Without VOCS, GenSpecs.user should remain empty by default.""" + from libensemble.specs import GenSpecs + + gs = GenSpecs(outputs=[("x", float, (1,))]) + assert "lb" not in gs.user, "lb should not be auto-populated without VOCS" + assert "ub" not in gs.user, "ub should not be auto-populated without VOCS" + + +def test_gen_specs_vocs_satisfies_legacy_user_params(): + """VOCS-populated user bounds should satisfy legacy gen_f consumers like + persistent_uniform (which require lb/ub to be numpy arrays and uses len(lb) + for dimension).""" + from gest_api.vocs import VOCS + + from libensemble.gen_funcs.persistent_sampling import _get_user_params + from libensemble.specs import GenSpecs + + vocs = VOCS(variables={"x0": [-3, 3], "x1": [-2, 2]}, objectives={"f": "EXPLORE"}) + gs = GenSpecs(vocs=vocs, initial_batch_size=10) + + # Convert to dict shape that _get_user_params expects + gs_dict = {"initial_batch_size": gs.initial_batch_size, "user": gs.user} + b, n, lb, ub = _get_user_params(gs_dict["user"], gs_dict) + assert b == 10 + assert n == 2 + assert isinstance(lb, np.ndarray) and lb.dtype == float + assert isinstance(ub, np.ndarray) and ub.dtype == float + assert np.array_equal(lb, np.array([-3.0, -2.0])) + assert np.array_equal(ub, np.array([3.0, 2.0])) + + +def test_gen_specs_vocs_integer_domain_yields_float_array(): + """Integer-valued VOCS domains should still produce float dtype lb/ub arrays.""" + from gest_api.vocs import VOCS + + from libensemble.specs import GenSpecs + + vocs = VOCS(variables={"x0": [0, 10], "x1": [-5, 5]}, objectives={"f": "EXPLORE"}) + gs = GenSpecs(vocs=vocs) + assert gs.user["lb"].dtype == float, "lb should be float dtype even for integer-domain variables" + assert gs.user["ub"].dtype == float, "ub should be float dtype even for integer-domain variables" + + if __name__ == "__main__": test_ensemble_init() test_ensemble_parse_args_false() @@ -283,3 +380,9 @@ def test_ready_happy_path(): test_ready_missing_nworkers_local() test_ready_field_mismatch() test_ready_happy_path() + test_gen_specs_vocs_populates_user_bounds() + test_gen_specs_vocs_does_not_overwrite_user_bounds() + test_gen_specs_vocs_partial_user_bounds() + test_gen_specs_no_vocs_leaves_user_empty() + test_gen_specs_vocs_satisfies_legacy_user_params() + test_gen_specs_vocs_integer_domain_yields_float_array() diff --git a/libensemble/tests/unit_tests/test_models.py b/libensemble/tests/unit_tests/test_models.py index fa6e2c1f9..01341ed94 100644 --- a/libensemble/tests/unit_tests/test_models.py +++ b/libensemble/tests/unit_tests/test_models.py @@ -174,6 +174,38 @@ def test_vocs_to_gen_specs(): assert gs2.persis_in == ["custom"] and gs2.outputs == [("custom_out", int)] +class _VariableWithDomain: + def __init__(self, domain): + self.domain = domain + + +class _VocsWithVariableDomains: + variables = {"x0": _VariableWithDomain([-3, 3]), "x1": _VariableWithDomain([-2, 2])} + constants = {} + objectives = {} + observables = {} + constraints = {} + + +def test_gen_specs_sets_user_bounds_from_vocs_variable_domains(): + """GenSpecs should populate user bounds from VOCS variable domain attributes.""" + + gs = GenSpecs(vocs=_VocsWithVariableDomains(), user=None) + + assert np.array_equal(gs.user["lb"], np.array([-3.0, -2.0])) + assert np.array_equal(gs.user["ub"], np.array([3.0, 2.0])) + + +def test_gen_specs_preserves_partial_user_bounds_from_vocs_variable_domains(): + """GenSpecs should only fill missing bounds from VOCS variable domain attributes.""" + + explicit_lb = np.array([0.0, 0.0]) + gs = GenSpecs(vocs=_VocsWithVariableDomains(), user={"lb": explicit_lb}) + + assert np.array_equal(gs.user["lb"], explicit_lb) + assert np.array_equal(gs.user["ub"], np.array([3.0, 2.0])) + + if __name__ == "__main__": test_sim_gen_alloc_exit_specs() test_sim_gen_alloc_exit_specs_invalid() @@ -182,3 +214,5 @@ def test_vocs_to_gen_specs(): test_ensemble_specs() test_vocs_to_sim_specs() test_vocs_to_gen_specs() + test_gen_specs_sets_user_bounds_from_vocs_variable_domains() + test_gen_specs_preserves_partial_user_bounds_from_vocs_variable_domains()