Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,3 @@ If the minimum objective value is exactly 0.0, check whether those rows have
`sim_ended == True`. Unevaluated rows often have fields initialized to zero.
This is common for the last few rows when the simulation budget is exhausted — they were
allocated by the generator but never evaluated.

2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@ repos:
rev: v1.19.1
hooks:
- id: mypy
exclude: ^docs/conf\.py$|libensemble/utils/(launcher|loc_stack|runners|pydantic|output_directory)\.py$|libensemble/tests/(regression_tests|functionality_tests|unit_tests|scaling_tests)/.*
pass_filenames: false
24 changes: 21 additions & 3 deletions install/install_minq.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,26 @@
#!/usr/bin/env bash

set -euo pipefail

# IBCDFO pins the exact MINQ commit it supports, and refuses to run (via
# sys.exit) if the MINQ clone is at any other commit. Read the required SHA
# from the installed ibcdfo package rather than hardcoding it here, so that
# bumping ibcdfo in pixi.lock can never silently desynchronize the two.
MINQ_COMMIT=$(python -c "
import pathlib, ibcdfo
print((pathlib.Path(ibcdfo.__file__).parent / 'PkgData' / 'REQUIRED_MINQ_COMMIT').read_text().strip())
")

if [ -z "$MINQ_COMMIT" ]; then
echo "ERROR: could not determine required MINQ commit from installed ibcdfo" >&2
exit 1
fi

echo "Installing MINQ at ibcdfo-required commit ${MINQ_COMMIT}"

git clone https://github.com/POptUS/MINQ
git -C MINQ checkout 7749b83645ea21e303a94e1200542f7028499bb8
git -C MINQ checkout "$MINQ_COMMIT"
pushd MINQ/py/minq5/
export PYTHONPATH="$PYTHONPATH:$(pwd)"
echo "PYTHONPATH=$PYTHONPATH" >> $GITHUB_ENV
export PYTHONPATH="${PYTHONPATH:-}:$(pwd)"
echo "PYTHONPATH=$PYTHONPATH" >> "${GITHUB_ENV:-/dev/null}"
popd
424 changes: 322 additions & 102 deletions libensemble/gen_classes/aposmm.py

Large diffs are not rendered by default.

4 changes: 1 addition & 3 deletions libensemble/gen_classes/sampling.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,7 @@ class UniformSampleWithVariableResources(LibensembleGenerator):
path was tested with the default alloc.
"""

def __init__(
self, vocs: VOCS, max_resource_sets: int, random_seed: int = 1, *args, **kwargs
):
def __init__(self, vocs: VOCS, max_resource_sets: int, random_seed: int = 1, *args, **kwargs):
super().__init__(vocs, *args, **kwargs)
self.rng = np.random.default_rng(random_seed)
self.max_rsets = max_resource_sets
Expand Down
2 changes: 1 addition & 1 deletion libensemble/gen_funcs/persistent_botorch_mfkg_branin.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@

# Custom function to project posterior to target fidelity (defer to default)
def project(X):
return project_to_target_fidelity(X=X, target_fidelities=target_fidelities)
return project_to_target_fidelity(X=X, target_fidelities=target_fidelities, d=3)


# Wrapper function for compatibility with existing code
Expand Down
9 changes: 9 additions & 0 deletions libensemble/specs.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,15 @@ def set_fields_from_vocs(self):
out_fields.append(_convert_dtype_to_output_tuple(name, dtype))
self.outputs = out_fields

# Merge in any additional fields from generator.gen_specs["out"] (e.g., x_on_cube, local_min)
if self.generator is not None and hasattr(self.generator, "gen_specs"):
gen_out = self.generator.gen_specs.get("out", [])
existing_names = {f[0] for f in self.outputs}
for field in gen_out:
if field[0] not in existing_names:
self.outputs.append(field)
existing_names.add(field[0])

# Add _id field if generator returns_id is True
if self.generator is not None and getattr(self.generator, "returns_id", False):
if self.outputs is None:
Expand Down
7 changes: 4 additions & 3 deletions libensemble/tests/regression_tests/test_aposmm_nlopt.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,13 @@ def six_hump_camel_func(x):
H, _, _ = workflow.run()

if workflow.is_manager:
print("[Manager]:", H[np.where(H["local_min"])]["x"])
x_min = np.column_stack([H[H["local_min"]]["x0"], H[H["local_min"]]["x1"]])
print("[Manager]:", x_min)
print("[Manager]: Time taken =", time() - start_time, flush=True)

tol = 1e-5
for m in minima:
# The minima are known on this test problem.
# We use their values to test APOSMM has identified all minima
print(np.min(np.sum((H[H["local_min"]]["x"] - m) ** 2, 1)), flush=True)
assert np.min(np.sum((H[H["local_min"]]["x"] - m) ** 2, 1)) < tol
print(np.min(np.sum((x_min - m) ** 2, 1)), flush=True)
assert np.min(np.sum((x_min - m) ** 2, 1)) < tol
7 changes: 4 additions & 3 deletions libensemble/tests/regression_tests/test_aposmm_scipy.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,16 @@ def six_hump_camel_func(x):
H, _, _ = workflow.run()

if workflow.is_manager:
print("[Manager]:", H[np.where(H["local_min"])]["x"])
x_min = np.column_stack([H[H["local_min"]]["x0"], H[H["local_min"]]["x1"]])
print("[Manager]:", x_min)
print("[Manager]: Time taken =", time() - start_time, flush=True)

tol = 1e-3
min_found = 0
for m in minima:
# The minima are known on this test problem.
# We use their values to test APOSMM has identified all minima
print(np.min(np.sum((H[H["local_min"]]["x"] - m) ** 2, 1)), flush=True)
if np.min(np.sum((H[H["local_min"]]["x"] - m) ** 2, 1)) < tol:
print(np.min(np.sum((x_min - m) ** 2, 1)), flush=True)
if np.min(np.sum((x_min - m) ** 2, 1)) < tol:
min_found += 1
assert min_found >= 2, f"Found {min_found} minima"
11 changes: 5 additions & 6 deletions libensemble/tests/unit_tests/test_persistent_aposmm.py
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@ def test_asktell_ingest_first():

@pytest.mark.extra
def test_asktell_consecutive_during_sample():
"""Test consecutive suggest and ingest during sample"""
"""Test consecutive ingest during sampling"""

from gest_api.vocs import VOCS

Expand Down Expand Up @@ -503,12 +503,11 @@ def test_asktell_consecutive_during_sample():
dist_to_bound_multiple=0.01,
)

# Test consecutive suggest
first = my_APOSMM.suggest(1)
first[0]["energy"] = six_hump_camel_func(np.array([first[0]["core"], first[0]["edge"]]))
my_APOSMM.ingest(first)
second = my_APOSMM.suggest(1)
second += my_APOSMM.suggest(4)
second = my_APOSMM.suggest(5)

for point in second:
point["energy"] = six_hump_camel_func(np.array([point["core"], point["edge"]]))
# Test consecutive ingest
Expand All @@ -522,8 +521,8 @@ def test_asktell_consecutive_during_sample():

while total_evals < eval_max:

sample, detected_minima = my_APOSMM.suggest(3), my_APOSMM.suggest_updates()
sample += my_APOSMM.suggest(3)
sample = my_APOSMM.suggest(6)
detected_minima = my_APOSMM.suggest_updates()
if len(detected_minima):
for m in detected_minima:
potential_minima.append(m)
Expand Down
6 changes: 3 additions & 3 deletions libensemble/tools/test_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,9 +244,9 @@ def check_gpu_setting(task, assert_setting=True, print_setting=False, resources=
if assert_setting:
if isinstance(expected, dict):
for key, value in expected.items():
assert key in gpu_setting, (
f"Worker {task.workerID}: Expected env key '{key}' not found in GPU setting: {gpu_setting}"
)
assert (
key in gpu_setting
), f"Worker {task.workerID}: Expected env key '{key}' not found in GPU setting: {gpu_setting}"
assert gpu_setting[key] == value, (
f"Worker {task.workerID}: GPU setting key '{key}' has value '{gpu_setting[key]}', "
f"expected '{value}'"
Expand Down
22 changes: 12 additions & 10 deletions libensemble/utils/runners.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,31 +200,33 @@ def _result(self, calc_in: npt.NDArray, persis_info: dict, libE_info: dict) -> (


class LibensembleGenRunner(StandardGenRunner):
def _get_initial_suggest(self, libE_info) -> npt.NDArray:
"""Get initial batch from a LibensembleGenerator.
def _get_mapping_for_outputs(self) -> dict:
"""Return mappings whose internal fields are declared generator outputs."""
output_names = {field[0] for field in self.specs.get("out", [])}
return {
name: fields for name, fields in getattr(self.gen, "variables_mapping", {}).items() if name in output_names
}

LibensembleGenerator.suggest_numpy emits VOCS-field-named structured arrays
(e.g. x0/x1, energy). The manager-side history expects mapped fields (x, f)
unless the user explicitly requested otherwise.
"""
def _get_initial_suggest(self, libE_info) -> npt.NDArray:
"""Get initial batch from a LibensembleGenerator."""
initial_batch = self.specs.get("initial_batch_size") or self.specs.get("batch_size") or libE_info["batch_size"]
H_out = self.gen.suggest_numpy(initial_batch)
return map_numpy_array(H_out, mapping=getattr(self.gen, "variables_mapping", {}))
return map_numpy_array(H_out, mapping=self._get_mapping_for_outputs())

def _get_points_updates(self, batch_size: int) -> (npt.NDArray, list):
numpy_out = self.gen.suggest_numpy(batch_size)
numpy_out = map_numpy_array(numpy_out, mapping=getattr(self.gen, "variables_mapping", {}))
numpy_out = map_numpy_array(numpy_out, mapping=self._get_mapping_for_outputs())
if callable(getattr(self.gen, "suggest_updates", None)):
updates = self.gen.suggest_updates()
else:
updates = None
return numpy_out, updates

def _convert_ingest(self, x: npt.NDArray) -> list:
self.gen.ingest_numpy(unmap_numpy_array(x, mapping=getattr(self.gen, "variables_mapping", {})))
self.gen.ingest_numpy(unmap_numpy_array(x, mapping=self._get_mapping_for_outputs()))

def _convert_initial_ingest(self, x: npt.NDArray) -> list:
self.gen.ingest_numpy(unmap_numpy_array(x, mapping=getattr(self.gen, "variables_mapping", {})))
self.gen.ingest_numpy(unmap_numpy_array(x, mapping=self._get_mapping_for_outputs()))


class LibensembleGenThreadRunner(StandardGenRunner):
Expand Down
Loading