diff --git a/.agents/skills/generate-scripts-2/SKILL.md b/.agents/skills/generate-scripts-2/SKILL.md new file mode 100644 index 000000000..e71e22f2b --- /dev/null +++ b/.agents/skills/generate-scripts-2/SKILL.md @@ -0,0 +1,103 @@ +--- +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. 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. + +## 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. 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 + 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`. 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 + 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. 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 + 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. + 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 + +- 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 +- `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 +- `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/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/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..55977a027 --- /dev/null +++ b/.agents/skills/generate-scripts-2/references/canonical-patterns.md @@ -0,0 +1,100 @@ +# 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. 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 + +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 + +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 +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.vocs = vocs + 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 +``` + +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. + +## 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. 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 new file mode 100644 index 000000000..6a651340c --- /dev/null +++ b/.agents/skills/generate-scripts-2/references/executors-and-files.md @@ -0,0 +1,162 @@ +# 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 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. + +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 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 +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..de44d58b0 --- /dev/null +++ b/.agents/skills/generate-scripts-2/references/hpc-resources.md @@ -0,0 +1,119 @@ +# 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. + +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; +- 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 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. + +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..bd04024bd --- /dev/null +++ b/.agents/skills/generate-scripts-2/references/intake-and-validation.md @@ -0,0 +1,96 @@ +# 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 `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. + +## 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. 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. 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 + +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..d31244ce3 --- /dev/null +++ b/.agents/skills/generate-scripts-2/references/running-and-results.md @@ -0,0 +1,136 @@ +# 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. 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 + +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`, 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: + +```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 diff --git a/docs/conf.py b/docs/conf.py index 29914d904..e03aa9d34 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. # @@ -19,10 +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 -exec(open("../libensemble/version.py").read()) +__version__ = run_path(os.path.join(os.path.dirname(__file__), "../libensemble/version.py"))["__version__"] 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(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" @@ -195,12 +193,13 @@ 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) - target = target.strip() - nitpick_ignore.append((dtype, target)) +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)) # -- Options for HTML output ---------------------------------------------- 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`