Skip to content

Commit 6824bb3

Browse files
committed
Update initial_sample_method to work with sampler instance with options
1 parent 40c1882 commit 6824bb3

3 files changed

Lines changed: 131 additions & 15 deletions

File tree

libensemble/specs.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -242,14 +242,22 @@ class GenSpecs(BaseModel):
242242
completed evaluations most recently told to the generator.
243243
"""
244244

245-
initial_sample_method: str | None = None
245+
initial_sample_method: str | object | None = None
246246
"""
247247
Method for producing initial sample points before starting the generator.
248248
If None (default), the generator is responsible for producing its own initial
249-
sample via ``suggest()``. Set to ``"uniform"`` to have libEnsemble generate
250-
uniform random samples from VOCS bounds, evaluate them, and ingest the results
251-
into the generator before optimization begins. The number of sample points is
252-
determined by ``initial_batch_size``.
249+
sample via ``suggest()``. May be set to either:
250+
251+
- a string naming a built-in sampler — currently ``"uniform"`` or
252+
``"latin_hypercube"`` — which libEnsemble instantiates with the VOCS, or
253+
- a pre-constructed sampler instance (any object with a ``suggest()`` method,
254+
typically a ``LibensembleGenerator`` subclass from ``gen_classes.sampling``).
255+
Use this form when you need to pass extra constructor arguments
256+
(``random_seed``, ``max_resource_sets``, ``components``, etc.) or want to
257+
use a custom sampler.
258+
259+
libEnsemble draws ``initial_batch_size`` points from the sampler, evaluates
260+
them, and ingests the results into the generator before optimization begins.
253261
"""
254262

255263
threaded: bool | None = False
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
"""
2+
Tests libEnsemble with Xopt ExpectedImprovementGenerator using a
3+
pre-constructed sampler instance for ``initial_sample_method``.
4+
5+
Companion to ``test_xopt_EI_initial_sample.py``, which uses the string form
6+
(``initial_sample_method="uniform"``). This test instead passes a pre-configured
7+
``LatinHypercubeSample`` instance — exercising the path that lets the user
8+
supply constructor kwargs (here, ``random_seed``) and choose any sampler from
9+
``gen_classes.sampling`` (or a custom one) without going through the string
10+
registry in ``runners.py``.
11+
12+
Execute via one of the following commands (e.g. 4 workers):
13+
mpiexec -np 5 python test_xopt_EI_initial_sample_instance.py
14+
python test_xopt_EI_initial_sample_instance.py -n 4
15+
"""
16+
17+
# Do not change these lines - they are parsed by run-tests.sh
18+
# TESTSUITE_COMMS: local
19+
# TESTSUITE_NPROCS: 4
20+
# TESTSUITE_EXTRA: true
21+
# TESTSUITE_EXCLUDE: true
22+
23+
import numpy as np
24+
from gest_api.vocs import VOCS
25+
from xopt.generators.bayesian.expected_improvement import ExpectedImprovementGenerator
26+
27+
from libensemble import Ensemble
28+
from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f
29+
from libensemble.gen_classes.sampling import LatinHypercubeSample
30+
from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, LibeSpecs, SimSpecs
31+
32+
33+
def xtest_sim(H, persis_info, sim_specs, _):
34+
"""y1 = x2, c1 = x1"""
35+
batch = len(H)
36+
H_o = np.zeros(batch, dtype=sim_specs["out"])
37+
for i in range(batch):
38+
H_o["y1"][i] = H["x2"][i]
39+
H_o["c1"][i] = H["x1"][i]
40+
return H_o, persis_info
41+
42+
43+
if __name__ == "__main__":
44+
45+
batch_size = 4
46+
47+
libE_specs = LibeSpecs(gen_on_manager=True, nworkers=batch_size)
48+
libE_specs.reuse_output_dir = True
49+
50+
vocs = VOCS(
51+
variables={"x1": [0, 1.0], "x2": [0, 10.0]},
52+
objectives={"y1": "MINIMIZE"},
53+
constraints={"c1": ["GREATER_THAN", 0.5]},
54+
constants={"constant1": 1.0},
55+
)
56+
57+
gen = ExpectedImprovementGenerator(vocs=vocs)
58+
59+
# Pre-constructed sampler with a custom random_seed — not reachable via the
60+
# string form, which always instantiates with sampler defaults.
61+
initial_sampler = LatinHypercubeSample(vocs=vocs, random_seed=42)
62+
63+
gen_specs = GenSpecs(
64+
generator=gen,
65+
initial_batch_size=batch_size,
66+
initial_sample_method=initial_sampler,
67+
batch_size=batch_size,
68+
vocs=vocs,
69+
)
70+
71+
sim_specs = SimSpecs(
72+
sim_f=xtest_sim,
73+
vocs=vocs,
74+
)
75+
76+
alloc_specs = AllocSpecs(alloc_f=alloc_f)
77+
exit_criteria = ExitCriteria(sim_max=20)
78+
79+
workflow = Ensemble(
80+
libE_specs=libE_specs,
81+
sim_specs=sim_specs,
82+
alloc_specs=alloc_specs,
83+
gen_specs=gen_specs,
84+
exit_criteria=exit_criteria,
85+
)
86+
87+
H, _, _ = workflow.run()
88+
89+
if workflow.is_manager:
90+
print(f"Completed {len(H)} simulations")
91+
assert len(H) >= 8, f"Expected at least 8 sims, got {len(H)}"
92+
print("Test passed")

libensemble/utils/runners.py

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -159,16 +159,32 @@ def _start_generator_loop(self, tag, Work, H_in):
159159
return self._loop_over_gen(tag, Work, H_in)
160160

161161
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)
162+
"""Create initial sample points using the specified sampling method.
163+
164+
``sample_method`` may be either a string naming a built-in sampler
165+
(instantiated here with the VOCS), or a pre-constructed sampler
166+
instance with a ``suggest()`` method (used directly).
167+
"""
168+
from libensemble.gen_classes.sampling import LatinHypercubeSample, UniformSample
169+
170+
if isinstance(sample_method, str):
171+
samplers = {
172+
"uniform": UniformSample,
173+
"latin_hypercube": LatinHypercubeSample,
174+
}
175+
if sample_method not in samplers:
176+
raise ValueError(
177+
f"Unknown initial_sample_method: {sample_method!r}. "
178+
f"Supported: {list(samplers.keys())}"
179+
)
180+
sampler = samplers[sample_method](vocs=self.specs.get("vocs"))
181+
else:
182+
sampler = sample_method
183+
if not hasattr(sampler, "suggest"):
184+
raise TypeError(
185+
"initial_sample_method must be a string name or an object "
186+
f"with a suggest() method; got {type(sampler).__name__}"
187+
)
172188
return sampler.suggest(num_points)
173189

174190
def _persistent_result(self, calc_in, persis_info, libE_info):

0 commit comments

Comments
 (0)