From a4ead368121d1aec807d66085dae00c292cd48d4 Mon Sep 17 00:00:00 2001 From: shudson Date: Thu, 21 Aug 2025 10:29:17 -0500 Subject: [PATCH 01/28] Split finalize and export --- libensemble/generators.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index fa91ec407..d5cbaef21 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -176,7 +176,14 @@ def ingest_numpy(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: else: self.running_gen_f.send(tag, None) - def finalize(self, results: npt.NDArray = None) -> (npt.NDArray, dict, int): + # SH TODO: This violates standard - finalize takes no arguments (and returns nothing) + def finalize(self, results: npt.NDArray = None) -> None: """Send any last results to the generator, and it to close down.""" self.ingest_numpy(results, PERSIS_STOP) # conversion happens in ingest + + # SH TODO: Decide name (get_data/export_data etc) and implement higher up in the class hierarchy? + # SH TODO: Options to unmap variables/objectives? + # SH TODO: Options to export as pandas dataframe? or list of dicts? + def export(self) -> (npt.NDArray, dict, int): + """Return the generator's state.""" return self.running_gen_f.result() From 3622219dfe4109f3f721a397e93a9d2dc1bb6ac2 Mon Sep 17 00:00:00 2001 From: shudson Date: Thu, 21 Aug 2025 15:42:08 -0500 Subject: [PATCH 02/28] aposmm uses x mapping to set bounds and size --- libensemble/gen_classes/aposmm.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index 45a522279..fcbd1365e 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -31,19 +31,26 @@ def __init__( self.n = len(list(self.VOCS.variables.keys())) gen_specs["user"] = {} - gen_specs["user"]["lb"] = np.array([vocs.variables[i].domain[0] for i in vocs.variables]) - gen_specs["user"]["ub"] = np.array([vocs.variables[i].domain[1] for i in vocs.variables]) + + super().__init__(vocs, History, persis_info, gen_specs, libE_info, **kwargs) + + # Set bounds using the correct x mapping + x_mapping = self.variables_mapping["x"] + self.gen_specs["user"]["lb"] = np.array([vocs.variables[var].domain[0] for var in x_mapping]) + self.gen_specs["user"]["ub"] = np.array([vocs.variables[var].domain[1] for var in x_mapping]) if not gen_specs.get("out"): # gen_specs never especially changes for aposmm even as the problem varies + x_size = len(self.variables_mapping.get("x", [self.n])) + x_on_cube_size = len(self.variables_mapping.get("x_on_cube", [self.n])) + print(f'x_size: {x_size}, x_on_cube_size: {x_on_cube_size}') gen_specs["out"] = [ - ("x", float, self.n), - ("x_on_cube", float, self.n), + ("x", float, x_size), + ("x_on_cube", float, x_on_cube_size), ("sim_id", int), ("local_min", bool), ("local_pt", bool), ] gen_specs["persis_in"] = ["x", "f", "local_pt", "sim_id", "sim_ended", "x_on_cube", "local_min"] - super().__init__(vocs, History, persis_info, gen_specs, libE_info, **kwargs) if not self.persis_info.get("nworkers"): self.persis_info["nworkers"] = kwargs.get("nworkers", gen_specs["user"].get("max_active_runs", 4)) From 9b3429b0f2aa9cb5bb3878148fe775d7f6655840 Mon Sep 17 00:00:00 2001 From: shudson Date: Fri, 22 Aug 2025 11:53:43 -0500 Subject: [PATCH 03/28] Fix finalize and export functions --- libensemble/generators.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index d5cbaef21..b40a0cfa7 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -119,6 +119,7 @@ def __init__( self.History = History self.libE_info = libE_info self.running_gen_f = None + self.gen_result = None def setup(self) -> None: """Must be called once before calling suggest/ingest. Initializes the background thread.""" @@ -176,14 +177,23 @@ def ingest_numpy(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: else: self.running_gen_f.send(tag, None) - # SH TODO: This violates standard - finalize takes no arguments (and returns nothing) - def finalize(self, results: npt.NDArray = None) -> None: - """Send any last results to the generator, and it to close down.""" - self.ingest_numpy(results, PERSIS_STOP) # conversion happens in ingest + def finalize(self) -> None: + """Stop the generator process and store the returned data.""" + self.ingest_numpy(None, PERSIS_STOP) # conversion happens in ingest + self.gen_result = self.running_gen_f.result() - # SH TODO: Decide name (get_data/export_data etc) and implement higher up in the class hierarchy? # SH TODO: Options to unmap variables/objectives? - # SH TODO: Options to export as pandas dataframe? or list of dicts? - def export(self) -> (npt.NDArray, dict, int): - """Return the generator's state.""" - return self.running_gen_f.result() + def export(self) -> tuple[npt.NDArray | None, dict | None, int | None]: + """Return the generator's results + + Returns + ------- + local_H : npt.NDArray + Generator history array. + persis_info : dict + Persistent information. + tag : int + Status flag (e.g., FINISHED_PERSISTENT_GEN_TAG). + """ + + return self.gen_result or (None, None, None) From a2c58fca79198f7ee66048307d9fe43c36dfde41 Mon Sep 17 00:00:00 2001 From: shudson Date: Fri, 22 Aug 2025 12:41:20 -0500 Subject: [PATCH 04/28] Option to export with user fields --- libensemble/generators.py | 23 +++++++++++++++----- libensemble/utils/misc.py | 46 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index b40a0cfa7..6ac8fd563 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -10,7 +10,7 @@ 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 +from libensemble.utils.misc import list_dicts_to_np, np_to_list_dicts, unmap_numpy_array class GeneratorNotStartedException(Exception): @@ -182,18 +182,31 @@ def finalize(self) -> None: self.ingest_numpy(None, PERSIS_STOP) # conversion happens in ingest self.gen_result = self.running_gen_f.result() - # SH TODO: Options to unmap variables/objectives? - def export(self) -> tuple[npt.NDArray | None, dict | None, int | None]: + def export(self, user_fields: bool = False) -> tuple[npt.NDArray | None, dict | None, int | None]: """Return the generator's results + Parameters + ---------- + user_fields : bool, optional + If True, return local_H with variables unmapped from arrays back to individual fields. + Default is False. + Returns ------- local_H : npt.NDArray - Generator history array. + Generator history array (unmapped if user_fields=True). persis_info : dict Persistent information. tag : int Status flag (e.g., FINISHED_PERSISTENT_GEN_TAG). """ + if not self.gen_result: + return (None, None, None) + + local_H, persis_info, tag = self.gen_result + + if user_fields and local_H is not None and self.variables_mapping: + unmapped_H = unmap_numpy_array(local_H, self.variables_mapping) + return (unmapped_H, persis_info, tag) - return self.gen_result or (None, None, None) + return self.gen_result diff --git a/libensemble/utils/misc.py b/libensemble/utils/misc.py index 0c03d6369..bd006ee27 100644 --- a/libensemble/utils/misc.py +++ b/libensemble/utils/misc.py @@ -186,6 +186,52 @@ def _is_singledim(selection: npt.NDArray) -> bool: return (hasattr(selection, "__len__") and len(selection) == 1) or selection.shape == () +def unmap_numpy_array(array: npt.NDArray, mapping: dict = {}) -> npt.NDArray: + """Convert numpy array with mapped fields back to individual scalar fields. + + Parameters + ---------- + array : npt.NDArray + Input array with mapped fields like x = [x0, x1, x2] + mapping : dict + Mapping from field names to variable names + + Returns + ------- + npt.NDArray + Array with unmapped fields like x0, x1, x2 as individual scalars + """ + if not mapping or array is None: + return array + + # Create new dtype with unmapped fields + new_fields = [] + for field in array.dtype.names: + if field in mapping: + for var_name in mapping[field]: + new_fields.append((var_name, array[field].dtype.type)) + elif len(array[field].shape) <= 1: + new_fields.append((field, array[field].dtype)) + + unmapped_array = np.zeros(len(array), dtype=new_fields) + + for field in array.dtype.names: + if field in mapping: + # Unmap array fields + if len(array[field].shape) == 1: + # Single dimension array (e.g., one variable mapped to x) + unmapped_array[mapping[field][0]] = array[field] + else: + # Multi-dimension array + for i, var_name in enumerate(mapping[field]): + unmapped_array[var_name] = array[field][:, i] + elif len(array[field].shape) <= 1: + # Copy scalar or 1D non-mapped fields + unmapped_array[field] = array[field] + + return unmapped_array + + def np_to_list_dicts(array: npt.NDArray, mapping: dict = {}) -> List[dict]: if array is None: return None From 012227a8b330b15f472a09f4274ddfc899ccf159 Mon Sep 17 00:00:00 2001 From: shudson Date: Mon, 25 Aug 2025 11:50:59 -0500 Subject: [PATCH 05/28] Add unit tests of unmap_numpy_array --- libensemble/tests/unit_tests/test_asktell.py | 56 ++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/libensemble/tests/unit_tests/test_asktell.py b/libensemble/tests/unit_tests/test_asktell.py index 3575bfc07..6a548f213 100644 --- a/libensemble/tests/unit_tests/test_asktell.py +++ b/libensemble/tests/unit_tests/test_asktell.py @@ -1,4 +1,5 @@ import numpy as np +from libensemble.utils.misc import unmap_numpy_array def _check_conversion(H, npp, mapping={}): @@ -92,6 +93,61 @@ def test_awkward_H(): _check_conversion(H, npp) +def test_unmap_numpy_array_basic(): + """Test basic unmapping of x and x_on_cube arrays""" + + dtype = [("sim_id", int), ("x", float, (3,)), ("x_on_cube", float, (3,)), ("f", float)] + H = np.zeros(2, dtype=dtype) + H[0] = (0, [1.1, 2.2, 3.3], [0.1, 0.2, 0.3], 10.5) + H[1] = (1, [4.4, 5.5, 6.6], [0.4, 0.5, 0.6], 20.7) + + mapping = {"x": ["x0", "x1", "x2"], "x_on_cube": ["x0_cube", "x1_cube", "x2_cube"]} + H_unmapped = unmap_numpy_array(H, mapping) + + expected_fields = ["sim_id", "x0", "x1", "x2", "x0_cube", "x1_cube", "x2_cube", "f"] + assert all(field in H_unmapped.dtype.names for field in expected_fields) + + assert H_unmapped["x0"][0] == 1.1 + assert H_unmapped["x1"][0] == 2.2 + assert H_unmapped["x2"][0] == 3.3 + assert H_unmapped["x0_cube"][0] == 0.1 + assert H_unmapped["x1_cube"][0] == 0.2 + assert H_unmapped["x2_cube"][0] == 0.3 + + +def test_unmap_numpy_array_single_dimension(): + """Test unmapping with single dimension""" + + dtype = [("sim_id", int), ("x", float, (1,)), ("f", float)] + H = np.zeros(1, dtype=dtype) + H[0] = (0, [5.5], 15.0) + + mapping = {"x": ["x0"]} + H_unmapped = unmap_numpy_array(H, mapping) + + assert "x0" in H_unmapped.dtype.names + assert H_unmapped["x0"][0] == 5.5 + + +def test_unmap_numpy_array_edge_cases(): + """Test edge cases for unmap_numpy_array""" + + dtype = [("sim_id", int), ("x", float, (2,)), ("f", float)] + H = np.zeros(1, dtype=dtype) + H[0] = (0, [1.0, 2.0], 10.0) + + # No mapping + H_no_mapping = unmap_numpy_array(H, {}) + assert H_no_mapping is H + + # None array + H_none = unmap_numpy_array(None, {"x": ["x0", "x1"]}) + assert H_none is None + + if __name__ == "__main__": test_awkward_list_dict() test_awkward_H() + test_unmap_numpy_array_basic() + test_unmap_numpy_array_single_dimension() + test_unmap_numpy_array_edge_cases() From 03420b39e5dfac84dd74f56d87c2bd0e814b1e08 Mon Sep 17 00:00:00 2001 From: shudson Date: Mon, 25 Aug 2025 11:59:28 -0500 Subject: [PATCH 06/28] Remove unneeded branch --- libensemble/utils/misc.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/libensemble/utils/misc.py b/libensemble/utils/misc.py index bd006ee27..659f46b54 100644 --- a/libensemble/utils/misc.py +++ b/libensemble/utils/misc.py @@ -218,13 +218,8 @@ def unmap_numpy_array(array: npt.NDArray, mapping: dict = {}) -> npt.NDArray: for field in array.dtype.names: if field in mapping: # Unmap array fields - if len(array[field].shape) == 1: - # Single dimension array (e.g., one variable mapped to x) - unmapped_array[mapping[field][0]] = array[field] - else: - # Multi-dimension array - for i, var_name in enumerate(mapping[field]): - unmapped_array[var_name] = array[field][:, i] + for i, var_name in enumerate(mapping[field]): + unmapped_array[var_name] = array[field][:, i] elif len(array[field].shape) <= 1: # Copy scalar or 1D non-mapped fields unmapped_array[field] = array[field] From 682425a14e726a713bf04152660dcbebb5507a90 Mon Sep 17 00:00:00 2001 From: shudson Date: Mon, 25 Aug 2025 12:10:01 -0500 Subject: [PATCH 07/28] Add expected variables mapping for APOSMM --- libensemble/gen_classes/aposmm.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index fcbd1365e..fc713b03a 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -12,6 +12,24 @@ class APOSMM(PersistentGenInterfacer): """ Standalone object-oriented APOSMM generator + + VOCS variables must include both regular and *_on_cube versions. E.g.,: + + vars_std = { + "var1": [0.0, 1.0], + "var2": [0.0, 1.0], + "var3": [0.0, 1.0], + "var1_on_cube": [0, 1.0], + "var2_on_cube": [0, 1.0], + "var3_on_cube": [0, 1.0] + } + + variables_mapping = { + "x": ["var1", "var2", "var3"], + "x_on_cube": ["var1_on_cube", "var2_on_cube", "var3_on_cube"], + } + + gen = APOSMM(vocs, variables_mapping=variables_mapping, ...) """ def __init__( From b20990111edaa55356d8337ddac440bc2cb3ec6f Mon Sep 17 00:00:00 2001 From: shudson Date: Mon, 25 Aug 2025 12:11:54 -0500 Subject: [PATCH 08/28] Better example bounds --- 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 fc713b03a..6777b3dab 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -16,9 +16,9 @@ class APOSMM(PersistentGenInterfacer): VOCS variables must include both regular and *_on_cube versions. E.g.,: vars_std = { - "var1": [0.0, 1.0], - "var2": [0.0, 1.0], - "var3": [0.0, 1.0], + "var1": [-10.0, 10.0], + "var2": [0.0, 100.0], + "var3": [1.0, 50.0], "var1_on_cube": [0, 1.0], "var2_on_cube": [0, 1.0], "var3_on_cube": [0, 1.0] From fd630eb70fb4c62eeb650123f692c9a96d119c38 Mon Sep 17 00:00:00 2001 From: shudson Date: Mon, 25 Aug 2025 13:56:49 -0500 Subject: [PATCH 09/28] Allow pass through of unmapped arrays --- libensemble/utils/misc.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/libensemble/utils/misc.py b/libensemble/utils/misc.py index 659f46b54..c21007620 100644 --- a/libensemble/utils/misc.py +++ b/libensemble/utils/misc.py @@ -210,8 +210,10 @@ def unmap_numpy_array(array: npt.NDArray, mapping: dict = {}) -> npt.NDArray: if field in mapping: for var_name in mapping[field]: new_fields.append((var_name, array[field].dtype.type)) - elif len(array[field].shape) <= 1: - new_fields.append((field, array[field].dtype)) + else: + # Preserve the original field structure including per-row shape + field_dtype = array.dtype[field] + new_fields.append((field, field_dtype)) unmapped_array = np.zeros(len(array), dtype=new_fields) @@ -220,14 +222,14 @@ def unmap_numpy_array(array: npt.NDArray, mapping: dict = {}) -> npt.NDArray: # Unmap array fields for i, var_name in enumerate(mapping[field]): unmapped_array[var_name] = array[field][:, i] - elif len(array[field].shape) <= 1: - # Copy scalar or 1D non-mapped fields + else: + # Copy non-mapped fields unmapped_array[field] = array[field] return unmapped_array -def np_to_list_dicts(array: npt.NDArray, mapping: dict = {}) -> List[dict]: +def np_to_list_dicts(array: npt.NDArray, mapping: dict = {}, allow_arrays: bool = False) -> List[dict]: if array is None: return None out = [] @@ -237,9 +239,8 @@ def np_to_list_dicts(array: npt.NDArray, mapping: dict = {}) -> List[dict]: for field in row.dtype.names: # non-string arrays, lists, etc. - if field not in list(mapping.keys()): - if _is_multidim(row[field]): + if _is_multidim(row[field]) and not allow_arrays: for i, x in enumerate(row[field]): new_dict[field + str(i)] = x From 57a8de97ef41cd2c2aca0662c65b07f5b901f1b0 Mon Sep 17 00:00:00 2001 From: shudson Date: Mon, 25 Aug 2025 14:02:23 -0500 Subject: [PATCH 10/28] Allow export as list of dictionaries --- libensemble/generators.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/libensemble/generators.py b/libensemble/generators.py index 6ac8fd563..c0ec5ed3b 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -182,7 +182,9 @@ def finalize(self) -> None: self.ingest_numpy(None, PERSIS_STOP) # conversion happens in ingest self.gen_result = self.running_gen_f.result() - def export(self, user_fields: bool = False) -> tuple[npt.NDArray | None, dict | None, int | None]: + def export( + self, user_fields: bool = False, as_dicts: bool = False + ) -> tuple[npt.NDArray | list | None, dict | None, int | None]: """Return the generator's results Parameters @@ -190,11 +192,14 @@ def export(self, user_fields: bool = False) -> tuple[npt.NDArray | None, dict | user_fields : bool, optional If True, return local_H with variables unmapped from arrays back to individual fields. Default is False. + as_dicts : bool, optional + If True, return local_H as list of dictionaries instead of numpy array. + Default is False. Returns ------- - local_H : npt.NDArray - Generator history array (unmapped if user_fields=True). + local_H : npt.NDArray | list + Generator history array (unmapped if user_fields=True, as dicts if as_dicts=True). persis_info : dict Persistent information. tag : int @@ -206,7 +211,12 @@ def export(self, user_fields: bool = False) -> tuple[npt.NDArray | None, dict | local_H, persis_info, tag = self.gen_result if user_fields and local_H is not None and self.variables_mapping: - unmapped_H = unmap_numpy_array(local_H, self.variables_mapping) - return (unmapped_H, persis_info, tag) + local_H = unmap_numpy_array(local_H, self.variables_mapping) + + if as_dicts and local_H is not None: + if user_fields and self.variables_mapping: + local_H = np_to_list_dicts(local_H, self.variables_mapping, allow_arrays=True) + else: + local_H = np_to_list_dicts(local_H, allow_arrays=True) - return self.gen_result + return (local_H, persis_info, tag) From 050c22de9bb5caad8efdb719d007042b51ea20a6 Mon Sep 17 00:00:00 2001 From: shudson Date: Mon, 25 Aug 2025 14:03:04 -0500 Subject: [PATCH 11/28] Add pass-through array to unmap test --- libensemble/tests/unit_tests/test_asktell.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/libensemble/tests/unit_tests/test_asktell.py b/libensemble/tests/unit_tests/test_asktell.py index 6a548f213..1f135745c 100644 --- a/libensemble/tests/unit_tests/test_asktell.py +++ b/libensemble/tests/unit_tests/test_asktell.py @@ -96,10 +96,10 @@ def test_awkward_H(): def test_unmap_numpy_array_basic(): """Test basic unmapping of x and x_on_cube arrays""" - dtype = [("sim_id", int), ("x", float, (3,)), ("x_on_cube", float, (3,)), ("f", float)] + dtype = [("sim_id", int), ("x", float, (3,)), ("x_on_cube", float, (3,)), ("f", float), ("grad", float, (3,))] H = np.zeros(2, dtype=dtype) - H[0] = (0, [1.1, 2.2, 3.3], [0.1, 0.2, 0.3], 10.5) - H[1] = (1, [4.4, 5.5, 6.6], [0.4, 0.5, 0.6], 20.7) + H[0] = (0, [1.1, 2.2, 3.3], [0.1, 0.2, 0.3], 10.5, [0.1, 0.2, 0.3]) + H[1] = (1, [4.4, 5.5, 6.6], [0.4, 0.5, 0.6], 20.7, [0.4, 0.5, 0.6]) mapping = {"x": ["x0", "x1", "x2"], "x_on_cube": ["x0_cube", "x1_cube", "x2_cube"]} H_unmapped = unmap_numpy_array(H, mapping) @@ -113,6 +113,10 @@ def test_unmap_numpy_array_basic(): assert H_unmapped["x0_cube"][0] == 0.1 assert H_unmapped["x1_cube"][0] == 0.2 assert H_unmapped["x2_cube"][0] == 0.3 + + # Test that non-mapped array fields are passed through unchanged + assert "grad" in H_unmapped.dtype.names + assert np.array_equal(H_unmapped["grad"], H["grad"]) def test_unmap_numpy_array_single_dimension(): From 5d31b6327e03c7ce4ed2f25b9a036ad1db9f6c10 Mon Sep 17 00:00:00 2001 From: shudson Date: Mon, 25 Aug 2025 15:04:12 -0500 Subject: [PATCH 12/28] Add export unit tests and fix up unmap --- .../unit_tests/test_persistent_aposmm.py | 67 +++++++++++++++++-- libensemble/utils/misc.py | 9 ++- 2 files changed, 68 insertions(+), 8 deletions(-) diff --git a/libensemble/tests/unit_tests/test_persistent_aposmm.py b/libensemble/tests/unit_tests/test_persistent_aposmm.py index d04d56198..4cb09ea21 100644 --- a/libensemble/tests/unit_tests/test_persistent_aposmm.py +++ b/libensemble/tests/unit_tests/test_persistent_aposmm.py @@ -225,7 +225,8 @@ def test_asktell_with_persistent_aposmm(): point["energy"] = six_hump_camel_func(np.array([point["core"], point["edge"]])) total_evals += 1 my_APOSMM.ingest(sample) - H, persis_info, exit_code = my_APOSMM.finalize() + my_APOSMM.finalize() + H, persis_info, exit_code = my_APOSMM.export() 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" @@ -243,9 +244,63 @@ def test_asktell_with_persistent_aposmm(): assert min_found >= 6, f"Found {min_found} minima" +def test_aposmm_export(): + """Test APOSMM export function with different options""" + from generator_standard.vocs import VOCS + from libensemble.gen_classes import APOSMM + + variables = {"core": [-3, 3], "edge": [-2, 2]} + objectives = {"energy": "MINIMIZE"} + vocs = VOCS(variables=variables, objectives=objectives) + + aposmm = APOSMM( + vocs, + initial_sample_size=10, + localopt_method="LN_BOBYQA", # Add required parameter + ) + + # Test basic export before finalize + H, _, _ = aposmm.export() + print(f"Export before finalize: {H}") # Debug + assert H is None # Should be None before finalize + + # Test export after suggest/ingest cycle + sample = aposmm.suggest(5) + for point in sample: + point["energy"] = 1.0 # Mock evaluation + aposmm.ingest(sample) + aposmm.finalize() + + # Test export with unmapped fields + H, _, _ = aposmm.export() + if H is not None: + assert "x" in H.dtype.names and H["x"].ndim == 2 + assert "f" in H.dtype.names and H["f"].ndim == 1 + + # Test export with user_fields + H_unmapped, _, _ = aposmm.export(user_fields=True) + print(f"H_unmapped: {H_unmapped}") # Debug + if H_unmapped is not None: + assert "core" in H_unmapped.dtype.names + assert "edge" in H_unmapped.dtype.names + + # Test export with as_dicts + H_dicts, _, _ = aposmm.export(as_dicts=True) + assert isinstance(H_dicts, list) + assert isinstance(H_dicts[0], dict) + assert "x" in H_dicts[0] # x remains as array + + # Test export with both options + H_both, _, _ = aposmm.export(user_fields=True, as_dicts=True) + assert isinstance(H_both, list) + assert "core" in H_both[0] + assert "edge" in H_both[0] + + if __name__ == "__main__": - test_persis_aposmm_localopt_test() - test_update_history_optimal() - test_standalone_persistent_aposmm() - test_standalone_persistent_aposmm_combined_func() - test_asktell_with_persistent_aposmm() + # test_persis_aposmm_localopt_test() + # test_update_history_optimal() + # test_standalone_persistent_aposmm() + # test_standalone_persistent_aposmm_combined_func() + # test_asktell_with_persistent_aposmm() + test_aposmm_export() diff --git a/libensemble/utils/misc.py b/libensemble/utils/misc.py index c21007620..88319ef43 100644 --- a/libensemble/utils/misc.py +++ b/libensemble/utils/misc.py @@ -220,8 +220,13 @@ def unmap_numpy_array(array: npt.NDArray, mapping: dict = {}) -> npt.NDArray: for field in array.dtype.names: if field in mapping: # Unmap array fields - for i, var_name in enumerate(mapping[field]): - unmapped_array[var_name] = array[field][:, i] + if len(array[field].shape) == 1: + # Scalar field mapped to single variable + unmapped_array[mapping[field][0]] = array[field] + else: + # Multi-dimensional field + for i, var_name in enumerate(mapping[field]): + unmapped_array[var_name] = array[field][:, i] else: # Copy non-mapped fields unmapped_array[field] = array[field] From d1d4b763b3003db5db93ec55458a70eead356b7d Mon Sep 17 00:00:00 2001 From: shudson Date: Mon, 25 Aug 2025 15:07:23 -0500 Subject: [PATCH 13/28] Re-enable APOSMM unit tests --- libensemble/tests/unit_tests/test_persistent_aposmm.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/libensemble/tests/unit_tests/test_persistent_aposmm.py b/libensemble/tests/unit_tests/test_persistent_aposmm.py index 4cb09ea21..835e7dfaa 100644 --- a/libensemble/tests/unit_tests/test_persistent_aposmm.py +++ b/libensemble/tests/unit_tests/test_persistent_aposmm.py @@ -298,9 +298,9 @@ def test_aposmm_export(): if __name__ == "__main__": - # test_persis_aposmm_localopt_test() - # test_update_history_optimal() - # test_standalone_persistent_aposmm() - # test_standalone_persistent_aposmm_combined_func() - # test_asktell_with_persistent_aposmm() + test_persis_aposmm_localopt_test() + test_update_history_optimal() + test_standalone_persistent_aposmm() + test_standalone_persistent_aposmm_combined_func() + test_asktell_with_persistent_aposmm() test_aposmm_export() From b05762ae52da62770310c5a6c122c30073e4808c Mon Sep 17 00:00:00 2001 From: shudson Date: Mon, 25 Aug 2025 16:17:01 -0500 Subject: [PATCH 14/28] Add checks for x and x_on_cube --- libensemble/gen_classes/aposmm.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index 6777b3dab..c3ab619d1 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -58,9 +58,10 @@ def __init__( self.gen_specs["user"]["ub"] = np.array([vocs.variables[var].domain[1] for var in x_mapping]) if not gen_specs.get("out"): # gen_specs never especially changes for aposmm even as the problem varies - x_size = len(self.variables_mapping.get("x", [self.n])) - x_on_cube_size = len(self.variables_mapping.get("x_on_cube", [self.n])) - print(f'x_size: {x_size}, x_on_cube_size: {x_on_cube_size}') + x_size = len(self.variables_mapping.get("x", [])) + x_on_cube_size = len(self.variables_mapping.get("x_on_cube", [])) + assert x_size > 0 and x_on_cube_size > 0, "Both x and x_on_cube must be specified in variables_mapping" + assert x_size == x_on_cube_size, f"x and x_on_cube must have same length but got {x_size} and {x_on_cube_size}" gen_specs["out"] = [ ("x", float, x_size), ("x_on_cube", float, x_on_cube_size), From 0b8cdec420307a6526990b201a42f97c7e88f117 Mon Sep 17 00:00:00 2001 From: shudson Date: Mon, 25 Aug 2025 16:17:20 -0500 Subject: [PATCH 15/28] Add export tests and fixup --- .../unit_tests/test_persistent_aposmm.py | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/libensemble/tests/unit_tests/test_persistent_aposmm.py b/libensemble/tests/unit_tests/test_persistent_aposmm.py index 835e7dfaa..ce8de178e 100644 --- a/libensemble/tests/unit_tests/test_persistent_aposmm.py +++ b/libensemble/tests/unit_tests/test_persistent_aposmm.py @@ -185,13 +185,24 @@ def test_asktell_with_persistent_aposmm(): n = 2 eval_max = 2000 - variables = {"core": [-3, 3], "edge": [-2, 2]} + variables = { + "core": [-3, 3], + "edge": [-2, 2], + "core_on_cube": [0, 1], + "edge_on_cube": [0, 1] + } objectives = {"energy": "MINIMIZE"} + variables_mapping = { + "x": ["core", "edge"], + "x_on_cube": ["core_on_cube", "edge_on_cube"] + } + vocs = VOCS(variables=variables, objectives=objectives) my_APOSMM = APOSMM( vocs, + variables_mapping=variables_mapping, initial_sample_size=100, sample_points=np.round(minima, 1), localopt_method="LN_BOBYQA", @@ -244,19 +255,37 @@ def test_asktell_with_persistent_aposmm(): assert min_found >= 6, f"Found {min_found} minima" +@pytest.mark.extra def test_aposmm_export(): """Test APOSMM export function with different options""" from generator_standard.vocs import VOCS from libensemble.gen_classes import APOSMM - variables = {"core": [-3, 3], "edge": [-2, 2]} + variables = { + "core": [-3, 3], + "edge": [-2, 2], + "core_on_cube": [0, 1], + "edge_on_cube": [0, 1], + } objectives = {"energy": "MINIMIZE"} + + variables_mapping = { + "x": ["core", "edge"], + "x_on_cube": ["core_on_cube", "edge_on_cube"] + } vocs = VOCS(variables=variables, objectives=objectives) aposmm = APOSMM( vocs, + variables_mapping=variables_mapping, initial_sample_size=10, - localopt_method="LN_BOBYQA", # Add required parameter + 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, ) # Test basic export before finalize From 1e52d99b60caa8469e063f3d972acbcce9ee71de Mon Sep 17 00:00:00 2001 From: shudson Date: Wed, 27 Aug 2025 15:12:43 -0500 Subject: [PATCH 16/28] Do not send local_min/pt to ingest --- libensemble/gen_classes/aposmm.py | 12 ++++++------ libensemble/generators.py | 20 ++++++++++++++------ 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index c3ab619d1..0a90870c9 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -44,12 +44,10 @@ def __init__( from libensemble.gen_funcs.persistent_aposmm import aposmm self.VOCS = vocs - gen_specs["gen_f"] = aposmm - self.n = len(list(self.VOCS.variables.keys())) - gen_specs["user"] = {} + self.n = len(list(self.VOCS.variables.keys())) super().__init__(vocs, History, persis_info, gen_specs, libE_info, **kwargs) # Set bounds using the correct x mapping @@ -57,7 +55,7 @@ def __init__( self.gen_specs["user"]["lb"] = np.array([vocs.variables[var].domain[0] for var in x_mapping]) self.gen_specs["user"]["ub"] = np.array([vocs.variables[var].domain[1] for var in x_mapping]) - if not gen_specs.get("out"): # gen_specs never especially changes for aposmm even as the problem varies + if not gen_specs.get("out"): x_size = len(self.variables_mapping.get("x", [])) x_on_cube_size = len(self.variables_mapping.get("x_on_cube", [])) assert x_size > 0 and x_on_cube_size > 0, "Both x and x_on_cube must be specified in variables_mapping" @@ -67,10 +65,12 @@ def __init__( ("x_on_cube", float, x_on_cube_size), ("sim_id", int), ("local_min", bool), - ("local_pt", 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"] = ["sim_id", "x", "x_on_cube", "f", "sim_ended"] + + # SH - Need to know if this is gen_on_manager or not. if not self.persis_info.get("nworkers"): self.persis_info["nworkers"] = kwargs.get("nworkers", gen_specs["user"].get("max_active_runs", 4)) self.all_local_minima = [] diff --git a/libensemble/generators.py b/libensemble/generators.py index c0ec5ed3b..2d79864b2 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -140,16 +140,24 @@ def setup(self) -> None: user_function=True, ) - # this is okay since the object isnt started until the first suggest + # This can be set here since the object isnt started until the first suggest self.libE_info["comm"] = self.running_gen_f.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)]) - for field in results.dtype.names: + def _prep_fields(self, results: npt.NDArray) -> npt.NDArray: + """Filter out fields that are not in persis_in and add sim_ended to the dtype""" + filtered_dtype = [ + (name, results.dtype[name]) for name in results.dtype.names if name in self.gen_specs["persis_in"] + ] + + new_dtype = filtered_dtype + [("sim_ended", bool)] + new_results = np.zeros(len(results), dtype=new_dtype) + + for field in new_results.dtype.names: try: new_results[field] = results[field] - except ValueError: # lets not slot in data that the gen doesnt need? + except ValueError: continue + new_results["sim_ended"] = True return new_results @@ -168,7 +176,7 @@ def suggest_numpy(self, num_points: int = 0) -> npt.NDArray: def ingest_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) + results = self._prep_fields(results) Work = {"libE_info": {"H_rows": np.copy(results["sim_id"]), "persistent": True, "executor": None}} self.running_gen_f.send(tag, Work) self.running_gen_f.send( From 3c2120222b4d1f31db0fa303780bbc10d35991d7 Mon Sep 17 00:00:00 2001 From: shudson Date: Thu, 28 Aug 2025 14:58:38 -0500 Subject: [PATCH 17/28] Autofill x and f variables_mapping separately --- libensemble/gen_classes/aposmm.py | 2 +- libensemble/generators.py | 22 +++++++++- .../unit_tests/test_persistent_aposmm.py | 41 ++++++++++++++----- 3 files changed, 52 insertions(+), 13 deletions(-) diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index 0a90870c9..66cd82119 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -65,7 +65,7 @@ def __init__( ("x_on_cube", float, x_on_cube_size), ("sim_id", int), ("local_min", bool), - ("local_pt", bool), + ("local_pt", bool), ] gen_specs["persis_in"] = ["sim_id", "x", "x_on_cube", "f", "sim_ended"] diff --git a/libensemble/generators.py b/libensemble/generators.py index 2d79864b2..8f723c803 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -55,12 +55,20 @@ def __init__( self.variables_mapping = variables_mapping if not self.variables_mapping: + self.variables_mapping = {} + + # Map variables to x if not already mapped + if "x" not in self.variables_mapping: + #SH TODO - is this check needed? if len(list(self.VOCS.variables.keys())) > 1 or list(self.VOCS.variables.keys())[0] != "x": - self.variables_mapping["x"] = list(self.VOCS.variables.keys()) + self.variables_mapping["x"] = self._get_unmapped_keys(self.VOCS.variables, "x") + + # Map objectives to f if not already mapped + if "f" not in self.variables_mapping: if ( len(list(self.VOCS.objectives.keys())) > 1 or list(self.VOCS.objectives.keys())[0] != "f" ): # e.g. {"f": ["f"]} doesn't need mapping - self.variables_mapping["f"] = list(self.VOCS.objectives.keys()) + self.variables_mapping["f"] = self._get_unmapped_keys(self.VOCS.objectives, "f") if len(kwargs) > 0: # so user can specify gen-specific parameters as kwargs to constructor if not self.gen_specs.get("user"): @@ -73,6 +81,16 @@ def __init__( def _validate_vocs(self, vocs) -> None: pass + + def _get_unmapped_keys(self, vocs_dict, default_key): + """Get keys from vocs_dict that aren't already mapped to other keys in variables_mapping.""" + # Get all variables that aren't already mapped to other keys + mapped_vars = [] + for mapped_list in self.variables_mapping.values(): + mapped_vars.extend(mapped_list) + + unmapped_vars = [v for v in list(vocs_dict.keys()) if v not in mapped_vars] + return unmapped_vars @abstractmethod def suggest_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 ce8de178e..0e8867877 100644 --- a/libensemble/tests/unit_tests/test_persistent_aposmm.py +++ b/libensemble/tests/unit_tests/test_persistent_aposmm.py @@ -195,7 +195,8 @@ def test_asktell_with_persistent_aposmm(): variables_mapping = { "x": ["core", "edge"], - "x_on_cube": ["core_on_cube", "edge_on_cube"] + "x_on_cube": ["core_on_cube", "edge_on_cube"], + "f": ["energy"], } vocs = VOCS(variables=variables, objectives=objectives) @@ -229,6 +230,8 @@ def test_asktell_with_persistent_aposmm(): while total_evals < eval_max: sample, detected_minima = my_APOSMM.suggest(6), my_APOSMM.suggest_updates() + if detected_minima: + print(f'sample {sample} detected_minima: {detected_minima}') if len(detected_minima): for m in detected_minima: potential_minima.append(m) @@ -239,8 +242,11 @@ def test_asktell_with_persistent_aposmm(): my_APOSMM.finalize() H, persis_info, exit_code = my_APOSMM.export() + print(f"Number of local_min points in H: {np.sum(H['local_min'])}", flush=True) + 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" + assert len(potential_minima) >= 6, f"Found {len(potential_minima)} minima" @@ -255,9 +261,8 @@ def test_asktell_with_persistent_aposmm(): assert min_found >= 6, f"Found {min_found} minima" -@pytest.mark.extra -def test_aposmm_export(): - """Test APOSMM export function with different options""" +def _run_aposmm_export_test(variables_mapping): + """Helper function to run APOSMM export tests with given variables_mapping""" from generator_standard.vocs import VOCS from libensemble.gen_classes import APOSMM @@ -269,19 +274,13 @@ def test_aposmm_export(): } objectives = {"energy": "MINIMIZE"} - variables_mapping = { - "x": ["core", "edge"], - "x_on_cube": ["core_on_cube", "edge_on_cube"] - } vocs = VOCS(variables=variables, objectives=objectives) aposmm = APOSMM( vocs, variables_mapping=variables_mapping, initial_sample_size=10, - 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, @@ -312,18 +311,40 @@ def test_aposmm_export(): if H_unmapped is not None: assert "core" in H_unmapped.dtype.names assert "edge" in H_unmapped.dtype.names + assert "energy" in H_unmapped.dtype.names # Test export with as_dicts H_dicts, _, _ = aposmm.export(as_dicts=True) assert isinstance(H_dicts, list) assert isinstance(H_dicts[0], dict) assert "x" in H_dicts[0] # x remains as array + assert "f" in H_dicts[0] # Test export with both options H_both, _, _ = aposmm.export(user_fields=True, as_dicts=True) assert isinstance(H_both, list) assert "core" in H_both[0] assert "edge" in H_both[0] + assert "energy" in H_both[0] + + +@pytest.mark.extra +def test_aposmm_export(): + """Test APOSMM export function with different options""" + + # Test with full variables_mapping + full_mapping = { + "x": ["core", "edge"], + "x_on_cube": ["core_on_cube", "edge_on_cube"], + "f": ["energy"], + } + _run_aposmm_export_test(full_mapping) + + # Test with just x_on_cube mapping (should auto-map x and f) + minimal_mapping = { + "x_on_cube": ["core_on_cube", "edge_on_cube"], + } + _run_aposmm_export_test(minimal_mapping) if __name__ == "__main__": From 1cb542fab9c8ed27c4a5d0413f8f4dba9837db30 Mon Sep 17 00:00:00 2001 From: shudson Date: Fri, 29 Aug 2025 10:39:02 -0500 Subject: [PATCH 18/28] Update asktell APOSMM regression test --- .../tests/regression_tests/test_asktell_aposmm_nlopt.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libensemble/tests/regression_tests/test_asktell_aposmm_nlopt.py b/libensemble/tests/regression_tests/test_asktell_aposmm_nlopt.py index 0eec667f7..2ec9411b0 100644 --- a/libensemble/tests/regression_tests/test_asktell_aposmm_nlopt.py +++ b/libensemble/tests/regression_tests/test_asktell_aposmm_nlopt.py @@ -53,12 +53,13 @@ workflow.exit_criteria = ExitCriteria(sim_max=2000) vocs = VOCS( - variables={"core": [-3, 3], "edge": [-2, 2]}, + variables={"core": [-3, 3], "edge": [-2, 2], "core_on_cube": [-3, 3], "edge_on_cube": [-2, 2]}, objectives={"energy": "MINIMIZE"}, ) aposmm = APOSMM( vocs, + variables_mapping={"x": ["core", "edge"], "x_on_cube": ["core_on_cube", "edge_on_cube"], "f": ["energy"]}, initial_sample_size=100, sample_points=minima, localopt_method="LN_BOBYQA", @@ -68,6 +69,7 @@ max_active_runs=workflow.nworkers, # should this match nworkers always? practically? ) + # SH TODO - dont want this stuff duplicated workflow.gen_specs = GenSpecs( persis_in=["x", "x_on_cube", "sim_id", "local_min", "local_pt", "f"], generator=aposmm, From 585c52150306c6922e71e8bf2c9751c764c4ff8f Mon Sep 17 00:00:00 2001 From: shudson Date: Mon, 22 Sep 2025 15:59:06 -0500 Subject: [PATCH 19/28] Add fvec when components is present --- 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 66cd82119..03437bda6 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -69,6 +69,8 @@ def __init__( ] gen_specs["persis_in"] = ["sim_id", "x", "x_on_cube", "f", "sim_ended"] + if "components" in kwargs or "components" in gen_specs.get("user", {}): + gen_specs["persis_in"].append("fvec") # SH - Need to know if this is gen_on_manager or not. if not self.persis_info.get("nworkers"): From cf36e85324425271c2330854bbffc20ae947e42f Mon Sep 17 00:00:00 2001 From: shudson Date: Mon, 22 Sep 2025 16:26:29 -0500 Subject: [PATCH 20/28] Send APOSMM errors as a string --- libensemble/gen_funcs/aposmm_localopt_support.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libensemble/gen_funcs/aposmm_localopt_support.py b/libensemble/gen_funcs/aposmm_localopt_support.py index 190e02dad..6f1d89c62 100644 --- a/libensemble/gen_funcs/aposmm_localopt_support.py +++ b/libensemble/gen_funcs/aposmm_localopt_support.py @@ -17,6 +17,7 @@ import numpy as np import psutil +import traceback import libensemble.gen_funcs from libensemble.message_numbers import EVAL_GEN_TAG, STOP_TAG # Only used to simulate receiving from manager @@ -586,7 +587,7 @@ def opt_runner(run_local_opt, user_specs, comm_queue, x0, f0, child_can_read, pa try: run_local_opt(user_specs, comm_queue, x0, f0, child_can_read, parent_can_read) except Exception as e: - comm_queue.put(ErrorMsg(e)) + comm_queue.put(ErrorMsg(traceback.format_exc())) parent_can_read.set() From 77efa2a9eecebc4fad8812cd4c4712acd7390550 Mon Sep 17 00:00:00 2001 From: shudson Date: Wed, 1 Oct 2025 14:12:28 -0500 Subject: [PATCH 21/28] Formatting --- libensemble/gen_classes/aposmm.py | 11 +++++------ libensemble/gen_funcs/aposmm_localopt_support.py | 2 +- libensemble/generators.py | 15 +++------------ libensemble/tests/unit_tests/test_asktell.py | 1 - .../tests/unit_tests/test_persistent_aposmm.py | 14 +++----------- libensemble/utils/misc.py | 6 ------ 6 files changed, 12 insertions(+), 37 deletions(-) diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index 03437bda6..9ee5f1b87 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -14,21 +14,18 @@ class APOSMM(PersistentGenInterfacer): Standalone object-oriented APOSMM generator VOCS variables must include both regular and *_on_cube versions. E.g.,: - vars_std = { "var1": [-10.0, 10.0], - "var2": [0.0, 100.0], + "var2": [0.0, 100.0], "var3": [1.0, 50.0], "var1_on_cube": [0, 1.0], "var2_on_cube": [0, 1.0], "var3_on_cube": [0, 1.0] } - variables_mapping = { "x": ["var1", "var2", "var3"], "x_on_cube": ["var1_on_cube", "var2_on_cube", "var3_on_cube"], } - gen = APOSMM(vocs, variables_mapping=variables_mapping, ...) """ @@ -59,13 +56,15 @@ def __init__( x_size = len(self.variables_mapping.get("x", [])) x_on_cube_size = len(self.variables_mapping.get("x_on_cube", [])) assert x_size > 0 and x_on_cube_size > 0, "Both x and x_on_cube must be specified in variables_mapping" - assert x_size == x_on_cube_size, f"x and x_on_cube must have same length but got {x_size} and {x_on_cube_size}" + assert x_size == x_on_cube_size, ( + f"x and x_on_cube must have same length but got {x_size} and {x_on_cube_size}" + ) gen_specs["out"] = [ ("x", float, x_size), ("x_on_cube", float, x_on_cube_size), ("sim_id", int), ("local_min", bool), - ("local_pt", bool), + ("local_pt", bool), ] gen_specs["persis_in"] = ["sim_id", "x", "x_on_cube", "f", "sim_ended"] diff --git a/libensemble/gen_funcs/aposmm_localopt_support.py b/libensemble/gen_funcs/aposmm_localopt_support.py index 6f1d89c62..901162783 100644 --- a/libensemble/gen_funcs/aposmm_localopt_support.py +++ b/libensemble/gen_funcs/aposmm_localopt_support.py @@ -586,7 +586,7 @@ def run_local_tao(user_specs, comm_queue, x0, f0, child_can_read, parent_can_rea def opt_runner(run_local_opt, user_specs, comm_queue, x0, f0, child_can_read, parent_can_read): try: run_local_opt(user_specs, comm_queue, x0, f0, child_can_read, parent_can_read) - except Exception as e: + except Exception: comm_queue.put(ErrorMsg(traceback.format_exc())) parent_can_read.set() diff --git a/libensemble/generators.py b/libensemble/generators.py index 8f723c803..624935224 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -56,13 +56,11 @@ def __init__( self.variables_mapping = variables_mapping if not self.variables_mapping: self.variables_mapping = {} - # Map variables to x if not already mapped if "x" not in self.variables_mapping: - #SH TODO - is this check needed? + # SH TODO - is this check needed? if len(list(self.VOCS.variables.keys())) > 1 or list(self.VOCS.variables.keys())[0] != "x": self.variables_mapping["x"] = self._get_unmapped_keys(self.VOCS.variables, "x") - # Map objectives to f if not already mapped if "f" not in self.variables_mapping: if ( @@ -81,14 +79,13 @@ def __init__( def _validate_vocs(self, vocs) -> None: pass - + def _get_unmapped_keys(self, vocs_dict, default_key): """Get keys from vocs_dict that aren't already mapped to other keys in variables_mapping.""" # Get all variables that aren't already mapped to other keys mapped_vars = [] for mapped_list in self.variables_mapping.values(): mapped_vars.extend(mapped_list) - unmapped_vars = [v for v in list(vocs_dict.keys()) if v not in mapped_vars] return unmapped_vars @@ -207,12 +204,11 @@ def finalize(self) -> None: """Stop the generator process and store the returned data.""" self.ingest_numpy(None, PERSIS_STOP) # conversion happens in ingest self.gen_result = self.running_gen_f.result() - + def export( self, user_fields: bool = False, as_dicts: bool = False ) -> tuple[npt.NDArray | list | None, dict | None, int | None]: """Return the generator's results - Parameters ---------- user_fields : bool, optional @@ -221,7 +217,6 @@ def export( as_dicts : bool, optional If True, return local_H as list of dictionaries instead of numpy array. Default is False. - Returns ------- local_H : npt.NDArray | list @@ -233,16 +228,12 @@ def export( """ if not self.gen_result: return (None, None, None) - local_H, persis_info, tag = self.gen_result - if user_fields and local_H is not None and self.variables_mapping: local_H = unmap_numpy_array(local_H, self.variables_mapping) - if as_dicts and local_H is not None: if user_fields and self.variables_mapping: local_H = np_to_list_dicts(local_H, self.variables_mapping, allow_arrays=True) else: local_H = np_to_list_dicts(local_H, allow_arrays=True) - return (local_H, persis_info, tag) diff --git a/libensemble/tests/unit_tests/test_asktell.py b/libensemble/tests/unit_tests/test_asktell.py index 1f135745c..d8c90d741 100644 --- a/libensemble/tests/unit_tests/test_asktell.py +++ b/libensemble/tests/unit_tests/test_asktell.py @@ -113,7 +113,6 @@ def test_unmap_numpy_array_basic(): assert H_unmapped["x0_cube"][0] == 0.1 assert H_unmapped["x1_cube"][0] == 0.2 assert H_unmapped["x2_cube"][0] == 0.3 - # Test that non-mapped array fields are passed through unchanged assert "grad" in H_unmapped.dtype.names assert np.array_equal(H_unmapped["grad"], H["grad"]) diff --git a/libensemble/tests/unit_tests/test_persistent_aposmm.py b/libensemble/tests/unit_tests/test_persistent_aposmm.py index 0e8867877..e1239ddc6 100644 --- a/libensemble/tests/unit_tests/test_persistent_aposmm.py +++ b/libensemble/tests/unit_tests/test_persistent_aposmm.py @@ -203,7 +203,7 @@ def test_asktell_with_persistent_aposmm(): my_APOSMM = APOSMM( vocs, - variables_mapping=variables_mapping, + variables_mapping=variables_mapping, initial_sample_size=100, sample_points=np.round(minima, 1), localopt_method="LN_BOBYQA", @@ -246,7 +246,6 @@ def test_asktell_with_persistent_aposmm(): 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" - assert len(potential_minima) >= 6, f"Found {len(potential_minima)} minima" @@ -265,7 +264,6 @@ def _run_aposmm_export_test(variables_mapping): """Helper function to run APOSMM export tests with given variables_mapping""" from generator_standard.vocs import VOCS from libensemble.gen_classes import APOSMM - variables = { "core": [-3, 3], "edge": [-2, 2], @@ -275,10 +273,9 @@ def _run_aposmm_export_test(variables_mapping): objectives = {"energy": "MINIMIZE"} vocs = VOCS(variables=variables, objectives=objectives) - aposmm = APOSMM( vocs, - variables_mapping=variables_mapping, + variables_mapping=variables_mapping, initial_sample_size=10, localopt_method="LN_BOBYQA", xtol_abs=1e-6, @@ -286,12 +283,10 @@ def _run_aposmm_export_test(variables_mapping): dist_to_bound_multiple=0.5, max_active_runs=6, ) - # Test basic export before finalize H, _, _ = aposmm.export() print(f"Export before finalize: {H}") # Debug assert H is None # Should be None before finalize - # Test export after suggest/ingest cycle sample = aposmm.suggest(5) for point in sample: @@ -312,14 +307,12 @@ def _run_aposmm_export_test(variables_mapping): assert "core" in H_unmapped.dtype.names assert "edge" in H_unmapped.dtype.names assert "energy" in H_unmapped.dtype.names - # Test export with as_dicts H_dicts, _, _ = aposmm.export(as_dicts=True) assert isinstance(H_dicts, list) assert isinstance(H_dicts[0], dict) assert "x" in H_dicts[0] # x remains as array assert "f" in H_dicts[0] - # Test export with both options H_both, _, _ = aposmm.export(user_fields=True, as_dicts=True) assert isinstance(H_both, list) @@ -339,13 +332,12 @@ def test_aposmm_export(): "f": ["energy"], } _run_aposmm_export_test(full_mapping) - # Test with just x_on_cube mapping (should auto-map x and f) minimal_mapping = { "x_on_cube": ["core_on_cube", "edge_on_cube"], } _run_aposmm_export_test(minimal_mapping) - + if __name__ == "__main__": test_persis_aposmm_localopt_test() diff --git a/libensemble/utils/misc.py b/libensemble/utils/misc.py index 88319ef43..dfc39e538 100644 --- a/libensemble/utils/misc.py +++ b/libensemble/utils/misc.py @@ -188,14 +188,12 @@ def _is_singledim(selection: npt.NDArray) -> bool: def unmap_numpy_array(array: npt.NDArray, mapping: dict = {}) -> npt.NDArray: """Convert numpy array with mapped fields back to individual scalar fields. - Parameters ---------- array : npt.NDArray Input array with mapped fields like x = [x0, x1, x2] mapping : dict Mapping from field names to variable names - Returns ------- npt.NDArray @@ -203,7 +201,6 @@ def unmap_numpy_array(array: npt.NDArray, mapping: dict = {}) -> npt.NDArray: """ if not mapping or array is None: return array - # Create new dtype with unmapped fields new_fields = [] for field in array.dtype.names: @@ -214,9 +211,7 @@ def unmap_numpy_array(array: npt.NDArray, mapping: dict = {}) -> npt.NDArray: # Preserve the original field structure including per-row shape field_dtype = array.dtype[field] new_fields.append((field, field_dtype)) - unmapped_array = np.zeros(len(array), dtype=new_fields) - for field in array.dtype.names: if field in mapping: # Unmap array fields @@ -230,7 +225,6 @@ def unmap_numpy_array(array: npt.NDArray, mapping: dict = {}) -> npt.NDArray: else: # Copy non-mapped fields unmapped_array[field] = array[field] - return unmapped_array From ed6604d65daf1feed242137961456ccb5b88d8d7 Mon Sep 17 00:00:00 2001 From: shudson Date: Wed, 1 Oct 2025 14:13:45 -0500 Subject: [PATCH 22/28] Blacken --- libensemble/gen_classes/aposmm.py | 6 +++--- .../tests/functionality_tests/check_libE_stats.py | 2 +- .../test_persistent_uniform_gen_decides_stop.py | 4 +--- .../test_persistent_gp_multitask_ax.py | 2 +- libensemble/tests/unit_tests/test_persistent_aposmm.py | 10 +++------- libensemble/tests/unit_tests_logger/test_logger.py | 2 +- scripts/plot_libe_calcs_util_v_time.py | 2 +- scripts/plot_libe_histogram.py | 2 +- scripts/plot_libe_tasks_util_v_time.py | 2 +- 9 files changed, 13 insertions(+), 19 deletions(-) diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index 9ee5f1b87..70fcd7d11 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -56,9 +56,9 @@ def __init__( x_size = len(self.variables_mapping.get("x", [])) x_on_cube_size = len(self.variables_mapping.get("x_on_cube", [])) assert x_size > 0 and x_on_cube_size > 0, "Both x and x_on_cube must be specified in variables_mapping" - assert x_size == x_on_cube_size, ( - f"x and x_on_cube must have same length but got {x_size} and {x_on_cube_size}" - ) + assert ( + x_size == x_on_cube_size + ), f"x and x_on_cube must have same length but got {x_size} and {x_on_cube_size}" gen_specs["out"] = [ ("x", float, x_size), ("x_on_cube", float, x_on_cube_size), diff --git a/libensemble/tests/functionality_tests/check_libE_stats.py b/libensemble/tests/functionality_tests/check_libE_stats.py index 424c07d8b..304925dc1 100644 --- a/libensemble/tests/functionality_tests/check_libE_stats.py +++ b/libensemble/tests/functionality_tests/check_libE_stats.py @@ -1,4 +1,4 @@ -""" Script to check format of libE_stats.txt +"""Script to check format of libE_stats.txt Checks matching start and end times existing for calculation and tasks if required. Checks that dates/times are in a valid format. diff --git a/libensemble/tests/functionality_tests/test_persistent_uniform_gen_decides_stop.py b/libensemble/tests/functionality_tests/test_persistent_uniform_gen_decides_stop.py index 68c8aaaa0..d9b946508 100644 --- a/libensemble/tests/functionality_tests/test_persistent_uniform_gen_decides_stop.py +++ b/libensemble/tests/functionality_tests/test_persistent_uniform_gen_decides_stop.py @@ -82,9 +82,7 @@ assert ( sum(counts == init_batch_size) >= ngens ), "The initial batch of each gen should be common among initial_batch_size number of points" - assert ( - len(counts) > 1 - ), "All gen_ended_times are the same; they should be different for the async case" + assert len(counts) > 1, "All gen_ended_times are the same; they should be different for the async case" gen_workers = np.unique(H["gen_worker"]) print("Generators that issued points", gen_workers) diff --git a/libensemble/tests/regression_tests/test_persistent_gp_multitask_ax.py b/libensemble/tests/regression_tests/test_persistent_gp_multitask_ax.py index 8c589161a..990493a17 100644 --- a/libensemble/tests/regression_tests/test_persistent_gp_multitask_ax.py +++ b/libensemble/tests/regression_tests/test_persistent_gp_multitask_ax.py @@ -50,7 +50,7 @@ def run_simulation(H, persis_info, sim_specs, libE_info): z = 8 elif task == "cheap_model": z = 1 - print('in sim', task) + print("in sim", task) libE_output = np.zeros(1, dtype=sim_specs["out"]) calc_status = WORKER_DONE diff --git a/libensemble/tests/unit_tests/test_persistent_aposmm.py b/libensemble/tests/unit_tests/test_persistent_aposmm.py index e1239ddc6..2d70fd895 100644 --- a/libensemble/tests/unit_tests/test_persistent_aposmm.py +++ b/libensemble/tests/unit_tests/test_persistent_aposmm.py @@ -185,12 +185,7 @@ def test_asktell_with_persistent_aposmm(): n = 2 eval_max = 2000 - variables = { - "core": [-3, 3], - "edge": [-2, 2], - "core_on_cube": [0, 1], - "edge_on_cube": [0, 1] - } + variables = {"core": [-3, 3], "edge": [-2, 2], "core_on_cube": [0, 1], "edge_on_cube": [0, 1]} objectives = {"energy": "MINIMIZE"} variables_mapping = { @@ -231,7 +226,7 @@ def test_asktell_with_persistent_aposmm(): sample, detected_minima = my_APOSMM.suggest(6), my_APOSMM.suggest_updates() if detected_minima: - print(f'sample {sample} detected_minima: {detected_minima}') + print(f"sample {sample} detected_minima: {detected_minima}") if len(detected_minima): for m in detected_minima: potential_minima.append(m) @@ -264,6 +259,7 @@ def _run_aposmm_export_test(variables_mapping): """Helper function to run APOSMM export tests with given variables_mapping""" from generator_standard.vocs import VOCS from libensemble.gen_classes import APOSMM + variables = { "core": [-3, 3], "edge": [-2, 2], diff --git a/libensemble/tests/unit_tests_logger/test_logger.py b/libensemble/tests/unit_tests_logger/test_logger.py index e06331b3d..fdf13725f 100644 --- a/libensemble/tests/unit_tests_logger/test_logger.py +++ b/libensemble/tests/unit_tests_logger/test_logger.py @@ -124,7 +124,7 @@ def test_custom_log_levels(): logger_test.manager_warning("This manager_warning message should log") logger_test.vdebug("This vdebug message should log") - with open(LogConfig.config.filename, 'r') as f: + with open(LogConfig.config.filename, "r") as f: file_content = f.read() assert "This manager_warning message should log" in file_content assert "This vdebug message should log" in file_content diff --git a/scripts/plot_libe_calcs_util_v_time.py b/scripts/plot_libe_calcs_util_v_time.py index 9f9f22edd..fc6750a10 100755 --- a/scripts/plot_libe_calcs_util_v_time.py +++ b/scripts/plot_libe_calcs_util_v_time.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -""" User function utilization plot +"""User function utilization plot Script to produce utilization plot based on how many workers are running user functions (sim or gens) at any given time. The plot is written to a file. diff --git a/scripts/plot_libe_histogram.py b/scripts/plot_libe_histogram.py index e5145bc05..936557140 100755 --- a/scripts/plot_libe_histogram.py +++ b/scripts/plot_libe_histogram.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -""" Histogram of user function run-times (completed & killed). +"""Histogram of user function run-times (completed & killed). Script to produce a histogram plot giving a count of user function (sim or gen) calls by run-time intervals. Color shows completed versus killed versus diff --git a/scripts/plot_libe_tasks_util_v_time.py b/scripts/plot_libe_tasks_util_v_time.py index ece34bdaf..cb5ced723 100644 --- a/scripts/plot_libe_tasks_util_v_time.py +++ b/scripts/plot_libe_tasks_util_v_time.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -""" User tasks utilization plot +"""User tasks utilization plot Script to produce utilisation plot based on how many workers are running user tasks (submitted via a libEnsemble executor) at any given time. This does not From 9cbca1e72fec4dcb68b86ebfcfeeb33930ad46c7 Mon Sep 17 00:00:00 2001 From: shudson Date: Wed, 1 Oct 2025 14:17:57 -0500 Subject: [PATCH 23/28] Clarify comment --- libensemble/tests/regression_tests/test_asktell_aposmm_nlopt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libensemble/tests/regression_tests/test_asktell_aposmm_nlopt.py b/libensemble/tests/regression_tests/test_asktell_aposmm_nlopt.py index 2ec9411b0..460b89574 100644 --- a/libensemble/tests/regression_tests/test_asktell_aposmm_nlopt.py +++ b/libensemble/tests/regression_tests/test_asktell_aposmm_nlopt.py @@ -69,7 +69,7 @@ max_active_runs=workflow.nworkers, # should this match nworkers always? practically? ) - # SH TODO - dont want this stuff duplicated + # SH TODO - dont want this stuff duplicated - pass with vocs instead workflow.gen_specs = GenSpecs( persis_in=["x", "x_on_cube", "sim_id", "local_min", "local_pt", "f"], generator=aposmm, From ec773d4a3607047de83151e0e7b99239a21a7ec4 Mon Sep 17 00:00:00 2001 From: shudson Date: Wed, 1 Oct 2025 14:32:08 -0500 Subject: [PATCH 24/28] Update generator_standard to gest_api --- docs/function_guides/ask_tell_generator.rst | 2 +- libensemble/gen_classes/aposmm.py | 2 +- libensemble/gen_classes/gpCAM.py | 2 +- libensemble/gen_classes/sampling.py | 2 +- libensemble/generators.py | 4 ++-- .../tests/functionality_tests/test_asktell_sampling.py | 4 ++-- .../tests/regression_tests/test_asktell_aposmm_nlopt.py | 2 +- libensemble/tests/regression_tests/test_asktell_gpCAM.py | 2 +- libensemble/tests/unit_tests/test_persistent_aposmm.py | 4 ++-- pyproject.toml | 2 +- 10 files changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/function_guides/ask_tell_generator.rst b/docs/function_guides/ask_tell_generator.rst index 6212b24f5..73f97124c 100644 --- a/docs/function_guides/ask_tell_generator.rst +++ b/docs/function_guides/ask_tell_generator.rst @@ -8,7 +8,7 @@ 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 +https://github.com/campa-consortium/gest-api libEnsemble is in the process of supporting generator objects that implement the following interface: diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index 70fcd7d11..05b938455 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -2,7 +2,7 @@ from typing import List import numpy as np -from generator_standard.vocs import VOCS +from gest_api.vocs import VOCS from numpy import typing as npt from libensemble.generators import PersistentGenInterfacer diff --git a/libensemble/gen_classes/gpCAM.py b/libensemble/gen_classes/gpCAM.py index 585fe4696..33c263090 100644 --- a/libensemble/gen_classes/gpCAM.py +++ b/libensemble/gen_classes/gpCAM.py @@ -4,7 +4,7 @@ from typing import List import numpy as np -from generator_standard.vocs import VOCS +from gest_api.vocs import VOCS from gpcam import GPOptimizer as GP from numpy import typing as npt diff --git a/libensemble/gen_classes/sampling.py b/libensemble/gen_classes/sampling.py index 72263750e..5e8102c22 100644 --- a/libensemble/gen_classes/sampling.py +++ b/libensemble/gen_classes/sampling.py @@ -1,7 +1,7 @@ """Generator classes providing points using sampling""" import numpy as np -from generator_standard.vocs import VOCS +from gest_api.vocs import VOCS from libensemble.generators import LibensembleGenerator diff --git a/libensemble/generators.py b/libensemble/generators.py index 624935224..7c7c5b933 100644 --- a/libensemble/generators.py +++ b/libensemble/generators.py @@ -2,8 +2,8 @@ from typing import List, Optional import numpy as np -from generator_standard import Generator -from generator_standard.vocs import VOCS +from gest_api import Generator +from gest_api.vocs import VOCS from numpy import typing as npt from libensemble.comms.comms import QCommProcess # , QCommThread diff --git a/libensemble/tests/functionality_tests/test_asktell_sampling.py b/libensemble/tests/functionality_tests/test_asktell_sampling.py index e4fb1a88b..55e3b7afc 100644 --- a/libensemble/tests/functionality_tests/test_asktell_sampling.py +++ b/libensemble/tests/functionality_tests/test_asktell_sampling.py @@ -14,8 +14,8 @@ # TESTSUITE_NPROCS: 2 4 import numpy as np -from generator_standard import Generator -from generator_standard.vocs import VOCS +from gest_api import Generator +from gest_api.vocs import VOCS # Import libEnsemble items for this test from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f diff --git a/libensemble/tests/regression_tests/test_asktell_aposmm_nlopt.py b/libensemble/tests/regression_tests/test_asktell_aposmm_nlopt.py index 460b89574..0f80e42ca 100644 --- a/libensemble/tests/regression_tests/test_asktell_aposmm_nlopt.py +++ b/libensemble/tests/regression_tests/test_asktell_aposmm_nlopt.py @@ -28,7 +28,7 @@ libensemble.gen_funcs.rc.aposmm_optimizers = "nlopt" from time import time -from generator_standard.vocs import VOCS +from gest_api.vocs import VOCS from libensemble import Ensemble from libensemble.alloc_funcs.persistent_aposmm_alloc import persistent_aposmm_alloc as alloc_f diff --git a/libensemble/tests/regression_tests/test_asktell_gpCAM.py b/libensemble/tests/regression_tests/test_asktell_gpCAM.py index 3a10a1072..b093a0df7 100644 --- a/libensemble/tests/regression_tests/test_asktell_gpCAM.py +++ b/libensemble/tests/regression_tests/test_asktell_gpCAM.py @@ -22,7 +22,7 @@ import warnings import numpy as np -from generator_standard.vocs import VOCS +from gest_api.vocs import VOCS 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 diff --git a/libensemble/tests/unit_tests/test_persistent_aposmm.py b/libensemble/tests/unit_tests/test_persistent_aposmm.py index 2d70fd895..8ea4eebed 100644 --- a/libensemble/tests/unit_tests/test_persistent_aposmm.py +++ b/libensemble/tests/unit_tests/test_persistent_aposmm.py @@ -172,7 +172,7 @@ def test_standalone_persistent_aposmm_combined_func(): def test_asktell_with_persistent_aposmm(): from math import gamma, pi, sqrt - from generator_standard.vocs import VOCS + from gest_api.vocs import VOCS import libensemble.gen_funcs from libensemble.gen_classes import APOSMM @@ -257,7 +257,7 @@ def test_asktell_with_persistent_aposmm(): def _run_aposmm_export_test(variables_mapping): """Helper function to run APOSMM export tests with given variables_mapping""" - from generator_standard.vocs import VOCS + from gest_api.vocs import VOCS from libensemble.gen_classes import APOSMM variables = { diff --git a/pyproject.toml b/pyproject.toml index 882bcbbb3..7d332d933 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ authors = [{name = "Jeffrey Larson"}, {name = "Stephen Hudson"}, {name = "Stefan M. Wild"}, {name = "David Bindel"}, {name = "John-Luke Navarro"}] -dependencies = [ "numpy", "psutil", "pyyaml", "tomli", "campa-generator-standard @ git+https://github.com/campa-consortium/generator_standard@main", "pydantic"] +dependencies = ["numpy", "psutil", "pyyaml", "tomli", "gest-api", "pydantic"] description = "A Python toolkit for coordinating asynchronous and dynamic ensembles of calculations." name = "libensemble" From 5ea9b2be294fe9596f38525408900b5d0ef6eda4 Mon Sep 17 00:00:00 2001 From: shudson Date: Wed, 1 Oct 2025 14:57:48 -0500 Subject: [PATCH 25/28] Fix gest-api in pyproject --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7d332d933..a9ebc5a28 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ authors = [{name = "Jeffrey Larson"}, {name = "Stephen Hudson"}, {name = "Stefan M. Wild"}, {name = "David Bindel"}, {name = "John-Luke Navarro"}] -dependencies = ["numpy", "psutil", "pyyaml", "tomli", "gest-api", "pydantic"] +dependencies = ["numpy", "psutil", "pyyaml", "tomli", "campa-gest-api @ git+https://github.com/campa-consortium/gest-api@main", "pydantic"] description = "A Python toolkit for coordinating asynchronous and dynamic ensembles of calculations." name = "libensemble" From b14b85dde1fb799eda3371f4ed540ad97977c4a4 Mon Sep 17 00:00:00 2001 From: shudson Date: Wed, 1 Oct 2025 15:04:52 -0500 Subject: [PATCH 26/28] Fix gest project name --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a9ebc5a28..69be28199 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ authors = [{name = "Jeffrey Larson"}, {name = "Stephen Hudson"}, {name = "Stefan M. Wild"}, {name = "David Bindel"}, {name = "John-Luke Navarro"}] -dependencies = ["numpy", "psutil", "pyyaml", "tomli", "campa-gest-api @ git+https://github.com/campa-consortium/gest-api@main", "pydantic"] +dependencies = ["numpy", "psutil", "pyyaml", "tomli", "gest @ git+https://github.com/campa-consortium/gest-api@main", "pydantic"] description = "A Python toolkit for coordinating asynchronous and dynamic ensembles of calculations." name = "libensemble" From f8d183323682a3fe3da3fcc7eb1b33b02c8f94eb Mon Sep 17 00:00:00 2001 From: shudson Date: Wed, 1 Oct 2025 16:26:55 -0500 Subject: [PATCH 27/28] Fix _validate_vocs for gpCAM --- libensemble/gen_classes/gpCAM.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libensemble/gen_classes/gpCAM.py b/libensemble/gen_classes/gpCAM.py index 33c263090..5118ffdbc 100644 --- a/libensemble/gen_classes/gpCAM.py +++ b/libensemble/gen_classes/gpCAM.py @@ -55,9 +55,9 @@ def __init__(self, VOCS: VOCS, ask_max_iter: int = 10, random_seed: int = 1, *ar self.noise = 1e-8 # 1e-12 self.ask_max_iter = ask_max_iter - def _validate_vocs(self, VOCS): - assert len(self.VOCS.variables), "VOCS must contain variables." - assert len(self.VOCS.objectives), "VOCS must contain at least one objective." + def _validate_vocs(self, vocs): + assert len(vocs.variables), "VOCS must contain variables." + assert len(vocs.objectives), "VOCS must contain at least one objective." def suggest_numpy(self, n_trials: int) -> npt.NDArray: if self.all_x.shape[0] == 0: From ad54abdb6cf274e8fda7ac0d8ff04ae74f8a607c Mon Sep 17 00:00:00 2001 From: shudson Date: Thu, 2 Oct 2025 13:04:36 -0500 Subject: [PATCH 28/28] Remove misleading n --- 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 05b938455..cd9a9c257 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -43,8 +43,6 @@ def __init__( self.VOCS = vocs gen_specs["gen_f"] = aposmm gen_specs["user"] = {} - - self.n = len(list(self.VOCS.variables.keys())) super().__init__(vocs, History, persis_info, gen_specs, libE_info, **kwargs) # Set bounds using the correct x mapping @@ -59,6 +57,7 @@ def __init__( assert ( x_size == x_on_cube_size ), f"x and x_on_cube must have same length but got {x_size} and {x_on_cube_size}" + gen_specs["out"] = [ ("x", float, x_size), ("x_on_cube", float, x_on_cube_size),