From c2c024f27d1c6bab465b199b5f78e6409ad927ae Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 2 Oct 2026 09:34:58 -0500 Subject: [PATCH 1/3] rename .claude to .agents. add gpt-sol investigated and created generate-scripts-2 SKILL and set of references. this is first draft --- .agents/skills/generate-scripts-2/SKILL.md | 97 ++++++++++ .../generate-scripts-2/references/aposmm.md | 106 +++++++++++ .../references/canonical-patterns.md | 168 ++++++++++++++++++ .../references/executors-and-files.md | 148 +++++++++++++++ .../references/external-generators.md | 96 ++++++++++ .../references/generator-selection.md | 78 ++++++++ .../references/hpc-resources.md | 110 ++++++++++++ .../references/intake-and-validation.md | 85 +++++++++ .../references/running-and-results.md | 129 ++++++++++++++ .../skills/generate-scripts/SKILL.md | 0 .../generate-scripts/references/aposmm.md | 0 .../references/finding_objectives.md | 0 .../generate-scripts/references/generators.md | 0 .../references/results_metadata.md | 0 14 files changed, 1017 insertions(+) create mode 100644 .agents/skills/generate-scripts-2/SKILL.md create mode 100644 .agents/skills/generate-scripts-2/references/aposmm.md create mode 100644 .agents/skills/generate-scripts-2/references/canonical-patterns.md create mode 100644 .agents/skills/generate-scripts-2/references/executors-and-files.md create mode 100644 .agents/skills/generate-scripts-2/references/external-generators.md create mode 100644 .agents/skills/generate-scripts-2/references/generator-selection.md create mode 100644 .agents/skills/generate-scripts-2/references/hpc-resources.md create mode 100644 .agents/skills/generate-scripts-2/references/intake-and-validation.md create mode 100644 .agents/skills/generate-scripts-2/references/running-and-results.md rename {.claude => .agents}/skills/generate-scripts/SKILL.md (100%) rename {.claude => .agents}/skills/generate-scripts/references/aposmm.md (100%) rename {.claude => .agents}/skills/generate-scripts/references/finding_objectives.md (100%) rename {.claude => .agents}/skills/generate-scripts/references/generators.md (100%) rename {.claude => .agents}/skills/generate-scripts/references/results_metadata.md (100%) diff --git a/.agents/skills/generate-scripts-2/SKILL.md b/.agents/skills/generate-scripts-2/SKILL.md new file mode 100644 index 000000000..de9fde8b6 --- /dev/null +++ b/.agents/skills/generate-scripts-2/SKILL.md @@ -0,0 +1,97 @@ +--- +name: generate-scripts-2 +description: Generate self-contained, modern libEnsemble 2.x workflows for sampling, optimization, external applications, and HPC resources +--- + +Generate runnable libEnsemble 2.x scripts from the user's requirements. Use only the +standardized gest-api/VOCS interfaces described here. Do not depend on access to the +libEnsemble repository and do not emit legacy `libE()`, `gen_f`, `sim_f`, bare-spec +dictionary, or explicit allocation-function patterns. + +## Workflow + +1. Read `references/intake-and-validation.md`. Extract the problem definition, + termination budget, parallelism, generator intent, simulator interface, dependencies, + files, and resources. Ask only questions whose answers materially change the script. + Never invent bounds, objective direction, executable paths, output parsing, or HPC + resource requirements. + +2. Read `references/generator-selection.md`. Preserve a user-specified gest-api generator + and its VOCS. Otherwise choose the least-complex suitable generator, preferring + libEnsemble's built-in sampling, APOSMM, or preloaded-sample classes before optional + ecosystems. Read `references/aposmm.md` or `references/external-generators.md` when + applicable. Check that every non-core package is installed before relying on it. + +3. Read `references/canonical-patterns.md` and generate an `Ensemble`-based script with: + - `VOCS` imported from `gest_api.vocs` + - typed `SimSpecs`, `GenSpecs`, and `LibeSpecs` + - a generator object supplied with `GenSpecs(generator=..., vocs=...)` + - a dict simulator supplied with `SimSpecs(simulator=..., vocs=...)` + - `if __name__ == "__main__":` + - termination criteria passed directly to `ensemble.run(...)` + - manager-only reporting and output saving, including partial results on timeout + +4. For an executable, read `references/executors-and-files.md`. Use `Executor` for a + normal subprocess and `MPIExecutor` only when the application itself needs an MPI or + resource-aware launcher. Keep application launch separate from manager/worker launch. + Use isolated simulation directories for file-producing applications. Never modify an + original user input file; copy or render it into each simulation directory. + +5. For clusters, GPUs, variable task sizes, or scheduler scripts, also read + `references/hpc-resources.md`. Do not guess machine topology, launcher, scheduler + directives, process counts, GPU counts, or platform settings. + +6. Validate the generated files against `references/intake-and-validation.md`. In + particular, verify exact names across VOCS, simulator inputs/returns, generator + mappings, executable registration/submission, parser output, and result analysis. + Run a syntax/import check when tools are available. + +7. Summarize generated files, generator choice, variables/bounds, objectives and + directions, batch size, workers, stopping criteria, dependencies, and application + resources. Identify every placeholder the user must replace. For production artifacts, + include a minimal dependency manifest with tested versions when the user wants one. + +8. Ask before executing a generated workflow. For a fixed local script run + `python script.py`; do not append `-n`, `--comms`, or use `mpirun`/`srun` unless the + script intentionally uses `Ensemble(parse_args=True)` and the user requested that + launch mode. `MPIExecutor` may launch simulation applications with MPI even when the + calling script itself runs with plain Python. + +9. If execution is approved, read `references/running-and-results.md`, run the smallest + useful validation first, fix actionable failures, and report only completed, finite + results. Do not claim a `.npy` result exists unless `save_output()` ran successfully. + +## Non-negotiable defaults + +- Generate modern standardized interfaces only. If the request requires a legacy-only + feature, explain that this skill does not generate it and ask whether a modern design + is acceptable. +- Prefer programmatic `LibeSpecs(nworkers=...)`; use `parse_args=True` only when requested. +- Standardized generators run on manager Worker 0 by default, so all `nworkers` are + normally available for simulations. Do not subtract a generator worker unless setting + `gen_on_worker=True` intentionally. +- Omit `AllocSpecs` unless a documented modern requirement cannot be represented through + `GenSpecs`/`LibeSpecs`. +- Do not set `async_return=True` globally. Choose it only when the generator supports + one-at-a-time feedback after initialization. +- Use `safe_mode=True` for generated production workflows unless a documented requirement + needs protected-field writes. Never return protected History metadata from a simulator. +- Use a reproducible seed where the selected generator supports one. +- Keep examples' numbers and paths out of user scripts unless they match the request. + +## References + +Read only what the request needs; all paths are relative to this skill directory. + +- `references/intake-and-validation.md` — requirements and final checks +- `references/canonical-patterns.md` — complete modern local templates +- `references/generator-selection.md` — generator decision table and batch behavior +- `references/aposmm.md` — standardized APOSMM configuration +- `references/external-generators.md` — Xopt, Optimas, and gpCAM +- `references/executors-and-files.md` — subprocesses, MPI applications, files, failures +- `references/hpc-resources.md` — schedulers, resource sets, GPUs, platform settings +- `references/running-and-results.md` — validation, execution, saving, interpretation + +## User request + +$ARGUMENTS diff --git a/.agents/skills/generate-scripts-2/references/aposmm.md b/.agents/skills/generate-scripts-2/references/aposmm.md new file mode 100644 index 000000000..baf0414fa --- /dev/null +++ b/.agents/skills/generate-scripts-2/references/aposmm.md @@ -0,0 +1,106 @@ +# Standardized APOSMM + +APOSMM runs multiple local optimizations concurrently to discover multiple minima. It is +not the default choice for a single global optimum, constrained optimization, categorical +variables, or multi-objective optimization. + +## SciPy template + +```python +import numpy as np + +from gest_api.vocs import VOCS +from libensemble import Ensemble +from libensemble.specs import GenSpecs, LibeSpecs, SimSpecs + + +def six_hump(inputs: dict, **kwargs) -> dict: + x0, x1 = inputs["x0"], inputs["x1"] + return { + "f": (4 - 2.1 * x0**2 + x0**4 / 3) * x0**2 + + x0 * x1 + + (-4 + 4 * x1**2) * x1**2 + } + + +if __name__ == "__main__": + import libensemble.gen_funcs + + libensemble.gen_funcs.rc.aposmm_optimizers = "scipy" + from libensemble.gen_classes import APOSMM + + vocs = VOCS( + variables={"x0": [-3.0, 3.0], "x1": [-2.0, 2.0]}, + objectives={"f": "MINIMIZE"}, + ) + generator = APOSMM( + vocs, + max_active_runs=4, + initial_sample_size=40, + localopt_method="scipy_Nelder-Mead", + opt_return_codes=[0], + variables_mapping={"x": ["x0", "x1"], "f": ["f"]}, + random_seed=1, + ) + ensemble = Ensemble( + sim_specs=SimSpecs(simulator=six_hump, vocs=vocs), + gen_specs=GenSpecs( + generator=generator, + vocs=vocs, + initial_batch_size=40, + batch_size=4, + ), + libE_specs=LibeSpecs(comms="local", nworkers=4, safe_mode=True), + ) + H, _, _ = ensemble.run(sim_max=500) + + if ensemble.is_manager: + completed_minima = H[ + H["sim_ended"] & H["local_min"] & np.isfinite(H["f"]) + ] + ensemble.save_output("aposmm_results", append_attrs=False) + print(completed_minima[["x0", "x1", "f"]]) +``` + +The standardized APOSMM class currently requires one compatibility setup through the +legacy backend registry: set `libensemble.gen_funcs.rc.aposmm_optimizers` before importing +`APOSMM`. Keep this isolated in the main block; the generated workflow still uses the +standardized generator API. Use `"nlopt"` for an NLopt method and ensure the selected +backend package is installed. + +## Required design choices + +- `max_active_runs`: concurrent local optimizer runs; size it to useful simulation + concurrency rather than blindly copying worker count. +- `initial_sample_size`: evaluated points APOSMM waits for before local optimization. +- `localopt_method`: must match the configured backend and objective information. +- `variables_mapping`: map APOSMM's vector `x` to ordered VOCS variable names and scalar + `f` to the objective. Add mappings required by gradient/residual methods. +- `initial_batch_size`: normally equal to `initial_sample_size` when APOSMM generates the + initial design itself. + +APOSMM can obtain initial data from its own sample, `sample_points`, warm-start History, +or pre-ingested records. Do not feed arbitrary new sample points after local optimization +has begun. + +## Constraints and constants + +Current standardized APOSMM ignores VOCS constraints and constants and warns about them. +Do not present it as a constrained optimizer. Ask the user to select a compatible method +or transform the problem only with explicit approval. + +## Local optimizer guidance + +- `scipy_Nelder-Mead`: derivative-free baseline; SciPy required. +- SciPy gradient methods require the corresponding derivative output/mapping. +- NLopt methods require `nlopt` and method-specific stopping tolerances/return codes. +- PETSc/TAO, DFO-LS, IBCDFO, and external local optimizers require their own packages and + output contracts. + +Do not guess return codes, gradient fields, residual shapes, or tolerances. Confirm them +for the chosen backend/version. + +## Results + +`local_min` marks minima identified by APOSMM. Filter by both `sim_ended` and `local_min`, +then require finite objectives. Generated-but-unevaluated rows are not minima to report. diff --git a/.agents/skills/generate-scripts-2/references/canonical-patterns.md b/.agents/skills/generate-scripts-2/references/canonical-patterns.md new file mode 100644 index 000000000..c1101c2c8 --- /dev/null +++ b/.agents/skills/generate-scripts-2/references/canonical-patterns.md @@ -0,0 +1,168 @@ +# Canonical modern patterns + +Use these complete patterns without repository access. Adapt names and values; do not copy +sample values when the user supplied different requirements. + +## Built-in sampling and Python simulator + +```python +import numpy as np +from gest_api.vocs import VOCS + +from libensemble import Ensemble +from libensemble.gen_classes.sampling import UniformSample +from libensemble.specs import GenSpecs, LibeSpecs, SimSpecs + + +def simulate(inputs: dict, **kwargs) -> dict: + x0 = inputs["x0"] + x1 = inputs["x1"] + return {"f": x0**2 + x1**2} + + +if __name__ == "__main__": + vocs = VOCS( + variables={"x0": [-3.0, 3.0], "x1": [-2.0, 2.0]}, + objectives={"f": "MINIMIZE"}, + ) + + ensemble = Ensemble( + sim_specs=SimSpecs(simulator=simulate, vocs=vocs), + gen_specs=GenSpecs( + generator=UniformSample(vocs, random_seed=1), + vocs=vocs, + initial_batch_size=4, + batch_size=4, + ), + libE_specs=LibeSpecs(comms="local", nworkers=4, safe_mode=True), + ) + + H, _, flag = ensemble.run(sim_max=100) + + if ensemble.is_manager: + completed = H[H["sim_ended"]] + finite = completed[np.isfinite(completed["f"])] + ensemble.save_output("sampling_results", append_attrs=False) + if len(finite): + best = finite[np.argmin(finite["f"])] + print(f"Best: x0={best['x0']}, x1={best['x1']}, f={best['f']}") + if flag != 0: + raise RuntimeError(f"libEnsemble exited with flag {flag}; partial results saved") +``` + +Use `LatinHypercubeSample` from the same module for a space-filling batch. Each separate +LHS `suggest()` call forms a separate Latin hypercube; use one initial batch equal to the +full design size when global stratification matters. + +`initial_batch_size` controls the first request; `batch_size` controls later requests. +Sampling ignores feedback, so either synchronous or asynchronous delivery is acceptable. + +## Preloaded points + +```python +from gest_api.vocs import VOCS + +from libensemble import Ensemble +from libensemble.gen_classes import PreloadedSampleGenerator +from libensemble.specs import GenSpecs, LibeSpecs, SimSpecs + + +def simulate(inputs: dict, **kwargs) -> dict: + return {"f": inputs["x0"] ** 2 + inputs["x1"] ** 2} + + +if __name__ == "__main__": + points = [ + {"x0": -1.0, "x1": 0.5}, + {"x0": 0.0, "x1": 0.0}, + {"x0": 1.0, "x1": 0.5}, + ] + vocs = VOCS( + variables={"x0": [-3.0, 3.0], "x1": [-2.0, 2.0]}, + objectives={"f": "MINIMIZE"}, + ) + + ensemble = Ensemble( + sim_specs=SimSpecs(simulator=simulate, vocs=vocs), + gen_specs=GenSpecs( + generator=PreloadedSampleGenerator(points, vocs=vocs, batch_size=2), + vocs=vocs, + ), + libE_specs=LibeSpecs(comms="local", nworkers=2, safe_mode=True), + ) + H, _, flag = ensemble.run(sim_max=len(points)) + if ensemble.is_manager: + ensemble.save_output("preloaded_results", append_attrs=False) + if flag != 0: + raise RuntimeError(f"libEnsemble exited with flag {flag}; partial results saved") +``` + +`PreloadedSampleGenerator` accepts a list of dictionaries or a NumPy structured array and +returns an empty suggestion after exhaustion. Use it instead of a pre-generated-work +allocator. Ensure every point has the simulator's required variable fields and any VOCS +constants; constants are not guaranteed to be injected by the generator. + +## Custom standardized generator + +```python +import numpy as np +from gest_api import Generator +from gest_api.vocs import VOCS + + +class RandomGenerator(Generator): + def __init__(self, vocs: VOCS, seed: int = 1): + self.rng = np.random.default_rng(seed) + super().__init__(vocs) + + def _validate_vocs(self, vocs: VOCS) -> None: + if not vocs.variables: + raise ValueError("VOCS must define variables") + + def suggest(self, n_trials: int) -> list[dict]: + return [ + { + **{ + name: self.rng.uniform(variable.domain[0], variable.domain[1]) + for name, variable in self.vocs.variables.items() + }, + **self.vocs.constants, + } + for _ in range(n_trials) + ] + + def ingest(self, calc_in: list[dict]) -> None: + pass +``` + +A custom adaptive generator uses `ingest(calc_in)` to update state from completed records. +Do not use this example for discrete/categorical/vector variables without implementing +sampling for their actual VOCS variable classes. + +## VOCS capabilities + +```python +vocs = VOCS( + variables={"x": [0.0, 1.0], "y": [-2.0, 2.0]}, + objectives={"cost": "MINIMIZE"}, + constraints={"temperature": ["LESS_THAN", 100.0]}, + observables=["runtime"], + constants={"material": 2.0}, +) +``` + +The simulator receives constants only when suggestions populate those fields; VOCS alone +may define History fields without assigning their values. Wrap or configure the generator +to add `vocs.constants`, and test them. The simulator must return `cost`, `temperature`, and +`runtime` when required. Confirm the selected generator supports the +specified variable types, multiple objectives, and constraints; declaring them in VOCS +does not make every algorithm support them. + +## Script organization + +Keep a small pure-Python simulator in the calling script. Put substantial parsing, +application control, or domain logic in an existing simulator module when one exists. +Create a new module only when separation is needed. Resolve configurable paths at runtime +from CLI/config/environment or relative to `Path(__file__)`, then convert them to absolute +paths for registration/copying. Output filenames inside simulation directories may be +relative. diff --git a/.agents/skills/generate-scripts-2/references/executors-and-files.md b/.agents/skills/generate-scripts-2/references/executors-and-files.md new file mode 100644 index 000000000..e59e62707 --- /dev/null +++ b/.agents/skills/generate-scripts-2/references/executors-and-files.md @@ -0,0 +1,148 @@ +# Executors and simulation files + +Choose the executor based on the application, independently of the generator. + +- `Executor`: serial/threaded local subprocess; no MPI launcher. +- `MPIExecutor`: an MPI or resource-aware application launched within assigned resources. + +## Serial external application fragment + +Combine this fragment with the complete calling-script pattern in +`canonical-patterns.md`; `vocs` and `gen_specs` come from that pattern. + +```python +from pathlib import Path +import numpy as np + +from libensemble import Ensemble +from libensemble.executors import Executor +from libensemble.executors.executor import TimeoutExpired + + +def run_external(inputs: dict, libE_info: dict, **kwargs) -> dict: + executor = libE_info["executor"] + task = executor.submit( + app_name="my_app", + app_args=f"--x {inputs['x']}", + stdout="app.out", + stderr="app.err", + ) + try: + task.wait(timeout=600) + except TimeoutExpired: + task.kill() + raise + if not task.success: + detail = task.read_stderr() if task.stderr_exists() else "" + raise RuntimeError( + f"Application failed: state={task.state}, rc={task.errcode}: {detail}" + ) + value = float(task.read_stdout().strip()) + if not np.isfinite(value): + raise ValueError(f"Application returned non-finite objective: {value}") + return {"f": value} + + +app_path = Path("/REPLACE/WITH/APP").resolve() +if not app_path.is_file(): + raise FileNotFoundError(app_path) +executor = Executor() +executor.register_app(full_path=str(app_path), app_name="my_app") + +ensemble = Ensemble( + executor=executor, + sim_specs=SimSpecs(simulator=run_external, vocs=vocs), + gen_specs=gen_specs, + libE_specs=LibeSpecs( + comms="local", + nworkers=4, + safe_mode=True, + sim_dirs_make=True, + ensemble_dir_path="ensemble", + ), +) +``` + +`app_args` is split into tokens as an argument string, not interpreted as a shell script. +Do not rely on pipes, redirects, shell expansion, or shell quoting to preserve embedded +whitespace. Put complex or whitespace-bearing values in an input file when possible. +Prefer explicit stdout/stderr filenames and application-specific parsing. +A `wait()` timeout does not kill the process automatically, so kill it before re-raising. +For manager cancellation responsiveness, use `executor.polling_loop(..., +poll_manager=True)` and require its result to represent successful completion. + +A standardized dict simulator cannot directly return a legacy calculation-status value. +Choose and document one failure policy: + +- **Fail fast:** raise as above. With default exception handling, one bad evaluation may + terminate the ensemble. +- **Fault tolerant:** after terminating the task, return NaN objective(s) plus a declared + user status output. Use this only when the generator tolerates non-finite observations, + and exclude failed status/NaN rows during analysis. + +Do not turn a failed process into a plausible objective. For cancellation-responsive +execution, `executor.polling_loop(task, timeout=..., poll_manager=True)` returns a status; +compare it with `WORKER_DONE` from `libensemble.message_numbers` and apply the chosen policy +for every other status. + +## MPI application + +Setup differs only in executor construction and optional submit resources: + +```python +from libensemble.executors import MPIExecutor + +executor = MPIExecutor() +executor.register_app(full_path=str(app_path), app_name="my_mpi_app") + +task = libE_info["executor"].submit( + app_name="my_mpi_app", + app_args="--input input.dat", + stdout="app.out", + stderr="app.err", +) +``` + +When resource management is active, omit `num_procs`, `num_nodes`, and GPU counts to use +the worker's assigned resources. Specify them only when the workflow has a validated fixed +geometry. Relevant options include `num_procs`, `num_nodes`, `procs_per_node`, `num_gpus`, +`auto_assign_gpus`, `match_procs_to_gpus`, and `extra_args`. + +## Simulation directories and inputs + +Concurrent applications writing fixed filenames need isolated directories: + +```python +LibeSpecs( + nworkers=4, + sim_dirs_make=True, + ensemble_dir_path="ensemble", + sim_dir_copy_files=["input.template"], +) +``` + +Other options include `sim_dir_symlink_files`, `sim_input_dir`, and `use_worker_dirs`. +Inside the simulator, the current working directory is already the simulation directory. +Use relative names there for generated input and output. + +If parameters must be written to an input file: + +1. Preserve the user's original file. +2. Copy a template into each simulation directory. +3. Render a new application input there using an already-installed templating library or + a safe application-specific writer. +4. Ensure every marker exactly matches a VOCS input name. + +Jinja2 is optional, not mandatory. If used, declare/check that dependency. Do not perform +fragile global string replacement on a scientific input file. + +## Parsing and safety + +- Define the exact output file/line/key and expected units. +- Verify the task succeeded and the output exists before parsing. +- Reject empty, malformed, non-finite, or stale output. +- Ensure separate tasks cannot read each other's files. +- Avoid `shell=True` and do not concatenate untrusted values into shell commands. +- Match `register_app(app_name=...)` and `submit(app_name=...)` exactly. +- Registered Python scripts may use their interpreter automatically; verify behavior for a + custom interpreter instead of embedding it into application arguments. diff --git a/.agents/skills/generate-scripts-2/references/external-generators.md b/.agents/skills/generate-scripts-2/references/external-generators.md new file mode 100644 index 000000000..2473479bc --- /dev/null +++ b/.agents/skills/generate-scripts-2/references/external-generators.md @@ -0,0 +1,96 @@ +# Optional generator ecosystems + +Preserve an existing Xopt, Optimas, or gpCAM generator and VOCS unless the user asks to +change algorithms. These packages are not guaranteed by a base libEnsemble installation; +check their imports and version-specific constructor APIs. + +## Xopt + +### Expected improvement + +```python +from xopt.generators.bayesian.expected_improvement import ExpectedImprovementGenerator + +generator = ExpectedImprovementGenerator(vocs=vocs) +gen_specs = GenSpecs( + generator=generator, + vocs=vocs, + initial_batch_size=8, + initial_sample_method="uniform", + batch_size=4, +) +``` + +Expected improvement requires evaluated training data. Use exactly one of: + +- libEnsemble warm-up through `initial_sample_method` and `initial_batch_size`; +- `generator.ingest([...])` with evaluated dictionaries before the run; +- valid warm-start History (`H0`). + +`initial_sample_method` also accepts `"latin_hypercube"` or a sampler object. Do not copy +an explicit allocator from transitional tests. Keep `async_return=False` unless the +installed Xopt generator is known to tolerate irregular one-at-a-time updates. + +Xopt EI can use VOCS constraints and constants when supported by the selected generator. +The simulator must return every objective and constraint named in VOCS. + +### Sequential Nelder-Mead + +```python +from xopt.generators.sequential.neldermead import NelderMeadGenerator + +generator = NelderMeadGenerator(vocs=vocs) +gen_specs = GenSpecs(generator=generator, vocs=vocs, batch_size=1) +``` + +It needs an initialized simplex/evaluated seed records. Treat its suggestions as +sequential; extra workers may still be useful for an initial design or other workflow +stages, but do not request parallel Nelder-Mead suggestions without support from the +installed version. + +## Optimas + +```python +from optimas.generators import AxSingleFidelityGenerator + +generator = AxSingleFidelityGenerator(vocs=vocs) +gen_specs = GenSpecs(generator=generator, vocs=vocs, batch_size=4) +``` + +Common choices: + +- `GridSamplingGenerator(vocs=vocs, n_steps=[...])` for a Cartesian grid. The `n_steps` + order must match VOCS variable order. +- `AxSingleFidelityGenerator(vocs=vocs)` for standard Ax optimization. +- `AxMultiFidelityGenerator(vocs=vocs)` when VOCS includes the expected fidelity variable. +- `AxMultitaskGenerator` with explicit high-/low-fidelity `Task` configurations for a + categorical task variable. + +Confirm constructors against the installed Optimas version. Representative workflows use +fixed batches. Multitask restart can pass prior History as `H0`, provided it contains all +required fields. + +## gpCAM + +```python +from libensemble.gen_classes.gpCAM import GP_CAM + +generator = GP_CAM(vocs, ask_max_iter=10, random_seed=1) +gen_specs = GenSpecs(generator=generator, vocs=vocs, batch_size=4) +``` + +Use `GP_CAM` for total-correlation acquisition or `GP_CAM_Covar` for posterior-covariance +sampling. Both generate an initial random batch and then train/update a GP. They are +batch-oriented; model fitting may dominate cheap simulations. Install a compatible +`gpcam` package before importing the module. Verify objective mapping and do not assume +constraint support. + +## Selection safeguards + +- Do not import Xopt's VOCS; use `gest_api.vocs.VOCS`. +- Do not replace a user's generator based solely on its name or apparent sampling behavior. +- Do not assume optional packages, GPU support, or optimizer backends exist. +- Do not claim support for constraints, categorical variables, multiple objectives, + asynchronous updates, or restart unless verified for the concrete generator version. +- Initial evaluated records must contain variables plus all outputs consumed by the + generator, not merely candidate coordinates. diff --git a/.agents/skills/generate-scripts-2/references/generator-selection.md b/.agents/skills/generate-scripts-2/references/generator-selection.md new file mode 100644 index 000000000..b227a44b6 --- /dev/null +++ b/.agents/skills/generate-scripts-2/references/generator-selection.md @@ -0,0 +1,78 @@ +# Generator selection + +Preserve a named user choice. Otherwise select by algorithmic need, package availability, +and simulator cost. Do not translate an existing Xopt or Optimas workflow to another +algorithm merely to simplify it. + +| Goal | Preferred generator | Package | Key behavior | +|---|---|---|---| +| Independent random sampling | `UniformSample` | libEnsemble | No evaluated seed data; reproducible seed | +| Space-filling bounded design | `LatinHypercubeSample` | libEnsemble | Each suggestion is a separate LHS design | +| Evaluate an existing design | `PreloadedSampleGenerator` | libEnsemble | List-of-dicts or structured array; stops on exhaustion | +| Find multiple local minima | `APOSMM` | libEnsemble + local optimizer | Concurrent local runs after initial evaluations | +| Bayesian optimization | Xopt `ExpectedImprovementGenerator` | Xopt | Requires evaluated initial data; supports constrained EI | +| Sequential simplex search | Xopt `NelderMeadGenerator` | Xopt | Treat as sequential with `batch_size=1` | +| Cartesian parameter scan | Optimas `GridSamplingGenerator` | Optimas | Generates a fixed grid | +| Ax optimization/fidelity/tasks | Optimas Ax generators | Optimas/Ax | Specialized single-, multi-fidelity, and multitask designs | +| GP adaptive sampling | `GP_CAM` / `GP_CAM_Covar` | gpCAM | Batch surrogate training/acquisition | +| Custom policy | subclass `gest_api.Generator` | gest-api | Implement `suggest` and `ingest` | + +## Default decisions + +- Prefer `UniformSample` for an unspecified sampling request. +- Prefer LHS only when the user wants a space-filling design; generate its complete design + in one request when stratification across the whole sample matters. +- Ask before choosing an optimizer when the user says only "optimize." Determine whether + they need one optimum, multiple minima, constraints, multiple objectives, noisy outputs, + derivatives, fidelities, or Bayesian uncertainty. +- Prefer built-in APOSMM for multiple local minima. Do not use a legacy APOSMM function. +- Use `PreloadedSampleGenerator` for user-supplied points; do not pass points as allocator + state. + +## `GenSpecs` controls + +```python +gen_specs = GenSpecs( + generator=generator, + vocs=vocs, + initial_batch_size=8, + batch_size=4, +) +``` + +- `initial_batch_size`: first number requested; defaults to `batch_size` when zero. +- `batch_size`: normal request size. +- `initial_sample_method`: `"uniform"`, `"latin_hypercube"`, or a sampler object. It + creates evaluated warm-up data before the main generator is asked for points. +- `async_return`: default `False`. Set `True` only if the generator can ingest one completed + result at a time after initialization and faster adaptation outweighs batching. + +Existing `H0` can warm-start standardized generators when it contains all required VOCS +fields. For a direct generator API, evaluated records can instead be passed to +`generator.ingest(list_of_dicts)` before constructing/running the ensemble. When VOCS has +constants, verify the selected generator emits their values; wrap suggestions to merge +`vocs.constants` when it does not. + +## Placement and concurrency + +Standardized generators run as a persistent manager-side thread by default. All +`LibeSpecs(nworkers=N)` workers are therefore normally simulation workers. Set +`gen_on_worker=True` only for a specific reason; that consumes a worker. + +Sequential generator logic does not automatically imply `nworkers=1`: initial designs or +queued evaluations may still run concurrently. Set `batch_size=1` where algorithmically +required and choose workers according to useful simulator concurrency. + +## Compatibility questions + +Before emitting a script, verify that the generator supports: + +- continuous versus discrete/categorical/vector variables; +- objective count and MINIMIZE/MAXIMIZE direction; +- constraints and constants; +- gradients or vector residuals if required; +- asynchronous or irregular feedback; +- restart data and cancellation behavior. + +VOCS describes a problem but does not guarantee that a selected generator implements all +of these features. diff --git a/.agents/skills/generate-scripts-2/references/hpc-resources.md b/.agents/skills/generate-scripts-2/references/hpc-resources.md new file mode 100644 index 000000000..220491aa3 --- /dev/null +++ b/.agents/skills/generate-scripts-2/references/hpc-resources.md @@ -0,0 +1,110 @@ +# HPC resources and launch modes + +The batch scheduler, libEnsemble manager/worker transport, and simulation application +launcher are separate layers. Do not conflate them. + +## Default cluster pattern + +1. A Slurm/PBS/LSF script requests an allocation. +2. The calling script starts once, commonly with plain `python workflow.py` and local + workers configured by `LibeSpecs(nworkers=N)`. +3. `MPIExecutor` launches each simulation inside resources assigned to its worker. + +Do not launch the calling script with `mpirun`, `srun`, or `mpiexec` merely because the +simulation application uses MPI. Distributed `mpi4py` manager/workers are a distinct mode +and must be explicitly requested and configured. + +## Resource sets + +```python +libE_specs = LibeSpecs( + comms="local", + nworkers=4, + num_resource_sets=4, + safe_mode=True, + sim_dirs_make=True, +) +``` + +A resource set is libEnsemble's schedulable partition of detected resources. If +`num_resource_sets` is omitted, resources are normally divided by workers. Standardized +generators run on the manager by default, so do not subtract one worker/resource set unless +`gen_on_worker=True`. + +Prefer scheduler/platform auto-detection. Use an override only from known machine facts: + +```python +resource_info={ + "cores_on_node": (physical_cores, logical_cores), + "gpus_on_node": gpu_count, +} +``` + +Never invent these values. `disable_resource_manager=True` disables resource detection and +assignment and should not be boilerplate. + +## MPIExecutor submission + +Normally let assigned resources determine geometry: + +```python +task = executor.submit(app_name="solver", app_args="--case input.dat") +``` + +For a validated fixed request: + +```python +task = executor.submit( + app_name="solver", + num_procs=8, + num_nodes=2, + procs_per_node=4, + num_gpus=2, + auto_assign_gpus=True, +) +``` + +The request must fit resources allocated to that simulation. Avoid hard-coded node names or +machinefiles unless the deployment specifically requires them. + +## Variable-size tasks + +Resource-aware standardized generator classes may emit reserved resource request fields +such as `num_procs`, `num_gpus`, or `resource_sets`. Prefer an existing tested class for +this behavior. A custom pure gest-api generator must deliberately define and map these +fields; do not add them as ordinary VOCS objectives or overwrite unrelated protected +History metadata. + +## Platform and runner settings + +`MPIExecutor()` can auto-detect an MPI runner. A known platform may instead provide +`platform_specs`, or `MPIExecutor(custom_info={"mpi_runner": "srun"})` may select a runner. +Valid launch behavior is system-specific; confirm it with site documentation. + +Do not guess: + +- scheduler account, queue/partition, walltime, node count; +- cores, hardware threads, GPUs, tiles, or affinity; +- MPI implementation/runner and runner-specific flags; +- whether manager/worker processes need dedicated nodes; +- environment modules or activation commands. + +## Scheduler script guidance + +Generate a scheduler script only when requested and machine facts are supplied. Request +nodes/resources in scheduler directives, then put per-simulation process/GPU geometry in +libEnsemble resource requests or `MPIExecutor.submit()`. On systems with nested job steps, +overly restrictive per-task scheduler directives can prevent child launches. + +If the user requests CLI-selectable worker counts, construct `Ensemble(parse_args=True)` +and show the matching invocation. Otherwise put `nworkers` in `LibeSpecs` and invoke the +script without `-n` or `--comms`. + +## GPU checks + +- Confirm whether one GPU maps to one rank or several ranks. +- Confirm whether the application uses visible-device environment variables or launcher + GPU flags. +- Use `auto_assign_gpus`/`match_procs_to_gpus` only with compatible runner behavior. +- Ensure aggregate concurrent requests fit the allocation. +- Keep CPU-only and GPU task requirements explicit in heterogeneous workflows. diff --git a/.agents/skills/generate-scripts-2/references/intake-and-validation.md b/.agents/skills/generate-scripts-2/references/intake-and-validation.md new file mode 100644 index 000000000..bf6a6ab9b --- /dev/null +++ b/.agents/skills/generate-scripts-2/references/intake-and-validation.md @@ -0,0 +1,85 @@ +# Intake and validation + +## Ask for missing requirements + +Ask only for information that cannot be inferred safely. + +### Problem + +- Variable names, types, shapes, and bounds or categories +- Objective/observable/constraint names and objective directions +- Constraint relation and threshold +- Constants passed to every simulation +- Sampling, one optimum, multiple minima, Bayesian optimization, grid scan, or preloaded points +- Seed/restart data and reproducibility seed + +### Execution + +- Python callable or external executable +- Executable path and whether it is serial, threaded, GPU-enabled, or MPI +- Exact command-line arguments, input files, and output parser +- Processes, nodes, cores, GPUs, and timeout per simulation +- Files/directories to copy or symlink and desired output directory +- Local machine or scheduler/platform details + +### Ensemble + +- `nworkers` (default to 4 only for a generic local workflow) +- Generator batch size and whether partial asynchronous feedback is supported +- At least one stopping criterion: `sim_max`, `gen_max`, `wallclock_max`, or `stop_val` +- Whether to save History and the output basename +- Whether the script should use fixed settings or `Ensemble(parse_args=True)` + +## Name contract + +Treat VOCS as the schema shared by generation, simulation, and results. + +- Every VOCS variable read by the simulator must use the exact same spelling and case. +- A dict simulator must return every objective and constraint needed by the generator. + Validate required keys, numeric types/shapes, and finiteness before returning; a missing key + may otherwise remain a plausible zero in History. +- Returned objective, observable, and constraint values must be scalar numeric values unless + the VOCS explicitly defines another dtype/shape. +- VOCS constants are not necessarily populated by every generator. Ensure every suggestion + includes the declared constant values (or wrap the generator to add them), then verify the + simulator receives them. Do not generate constants as variables. +- `MINIMIZE` and `MAXIMIZE` are not interchangeable. +- For adapter-based generators, verify every `variables_mapping` target and order. +- Avoid protected History names such as `sim_id`, `sim_started`, `sim_ended`, `sim_worker`, + `gen_worker`, `gen_informed`, timing fields, and `kill_sent` for user outputs. + +## Dependency check + +The base workflow requires `libensemble`, `gest-api`, and NumPy. Never assume optional +packages are installed. Check imports before generation or clearly provide installation +requirements for Xopt, Optimas, gpCAM, SciPy/NLopt optimizer backends, Jinja2, or application +libraries. Do not silently replace a requested generator because its package is missing. + +## Final checklist + +- Script has an `if __name__ == "__main__":` guard. +- Uses `Ensemble`, typed specs, VOCS, `generator=`, and `simulator=`. +- No legacy `libE()`, `gen_f`, `sim_f`, bare spec dictionaries, or custom allocator. +- Bounds, categories, dimensions, constants, constraints, and objective directions match. +- Generator can handle the requested variable types and constraints. +- `initial_batch_size`, initial sampling, and seed data satisfy generator requirements. +- `batch_size <= nworkers` unless queued generation is intentional. +- `async_return` is omitted unless explicitly justified. +- At least one run stopping criterion is present. +- Fixed-worker scripts do not advertise `-n` or `--comms` CLI arguments. +- The registered `app_name` exactly matches `submit(app_name=...)`. +- Executable and copied input paths exist or are conspicuous placeholders. +- External app arguments and parser agree with the application's real interface. +- Every simulation has isolated files when concurrent tasks write fixed filenames. +- Timeout handling terminates the application; failures are not reported as valid minima. +- MPI process/GPU requests fit each worker's assigned resource sets. +- Postprocessing runs only on the manager and filters `sim_ended` plus finite values. +- `save_output()` is present before promising a saved History file, and partial results are + saved before raising/reporting a timeout or nonzero exit flag. +- Syntax and imports are checked when an environment is available. + +## Placeholder policy + +Use loud placeholders such as `Path("/REPLACE/WITH/APP")` or raise a clear error rather +than inventing an executable path, parser, scheduler account, or resource count. Summarize +all placeholders after generating files. diff --git a/.agents/skills/generate-scripts-2/references/running-and-results.md b/.agents/skills/generate-scripts-2/references/running-and-results.md new file mode 100644 index 000000000..2e98278d9 --- /dev/null +++ b/.agents/skills/generate-scripts-2/references/running-and-results.md @@ -0,0 +1,129 @@ +# Running and results + +## Preflight + +Before running: + +1. Compile generated Python files (`python -m py_compile ...`). +2. Import-check optional packages. +3. Verify executable and input paths. +4. Use a tiny budget or dry-run mode when the simulator/application supports it. +5. Confirm the output directory will not overwrite valued data. Set + `reuse_output_dir=True` only intentionally. + +`Ensemble.ready()` returns `(ready: bool, issues: list[str])`, not a Boolean. It validates +configured state, including an already-known exit criterion, but it cannot inspect future +arguments to `run(sim_max=...)`. Do not write `if ensemble.ready():`, and do not manipulate +private exit-criteria attributes merely to call it. For ordinary generated scripts, rely +on typed-spec validation plus explicit `run(...)` criteria; use `ready()` only when the +workflow already stores a public, non-deprecated criterion supported by its installed +version. + +## Execution + +For fixed local workers: + +```text +python workflow.py +``` + +Only use `-n`/`--comms` when the script deliberately uses `Ensemble(parse_args=True)`. +Only use MPI to start manager/workers when distributed MPI communications were requested; +this is separate from MPIExecutor launching simulation applications. + +`run()` accepts `sim_max`, `gen_max`, `wallclock_max`, and `stop_val=(field, value)` and +returns `(H, persis_info, exit_flag)`. `stop_val` is a lower-threshold check (`field <= +value`); it does not reverse for a MAXIMIZE objective. Use a transformed metric or another +criterion for maximization. Prefer a hard wall-clock limit for expensive production runs +in addition to an evaluation budget. + +Exit flags commonly mean: + +- `0`: normal completion +- `1`: exception +- `2`: manager wall-clock timeout +- `3`: process outside the MPI communicator + +Treat nonzero flags as non-success unless the workflow intentionally expects them. + +## Checkpointing and restart + +For expensive runs, configure automatic History snapshots in `LibeSpecs`: + +```python +LibeSpecs( + save_every_k_sims=100, + save_H_on_completion=True, + save_H_with_date=True, + H_file_prefix="campaign_history", +) +``` + +Use unique run directories by default. Set `reuse_output_dir=True` only for a deliberate +resume. Restart with `H0` only after validating VOCS names/dtypes, required outputs, and +generator/package versions. History restart does not necessarily restore every third-party +generator's internal model; use its supported state restoration when required. Keep +checkpoint files from a timed-out run before returning a nonzero process status. + +## Result filtering + +History can include generated but unevaluated rows. Always begin with: + +```python +completed = H[H["sim_ended"]] +``` + +Then filter for finite objective values: + +```python +import numpy as np +valid = completed[np.isfinite(completed["f"])] +``` + +For multiple required outputs, combine finiteness masks. If the simulator returns a +user-defined status field, require its success value too. A row can have `sim_ended=True` +yet contain NaN from an application/parser failure, so `sim_ended` alone does not establish +a valid objective. + +Use `np.argmin` for MINIMIZE and `np.argmax` for MAXIMIZE. For APOSMM minima: + +```python +minima = H[H["sim_ended"] & H["local_min"] & np.isfinite(H["f"])] +``` + +For multiple objectives, do not reduce to a single "best" row without a user-specified +scalarization or Pareto analysis. + +## Saving + +```python +if ensemble.is_manager: + ensemble.save_output("run_results", append_attrs=False) +``` + +`save_output` saves History and persistent information. With `append_attrs=False`, the +provided History basename is preserved; default attribute suffixes otherwise alter it. +Guard custom reporting, plots, and extra writes with `ensemble.is_manager`. + +When loading a saved NumPy History: + +```python +H = np.load("run_results.npy", allow_pickle=False) +``` + +Use the actual filename produced by the installed version/options. Do not claim output was +saved if execution failed before `save_output()`. + +## Reporting + +Report: + +- exit flag and completed/valid evaluation counts; +- objective names and directions; +- best valid point for a scalar objective, or APOSMM minima count/locations; +- failed/non-finite count; +- output path actually written; +- wall-clock timeout or generator exhaustion when relevant. + +Never interpret default zeros in unevaluated rows as results. Never suppress application +failures merely to produce a best value. diff --git a/.claude/skills/generate-scripts/SKILL.md b/.agents/skills/generate-scripts/SKILL.md similarity index 100% rename from .claude/skills/generate-scripts/SKILL.md rename to .agents/skills/generate-scripts/SKILL.md diff --git a/.claude/skills/generate-scripts/references/aposmm.md b/.agents/skills/generate-scripts/references/aposmm.md similarity index 100% rename from .claude/skills/generate-scripts/references/aposmm.md rename to .agents/skills/generate-scripts/references/aposmm.md diff --git a/.claude/skills/generate-scripts/references/finding_objectives.md b/.agents/skills/generate-scripts/references/finding_objectives.md similarity index 100% rename from .claude/skills/generate-scripts/references/finding_objectives.md rename to .agents/skills/generate-scripts/references/finding_objectives.md diff --git a/.claude/skills/generate-scripts/references/generators.md b/.agents/skills/generate-scripts/references/generators.md similarity index 100% rename from .claude/skills/generate-scripts/references/generators.md rename to .agents/skills/generate-scripts/references/generators.md diff --git a/.claude/skills/generate-scripts/references/results_metadata.md b/.agents/skills/generate-scripts/references/results_metadata.md similarity index 100% rename from .claude/skills/generate-scripts/references/results_metadata.md rename to .agents/skills/generate-scripts/references/results_metadata.md From c32297862a8d2fd6049e3178751eded2e9a5bba6 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 2 Oct 2026 11:44:38 -0500 Subject: [PATCH 2/3] additions to new SKILL and references based on critique of subagents attempts at applying the new skill. Fix a bug where save_output() unintionally strips all prefixes for a specified name that contains the word "test" --- .agents/skills/generate-scripts-2/SKILL.md | 15 +++++++--- .../references/canonical-patterns.md | 19 +++++++++--- .../references/executors-and-files.md | 24 +++++++++++---- .../references/hpc-resources.md | 11 ++++++- .../references/intake-and-validation.md | 17 +++++++++-- .../references/running-and-results.md | 15 +++++++--- docs/conf.py | 23 +++++++------- libensemble/ensemble.py | 21 ++++++------- libensemble/tests/unit_tests/test_ensemble.py | 30 +++++++++++++++++++ libensemble/tools/tools.py | 7 +++-- 10 files changed, 134 insertions(+), 48 deletions(-) diff --git a/.agents/skills/generate-scripts-2/SKILL.md b/.agents/skills/generate-scripts-2/SKILL.md index de9fde8b6..4e0f11ce9 100644 --- a/.agents/skills/generate-scripts-2/SKILL.md +++ b/.agents/skills/generate-scripts-2/SKILL.md @@ -3,8 +3,10 @@ name: generate-scripts-2 description: Generate self-contained, modern libEnsemble 2.x workflows for sampling, optimization, external applications, and HPC resources --- -Generate runnable libEnsemble 2.x scripts from the user's requirements. Use only the -standardized gest-api/VOCS interfaces described here. Do not depend on access to the +Generate runnable libEnsemble 2.x scripts from the user's requirements. Resolve every +`references/...` path relative to this loaded `SKILL.md`; never assume the skill lives at a +specific `.claude`, `.agents`, or repository path. Use only the standardized gest-api/VOCS +interfaces described here. Do not depend on access to the libEnsemble repository and do not emit legacy `libE()`, `gen_f`, `sim_f`, bare-spec dictionary, or explicit allocation-function patterns. @@ -14,7 +16,8 @@ dictionary, or explicit allocation-function patterns. termination budget, parallelism, generator intent, simulator interface, dependencies, files, and resources. Ask only questions whose answers materially change the script. Never invent bounds, objective direction, executable paths, output parsing, or HPC - resource requirements. + resource requirements. Missing values block execution, not necessarily generation: + produce a clearly marked scaffold when its structure is still useful and safe. 2. Read `references/generator-selection.md`. Preserve a user-specified gest-api generator and its VOCS. Otherwise choose the least-complex suitable generator, preferring @@ -44,7 +47,9 @@ dictionary, or explicit allocation-function patterns. 6. Validate the generated files against `references/intake-and-validation.md`. In particular, verify exact names across VOCS, simulator inputs/returns, generator mappings, executable registration/submission, parser output, and result analysis. - Run a syntax/import check when tools are available. + Run a syntax/import check when tools are available. For built-in adapter generators, + also reject ambiguous field mappings such as a scalar VOCS variable named `x` in a + multi-variable problem unless an explicit tested mapping resolves the collision. 7. Summarize generated files, generator choice, variables/bounds, objectives and directions, batch size, workers, stopping criteria, dependencies, and application @@ -60,6 +65,8 @@ dictionary, or explicit allocation-function patterns. 9. If execution is approved, read `references/running-and-results.md`, run the smallest useful validation first, fix actionable failures, and report only completed, finite results. Do not claim a `.npy` result exists unless `save_output()` ran successfully. + Treat an exception raised from `ensemble.run()` separately: manager-only code after the + call will not execute, though libEnsemble may write its own abort checkpoint. ## Non-negotiable defaults diff --git a/.agents/skills/generate-scripts-2/references/canonical-patterns.md b/.agents/skills/generate-scripts-2/references/canonical-patterns.md index c1101c2c8..9f2c949e6 100644 --- a/.agents/skills/generate-scripts-2/references/canonical-patterns.md +++ b/.agents/skills/generate-scripts-2/references/canonical-patterns.md @@ -1,7 +1,11 @@ # Canonical modern patterns Use these complete patterns without repository access. Adapt names and values; do not copy -sample values when the user supplied different requirements. +sample values when the user supplied different requirements. With built-in adapter +samplers, avoid a scalar VOCS variable named `x` when other variables are present: internal +vector field `x` is automatically mapped to all variables and can collide with it. Rename +that field (for example `x0`) and disclose the change; do not substitute an untested custom +generator just to preserve the ambiguous name. ## Built-in sampling and Python simulator @@ -42,7 +46,8 @@ if __name__ == "__main__": if ensemble.is_manager: completed = H[H["sim_ended"]] finite = completed[np.isfinite(completed["f"])] - ensemble.save_output("sampling_results", append_attrs=False) + history_path = ensemble.save_output("sampling_results", append_attrs=False) + print(f"Saved History to {history_path}") if len(finite): best = finite[np.argmin(finite["f"])] print(f"Best: x0={best['x0']}, x1={best['x1']}, f={best['f']}") @@ -92,7 +97,8 @@ if __name__ == "__main__": ) H, _, flag = ensemble.run(sim_max=len(points)) if ensemble.is_manager: - ensemble.save_output("preloaded_results", append_attrs=False) + history_path = ensemble.save_output("preloaded_results", append_attrs=False) + print(f"Saved History to {history_path}") if flag != 0: raise RuntimeError(f"libEnsemble exited with flag {flag}; partial results saved") ``` @@ -112,6 +118,7 @@ from gest_api.vocs import VOCS class RandomGenerator(Generator): def __init__(self, vocs: VOCS, seed: int = 1): + self.vocs = vocs self.rng = np.random.default_rng(seed) super().__init__(vocs) @@ -135,6 +142,7 @@ class RandomGenerator(Generator): pass ``` +This is a generator fragment to combine with the complete calling-script pattern above. A custom adaptive generator uses `ingest(calc_in)` to update state from completed records. Do not use this example for discrete/categorical/vector variables without implementing sampling for their actual VOCS variable classes. @@ -162,7 +170,10 @@ does not make every algorithm support them. Keep a small pure-Python simulator in the calling script. Put substantial parsing, application control, or domain logic in an existing simulator module when one exists. -Create a new module only when separation is needed. Resolve configurable paths at runtime +Create a new module only when separation is needed. When rerunning in the same location, +use a new `ensemble_dir_path`, remove/archive the old workflow directory with user approval, +or intentionally set `reuse_output_dir=True`; a nonempty existing ensemble directory causes +startup failure by design. Resolve configurable paths at runtime from CLI/config/environment or relative to `Path(__file__)`, then convert them to absolute paths for registration/copying. Output filenames inside simulation directories may be relative. diff --git a/.agents/skills/generate-scripts-2/references/executors-and-files.md b/.agents/skills/generate-scripts-2/references/executors-and-files.md index e59e62707..6a651340c 100644 --- a/.agents/skills/generate-scripts-2/references/executors-and-files.md +++ b/.agents/skills/generate-scripts-2/references/executors-and-files.md @@ -63,10 +63,20 @@ ensemble = Ensemble( ) ``` -`app_args` is split into tokens as an argument string, not interpreted as a shell script. -Do not rely on pipes, redirects, shell expansion, or shell quoting to preserve embedded -whitespace. Put complex or whitespace-bearing values in an input file when possible. -Prefer explicit stdout/stderr filenames and application-specific parsing. +`app_args` is tokenized with plain whitespace splitting (`app_args.split()`). Quotes are +passed as literal characters; they neither group tokens nor preserve embedded whitespace. +Do not put a `sh -c`/`bash -c` program, pipes, redirects, shell expansion, or quoted values +in `app_args`. Instead use one of these safe designs: + +1. register the real application and pass only whitespace-free option/value tokens; +2. write complex values to an input file in the simulation directory; or +3. if the user explicitly requires shell syntax, create/register a separate wrapper script + whose positional arguments are simple tokens. Do not claim the exact inline shell + invocation can be represented when it cannot. + +Before emitting a workflow, compute the expected `app_args.split()` token list and verify it +matches the target application's argv. Prefer explicit stdout/stderr filenames and +application-specific parsing. A `wait()` timeout does not kill the process automatically, so kill it before re-raising. For manager cancellation responsiveness, use `executor.polling_loop(..., poll_manager=True)` and require its result to represent successful completion. @@ -75,11 +85,15 @@ A standardized dict simulator cannot directly return a legacy calculation-status Choose and document one failure policy: - **Fail fast:** raise as above. With default exception handling, one bad evaluation may - terminate the ensemble. + terminate the ensemble before `run()` returns, so manager-side `save_output()` after + `run()` will not execute. Keep `save_H_and_persis_on_abort=True` (the default) for an + automatic abort checkpoint and do not promise the requested save basename on this path. - **Fault tolerant:** after terminating the task, return NaN objective(s) plus a declared user status output. Use this only when the generator tolerates non-finite observations, and exclude failed status/NaN rows during analysis. +Do not wrap `ensemble.run()` in `finally: ensemble.save_output(...)` unless verified that +History was assigned; `Ensemble.run()` assigns `ensemble.H` only after `libE()` returns. Do not turn a failed process into a plausible objective. For cancellation-responsive execution, `executor.polling_loop(task, timeout=..., poll_manager=True)` returns a status; compare it with `WORKER_DONE` from `libensemble.message_numbers` and apply the chosen policy diff --git a/.agents/skills/generate-scripts-2/references/hpc-resources.md b/.agents/skills/generate-scripts-2/references/hpc-resources.md index 220491aa3..de44d58b0 100644 --- a/.agents/skills/generate-scripts-2/references/hpc-resources.md +++ b/.agents/skills/generate-scripts-2/references/hpc-resources.md @@ -81,6 +81,13 @@ History metadata. `platform_specs`, or `MPIExecutor(custom_info={"mpi_runner": "srun"})` may select a runner. Valid launch behavior is system-specific; confirm it with site documentation. +Unknown site details generally block execution and a finalized scheduler script, not a +useful Python scaffold. When the solver interface, parser, or stopping criterion is unknown, +produce a clearly non-runnable scaffold with loud placeholders and list those blockers. +Let `MPIExecutor()` and resource detection remain automatic rather than inventing a runner +or topology. Omit explicit process/GPU geometry until it is validated. Generate the Slurm +script separately only after scheduler-specific values are known. + Do not guess: - scheduler account, queue/partition, walltime, node count; @@ -91,7 +98,9 @@ Do not guess: ## Scheduler script guidance -Generate a scheduler script only when requested and machine facts are supplied. Request +Generate a finalized scheduler script only when requested and machine facts are supplied. +A visibly incomplete template is acceptable if the user requested planning, provided every +unknown directive/module is a loud placeholder and it is labeled non-runnable. Request nodes/resources in scheduler directives, then put per-simulation process/GPU geometry in libEnsemble resource requests or `MPIExecutor.submit()`. On systems with nested job steps, overly restrictive per-task scheduler directives can prevent child launches. diff --git a/.agents/skills/generate-scripts-2/references/intake-and-validation.md b/.agents/skills/generate-scripts-2/references/intake-and-validation.md index bf6a6ab9b..bd04024bd 100644 --- a/.agents/skills/generate-scripts-2/references/intake-and-validation.md +++ b/.agents/skills/generate-scripts-2/references/intake-and-validation.md @@ -44,6 +44,13 @@ Treat VOCS as the schema shared by generation, simulation, and results. includes the declared constant values (or wrap the generator to add them), then verify the simulator receives them. Do not generate constants as variables. - `MINIMIZE` and `MAXIMIZE` are not interchangeable. +- For `LibensembleGenerator` adapters (including built-in `UniformSample` and + `LatinHypercubeSample`), internal `x` normally maps to all VOCS variables. If a + multi-variable VOCS itself contains a scalar variable named `x`, automatic mapping can + collide with that field and produce shape errors. Rename that History/VOCS field (for + example `x` to `x0`) and explicitly tell the user. Do not invent an ad hoc pure generator + merely to preserve the name, and do not assume `variables_mapping` can make one History + field simultaneously scalar and vector. Never emit the ambiguous case unchecked. - For adapter-based generators, verify every `variables_mapping` target and order. - Avoid protected History names such as `sim_id`, `sim_started`, `sim_ended`, `sim_worker`, `gen_worker`, `gen_informed`, timing fields, and `kill_sent` for user outputs. @@ -69,13 +76,17 @@ libraries. Do not silently replace a requested generator because its package is - Fixed-worker scripts do not advertise `-n` or `--comms` CLI arguments. - The registered `app_name` exactly matches `submit(app_name=...)`. - Executable and copied input paths exist or are conspicuous placeholders. -- External app arguments and parser agree with the application's real interface. +- External app arguments and parser agree with the application's real interface. Validate + the exact token list produced by plain whitespace splitting; quotes in `app_args` are not + removed or honored. - Every simulation has isolated files when concurrent tasks write fixed filenames. - Timeout handling terminates the application; failures are not reported as valid minima. - MPI process/GPU requests fit each worker's assigned resource sets. - Postprocessing runs only on the manager and filters `sim_ended` plus finite values. -- `save_output()` is present before promising a saved History file, and partial results are - saved before raising/reporting a timeout or nonzero exit flag. +- `save_output()` is present before promising a saved History file. For a returned nonzero + exit flag, save before raising/reporting it. For exceptions raised inside `run()`, code + after `run()` cannot save; rely on `save_H_and_persis_on_abort=True` or choose a compatible + fault-tolerant simulator policy, and describe the resulting filename behavior accurately. - Syntax and imports are checked when an environment is available. ## Placeholder policy diff --git a/.agents/skills/generate-scripts-2/references/running-and-results.md b/.agents/skills/generate-scripts-2/references/running-and-results.md index 2e98278d9..d31244ce3 100644 --- a/.agents/skills/generate-scripts-2/references/running-and-results.md +++ b/.agents/skills/generate-scripts-2/references/running-and-results.md @@ -44,7 +44,11 @@ Exit flags commonly mean: - `2`: manager wall-clock timeout - `3`: process outside the MPI communicator -Treat nonzero flags as non-success unless the workflow intentionally expects them. +Treat nonzero flags as non-success unless the workflow intentionally expects them. A +worker/simulator exception may cause `ensemble.run()` itself to raise rather than return a +flag. In that case, post-run manager code does not execute and a requested `save_output()` +basename is not produced. With `save_H_and_persis_on_abort=True`, libEnsemble writes an +automatic abort checkpoint; report its actual path rather than claiming the requested one. ## Checkpointing and restart @@ -101,9 +105,12 @@ if ensemble.is_manager: ensemble.save_output("run_results", append_attrs=False) ``` -`save_output` saves History and persistent information. With `append_attrs=False`, the -provided History basename is preserved; default attribute suffixes otherwise alter it. -Guard custom reporting, plots, and extra writes with `ensemble.is_manager`. +`save_output` saves History and persistent information. With `append_attrs=False`, no run +attribute suffix is added, but libEnsemble still reduces the argument to a short basename: +it drops directories/extensions and strips a leading portion through `test_`. Avoid names +containing `test_` when the literal filename matters, capture the returned History path, +and report that actual path. Guard custom reporting, plots, and extra writes with +`ensemble.is_manager`. When loading a saved NumPy History: diff --git a/docs/conf.py b/docs/conf.py index 29914d904..545ae7e43 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,6 +1,3 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# # liEensemble documentation build configuration file, created by # sphinx-quickstart on Fri Aug 18 11:52:31 2017. # @@ -22,7 +19,8 @@ from datetime import datetime from unittest.mock import MagicMock -exec(open("../libensemble/version.py").read()) +with open("../libensemble/version.py") as f: + __version__ = f.read() class Mock(MagicMock): @@ -135,9 +133,9 @@ class AxParameterWarning(Warning): # Ensure it's a real warning subclass autodoc_pydantic_model_show_json = False extlinks = { - "duref": ("http://docutils.sourceforge.net/docs/ref/rst/" "restructuredtext.html#%s", ""), - "durole": ("http://docutils.sourceforge.net/docs/ref/rst/" "roles.html#%s", ""), - "dudir": ("http://docutils.sourceforge.net/docs/ref/rst/" "directives.html#%s", ""), + "duref": ("http://docutils.sourceforge.net/docs/ref/rst/", "restructuredtext.html#%s", ""), + "durole": ("http://docutils.sourceforge.net/docs/ref/rst/", "roles.html#%s", ""), + "dudir": ("http://docutils.sourceforge.net/docs/ref/rst/", "directives.html#%s", ""), } # Add any paths that contain templates here, relative to this directory. @@ -160,7 +158,7 @@ class AxParameterWarning(Warning): # Ensure it's a real warning subclass # General information about the project. project = "libEnsemble" -copyright = str(datetime.now().year) + " Argonne National Laboratory" +copyright = str(datetime.now("UTC").year) + " Argonne National Laboratory" author = "Jeffrey Larson, Stephen Hudson, Stefan M. Wild, David Bindel and John-Luke Navarro" today_fmt = "%B %-d, %Y" @@ -195,10 +193,11 @@ class AxParameterWarning(Warning): # Ensure it's a real warning subclass # Evaluate most references and links for correctness - but many are incorrect nitpicky = True nitpick_ignore = [] -for line in open("nitpicky"): - if line.strip() == "" or line.startswith("#"): - continue - dtype, target = line.split(None, 1) +with open("nitpicky") as f: + for line in f: + if line.strip() == "" or line.startswith("#"): + continue + dtype, target = line.split(None, 1) target = target.strip() nitpick_ignore.append((dtype, target)) diff --git a/libensemble/ensemble.py b/libensemble/ensemble.py index b90d07e7b..d68c3e7c4 100644 --- a/libensemble/ensemble.py +++ b/libensemble/ensemble.py @@ -486,23 +486,20 @@ def nworkers(self, value): if self._libE_specs: self._libE_specs.nworkers = value - def save_output(self, basename: str, append_attrs: bool = True): + def save_output(self, basename: str, append_attrs: bool = True) -> str | None: """ Writes out History array and persis_info to files. - If using a ``workflow_dir_path`` in ``libE_specs``, will place with specified filename in that directory. + If using a ``workflow_dir_path`` in ``libE_specs``, will place the files in that directory. - Parameters - ---------- - - Format: ``_results_History_length=_evals=_ranks=`` - - To have the filename be only the basename, set ``append_attrs=False`` + The basename is reduced to its filename stem. A leading ``test_`` is removed to + preserve the calling-script convention, while occurrences elsewhere are retained. + If ``append_attrs`` is true, run attributes are appended to the stem. - Format: ``_results_History_length=_evals=_ranks=`` + Returns the History filename on the manager and ``None`` on other ranks. """ if self.is_manager: if getattr(self.libE_specs, "workflow_dir_path", False): - save_libE_output( + return save_libE_output( self.H, self.persis_info, basename, @@ -510,5 +507,5 @@ def save_output(self, basename: str, append_attrs: bool = True): dest_path=self.libE_specs.workflow_dir_path, append_attrs=append_attrs, ) - else: - save_libE_output(self.H, self.persis_info, basename, self.nworkers, append_attrs=append_attrs) + return save_libE_output(self.H, self.persis_info, basename, self.nworkers, append_attrs=append_attrs) + return None diff --git a/libensemble/tests/unit_tests/test_ensemble.py b/libensemble/tests/unit_tests/test_ensemble.py index 1569a7690..767d80102 100644 --- a/libensemble/tests/unit_tests/test_ensemble.py +++ b/libensemble/tests/unit_tests/test_ensemble.py @@ -23,6 +23,36 @@ class Node: assert partner["state"]["partner"]["state"] == "" +def test_save_output_preserves_embedded_test_prefix(tmp_path, monkeypatch): + from libensemble.ensemble import Ensemble + from libensemble.specs import LibeSpecs + + ensemble = Ensemble(libE_specs=LibeSpecs(comms="local", nworkers=1)) + ensemble.H = np.zeros(1, dtype=[("sim_ended", bool)]) + ensemble.persis_info = {} + + monkeypatch.chdir(tmp_path) + output_path = ensemble.save_output("sampling_retest_renamed.py", append_attrs=False) + + assert output_path == "sampling_retest_renamed.npy" + assert (tmp_path / "sampling_retest_renamed.npy").is_file() + assert (tmp_path / "sampling_retest_renamed.pickle").is_file() + + +def test_save_output_strips_only_leading_test_prefix(tmp_path, monkeypatch): + from libensemble.ensemble import Ensemble + from libensemble.specs import LibeSpecs + + ensemble = Ensemble(libE_specs=LibeSpecs(comms="local", nworkers=1)) + ensemble.H = np.zeros(1, dtype=[("sim_ended", bool)]) + ensemble.persis_info = {} + + monkeypatch.chdir(tmp_path) + output_path = ensemble.save_output("test_sampling.py", append_attrs=False) + + assert output_path == "sampling.npy" + + def test_ensemble_init(): """testing init attrs""" from libensemble.ensemble import Ensemble diff --git a/libensemble/tools/tools.py b/libensemble/tools/tools.py index c95be227e..b37c1e68b 100644 --- a/libensemble/tools/tools.py +++ b/libensemble/tools/tools.py @@ -65,8 +65,7 @@ def _get_shortname(basename): script_name = os.path.splitext(os.path.basename(basename))[0] - short_name = script_name.split("test_", 1).pop() - return short_name + return script_name.removeprefix("test_") # =================== save libE output to pickle and np ======================== @@ -108,7 +107,9 @@ def save_libE_output( basename : :obj:`str` Name of user-calling script (or user chosen name) to prefix output files. - The convention is to send __file__ from user calling script. + The convention is to send __file__ from user calling script. A leading + ``test_`` is removed from the filename stem, but occurrences elsewhere + in the name are preserved. nworkers: :obj:`int` From 69d0fa5e49520a35875602cafd4a0a13f0280e1d Mon Sep 17 00:00:00 2001 From: jlnav Date: Tue, 6 Oct 2026 14:36:51 -0500 Subject: [PATCH 3/3] tiny fixes to conf.py. move example python scripts for agents using new skill from canonical-patterns to an examples directory. --- .agents/skills/generate-scripts-2/SKILL.md | 17 ++- .../examples/local_sampling.py | 56 ++++++++++ .../examples/preloaded_points.py | 54 +++++++++ .../references/canonical-patterns.md | 103 ++---------------- docs/conf.py | 18 +-- 5 files changed, 139 insertions(+), 109 deletions(-) create mode 100644 .agents/skills/generate-scripts-2/examples/local_sampling.py create mode 100644 .agents/skills/generate-scripts-2/examples/preloaded_points.py diff --git a/.agents/skills/generate-scripts-2/SKILL.md b/.agents/skills/generate-scripts-2/SKILL.md index 4e0f11ce9..e71e22f2b 100644 --- a/.agents/skills/generate-scripts-2/SKILL.md +++ b/.agents/skills/generate-scripts-2/SKILL.md @@ -25,14 +25,11 @@ dictionary, or explicit allocation-function patterns. ecosystems. Read `references/aposmm.md` or `references/external-generators.md` when applicable. Check that every non-core package is installed before relying on it. -3. Read `references/canonical-patterns.md` and generate an `Ensemble`-based script with: - - `VOCS` imported from `gest_api.vocs` - - typed `SimSpecs`, `GenSpecs`, and `LibeSpecs` - - a generator object supplied with `GenSpecs(generator=..., vocs=...)` - - a dict simulator supplied with `SimSpecs(simulator=..., vocs=...)` - - `if __name__ == "__main__":` - - termination criteria passed directly to `ensemble.run(...)` - - manager-only reporting and output saving, including partial results on timeout +3. Read `references/canonical-patterns.md`. For a basic Python simulator, adapt + `examples/local_sampling.py`. For an existing set of points, adapt + `examples/preloaded_points.py`. Keep the complete script structure: `Ensemble`, VOCS, + typed `SimSpecs`/`GenSpecs`/`LibeSpecs`, `generator=` and `simulator=`, a main guard, + explicit `ensemble.run(...)` stopping criteria, and manager-only result reporting/saving. 4. For an executable, read `references/executors-and-files.md`. Use `Executor` for a normal subprocess and `MPIExecutor` only when the application itself needs an MPI or @@ -91,7 +88,9 @@ dictionary, or explicit allocation-function patterns. Read only what the request needs; all paths are relative to this skill directory. - `references/intake-and-validation.md` — requirements and final checks -- `references/canonical-patterns.md` — complete modern local templates +- `examples/local_sampling.py` — runnable built-in sampling workflow +- `examples/preloaded_points.py` — runnable workflow for evaluating supplied points +- `references/canonical-patterns.md` — adaptation guidance and generator/VOCS pitfalls - `references/generator-selection.md` — generator decision table and batch behavior - `references/aposmm.md` — standardized APOSMM configuration - `references/external-generators.md` — Xopt, Optimas, and gpCAM diff --git a/.agents/skills/generate-scripts-2/examples/local_sampling.py b/.agents/skills/generate-scripts-2/examples/local_sampling.py new file mode 100644 index 000000000..29c125e72 --- /dev/null +++ b/.agents/skills/generate-scripts-2/examples/local_sampling.py @@ -0,0 +1,56 @@ +"""Run a small, reproducible Latin-hypercube sampling workflow locally. + +Run with ``python local_sampling.py`` from a fresh working directory. The run +writes ``local_sampling.npy`` and ``local_sampling.pickle`` in that directory. +""" + +import numpy as np +from gest_api.vocs import VOCS + +from libensemble import Ensemble +from libensemble.gen_classes.sampling import LatinHypercubeSample +from libensemble.specs import GenSpecs, LibeSpecs, SimSpecs + + +def simulate(inputs: dict, **kwargs) -> dict: + """Evaluate a simple objective using the two sampled variables.""" + x0 = inputs["x0"] + x1 = inputs["x1"] + return {"f": x0**2 + x1**2} + + +if __name__ == "__main__": + vocs = VOCS( + variables={"x0": [-3.0, 3.0], "x1": [-2.0, 2.0]}, + objectives={"f": "MINIMIZE"}, + ) + + # Keep the complete LHS in one initial batch. Splitting it across requests + # would create multiple smaller designs instead of one 12-point design. + generator = LatinHypercubeSample(vocs, random_seed=1) + ensemble = Ensemble( + sim_specs=SimSpecs(simulator=simulate, vocs=vocs), + gen_specs=GenSpecs( + generator=generator, + vocs=vocs, + initial_batch_size=12, + batch_size=12, + ), + # Using local workers lets this example run with plain Python. The default + # ensemble directory is created in the fresh working directory. + libE_specs=LibeSpecs(comms="local", nworkers=4, safe_mode=True), + ) + + history, _, exit_flag = ensemble.run(sim_max=12) + + if ensemble.is_manager: + completed = history[history["sim_ended"]] + finite = completed[np.isfinite(completed["f"])] + history_path = ensemble.save_output("local_sampling", append_attrs=False) + print(f"Saved History to {history_path}") + print(f"Completed {len(completed)} of 12 evaluations; exit flag: {exit_flag}") + if len(finite): + best = finite[np.argmin(finite["f"])] + print(f"Best point: x0={best['x0']:.4f}, x1={best['x1']:.4f}, f={best['f']:.6f}") + if exit_flag != 0: + raise RuntimeError(f"libEnsemble exited with flag {exit_flag}") diff --git a/.agents/skills/generate-scripts-2/examples/preloaded_points.py b/.agents/skills/generate-scripts-2/examples/preloaded_points.py new file mode 100644 index 000000000..b779dc85f --- /dev/null +++ b/.agents/skills/generate-scripts-2/examples/preloaded_points.py @@ -0,0 +1,54 @@ +"""Evaluate a fixed list of points with the standardized preloaded generator. + +Run with ``python preloaded_points.py`` from a fresh working directory. The +run writes ``preloaded_points.npy`` and ``preloaded_points.pickle`` there. +""" + +import numpy as np +from gest_api.vocs import VOCS + +from libensemble import Ensemble +from libensemble.gen_classes.preloaded import PreloadedSampleGenerator +from libensemble.specs import GenSpecs, LibeSpecs, SimSpecs + + +def simulate(inputs: dict, **kwargs) -> dict: + """Return the objective for one point supplied by the preloaded generator.""" + x0 = inputs["x0"] + x1 = inputs["x1"] + return {"f": x0**2 + x1**2} + + +if __name__ == "__main__": + # Every point must provide the variable fields the simulator reads. Constants, + # if present in VOCS, should also be included in each point or added explicitly. + points = [ + {"x0": -1.0, "x1": 0.5}, + {"x0": 0.0, "x1": 0.0}, + {"x0": 1.0, "x1": 0.5}, + ] + vocs = VOCS( + variables={"x0": [-3.0, 3.0], "x1": [-2.0, 2.0]}, + objectives={"f": "MINIMIZE"}, + ) + + generator = PreloadedSampleGenerator(points, vocs=vocs, batch_size=2) + ensemble = Ensemble( + sim_specs=SimSpecs(simulator=simulate, vocs=vocs), + gen_specs=GenSpecs(generator=generator, vocs=vocs), + libE_specs=LibeSpecs(comms="local", nworkers=2, safe_mode=True), + ) + + history, _, exit_flag = ensemble.run(sim_max=len(points)) + + if ensemble.is_manager: + completed = history[history["sim_ended"]] + finite = completed[np.isfinite(completed["f"])] + history_path = ensemble.save_output("preloaded_points", append_attrs=False) + print(f"Saved History to {history_path}") + print(f"Evaluated {len(completed)} of {len(points)} supplied points") + if len(finite): + best = finite[np.argmin(finite["f"])] + print(f"Lowest objective: x0={best['x0']:.4f}, x1={best['x1']:.4f}, f={best['f']:.6f}") + if exit_flag != 0: + raise RuntimeError(f"libEnsemble exited with flag {exit_flag}") diff --git a/.agents/skills/generate-scripts-2/references/canonical-patterns.md b/.agents/skills/generate-scripts-2/references/canonical-patterns.md index 9f2c949e6..55977a027 100644 --- a/.agents/skills/generate-scripts-2/references/canonical-patterns.md +++ b/.agents/skills/generate-scripts-2/references/canonical-patterns.md @@ -9,100 +9,21 @@ generator just to preserve the ambiguous name. ## Built-in sampling and Python simulator -```python -import numpy as np -from gest_api.vocs import VOCS - -from libensemble import Ensemble -from libensemble.gen_classes.sampling import UniformSample -from libensemble.specs import GenSpecs, LibeSpecs, SimSpecs - - -def simulate(inputs: dict, **kwargs) -> dict: - x0 = inputs["x0"] - x1 = inputs["x1"] - return {"f": x0**2 + x1**2} - - -if __name__ == "__main__": - vocs = VOCS( - variables={"x0": [-3.0, 3.0], "x1": [-2.0, 2.0]}, - objectives={"f": "MINIMIZE"}, - ) - - ensemble = Ensemble( - sim_specs=SimSpecs(simulator=simulate, vocs=vocs), - gen_specs=GenSpecs( - generator=UniformSample(vocs, random_seed=1), - vocs=vocs, - initial_batch_size=4, - batch_size=4, - ), - libE_specs=LibeSpecs(comms="local", nworkers=4, safe_mode=True), - ) - - H, _, flag = ensemble.run(sim_max=100) - - if ensemble.is_manager: - completed = H[H["sim_ended"]] - finite = completed[np.isfinite(completed["f"])] - history_path = ensemble.save_output("sampling_results", append_attrs=False) - print(f"Saved History to {history_path}") - if len(finite): - best = finite[np.argmin(finite["f"])] - print(f"Best: x0={best['x0']}, x1={best['x1']}, f={best['f']}") - if flag != 0: - raise RuntimeError(f"libEnsemble exited with flag {flag}; partial results saved") -``` - -Use `LatinHypercubeSample` from the same module for a space-filling batch. Each separate -LHS `suggest()` call forms a separate Latin hypercube; use one initial batch equal to the -full design size when global stratification matters. - -`initial_batch_size` controls the first request; `batch_size` controls later requests. -Sampling ignores feedback, so either synchronous or asynchronous delivery is acceptable. +Use the runnable `examples/local_sampling.py` as the canonical full script. It uses +`LatinHypercubeSample` for one complete initial design, a pure-Python simulator, finite +result filtering, and manager-only output saving. Adapt variable names, bounds, objective, +worker count, and simulation budget to the user's requirements. Do not copy its sample +values as if they were user requirements. + +Each `LatinHypercubeSample.suggest()` call forms a separate Latin hypercube. Use one initial +batch equal to the full design size when global stratification across the whole sample +matters. `initial_batch_size` controls the first request; `batch_size` controls later +requests. Sampling ignores feedback, so either synchronous or asynchronous delivery is +acceptable. ## Preloaded points -```python -from gest_api.vocs import VOCS - -from libensemble import Ensemble -from libensemble.gen_classes import PreloadedSampleGenerator -from libensemble.specs import GenSpecs, LibeSpecs, SimSpecs - - -def simulate(inputs: dict, **kwargs) -> dict: - return {"f": inputs["x0"] ** 2 + inputs["x1"] ** 2} - - -if __name__ == "__main__": - points = [ - {"x0": -1.0, "x1": 0.5}, - {"x0": 0.0, "x1": 0.0}, - {"x0": 1.0, "x1": 0.5}, - ] - vocs = VOCS( - variables={"x0": [-3.0, 3.0], "x1": [-2.0, 2.0]}, - objectives={"f": "MINIMIZE"}, - ) - - ensemble = Ensemble( - sim_specs=SimSpecs(simulator=simulate, vocs=vocs), - gen_specs=GenSpecs( - generator=PreloadedSampleGenerator(points, vocs=vocs, batch_size=2), - vocs=vocs, - ), - libE_specs=LibeSpecs(comms="local", nworkers=2, safe_mode=True), - ) - H, _, flag = ensemble.run(sim_max=len(points)) - if ensemble.is_manager: - history_path = ensemble.save_output("preloaded_results", append_attrs=False) - print(f"Saved History to {history_path}") - if flag != 0: - raise RuntimeError(f"libEnsemble exited with flag {flag}; partial results saved") -``` - +Use the runnable `examples/preloaded_points.py` as the canonical complete example. `PreloadedSampleGenerator` accepts a list of dictionaries or a NumPy structured array and returns an empty suggestion after exhaustion. Use it instead of a pre-generated-work allocator. Ensure every point has the simulator's required variable fields and any VOCS diff --git a/docs/conf.py b/docs/conf.py index 545ae7e43..e03aa9d34 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,11 +16,11 @@ # import os import sys -from datetime import datetime +from datetime import datetime, timezone +from runpy import run_path from unittest.mock import MagicMock -with open("../libensemble/version.py") as f: - __version__ = f.read() +__version__ = run_path(os.path.join(os.path.dirname(__file__), "../libensemble/version.py"))["__version__"] class Mock(MagicMock): @@ -133,9 +133,9 @@ class AxParameterWarning(Warning): # Ensure it's a real warning subclass autodoc_pydantic_model_show_json = False extlinks = { - "duref": ("http://docutils.sourceforge.net/docs/ref/rst/", "restructuredtext.html#%s", ""), - "durole": ("http://docutils.sourceforge.net/docs/ref/rst/", "roles.html#%s", ""), - "dudir": ("http://docutils.sourceforge.net/docs/ref/rst/", "directives.html#%s", ""), + "duref": ("http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#%s", ""), + "durole": ("http://docutils.sourceforge.net/docs/ref/rst/roles.html#%s", ""), + "dudir": ("http://docutils.sourceforge.net/docs/ref/rst/directives.html#%s", ""), } # Add any paths that contain templates here, relative to this directory. @@ -158,7 +158,7 @@ class AxParameterWarning(Warning): # Ensure it's a real warning subclass # General information about the project. project = "libEnsemble" -copyright = str(datetime.now("UTC").year) + " Argonne National Laboratory" +copyright = str(datetime.now(timezone.utc).year) + " Argonne National Laboratory" author = "Jeffrey Larson, Stephen Hudson, Stefan M. Wild, David Bindel and John-Luke Navarro" today_fmt = "%B %-d, %Y" @@ -198,8 +198,8 @@ class AxParameterWarning(Warning): # Ensure it's a real warning subclass if line.strip() == "" or line.startswith("#"): continue dtype, target = line.split(None, 1) - target = target.strip() - nitpick_ignore.append((dtype, target)) + target = target.strip() + nitpick_ignore.append((dtype, target)) # -- Options for HTML output ----------------------------------------------