Skip to content

Commit 4a3cb6a

Browse files
authored
Option to create sample points outside gen (#1697)
1 parent 9223a3d commit 4a3cb6a

4 files changed

Lines changed: 139 additions & 9 deletions

File tree

libensemble/libE.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,9 +242,11 @@ def libE(
242242
]
243243
exit_criteria = specs_dump(ensemble.exit_criteria, by_alias=True, exclude_none=True)
244244

245-
# Restore the generator object (don't use serialized version)
245+
# Restore objects that don't survive serialization via model_dump
246246
if hasattr(ensemble.gen_specs, "generator") and ensemble.gen_specs.generator is not None:
247247
gen_specs["generator"] = ensemble.gen_specs.generator
248+
if hasattr(ensemble.gen_specs, "vocs") and ensemble.gen_specs.vocs is not None:
249+
gen_specs["vocs"] = ensemble.gen_specs.vocs
248250

249251
# Extract platform info from settings or environment
250252
platform_info = get_platform(libE_specs)

libensemble/specs.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,16 @@ class GenSpecs(BaseModel):
216216
batch sizes via ``gen_specs["user"]`` or other methods.
217217
"""
218218

219+
initial_sample_method: str | None = None
220+
"""
221+
Method for producing initial sample points before starting the generator.
222+
If None (default), the generator is responsible for producing its own initial
223+
sample via ``suggest()``. Set to ``"uniform"`` to have libEnsemble generate
224+
uniform random samples from VOCS bounds, evaluate them, and ingest the results
225+
into the generator before optimization begins. The number of sample points is
226+
determined by ``initial_batch_size``.
227+
"""
228+
219229
threaded: bool | None = False
220230
"""
221231
Instruct Worker process to launch user function to a thread.
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
"""
2+
Tests libEnsemble with Xopt ExpectedImprovementGenerator using
3+
initial_sample_method="uniform" to produce initial sample points.
4+
5+
EI requires pre-evaluated data before it can suggest points. This test
6+
verifies that setting initial_sample_method="uniform" in GenSpecs causes
7+
libEnsemble to generate uniform random samples, evaluate them through
8+
the sim, and ingest results into the generator before optimization begins.
9+
10+
Execute via one of the following commands (e.g. 4 workers):
11+
mpiexec -np 5 python test_xopt_EI_initial_sample.py
12+
python test_xopt_EI_initial_sample.py -n 4
13+
14+
"""
15+
16+
# Do not change these lines - they are parsed by run-tests.sh
17+
# TESTSUITE_COMMS: local
18+
# TESTSUITE_NPROCS: 4
19+
# TESTSUITE_EXTRA: true
20+
# TESTSUITE_EXCLUDE: true
21+
22+
import numpy as np
23+
from gest_api.vocs import VOCS
24+
from xopt.generators.bayesian.expected_improvement import ExpectedImprovementGenerator
25+
26+
from libensemble import Ensemble
27+
from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f
28+
from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, LibeSpecs, SimSpecs
29+
30+
31+
def xtest_sim(H, persis_info, sim_specs, _):
32+
"""y1 = x2, c1 = x1"""
33+
batch = len(H)
34+
H_o = np.zeros(batch, dtype=sim_specs["out"])
35+
for i in range(batch):
36+
H_o["y1"][i] = H["x2"][i]
37+
H_o["c1"][i] = H["x1"][i]
38+
return H_o, persis_info
39+
40+
41+
if __name__ == "__main__":
42+
43+
batch_size = 4
44+
45+
libE_specs = LibeSpecs(gen_on_manager=True, nworkers=batch_size)
46+
libE_specs.reuse_output_dir = True
47+
48+
vocs = VOCS(
49+
variables={"x1": [0, 1.0], "x2": [0, 10.0]},
50+
objectives={"y1": "MINIMIZE"},
51+
constraints={"c1": ["GREATER_THAN", 0.5]},
52+
constants={"constant1": 1.0},
53+
)
54+
55+
gen = ExpectedImprovementGenerator(vocs=vocs)
56+
57+
# NO pre-ingested data — libEnsemble handles initial sampling.
58+
gen_specs = GenSpecs(
59+
generator=gen,
60+
initial_batch_size=batch_size,
61+
initial_sample_method="uniform",
62+
batch_size=batch_size,
63+
vocs=vocs,
64+
)
65+
66+
sim_specs = SimSpecs(
67+
sim_f=xtest_sim,
68+
vocs=vocs,
69+
)
70+
71+
alloc_specs = AllocSpecs(alloc_f=alloc_f)
72+
exit_criteria = ExitCriteria(sim_max=20)
73+
74+
workflow = Ensemble(
75+
libE_specs=libE_specs,
76+
sim_specs=sim_specs,
77+
alloc_specs=alloc_specs,
78+
gen_specs=gen_specs,
79+
exit_criteria=exit_criteria,
80+
)
81+
82+
H, _, _ = workflow.run()
83+
84+
if workflow.is_manager:
85+
print(f"Completed {len(H)} simulations")
86+
assert len(H) >= 8, f"Expected at least 8 sims, got {len(H)}"
87+
print("Test passed")

libensemble/utils/runners.py

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,19 @@ def _start_generator_loop(self, tag, Work, H_in):
158158
self._convert_initial_ingest(H_in)
159159
return self._loop_over_gen(tag, Work, H_in)
160160

161+
def _create_initial_sample(self, sample_method, num_points):
162+
"""Create initial sample points using the specified sampling method."""
163+
from libensemble.gen_classes.sampling import UniformSample
164+
165+
vocs = self.specs.get("vocs")
166+
samplers = {
167+
"uniform": UniformSample,
168+
}
169+
if sample_method not in samplers:
170+
raise ValueError(f"Unknown initial_sample_method: {sample_method!r}. Supported: {list(samplers.keys())}")
171+
sampler = samplers[sample_method](vocs=vocs)
172+
return sampler.suggest(num_points)
173+
161174
def _persistent_result(self, calc_in, persis_info, libE_info):
162175
"""Setup comms with manager, setup gen, loop gen to completion, return gen's results"""
163176
self.ps = PersistentSupport(libE_info, EVAL_GEN_TAG)
@@ -166,14 +179,32 @@ def _persistent_result(self, calc_in, persis_info, libE_info):
166179
if calc_in is not None and len(calc_in) > 0:
167180
self._convert_initial_ingest(calc_in)
168181

169-
# libE gens will hit the following line, but list_dicts_to_np will passthrough if the output is a numpy array
170-
H_out = list_dicts_to_np(
171-
self._get_initial_suggest(libE_info),
172-
dtype=self.specs.get("out"),
173-
mapping=getattr(self.gen, "variables_mapping", {}),
174-
)
175-
tag, Work, H_in = self.ps.send_recv(H_out) # evaluate the initial sample
176-
final_H_out = self._start_generator_loop(tag, Work, H_in)
182+
sample_method = self.specs.get("initial_sample_method")
183+
if sample_method is not None:
184+
# libEnsemble produces the initial sample, evaluates it, and
185+
# ingests results into the generator before optimization begins.
186+
initial_batch = self.specs.get("initial_batch_size")
187+
if not initial_batch:
188+
raise ValueError("initial_sample_method requires initial_batch_size to be set in GenSpecs.")
189+
H_sample = list_dicts_to_np(
190+
self._create_initial_sample(sample_method, initial_batch),
191+
dtype=self.specs.get("out"),
192+
mapping=getattr(self.gen, "variables_mapping", {}),
193+
)
194+
tag, Work, H_in = self.ps.send_recv(H_sample)
195+
self._convert_initial_ingest(H_in)
196+
# Generator now has evaluated data — enter the normal loop
197+
final_H_out = self._loop_over_gen(tag, Work, H_in)
198+
else:
199+
# Generator handles its own initial sampling
200+
H_out = list_dicts_to_np(
201+
self._get_initial_suggest(libE_info),
202+
dtype=self.specs.get("out"),
203+
mapping=getattr(self.gen, "variables_mapping", {}),
204+
)
205+
tag, Work, H_in = self.ps.send_recv(H_out) # evaluate the initial sample
206+
final_H_out = self._start_generator_loop(tag, Work, H_in)
207+
177208
self.gen.finalize()
178209
return final_H_out, FINISHED_PERSISTENT_GEN_TAG
179210

0 commit comments

Comments
 (0)