diff --git a/.claude/skills/generate-scripts/SKILL.md b/.claude/skills/generate-scripts/SKILL.md new file mode 100644 index 0000000000..ebdd893cc4 --- /dev/null +++ b/.claude/skills/generate-scripts/SKILL.md @@ -0,0 +1,147 @@ +--- +name: generate-scripts +description: Generate libEnsemble calling scripts based on user requirements +--- + +You are generating libEnsemble scripts. libEnsemble coordinates parallel simulations +with generator-directed optimization or sampling. You will produce a calling script +and, when an external application is involved, a sim function file. + +libEnsemble repository: https://github.com/Libensemble/libensemble +If not running inside the libEnsemble repo, find examples and source code there. + +## Workflow + +1. If converting an existing Xopt or Optimas workflow to libEnsemble, use the + existing generator and VOCS settings exactly as-is — even if it is a sampling + or exploration generator. Do not switch to a classic generator unless the user + specifically asks. + Otherwise, if there is not a clear generator to use, read `references/generators.md` + to determine which generator and + style to use. If a specific generator is identified (e.g., APOSMM), read its + dedicated guide (e.g., `references/aposmm.md`). + +2. Find a relevant example in `libensemble/tests/regression_tests/` and read it as a + reference. Some examples: + - Xopt Bayesian optimization (VOCS): `test_xopt_EI_initial_sample.py` — best Xopt + example as it demonstrates the initial sampling approach Xopt generators need + - Optimas Ax optimization (VOCS): `test_optimas_ax_sf.py` + - APOSMM with NLopt (classic): `test_persistent_aposmm_nlopt.py` + - Random uniform sampling (classic): `test_1d_sampling.py` + Use glob and grep to find others matching the generator or pattern needed. + The regression tests have clear descriptions in the docstring. + +3. Write the calling script adapting the example to the user's requirements. + Do not copy test boilerplate from examples + (e.g., "Execute via one of the following commands..." headers). Set nworkers + directly in the script (in LibeSpecs) — do not use parse_args or command-line + arguments unless the user asks for that. If parse_args is not used and no + options are taken, then do not ever suggest running with "-n/nworkers" or comms. + Those optins are used only with parse_args (used in tests). + +4. If the user has an external application (executable), also write a sim function file + that uses the executor to run it. + +5. If the user provides an input file, check whether it has Jinja2 template markers + (`{{ varname }}`). If not, create a templated copy: replace parameter values with + `{{ name }}` markers matching `input_names` in sim_specs (case-sensitive). The sim + function uses `jinja2.Template` to render the file before each simulation. Never + modify the user's original file. + +6. Verify the scripts: + - Bounds and dimension match the user's request + - Executable path is correct + - For VOCS: variable names are consistent between VOCS definition and sim function + - For APOSMM: gen_specs outputs include all required fields + - Input file template markers match input_names (case-sensitive) + - The app_name in submit() matches register_app() + +7. Present a concise summary highlighting: generator choice, bounds, parameters, + sim_max, and objective field. Do NOT suggest `mpirun` or other MPI + runner (srun, mpiexec, etc.) to launch libEnsemble unless the user explicitly + asks for MPI-based comms. + +8. Ask the user if they want to run the scripts. + +9. If running: execute with `python script.py`. Do not use `mpirun` or other MPI + runner (srun, mpiexec, etc.) to launch libEnsemble unless the user explicitly + asks for MPI-based comms for distributing workers. This is unrelated to + MPIExecutor, which workers use to launch simulation applications across nodes + — libEnsemble manages node allocation. + If scripts fail, retry if you can see a fix, otherwise stop. After a successful + run, read `references/results_metadata.md` and + `references/finding_objectives.md` to interpret the output. + +## Generator style + +VOCS (gest-api) is the default style. It uses a VOCS object to define variables and +objectives, and a generator object from Xopt or Optimas. Use VOCS unless the user +explicitly asks for the classic style or the generator only exists in classic form +(e.g., APOSMM, persistent_sampling). + +## Defaults + +- nworkers defaults to 4 unless the user specifies otherwise (or 1 for sequential + generators like Nelder-Mead) +- All nworkers are available for simulations +- No alloc_specs needed — all allocator options are available as GenSpecs parameters +- Use `async_return=True` in GenSpecs unless there is a reason to use batch returns + +## VOCS generators (Xopt / Optimas) + +Key patterns: +- Variables named individually in VOCS: `{"x0": [lb, ub], "x1": [lb, ub]}` +- Objectives named in VOCS: `{"f": "MINIMIZE"}` +- GenSpecs uses `generator=`, `vocs=`, `batch_size=` +- SimSpecs uses `vocs=` or `simulator=` for gest-api style sim functions +- No `add_random_streams()` needed +- Xopt generators need `initial_sample_method="uniform"` and `initial_batch_size=` + for initial evaluated data. Optimas handles its own sampling. + +See `references/generators.md` for the full generator selection guide. + +## Classic generators + +Used only when the generator has no VOCS version or the user explicitly requests it. +- One worker is consumed by the persistent generator +- Requires `add_random_streams()` +- APOSMM: see `references/aposmm.md` for full configuration details + +## Sim function patterns + +**Inline sim function** (no external app): Takes `(H, persis_info, sim_specs, libE_info)` +and returns `(H_o, persis_info)`. Or for VOCS gest-api style, takes `input_dict: dict` +and returns a dict. See `libensemble/sim_funcs/` for built-in examples. + +**Executor-based sim function** (external app): Uses MPIExecutor to run an application. +Pattern: +1. Register app in calling script: `exctr.register_app(full_path=..., app_name=...)` +2. In sim function: get executor from `libE_info["executor"]`, submit with + `exctr.submit(app_name=...)`, wait with `task.wait()` +3. Read output file to get objective value +4. Set `sim_dirs_make=True` in LibeSpecs +5. If using input file templating, set `sim_dir_copy_files=[input_file]` + +## Results interpretation + +After a successful run: +- Load the .npy output file with `np.load()` +- Always filter by `sim_ended == True` before analyzing — rows where sim_ended is False + contain uninitialized values (often zeros) that are NOT real results +- For APOSMM: check rows where `local_min == True` to find identified minima +- Report the count, location, and objective value of minima or best points found +- If the best objective value is exactly 0.0, verify those rows have sim_ended == True +- See `references/results_metadata.md` for full details + +## Reference docs (read as needed) + +All paths relative to this skill's directory: + +- `references/generators.md` — Generator selection guide, VOCS vs classic +- `references/aposmm.md` — APOSMM configuration, optimizer options, tuning +- `references/finding_objectives.md` — Identifying objective fields in results +- `references/results_metadata.md` — Interpreting history array, filtering results + +## User request + +$ARGUMENTS diff --git a/.claude/skills/generate-scripts/references/aposmm.md b/.claude/skills/generate-scripts/references/aposmm.md new file mode 100644 index 0000000000..bac7c2d8c3 --- /dev/null +++ b/.claude/skills/generate-scripts/references/aposmm.md @@ -0,0 +1,142 @@ +# APOSMM — Asynchronously Parallel Optimization Solver for Multiple Minima + +APOSMM coordinates concurrent local optimization runs to find multiple local minima on parallel hardware. Use when the user wants to find minima, optimize, or explore an optimization landscape. + +Module: `persistent_aposmm` +Function: `aposmm` +Allocator: `persistent_aposmm_alloc` (NOT the default `start_only_persistent`) +Requirements: mpmath, SciPy (plus optional packages for specific local optimizers) + +## APOSMM gen_specs in generated scripts + +When the MCP tool generates APOSMM scripts, run_libe.py gets this gen_specs structure: + +```python +gen_specs = GenSpecs( + gen_f=gen_f, + inputs=[], + persis_in=["sim_id", "x", "x_on_cube", "f"], + outputs=[("x", float, n), ("x_on_cube", float, n), ("sim_id", int), + ("local_min", bool), ("local_pt", bool)], + user={ + "initial_sample_size": num_workers, + "localopt_method": "scipy_Nelder-Mead", + "opt_return_codes": [0], + "nu": 1e-8, + "mu": 1e-8, + "dist_to_bound_multiple": 0.01, + "max_active_runs": 6, + "lb": np.array([...]), # MUST match user's requested bounds + "ub": np.array([...]), # MUST match user's requested bounds + } +) +``` + +With allocator: +```python +from libensemble.alloc_funcs.persistent_aposmm_alloc import persistent_aposmm_alloc as alloc_f +``` + +## Required gen_specs["user"] Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `lb` | n floats | Lower bounds on search domain | +| `ub` | n floats | Upper bounds on search domain | +| `localopt_method` | str | Local optimizer (see table below) | +| `initial_sample_size` | int | Uniform samples before starting local runs | + +When using a SciPy method, must also supply `opt_return_codes` — e.g. [0] for Nelder-Mead/BFGS, [1] for COBYLA. + +## Optional gen_specs["user"] Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `max_active_runs` | int | Max concurrent local optimization runs. Must not exceed nworkers. | +| `dist_to_bound_multiple` | float (0,1] | Fraction of distance to boundary for initial step size | +| `mu` | float | Min distance from boundary for starting points | +| `nu` | float | Min distance from identified minima for starting points | +| `stop_after_k_minima` | int | Stop after this many local minima found | +| `stop_after_k_runs` | int | Stop after this many runs ended | +| `sample_points` | numpy array | Specific points to sample (original domain) | +| `lhs_divisions` | int | Latin hypercube partitions (0 or 1 = uniform) | +| `rk_const` | float | Multiplier for r_k value | + +## Local Optimizer Methods + +### SciPy (no extra install) + +| Method | Gradient? | `opt_return_codes` | +|--------|-----------|-------------------| +| `scipy_Nelder-Mead` | No | [0] | +| `scipy_COBYLA` | No | [1] | +| `scipy_BFGS` | Yes | [0] | + +### NLopt (requires nlopt package) + +| Method | Gradient? | Description | +|--------|-----------|-------------| +| `LN_SBPLX` | No | Subplex. Good for noisy/nonsmooth | +| `LN_BOBYQA` | No | Quadratic model. Good for smooth problems | +| `LN_COBYLA` | No | Constrained optimization | +| `LN_NEWUOA` | No | Unconstrained quadratic model | +| `LN_NELDERMEAD` | No | Classic simplex | +| `LD_MMA` | Yes | Method of Moving Asymptotes | + +NLopt methods require convergence tolerances. If the user does not specify tolerances, use these defaults: + +```python +"xtol_abs": 1e-6, +"ftol_abs": 1e-6, +``` + +When using an NLopt method, always include `rk_const` scaled to the problem dimension: + +```python +from math import gamma, pi, sqrt +n = +rk_const = 0.5 * ((gamma(1 + (n / 2)) * 5) ** (1 / n)) / sqrt(pi) +``` + +Use this formula directly in the generated script — do not precompute the value. + +### PETSc/TAO (requires petsc4py package) + +| Method | Needs | Description | +|--------|-------|-------------| +| `pounders` | fvec | Least-squares trust-region | +| `blmvm` | grad | Bounded limited-memory variable metric | +| `nm` | f only | Nelder-Mead variant | + +### DFO-LS (requires dfols package) + +| Method | Needs | Description | +|--------|-------|-------------| +| `dfols` | fvec | Derivative-free least-squares | + +## Choosing a Local Optimizer + +- **Default / simple**: `scipy_Nelder-Mead` — no extra packages +- **Smooth, bounded**: `LN_BOBYQA` (NLopt) +- **Noisy objectives**: `LN_SBPLX` (NLopt) or `scipy_Nelder-Mead` +- **Gradient available**: `scipy_BFGS` or `LD_MMA` +- **Least-squares (vector output)**: `pounders` (PETSc) or `dfols` +- **Constrained**: `scipy_COBYLA` or `LN_COBYLA` + +## Interpreting Results + +After a run, report the number of minima found. Load the results `.npy` file, +filter by `sim_ended == True`, then check `local_min == True` rows. +Report the count, objective value, and location of each minimum. + +## Tuning + +If APOSMM is not finding minima, try increasing the multiplier in `rk_const` (e.g., from 0.5 to a larger value) to make it more aggressive about starting new local optimization runs in different regions. + +Use this formula directly in the generated script — do not precompute the value. +Also consider increasing `dist_to_bound_multiple` (e.g., 0.5) for a larger initial +step size. + +## Important + +Always use the bounds, sim_max, and paths from the user's request. Never substitute values from examples or known problem domains. diff --git a/.claude/skills/generate-scripts/references/finding_objectives.md b/.claude/skills/generate-scripts/references/finding_objectives.md new file mode 100644 index 0000000000..e8f8e4bde8 --- /dev/null +++ b/.claude/skills/generate-scripts/references/finding_objectives.md @@ -0,0 +1,31 @@ +# Finding Objective Fields +How to find objective field names in results files. + +## VOCS scripts + +The objective field name is defined in the VOCS object: +```python +vocs = VOCS( + variables={"x0": [-2, 2], "x1": [-1, 1]}, + objectives={"f": "MINIMIZE"}, +) +``` + +The key in `objectives` (e.g. `"f"`) is the objective field name in the results. + +## Classic scripts + +The objective field name is defined in `sim_specs` outputs: +```python +sim_specs = SimSpecs( + ... + outputs=[("f", float)], # "f" is the objective field name +) +``` + +The field name in `outputs` (e.g. `"f"`) matches the field name in the `.npy` results file. + +## Common patterns +- Single objective: `{"f": "MINIMIZE"}` (VOCS) or `outputs=[("f", float)]` (classic) +- Multiple outputs: `"f"` is typically the objective — the scalar float used by the generator +- The objective field name in the VOCS definition or sim_specs outputs matches the field in the results diff --git a/.claude/skills/generate-scripts/references/generators.md b/.claude/skills/generate-scripts/references/generators.md new file mode 100644 index 0000000000..e6f6b3ccc9 --- /dev/null +++ b/.claude/skills/generate-scripts/references/generators.md @@ -0,0 +1,81 @@ +# libEnsemble Generator Functions + +This guide is for choosing a generator when one is not already provided. If the user +is converting an existing workflow that already has a generator, use that generator +as-is — do not use this guide to replace it. + +libEnsemble supports two styles of generator configuration: + +- **VOCS generators (gest-api)** — The default style. Uses a VOCS object to define variables, objectives, and constraints. The generator is passed as an object from Xopt, Optimas, or another gest-api compatible library. +- **Classic generators** — libEnsemble-native gen functions configured via `gen_f`, explicit `inputs`/`outputs`, and `user` dicts with bounds/parameters. Used only when the generator has no VOCS version or the user explicitly requests it. + +## When to Choose a Generator Style + +**VOCS is the default style.** Any generator from Xopt or Optimas is always VOCS — these libraries provide many generators covering optimization, sampling, surrogate modeling, and more. Do not switch an Xopt or Optimas generator to a classic libEnsemble generator. + +Use **classic generators** only when: +- The user explicitly asks for the classic/traditional style +- The generator does not have a VOCS version (APOSMM, persistent_sampling) + +## Choosing a generator + +| Goal | Suggested generator | Style | Package | +|------|---------------------|-------|---------| +| Bayesian optimization | Xopt (e.g., Expected Improvement) | VOCS | `xopt` | +| Sampling / exploration | Xopt (e.g., Latin Hypercube) | VOCS | `xopt` | +| Ax-based optimization, multi-fidelity, multi-task | Optimas | VOCS | `optimas` | +| Simplex optimization | Xopt Nelder-Mead | VOCS | `xopt` | +| Multi-objective Bayesian | Xopt MOBO | VOCS | `xopt` | +| GP-based adaptive sampling | gpCAM | VOCS or Classic | `gen_classes/gpCAM` | +| Find multiple local minima | APOSMM | VOCS or Classic | `gen_classes/aposmm` | +| Random/uniform sampling | Sampling | VOCS or Classic | `gen_classes/sampling` | + +Xopt and Optimas each provide many generators beyond those listed here. If the +generator choice is not clear, check the library documentation: +- Xopt: https://github.com/xopt-org/Xopt — algorithms at https://xopt.xopt.org/algorithms/ +- Optimas: https://github.com/optimas-org/optimas + +If the user says "optimize" without specifics -> Xopt (VOCS). +If the user says "Xopt", "VOCS", "Optimas", or names a specific generator from those libraries -> VOCS style. +If the user says "Ax", "multi-fidelity", "multi-task" -> Optimas (VOCS). +If the user says "find minima", "multiple local minima" -> APOSMM (classic). +If the user says "sample", "explore", "sweep" -> Xopt or Optimas can do this (VOCS), or persistent sampling (classic). + +## VOCS Generators (gest-api) + +VOCS is the default configuration style for generators in libEnsemble. Configuration uses a VOCS object to define the optimization problem and a generator object. Generators may come from Xopt, Optimas, libEnsemble, or other gest-api compatible libraries. + +### Key patterns + +- Variables are named individually in VOCS (`{"x0": [lb, ub], "x1": [lb, ub]}`) +- Objectives are named in VOCS (`{"f": "MINIMIZE"}`) +- GenSpecs uses `generator=`, `vocs=`, and `batch_size=` +- SimSpecs uses `vocs=` or `simulator=` for gest-api style sim functions +- No alloc_specs needed (default is correct) +- No `add_random_streams()` needed +- Use `async_return=True` in GenSpecs unless the generator requires batch returns + +### Initial sampling + +Some generators require evaluated data before they can suggest points. Set `initial_sample_method` in GenSpecs to have libEnsemble produce and evaluate an initial sample before starting the generator: + +- `initial_sample_method="uniform"` — uniform random sample from VOCS bounds +- `initial_batch_size` — required, specifies how many sample points to produce + +Generators that handle their own sampling do not need this. + +### Sim function adaptation + +When using VOCS generators with an executor-based sim function, the sim must read individual variable names from H rather than unpacking `H["x"]`. The `input_names` in `sim_specs["user"]` should match the VOCS variable names directly. + +## Classic Generators + +### persistent_sampling (persistent_uniform) +Random uniform sampling across parameter space. After the initial batch, creates p new random points for every p points returned. + +gen_specs["user"]: `lb`, `ub`, `initial_batch_size` +gen_specs outputs: `x (float, n)` + +### APOSMM (persistent_aposmm) +See `reference_docs/aposmm.md` for full details. +Asynchronously Parallel Optimization Solver for finding Multiple Minima. diff --git a/.claude/skills/generate-scripts/references/results_metadata.md b/.claude/skills/generate-scripts/references/results_metadata.md new file mode 100644 index 0000000000..97a71c14b2 --- /dev/null +++ b/.claude/skills/generate-scripts/references/results_metadata.md @@ -0,0 +1,42 @@ +# Results Metadata +How to interpret libEnsemble history array fields and filter for completed simulations. + +## History array (H) + +The `.npy` output file contains the history array H with both user-defined fields and +metadata fields added by libEnsemble. + +## Key metadata fields + +- `sim_ended`: True if the simulation completed. Only rows with `sim_ended == True` have valid results. +- `sim_started`: True if the simulation was dispatched to a worker. +- `returned`: True if results were returned to the manager. +- `sim_id`: Unique simulation ID (0-indexed). +- `gen_informed`: True if the generator has been informed of this result. + +## Filtering for valid results + +When analyzing results (e.g., finding the minimum objective value), always filter for +completed simulations: + +```python +H = np.load("results.npy") +done = H[H["sim_ended"]] # Only completed simulations +``` + +Rows where `sim_ended` is False may have default/zero values that are not real results. +This is common for the last few rows when the simulation budget is exhausted — they were +allocated by the generator but never evaluated. + +## Reporting results + +After a successful run, report any minima found in the results. See the generator-specific +guide for which fields indicate identified minima. + +## Common pitfall + +If the minimum objective value is exactly 0.0, check whether those rows have +`sim_ended == True`. Unevaluated rows often have fields initialized to zero. +This is common for the last few rows when the simulation budget is exhausted — they were +allocated by the generator but never evaluated. + diff --git a/.codecov.yml b/.codecov.yml index bf866b089c..c9dc0d1f29 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -4,3 +4,4 @@ ignore: - "libensemble/tools/live_data/*" - "libensemble/sim_funcs/executor_hworld.py" - "libensemble/gen_funcs/persistent_tasmanian.py" + - "libensemble/gen_classes/gpCAM.py" diff --git a/.flake8 b/.flake8 index 073989807d..87d249502e 100644 --- a/.flake8 +++ b/.flake8 @@ -38,6 +38,7 @@ per-file-ignores = # Need to set something before the APOSMM import libensemble/tests/regression_tests/test_persistent_aposmm*:E402 + libensemble/tests/regression_tests/test_asktell_aposmm_nlopt.py:E402 libensemble/tests/regression_tests/test_persistent_gp_multitask_ax.py:E402 libensemble/tests/functionality_tests/test_uniform_sampling_then_persistent_localopt_runs.py:E402 libensemble/tests/functionality_tests/test_stats_output.py:E402 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..d9f4f2f803 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +pixi.lock filter=lfs diff=lfs merge=lfs -text diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 21ca389e09..b3000567e8 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,7 +10,7 @@ updates: - package-ecosystem: github-actions directory: / schedule: - interval: weekly # Reduced frequency + interval: monthly # Reduced frequency target-branch: "develop" groups: actions-updates: @@ -20,7 +20,7 @@ updates: - package-ecosystem: pip directory: / schedule: - interval: weekly # Reduced frequency + interval: monthly # Reduced frequency target-branch: "develop" groups: python-updates: @@ -30,7 +30,7 @@ updates: - package-ecosystem: gitsubmodule directory: / schedule: - interval: weekly # Reduced frequency + interval: monthly # Reduced frequency target-branch: "develop" groups: submodule-updates: diff --git a/.github/workflows/basic.yml b/.github/workflows/basic.yml index aa363f4262..bd353bd301 100644 --- a/.github/workflows/basic.yml +++ b/.github/workflows/basic.yml @@ -9,110 +9,93 @@ on: - synchronize jobs: - test-libE: - if: '! github.event.pull_request.draft' - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest] - mpi-version: [mpich] - python-version: ["3.10", "3.11", "3.12", "3.13"] - comms-type: [m, l] - include: - - os: macos-latest - python-version: "3.11" - mpi-version: mpich - comms-type: m - - os: macos-latest - python-version: "3.11" - mpi-version: mpich - comms-type: l - + test-libE: + if: "! github.event.pull_request.draft" + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + mpi-version: [mpich] + python-version: ["py311", "py312", "py313", "py314"] + comms-type: [m, l] + include: + - os: macos-latest + python-version: "py311" + mpi-version: mpich + comms-type: m + - os: macos-latest + python-version: "py311" + mpi-version: mpich + comms-type: l + + env: + HYDRA_LAUNCHER: "fork" + TERM: xterm-256color + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + defaults: + run: + shell: bash -l {0} + + steps: + - uses: actions/checkout@v6 + with: + lfs: true + + - name: Checkout lockfile + run: git lfs checkout + + - uses: prefix-dev/setup-pixi@v0.9.6 + with: + pixi-version: v0.69.0 + frozen: true + environments: ${{ matrix.python-version }} + activate-environment: ${{ matrix.python-version }} + + - name: Install minq + run: | + pixi run -e ${{ matrix.python-version }} ./install/install_minq.sh + + - name: Install libEnsemble, test flake8 + run: | + pip install -e . + flake8 libensemble + + - name: Install mypy + run: pip install mypy + + - name: Run mypy (limited scope) + run: mypy + + - name: Remove various tests on newer pythons + if: matrix.python-version == 'py311' || matrix.python-version == 'py312' || matrix.python-version == 'py313' || matrix.python-version == 'py314' + run: | + rm ./libensemble/tests/functionality_tests/test_local_sine_tutorial*.py # matplotlib errors on py312 + + - name: Run simple tests, Ubuntu + if: matrix.os == 'ubuntu-latest' + run: | + ./libensemble/tests/run_tests.py -A "-W error" -${{ matrix.comms-type }} + + - name: Run simple tests, macOS + if: matrix.os == 'macos-latest' + run: | + pixi run -e ${{ matrix.python-version }} ./libensemble/tests/run_tests.py -A "-W error" -${{ matrix.comms-type }} + + - name: Merge coverage + run: | + mv libensemble/tests/.cov* . + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v6 env: - HYDRA_LAUNCHER: "fork" - TERM: xterm-256color - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - defaults: - run: - shell: bash -l {0} - - steps: - - uses: actions/checkout@v5 - - name: Setup conda - Python ${{ matrix.python-version }} - uses: conda-incubator/setup-miniconda@v3 - with: - activate-environment: condaenv - miniconda-version: "latest" - python-version: ${{ matrix.python-version }} - channels: conda-forge - channel-priority: strict - auto-update-conda: true - - - name: Force-update certifi and pip - run: | - python --version - python -m pip install --upgrade pip - python -m pip install -I --upgrade certifi - - - name: Install Ubuntu compilers - if: matrix.os == 'ubuntu-latest' - run: | - conda install -c conda-forge gcc_linux-64 - pip install nlopt==2.9.0 - - # Roundabout solution on macos for proper linking with mpicc - - name: Install macOS compilers - if: matrix.os == 'macos-latest' - run: | - conda install clang_osx-64 - pip install nlopt==2.8.0 - - - name: Install basic testing/feature dependencies - run: | - pip install -r install/testing_requirements.txt - pip install -r install/misc_feature_requirements.txt - source install/install_ibcdfo.sh - conda install numpy scipy - - - name: Install mpi4py and MPI from conda - run: | - conda install mpi4py ${{ matrix.mpi-version }} - - - name: Install libEnsemble, test flake8 - run: | - pip install -e . - flake8 libensemble - - - name: Remove various tests on newer pythons - if: matrix.python-version >= '3.11' - run: | - rm ./libensemble/tests/functionality_tests/test_local_sine_tutorial*.py # matplotlib errors on 3.12 - - - name: Run simple tests, Ubuntu - if: matrix.os == 'ubuntu-latest' - run: | - ./libensemble/tests/run_tests.py -A "-W error" -${{ matrix.comms-type }} - - - name: Run simple tests, macOS - if: matrix.os == 'macos-latest' - run: | - ./libensemble/tests/run_tests.py -A "-W error" -${{ matrix.comms-type }} - - - name: Merge coverage - run: | - mv libensemble/tests/.cov* . - - - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v5 - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - - spellcheck: - name: Spellcheck release branch - if: contains(github.base_ref, 'develop') - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - uses: crate-ci/typos@v1.38.1 + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + spellcheck: + name: Spellcheck release branch + if: contains(github.base_ref, 'develop') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: crate-ci/typos@v1.46.3 diff --git a/.github/workflows/extra.yml b/.github/workflows/extra.yml index e41de99aff..0f4e1fb2ef 100644 --- a/.github/workflows/extra.yml +++ b/.github/workflows/extra.yml @@ -4,144 +4,111 @@ on: workflow_dispatch: jobs: - test-libE: - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest] - mpi-version: [mpich] - python-version: ['3.10', '3.11', '3.12', '3.13'] - comms-type: [m, l] - include: - - os: macos-latest - python-version: '3.13' - mpi-version: mpich - comms-type: m - - os: macos-latest - python-version: '3.13' - mpi-version: mpich - comms-type: l - - os: ubuntu-latest - python-version: '3.12' - mpi-version: mpich - comms-type: t - - os: ubuntu-latest - mpi-version: 'openmpi' - python-version: '3.12' - comms-type: l - + test-libE: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + mpi-version: [mpich] + python-version: ["py311e", "py312e", "py313e", "py314e"] + comms-type: [m, l] + include: + - os: macos-latest + python-version: "py312e" + mpi-version: mpich + comms-type: m + - os: macos-latest + python-version: "py312e" + mpi-version: mpich + comms-type: l + - os: ubuntu-latest + python-version: "py312e" + mpi-version: mpich + comms-type: t + - os: ubuntu-latest + mpi-version: openmpi + python-version: "py312e" + comms-type: l + + env: + HYDRA_LAUNCHER: "fork" + TERM: xterm-256color + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + defaults: + run: + shell: bash -l {0} + + steps: + - uses: actions/checkout@v6 + with: + lfs: true + + - name: Checkout lockfile + run: git lfs checkout + + - uses: prefix-dev/setup-pixi@v0.9.6 + with: + pixi-version: v0.69.0 + cache: true + frozen: true + environments: ${{ matrix.python-version }} + activate-environment: ${{ matrix.python-version }} + + - name: Install other testing dependencies + run: | + pixi run -e ${{ matrix.python-version }} install/install_minq.sh + + - name: Install libEnsemble, flake8, lock environment + run: | + pip install -e . + flake8 libensemble + + - name: Install gpcam + if: matrix.python-version != 'py313e' && matrix.python-version != 'py314e' + run: | + pixi run -e ${{ matrix.python-version }} pip install gpcam==8.1.13 + + - name: Remove test using octave, gpcam, globus-compute on Python 3.13 + if: matrix.python-version == 'py313e' || matrix.python-version == 'py314e' + run: | + rm ./libensemble/tests/unit_tests/test_ufunc_runners.py # needs globus-compute + rm ./libensemble/tests/regression_tests/test_gpCAM.py # needs gpcam, which doesn't build on 3.13 + rm ./libensemble/tests/regression_tests/test_asktell_gpCAM.py # needs gpcam, which doesn't build on 3.13 + rm ./libensemble/tests/regression_tests/test_persistent_gp_multitask_ax.py # needs ax-platform, which doesn't yet support 3.14 + rm ./libensemble/tests/regression_tests/test_optimas_ax_mf.py # needs ax-platform, which doesn't yet support 3.14 + rm ./libensemble/tests/regression_tests/test_optimas_ax_sf.py # needs ax-platform, which doesn't yet support 3.14 + + - name: Start Redis + if: matrix.os == 'ubuntu-latest' + uses: supercharge/redis-github-action@v2 + with: + redis-version: 7 + + - name: Run extensive tests, Ubuntu + if: matrix.os == 'ubuntu-latest' + run: | + ./libensemble/tests/run_tests.py -e -${{ matrix.comms-type }} + + - name: Run extensive tests, macOS + if: matrix.os == 'macos-latest' + run: | + pixi run -e ${{ matrix.python-version }} ./libensemble/tests/run_tests.py -e -${{ matrix.comms-type }} + + - name: Merge coverage + run: | + mv libensemble/tests/.cov* . + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v6 env: - HYDRA_LAUNCHER: 'fork' - TERM: xterm-256color - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - defaults: - run: - shell: bash -l {0} - - steps: - - uses: actions/checkout@v5 - - name: Setup conda - Python ${{ matrix.python-version }} - uses: conda-incubator/setup-miniconda@v3 - with: - activate-environment: condaenv - miniconda-version: 'latest' - python-version: ${{ matrix.python-version }} - channels: conda-forge - channel-priority: strict - auto-update-conda: true - - - name: Force-update certifi - run: | - python --version - pip install -I --upgrade certifi - - - name: Install Ubuntu compilers - if: matrix.os == 'ubuntu-latest' - run: | - conda install -c conda-forge gcc_linux-64 - pip install nlopt==2.9.0 - - # Roundabout solution on macos for proper linking with mpicc - - name: Install macOS compilers - if: matrix.os == 'macos-latest' - run: | - conda install clang_osx-64 - pip install nlopt==2.8.0 - - - name: Install mpi4py and MPI from conda - run: | - conda install mpi4py ${{ matrix.mpi-version }} - - - name: Install generator dependencies - run: | - conda env update --file install/gen_deps_environment.yml - - - name: Install gpcam and octave # Neither yet support 3.13 - if: matrix.python-version <= '3.12' - run: | - pip install gpcam==8.1.13 - conda install octave - - - name: Install surmise and Tasmanian - if: matrix.os == 'ubuntu-latest' - run: | - pip install --upgrade git+https://github.com/bandframework/surmise.git - pip install Tasmanian --user - - - name: Install generator dependencies for Ubuntu tests - if: matrix.os == 'ubuntu-latest' && matrix.python-version <= '3.12' - run: | - pip install scikit-build packaging - - - name: Install other testing dependencies - run: | - pip install -r install/testing_requirements.txt - pip install -r install/misc_feature_requirements.txt - source install/install_ibcdfo.sh - conda install numpy scipy - - - name: Install libEnsemble, flake8, lock environment - run: | - pip install -e . - flake8 libensemble - - - name: Remove test using octave, gpcam on Python 3.13 - if: matrix.python-version >= '3.13' - run: | - rm ./libensemble/tests/regression_tests/test_persistent_fd_param_finder.py # needs octave, which doesn't yet support 3.13 - rm ./libensemble/tests/regression_tests/test_persistent_aposmm_external_localopt.py # needs octave, which doesn't yet support 3.13 - rm ./libensemble/tests/regression_tests/test_gpCAM.py # needs gpcam, which doesn't build on 3.13 - - - name: Install redis/proxystore - run: | - pip install redis - pip install proxystore==0.7.0 - - - name: Start Redis - if: matrix.os == 'ubuntu-latest' - uses: supercharge/redis-github-action@1.8.0 - with: - redis-version: 7 - - - name: Run extensive tests - run: | - ./libensemble/tests/run_tests.py -e -${{ matrix.comms-type }} - - - name: Merge coverage - run: | - mv libensemble/tests/.cov* . - - - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v5 - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - - spellcheck: - name: Spellcheck release branch - if: contains(github.base_ref, 'develop') - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - uses: crate-ci/typos@v1.38.1 + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + spellcheck: + name: Spellcheck release branch + if: contains(github.base_ref, 'develop') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: crate-ci/typos@v1.46.3 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ab7687d1ef..69c11918b9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: end-of-file-fixer exclude: ^(.*\.xml|.*\.svg)$ @@ -8,27 +8,33 @@ repos: exclude: ^(.*\.xml|.*\.svg)$ - repo: https://github.com/pycqa/isort - rev: 6.0.0 + rev: 7.0.0 hooks: - id: isort args: [--profile=black, --line-length=120] - repo: https://github.com/psf/black - rev: 25.1.0 + rev: 25.12.0 hooks: - id: black args: [--line-length=120] - repo: https://github.com/PyCQA/flake8 - rev: 7.1.1 + rev: 7.3.0 hooks: - id: flake8 args: [--max-line-length=120] - repo: https://github.com/asottile/blacken-docs - rev: 1.19.1 + rev: 1.20.0 hooks: - id: blacken-docs additional_dependencies: [black==22.12.0] files: ^(.*\.py|.*\.rst)$ args: [--line-length=120] + +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.19.1 + hooks: + - id: mypy + exclude: ^docs/conf\.py$|libensemble/utils/(launcher|loc_stack|runners|pydantic|output_directory)\.py$|libensemble/tests/(regression_tests|functionality_tests|unit_tests|scaling_tests)/.* diff --git a/.readthedocs.yml b/.readthedocs.yml index 50b0395fae..d7c674392b 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -3,18 +3,35 @@ version: 2 build: os: "ubuntu-22.04" tools: - python: "3.10" + python: "3.12" + commands: + # from https://docs.readthedocs.com/platform/stable/build-customization.html#support-git-lfs-large-file-storage + # Download and uncompress the binary + # https://git-lfs.github.com/ + - wget https://github.com/git-lfs/git-lfs/releases/download/v3.1.4/git-lfs-linux-amd64-v3.1.4.tar.gz + - tar xvfz git-lfs-linux-amd64-v3.1.4.tar.gz git-lfs + # Modify LFS config paths to point where git-lfs binary was downloaded + - git config filter.lfs.process "`pwd`/git-lfs filter-process" + - git config filter.lfs.smudge "`pwd`/git-lfs smudge -- %f" + - git config filter.lfs.clean "`pwd`/git-lfs clean -- %f" + # Make LFS available in current repository + - ./git-lfs install + # Download content from remote + - ./git-lfs fetch + # Make local files to have the real content on them + - ./git-lfs checkout + - asdf plugin add pixi + - asdf install pixi latest + - asdf global pixi latest + - pixi run -e docs build-docs + - mkdir -p $READTHEDOCS_OUTPUT/html/ + - cp -r docs/_build/html/** $READTHEDOCS_OUTPUT/html/ + - pixi run -e docs build-pdf + - mkdir -p $READTHEDOCS_OUTPUT/pdf/ + - cp -r docs/_build/latex/libEnsemble.pdf $READTHEDOCS_OUTPUT/pdf/ sphinx: configuration: docs/conf.py formats: - pdf - -python: - install: - - requirements: docs/requirements.txt - - method: pip - path: . - extra_requirements: - - docs diff --git a/.wci.yml b/.wci.yml index f03aa0c635..db127beebe 100644 --- a/.wci.yml +++ b/.wci.yml @@ -16,8 +16,8 @@ description: | language: Python release: - version: 1.5.0 - date: 2025-04-10 + version: 1.6.0 + date: 2026-03-04 documentation: general: https://libensemble.readthedocs.io diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..f5673f64a7 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,101 @@ +Agent Contributor Guidelines and Information +============================================ + +Read the ``README.rst`` for an overview of libEnsemble. + +- libEnsemble uses a manager-worker architecture. Points are generated by a generator and sent to a worker, which runs a simulator. +- The manager determines how and when points get passed to workers via an allocation function. +- See ``libensemble/tests/regression_tests/test_1d_sampling.py`` for a simple example of the libEnsemble interface. + +Critical Repository Layout Information +-------------------------------------- + +- ``libensemble/`` - Source code. +- ``/alloc_funcs`` - Allocation functions. Policies for passing work between the manager and workers. +- ``/comms`` - Modules and abstractions for communication between the manager and workers. +- ``/executors`` - An interface for launching executables, often simulations. +- ``/gen_classes`` - Generators that adhere to the `gest-api` standard. + Recommended over entries from ``/gen_funcs`` that perform similar functionality. +- ``/gen_funcs`` - Generator functions. Modules for producing points for simulations. (Legacy) +- ``/resources`` - Classes and functions for managing compute resources for MPI tasks, libensemble workers. +- ``/sim_funcs`` - Simulator functions. Modules for running simulations or performing experiments. +- ``/tests`` - Tests. + - ``/functionality_tests`` - Primarily tests libEnsemble code only. + - ``/regression_tests`` - Tests libEnsemble code with external code. Often more closely resembles actual use-cases. + - ``/unit_tests`` - Tests for individual modules. +- ``ensemble.py`` - The primary interface for parameterizing and running libEnsemble. The ``Ensemble`` class in this module wraps the lower-level ``libE`` function and automates argument parsing and state management. +- ``generators.py`` - Base classes for generators that adhere to the `gest-api` standard. +- ``history.py`` - Module for recording points that have been generated and simulation results. NumPy structured array. +- ``libE.py`` - libE main file. Previous primary interface for parameterizing and running libEnsemble. The primary interface in ``ensemble.py`` wraps this function. +- ``manager.py`` - Module for maintaining the history array and passing points between the workers. +- ``message_numbers.py`` - Constants that represent states of the ensemble. +- ``specs.py`` - Dataclasses for parameterizing the ensemble. Most importantly, contains ``LibeSpecs, SimSpecs, GenSpecs``. +- ``worker.py`` - Module for running generators and simulators. Communicates with the manager. +- ``examples/`` - The ``*_funcs`` and ``calling_scripts`` directories contain symlinks to examples further in the source code. +- ``/libE_submission_scripts`` - Example scripts for submitting libEnsemble jobs to HPC systems. +- ``/tutorials`` - Tutorials on how to use libEnsemble. + +Information about Generators +---------------------------- + +- Generators are functions or objects that produce points for simulations. +- The History array is a NumPy structured array that stores points that have been generated and simulation results. +Its fields match ``sim_specs/gen_specs["out"]`` or ``vocs`` attributes, plus additional reserved fields for metadata. +- Prior to libEnsemble v1.6.0, generators were plain functions. They often ran in "persistent" mode, meaning they executed in a +long-running loop, sending and receiving points to and from the manager until the ensemble was complete. +- A ``gest-api`` or "standardized" generator is a class that inherits from ``gest_api.Generator``, implements ``suggest`` and ``ingest`` methods (which process lists of dictionaries, not NumPy arrays), and is parameterized by a ``vocs``. +- See ``libensemble/gen_classes/external/sampling.py`` for simple examples of the pure ``gest-api`` interface. (Note: ``libensemble.generators.LibensembleGenerator`` exists to wrap legacy NumPy-based workflows, but pure ``gest_api.Generator`` is preferred). +- Generators are often used for simple sampling, optimization, calibration, uncertainty quantification, and other simulation-based tasks. +- **Automatic Variable Mapping**: When using ``LibensembleGenerator`` subclasses, they automatically map all ``VOCS`` variables to a single multi-dimensional ``"x"`` field in the History array if no explicit ``variables_mapping`` is provided. Pure ``gest_api.Generator`` classes handle variables natively. +- **Mandatory Input Fields**: Even for simple generators that don't ingest data, ``gen_specs["in"]`` or ``gen_specs["persis_in"]`` must be defined if using an allocation function like ``only_persistent_gens`` that attempts to send rows. If these are empty, the manager will raise an ``AssertionError`` stating that no fields were requested to be sent. +- **Default Allocator**: ``only_persistent_gens`` is the default allocator for standardized ``gest-api`` generators. It treats these generators as persistent entities that communicate throughout the run. + +General Guidelines +------------------ + +- If using classic ``sim_specs`` and ``gen_specs``, then ensure that ``sim_specs["out"]`` and ``gen_specs["in"]`` field names match, and vice-versa. +- As-of libEnsemble v1.6.0, ``SimSpecs`` and ``GenSpecs`` can also be parameterized by a ``vocs`` object, imported from ``gest_api.vocs`` (NOT xopt.vocs). +- ``VOCS`` contains variables, objectives, constraints, and other settings that define the problem. +See ``libensemble/tests/regression_tests/test_xopt_EI.py`` for an example of how to use it. +- An MPI distribution is not required for libEnsemble to run, but is required to use the ``MPIExecutor``. ``mpich`` is recommended. +- New tests are heavily encouraged for new features, bug fixes, or integrations. See ``libensemble/tests/regression_tests`` for examples. +- Never use destructive git commands unless explicitly requested. +- Code is in the ``black`` style. This should be enforced by ``pre-commit``. +- When writing new code, prefer the ``LibeSpecs``, ``SimSpecs``, and ``GenSpecs`` dataclasses over the classic ``sim_specs`` and ``gen_specs`` bare dictionaries. +- Read ``CONTRIBUTING.md`` for more information. +- The external ``libE-community-examples`` repository contains past use-cases, generators, and other examples. + +Development Environment +----------------------- + +- ``pixi`` is the recommended environment manager for libEnsemble development. See ``pyproject.toml`` for the list +of dependencies and the available testing environments. (Note: If ``pixi`` is not in your system path, it can often be found in ``/opt/homebrew/bin/pixi`` or ``/usr/local/bin/pixi``). +- Enter the development environment with ``pixi shell -e dev``. This environment contains the most common dependencies for development and testing. +- For one-off commands, use ``pixi run -e dev``. This will run a single command in the development environment. +- If ``pixi`` is not available or not preferred by the user, ``pip install -e .`` can be used instead. Other dependencies may need to be installed manually. +- If committing, use ``pre-commit`` to ensure that code style and formatting are consistent. See ``.pre-commit-config.yaml`` for +the configuration and ``pyproject.toml`` for other configuration. + +Testing +------- + +- Run tests with the ``run_tests.py`` script: ``python libensemble/tests/run_tests.py``. See ``libensemble/tests/run_tests.py`` for usage information. +- Some tests require third party software to be installed. When developing a feature or fixing a bug, since the entire test suite will be run on Github Actions, +for local development running individual tests is sufficient. +- Individual unit tests can be run with ``pixi run -e dev pytest path/to/test_file``. +- A libEnsemble run typically outputs an ``ensemble.log`` and ``libE_stats.txt`` file in the working directory. Check these files for tracebacks or run statistics. +- An "ensemble" or "workflow" directory may also be created, often containing per-simulation output directories + +Modernizing Scripts for libEnsemble 2.0 +--------------------------------------- + +When modernizing existing libEnsemble scripts (functionality tests, regression tests, or user examples) for version 2.0, follow these steps: + +- **Switch to `gest-api` Generators**: Replace legacy generator functions (from `libensemble.gen_funcs`) with standardized generator classes (from `libensemble.gen_classes` or other `gest-api` compatible sources). +- **Use `VOCS` for Parameterization**: Standardized generators are parameterized by a `VOCS` object (from `gest_api.vocs`). Define variables and objectives within this object. +- **Set `gen_specs["generator"]`**: Instead of `gen_f`, use the `generator` field in `GenSpecs` to pass the initialized generator class. +- **Remove Explicit `AllocSpecs`**: In libEnsemble 2.0, `only_persistent_gens` is the default allocator. Scripts that previously used `give_sim_work_first` or other simple allocators can often remove `alloc_specs` entirely when switching to standardized generators. +- **Generator Placement**: By default, generators run on the manager thread (Worker 0). This means all allocated workers are available for simulation tasks unless `gen_on_worker` is explicitly set to `True` in `libE_specs`. +- **Mandatory Fields**: Ensure `gen_specs["in"]` or `gen_specs["persis_in"]` includes at least one field (e.g., `["sim_id"]`) if feedback is sent back to the generator, to satisfy the allocator's requirements. +- **gest-api Simulators**: The gest-api pattern also applies to simulators. Set `SimSpecs.simulator` to a callable with signature `(input_dict: dict, **kwargs) -> dict` instead of providing a `sim_f`. libEnsemble automatically wraps it with `gest_api_sim` from `libensemble.sim_funcs.gest_api_wrapper` and handles all NumPy conversions. `SimSpecs.inputs` and `SimSpecs.outputs` can be derived automatically when `SimSpecs.vocs` is provided. +- **`safe_mode` is opt-in**: `libE_specs["safe_mode"]` defaults to `False`, meaning protected History fields (`gen_worker`, `gen_started_time`, `gen_ended_time`, `sim_worker`, `sim_started`, `sim_started_time`, `sim_ended`, `sim_ended_time`, `gen_informed`, `gen_informed_time`, `kill_sent`) are freely overwritable by default. Set `safe_mode=True` to enable protection. Overwriting these fields without understanding their purpose may crash libEnsemble. diff --git a/CHANGELOG.rst b/CHANGELOG.rst index bbb2bee549..d950fdd3ca 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,6 +8,45 @@ GitHub issues are referenced, and can be viewed with hyperlinks on the `github r .. _`github releases page`: https://github.com/Libensemble/libensemble/releases +Release 1.6.0 +-------------- + +:Date: March 04, 2026 + +General Updates: + +* Support for ``gest-api`` generators (https://gest-api.readthedocs.io). #1307 + + * Support for any Xopt (v3.0+) and Optimas generators. + * libEnsemble's APOSMM, gpCAM, and random sampling generators are supplied in ``gest-api`` format. + * Support dictionary (Xopt-style) simulator functions. + +* Simulation container support - Executor precedent accepts ``%LIBENSEMBLE_SIM_DIR%`` placeholder. #1672 + +Examples: + +* Adding test for ibcdfo with jax. #1591 +* Optimas/Xopt examples. #1620 / #1635 +* Bayesian Optimization with Xopt tutorial / notebook. +* Tasmanian generators moved to community examples. + +Dependencies: + +* ``gest-api`` is now a required dependency. #1666 +* Remove Pydantic v1 support and Balsam. #1573 +* Python 3.14 supported. #1609 + + +:Note: + +* Tests were run on Linux and MacOS with Python versions 3.10, 3.11, 3.12, 3.13, 3.14 +* Heterogeneous workflows tested on Aurora (ALCF) and Perlmutter (NERSC). + +:Known Issues: + +* See known issues section in the documentation. + + Release 1.5.0 -------------- diff --git a/LICENSE b/LICENSE index 6a45c6a4cf..8a1c9cd833 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2018-2025, UChicago Argonne, LLC and the libEnsemble Development Team +Copyright (c) 2018-2026, UChicago Argonne, LLC and the libEnsemble Development Team All Rights Reserved. Redistribution and use in source and binary forms, with or without diff --git a/README.rst b/README.rst index 246eba14bf..77f73518db 100644 --- a/README.rst +++ b/README.rst @@ -22,8 +22,15 @@ and inference problems on the world's leading supercomputers such as Frontier, A `Quickstart`_ -**New:** Try out the |ScriptCreator| to generate customized scripts for running -ensembles with your MPI applications. +**New:** libEnsemble nows supports the `gest-api`_ generator standard, and can run with +Optimas and Xopt generators. + +.. before_script_creator_tag + +Try the |ScriptCreator| to generate customized scripts for running ensembles with your +MPI applications. + +.. after_script_creator_tag Installation ============ @@ -38,52 +45,75 @@ Basic Usage =========== Create an ``Ensemble``, then customize it with general settings, simulation and generator parameters, -and an exit condition. Run the following four-worker example via ``python this_file.py``: +and an exit condition. .. code-block:: python import numpy as np + from gest_api.vocs import VOCS from libensemble import Ensemble - from libensemble.gen_funcs.sampling import uniform_random_sample - from libensemble.sim_funcs.six_hump_camel import six_hump_camel - from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs + from libensemble.gen_classes.sampling import UniformSample + from libensemble.specs import LibeSpecs, SimSpecs, GenSpecs, ExitCriteria + + + def six_hump_camel_func(calc_in: dict): + """ + Definition of the six-hump camel test function. + """ + x0 = calc_in["x0"] + x1 = calc_in["x1"] + term1 = (4 - 2.1 * x0**2 + (x0**4) / 3) * x0**2 + term2 = x0 * x1 + term3 = (-4 + 4 * x1**2) * x1**2 + + return {"f": term1 + term2 + term3} + if __name__ == "__main__": + # Define problem using VOCS + vocs = VOCS( + variables={"x0": [-3, 3], "x1": [-2, 2]}, + objectives={"f": "EXPLORE"}, + ) + + # General settings libE_specs = LibeSpecs(nworkers=4) + # Specify the simulator function sim_specs = SimSpecs( - sim_f=six_hump_camel, - inputs=["x"], - outputs=[("f", float)], + simulator=six_hump_camel_func, + vocs=vocs, ) + # Initialize generator + generator = UniformSample(vocs) + + # Specify the generator and other parameters gen_specs = GenSpecs( - gen_f=uniform_random_sample, - outputs=[("x", float, 2)], - user={ - "gen_batch_size": 50, - "lb": np.array([-3, -2]), - "ub": np.array([3, 2]), - }, + generator=generator, + vocs=vocs, + batch_size=50, ) exit_criteria = ExitCriteria(sim_max=100) - sampling = Ensemble( + # Create ensemble + ensemble = Ensemble( libE_specs=libE_specs, sim_specs=sim_specs, gen_specs=gen_specs, exit_criteria=exit_criteria, ) - sampling.add_random_streams() - sampling.run() + # Run ensemble + ensemble.run() + + ensemble.save_output(__file__) + print("Some output data:\n", ensemble.H[["x0", "x1", "f"]][:10]) - if sampling.is_manager: - sampling.save_output(__file__) - print("Some output data:\n", sampling.H[["x", "f"]][:10]) +.. before_colab_tag |Inline Example| @@ -100,6 +130,10 @@ Try some other examples live in Colab. +---------------------------------------------------------------+-------------------------------------+ | Surrogate model generation with gpCAM. | |Surrogate Modeling| | +---------------------------------------------------------------+-------------------------------------+ +| Bayesian Optimization with Xopt. | |Bayesian Optimization with Xopt| | ++---------------------------------------------------------------+-------------------------------------+ + +.. after_colab_tag There are many more examples in the `regression tests`_ and `Community Examples repository`_. @@ -161,6 +195,7 @@ Resources .. _conda-forge: https://conda-forge.org/ .. _Contributions: https://github.com/Libensemble/libensemble/blob/main/CONTRIBUTING.rst .. _docs: https://libensemble.readthedocs.io/en/main/advanced_installation.html +.. _gest-api: https://gest-api.readthedocs.io .. _GitHub: https://github.com/Libensemble/libensemble .. _libEnsemble mailing list: https://lists.mcs.anl.gov/mailman/listinfo/libensemble .. _libEnsemble Slack page: https://libensemble.slack.com @@ -186,6 +221,9 @@ Resources .. |Surrogate Modeling| image:: https://colab.research.google.com/assets/colab-badge.svg :target: https://colab.research.google.com/github/Libensemble/libensemble/blob/develop/examples/tutorials/gpcam_surrogate_model/gpcam.ipynb -.. |ScriptCreator| image:: https://img.shields.io/badge/Script_Creator-purple?logo=magic +.. |Bayesian Optimization with Xopt| image:: https://colab.research.google.com/assets/colab-badge.svg + :target: https://colab.research.google.com/github/Libensemble/libensemble/blob/develop/examples/tutorials/xopt_bayesian_gen/xopt_EI_example.ipynb + +.. |ScriptCreator| image:: https://img.shields.io/badge/Script_Creator-purple :target: https://libensemble.github.io/script-creator/ :alt: Script Creator diff --git a/SUPPORT.rst b/SUPPORT.rst index e86b6a1e6a..bf6f68d9cb 100644 --- a/SUPPORT.rst +++ b/SUPPORT.rst @@ -1,6 +1,10 @@ Support ------- +Open issues on Github at: + +* https://github.com/Libensemble/libensemble/issues + Join the libEnsemble mailing list at: * https://lists.mcs.anl.gov/mailman/listinfo/libensemble diff --git a/docs/FAQ.rst b/docs/FAQ.rst index b8a3ea2ce5..0339dbb681 100644 --- a/docs/FAQ.rst +++ b/docs/FAQ.rst @@ -13,11 +13,6 @@ We recommend using the following options to help debug workflows:: logger.set_level("DEBUG") libE_specs["safe_mode"] = True -To make it easier to debug a generator try setting the **libE_specs** option ``gen_on_manager``. -To do so, add the following to your calling script:: - - libE_specs["gen_on_manager"] = True - With this, ``pdb`` breakpoints can be set as usual in the generator. For more debugging options see "How can I debug specific libEnsemble processes?" below. diff --git a/docs/_static/libE_logo.png b/docs/_static/libE_logo.png new file mode 100755 index 0000000000..17f051faab Binary files /dev/null and b/docs/_static/libE_logo.png differ diff --git a/docs/_static/libE_logo_white.png b/docs/_static/libE_logo_white.png new file mode 100644 index 0000000000..220de8766e Binary files /dev/null and b/docs/_static/libE_logo_white.png differ diff --git a/docs/advanced_installation.rst b/docs/advanced_installation.rst deleted file mode 100644 index c02a63ed63..0000000000 --- a/docs/advanced_installation.rst +++ /dev/null @@ -1,194 +0,0 @@ -Advanced Installation -===================== - -libEnsemble can be installed from ``pip``, ``Conda``, or ``Spack``. - -libEnsemble requires the following dependencies, which are typically -automatically installed alongside libEnsemble: - -* Python_ ``>= 3.10`` -* NumPy_ ``>= 1.21`` -* psutil_ ``>= 5.9.4`` -* `pydantic`_ ``>= 1.10.12`` -* pyyaml_ ``>= v6.0`` -* tomli_ ``>= 1.2.1`` - -Given libEnsemble's compiled dependencies, the following installation -methods each offer a trade-off between convenience and the ability -to customize builds, including platform-specific optimizations. - -We always recommend installing in a virtual environment from Conda or another source. - -Further recommendations for selected HPC systems are given in the -:ref:`HPC platform guides`. - -.. tab-set:: - - .. tab-item:: pip - - To install the latest PyPI_ release:: - - pip install libensemble - - To pip install libEnsemble from the latest develop branch:: - - python -m pip install --upgrade git+https://github.com/Libensemble/libensemble.git@develop - - **Installing with mpi4py** - - If you wish to use ``mpi4py`` with libEnsemble (choosing MPI out of the three - :doc:`communications options`), then this should - be installed to work with the existing MPI on your system. For example, - the following line:: - - pip install mpi4py - - will use the ``mpicc`` compiler wrapper on your PATH to identify the MPI library. - To specify a different compiler wrapper, add the ``MPICC`` option. - You also may wish to avoid existing binary builds; for example,:: - - MPICC=mpiicc pip install mpi4py --no-binary mpi4py - - On Summit, the following line is recommended (with gcc compilers):: - - CC=mpicc MPICC=mpicc pip install mpi4py --no-binary mpi4py - - .. tab-item:: conda - - Install libEnsemble with Conda_ from the conda-forge channel:: - - conda config --add channels conda-forge - conda install -c conda-forge libensemble - - This package comes with some useful optional dependencies, including - optimizers and will install quickly as ready binary packages. - - **Installing with mpi4py with Conda** - - If you wish to use ``mpi4py`` with libEnsemble (choosing MPI out of the three - :doc:`communications options`), you can use the - following. - - .. note:: - For clusters and HPC systems, always install ``mpi4py`` to use the - system MPI library (see pip instructions above). - - For a standalone build that comes with an MPI implementation, you can install - libEnsemble using one of the following variants. - - To install libEnsemble with MPICH_:: - - conda install -c conda-forge libensemble=*=mpi_mpich* - - To install libEnsemble with `Open MPI`_:: - - conda install -c conda-forge libensemble=*=mpi_openmpi* - - The asterisks will pick up the latest version and build. - - .. note:: - This syntax may not work without adjustments on macOS or any non-bash - shell. In these cases, try:: - - conda install -c conda-forge libensemble='*'=mpi_mpich'*' - - For a complete list of builds for libEnsemble on Conda:: - - conda search libensemble --channel conda-forge - - .. tab-item:: Spack - - Install libEnsemble using the Spack_ distribution:: - - spack install py-libensemble - - The above command will install the latest release of libEnsemble with - the required dependencies only. Other optional - dependencies can be specified through variants. The following - line installs libEnsemble version 0.7.2 with some common variants - (e.g., using :doc:`APOSMM<../examples/aposmm>`): - - .. code-block:: bash - - spack install py-libensemble @0.7.2 +mpi +scipy +mpmath +petsc4py +nlopt - - The list of variants can be found by running:: - - spack info py-libensemble - - On some platforms you may wish to run libEnsemble without ``mpi4py``, - using a serial PETSc build. This is often preferable if running on - the launch nodes of a three-tier system (e.g., Summit):: - - spack install py-libensemble +scipy +mpmath +petsc4py ^py-petsc4py~mpi ^petsc~mpi~hdf5~hypre~superlu-dist - - The installation will create modules for libEnsemble and the dependent - packages. These can be loaded by running:: - - spack load -r py-libensemble - - Any Python packages will be added to the PYTHONPATH when the modules are loaded. If you do not have - modules on your system you may need to install ``lmod`` (also available in Spack):: - - spack install lmod - . $(spack location -i lmod)/lmod/lmod/init/bash - spack load lmod - - Alternatively, Spack could be used to build the serial ``petsc4py``, and Conda could use this by loading - the ``py-petsc4py`` module thus created. - - **Hint**: When combining Spack and Conda, you can access your Conda Python and packages in your - ``~/.spack/packages.yaml`` while your Conda environment is activated, using ``CONDA_PREFIX`` - For example, if you have an activated Conda environment with Python 3.10 and SciPy installed: - - .. code-block:: yaml - - packages: - python: - externals: - - spec: "python" - prefix: $CONDA_PREFIX - buildable: False - py-numpy: - externals: - - spec: "py-numpy" - prefix: $CONDA_PREFIX/lib/python3.10/site-packages/numpy - buildable: False - py-scipy: - externals: - - spec: "py-scipy" - prefix: $CONDA_PREFIX/lib/python3.10/site-packages/scipy - buildable: True - - For more information on Spack builds and any particular considerations - for specific systems, see the spack_libe_ repository. In particular, this - includes some example ``packages.yaml`` files (which go in ``~/.spack/``). - These files are used to specify dependencies that Spack must obtain from - the given system (rather than building from scratch). This may include - ``Python`` and the packages distributed with it (e.g., ``numpy``), and will - often include the system MPI library. - -Optional Dependencies for Additional Features ---------------------------------------------- - -The following packages may be installed separately to enable additional features: - -* pyyaml_ and tomli_ - Parameterize libEnsemble via yaml or toml -* `Globus Compute`_ - Submit simulation or generator function instances to remote Globus Compute endpoints - -.. _conda-forge: https://conda-forge.org/ -.. _Conda: https://docs.conda.io/en/latest/ -.. _GitHub: https://github.com/Libensemble/libensemble -.. _Globus Compute: https://www.globus.org/compute -.. _MPICH: https://www.mpich.org/ -.. _NumPy: http://www.numpy.org -.. _Open MPI: https://www.open-mpi.org/ -.. _psutil: https://pypi.org/project/psutil/ -.. _pydantic: https://docs.pydantic.dev/1.10/ -.. _PyPI: https://pypi.org -.. _Python: http://www.python.org -.. _pyyaml: https://pyyaml.org/ -.. _Spack: https://spack.readthedocs.io/en/latest -.. _spack_libe: https://github.com/Libensemble/spack_libe -.. _tomli: https://pypi.org/project/tomli/ -.. _tqdm: https://tqdm.github.io/ diff --git a/docs/advanced_installation/advanced_installation.rst b/docs/advanced_installation/advanced_installation.rst new file mode 100644 index 0000000000..fc2e8546a4 --- /dev/null +++ b/docs/advanced_installation/advanced_installation.rst @@ -0,0 +1,41 @@ +Advanced Installation +===================== + +`pip `__ \|\| `uv `__ \|\| `pixi `__ \|\| `conda `__ \|\| `Spack `__ + +libEnsemble can be installed from ``pip``, ``uv``, ``pixi``, ``Conda``, or ``Spack``. + +libEnsemble requires the following dependencies, which are typically +automatically installed alongside libEnsemble: + +* Python_ ``>= 3.11`` +* NumPy_ ``>= 1.21`` +* psutil_ ``>= 5.9.4`` +* `pydantic`_ ``>= 2`` +* gest-api_ ``>= 0.1,<0.2`` + +We recommend installing in a virtual environment from ``uv``, ``conda`` or another source. + +Further recommendations for selected HPC systems are given in the +:ref:`HPC platform guides`. + +.. toctree:: + :hidden: + + advanced_installation_pip + advanced_installation_uv + advanced_installation_pixi + advanced_installation_conda + advanced_installation_spack + +Globus Compute +-------------- + +`Globus Compute`_ may be installed optionally to submit simulation function instances to remote Globus Compute endpoints. + +.. _Globus Compute: https://www.globus.org/compute +.. _Python: http://www.python.org +.. _NumPy: http://www.numpy.org +.. _psutil: https://pypi.org/project/psutil/ +.. _pydantic: https://docs.pydantic.dev/1.10/ +.. _gest-api: https://github.com/campa-consortium/gest-api diff --git a/docs/advanced_installation/advanced_installation_conda.rst b/docs/advanced_installation/advanced_installation_conda.rst new file mode 100644 index 0000000000..c34ce25b1a --- /dev/null +++ b/docs/advanced_installation/advanced_installation_conda.rst @@ -0,0 +1,49 @@ +conda +===== + +`Advanced Installation `__ \|\| `pip `__ \|\| `uv `__ \|\| `pixi `__ \|\| **conda** \|\| `Spack `__ + +Install libEnsemble with Conda_ from the conda-forge channel:: + + conda config --add channels conda-forge + conda install -c conda-forge libensemble + +This package comes with some useful optional dependencies, including +optimizers and will install quickly as ready binary packages. + +**Installing with mpi4py with Conda** + +If you wish to use ``mpi4py`` with libEnsemble (choosing MPI out of the three +:doc:`communications options<../running_libE>`), you can use the +following. + +.. note:: + For clusters and HPC systems, always install ``mpi4py`` to use the + system MPI library (see pip instructions above). + +For a standalone build that comes with an MPI implementation, you can install +libEnsemble using one of the following variants. + +To install libEnsemble with MPICH_:: + + conda install -c conda-forge libensemble=*=mpi_mpich* + +To install libEnsemble with `Open MPI`_:: + + conda install -c conda-forge libensemble=*=mpi_openmpi* + +The asterisks will pick up the latest version and build. + +.. note:: + This syntax may not work without adjustments on macOS or any non-bash + shell. In these cases, try:: + + conda install -c conda-forge libensemble='*'=mpi_mpich'*' + +For a complete list of builds for libEnsemble on Conda:: + + conda search libensemble --channel conda-forge + +.. _Conda: https://docs.conda.io/en/latest/ +.. _MPICH: https://www.mpich.org/ +.. _Open MPI: https://www.open-mpi.org/ diff --git a/docs/advanced_installation/advanced_installation_pip.rst b/docs/advanced_installation/advanced_installation_pip.rst new file mode 100644 index 0000000000..9416765b1c --- /dev/null +++ b/docs/advanced_installation/advanced_installation_pip.rst @@ -0,0 +1,29 @@ +pip +=== + +`Advanced Installation `__ \|\| **pip** \|\| `uv `__ \|\| `pixi `__ \|\| `conda `__ \|\| `Spack `__ + +To install the latest PyPI_ release:: + + pip install libensemble + +To pip install libEnsemble from the latest develop branch:: + + python -m pip install --upgrade git+https://github.com/Libensemble/libensemble.git@develop + +**Installing with mpi4py** + +If you wish to use ``mpi4py`` with libEnsemble (choosing MPI out of the three +:doc:`communications options<../running_libE>`), then this should +be installed to work with the existing MPI on your system. For example, +the following line:: + + pip install mpi4py + +will use the ``mpicc`` compiler wrapper on your PATH to identify the MPI library. +To specify a different compiler wrapper, add the ``MPICC`` option. +You also may wish to avoid existing binary builds; for example,:: + + MPICC=mpiicc pip install mpi4py --no-binary mpi4py + +.. _PyPI: https://pypi.org diff --git a/docs/advanced_installation/advanced_installation_pixi.rst b/docs/advanced_installation/advanced_installation_pixi.rst new file mode 100644 index 0000000000..8227fcbd87 --- /dev/null +++ b/docs/advanced_installation/advanced_installation_pixi.rst @@ -0,0 +1,20 @@ +pixi +==== + +`Advanced Installation `__ \|\| `pip `__ \|\| `uv `__ \|\| **pixi** \|\| `conda `__ \|\| `Spack `__ + +Add to your pixi_ environment:: + + pixi add libensemble + +libEnsemble is also distributed with locked pixi environments for different versions of Python +and various dependency sets, primarily for testing but also useful for guaranteed working environments. +See a list with:: + + pixi workspace environment list + +and activate with:: + + pixi shell -e + +.. _pixi: https://pixi.prefix.dev/latest/ diff --git a/docs/advanced_installation/advanced_installation_spack.rst b/docs/advanced_installation/advanced_installation_spack.rst new file mode 100644 index 0000000000..3e9b1132e3 --- /dev/null +++ b/docs/advanced_installation/advanced_installation_spack.rst @@ -0,0 +1,77 @@ +Spack +===== + +`Advanced Installation `__ \|\| `pip `__ \|\| `uv `__ \|\| `pixi `__ \|\| `conda `__ \|\| **Spack** + +Install libEnsemble using the Spack_ distribution:: + + spack install py-libensemble + +The above command will install the latest release of libEnsemble with +the required dependencies only. Other optional +dependencies can be specified through variants. The following +line installs libEnsemble version 1.5.0 with some common variants +(e.g., using :doc:`APOSMM<../examples/gest_api/aposmm>`): + +.. code-block:: bash + + spack install py-libensemble @1.5.0 +mpi +scipy +mpmath +petsc4py +nlopt + +The list of variants can be found by running:: + + spack info py-libensemble + +On some platforms you may wish to run libEnsemble without ``mpi4py``, +using a serial PETSc build. This is often preferable if running on +the launch nodes of a three-tier system:: + + spack install py-libensemble +scipy +mpmath +petsc4py ^py-petsc4py~mpi ^petsc~mpi~hdf5~hypre~superlu-dist + +The installation will create modules for libEnsemble and the dependent +packages. These can be loaded by running:: + + spack load -r py-libensemble + +Any Python packages will be added to the PYTHONPATH when the modules are loaded. If you do not have +modules on your system you may need to install ``lmod`` (also available in Spack):: + + spack install lmod + . $(spack location -i lmod)/lmod/lmod/init/bash + spack load lmod + +Alternatively, Spack could be used to build the serial ``petsc4py``, and Conda could use this by loading +the ``py-petsc4py`` module thus created. + +**Hint**: When combining Spack and Conda, you can access your Conda Python and packages in your +``~/.spack/packages.yaml`` while your Conda environment is activated, using ``CONDA_PREFIX`` +For example, if you have an activated Conda environment with Python 3.11 and SciPy installed: + +.. code-block:: yaml + + packages: + python: + externals: + - spec: "python" + prefix: $CONDA_PREFIX + buildable: False + py-numpy: + externals: + - spec: "py-numpy" + prefix: $CONDA_PREFIX/lib/python3.11/site-packages/numpy + buildable: False + py-scipy: + externals: + - spec: "py-scipy" + prefix: $CONDA_PREFIX/lib/python3.11/site-packages/scipy + buildable: True + +For more information on Spack builds and any particular considerations +for specific systems, see the spack_libe_ repository. In particular, this +includes some example ``packages.yaml`` files (which go in ``~/.spack/``). +These files are used to specify dependencies that Spack must obtain from +the given system (rather than building from scratch). This may include +``Python`` and the packages distributed with it (e.g., ``numpy``), and will +often include the system MPI library. + +.. _Spack: https://spack.readthedocs.io/en/latest +.. _spack_libe: https://github.com/Libensemble/spack_libe diff --git a/docs/advanced_installation/advanced_installation_uv.rst b/docs/advanced_installation/advanced_installation_uv.rst new file mode 100644 index 0000000000..b10b64bfa5 --- /dev/null +++ b/docs/advanced_installation/advanced_installation_uv.rst @@ -0,0 +1,11 @@ +uv +== + +`Advanced Installation `__ \|\| `pip `__ \|\| **uv** \|\| `pixi `__ \|\| `conda `__ \|\| `Spack `__ + +To install the latest PyPI_ release via uv_:: + + uv pip install libensemble + +.. _PyPI: https://pypi.org +.. _uv: https://docs.astral.sh/uv/ diff --git a/docs/conf.py b/docs/conf.py index 7686b741f8..8647bbf4f7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -31,7 +31,7 @@ def __getattr__(cls, name): return MagicMock() -autodoc_mock_imports = ["ax", "balsam", "gpcam", "IPython", "matplotlib", "pandas", "scipy", "surmise"] +autodoc_mock_imports = ["ax", "gpcam", "IPython", "matplotlib", "pandas", "scipy", "surmise"] MOCK_MODULES = [ "argparse", @@ -59,17 +59,12 @@ class AxParameterWarning(Warning): # Ensure it's a real warning subclass sys.modules["ax.exceptions.core"] = MagicMock() sys.modules["ax.exceptions.core"].AxParameterWarning = AxParameterWarning -# from libensemble import * -# from libensemble.alloc_funcs import * -# from libensemble.gen_funcs import * -# from libensemble.sim_funcs import * - # sys.path.insert(0, os.path.abspath('.')) sys.path.append(os.path.abspath("../libensemble")) -##sys.path.append(os.path.abspath('../libensemble')) sys.path.append(os.path.abspath("../libensemble/alloc_funcs")) sys.path.append(os.path.abspath("../libensemble/gen_funcs")) +sys.path.append(os.path.abspath("../libensemble/gen_classes")) sys.path.append(os.path.abspath("../libensemble/sim_funcs")) sys.path.append(os.path.abspath("../libensemble/comms")) sys.path.append(os.path.abspath("../libensemble/utils")) @@ -96,11 +91,11 @@ class AxParameterWarning(Warning): # Ensure it's a real warning subclass "sphinx.ext.napoleon", # 'sphinx.ext.autosectionlabel', "sphinx.ext.intersphinx", - "sphinx.ext.imgconverter", "sphinx.ext.mathjax", "sphinxcontrib.autodoc_pydantic", "sphinx_design", "sphinx_copybutton", + "sphinx_lfs_content", ] spelling_word_list_filename = "spelling_wordlist.txt" @@ -111,13 +106,7 @@ class AxParameterWarning(Warning): # Ensure it's a real warning subclass bibtex_bibfiles = ["references.bib"] bibtex_default_style = "unsrt" -# autosectionlabel_prefix_document = True -# extensions = ['sphinx.ext.autodoc', 'sphinx.ext.napoleon', 'sphinx.ext.imgconverter'] -# breathe_projects = { "libEnsemble": "../code/src/xml/" } -# breathe_default_project = "libEnsemble" -##breathe_projects_source = {"libEnsemble" : ( "../code/src/", ["libE.py", "test.cpp"] )} -# breathe_projects_source = {"libEnsemble" : ( "../code/src/", ["test.cpp","test2.cpp"] )} autodoc_member_order = "bysource" model_show_field_summary = "bysource" @@ -184,6 +173,7 @@ class AxParameterWarning(Warning): # Ensure it's a real warning subclass # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" +pygments_dark_style = "monokai" # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False @@ -209,9 +199,9 @@ class AxParameterWarning(Warning): # Ensure it's a real warning subclass # html_theme = 'sphinxdoc' # html_theme = "sphinx_book_theme" -html_theme = "sphinx_rtd_theme" +html_theme = "furo" -html_logo = "./images/libE_logo_white.png" +# html_logo = "./images/libE_logo_white.png" html_favicon = "./images/libE_logo_circle.png" html_title = "libEnsemble" @@ -220,7 +210,12 @@ class AxParameterWarning(Warning): # Ensure it's a real warning subclass # documentation. # html_theme_options = { - "logo_only": True, + "announcement": "libEnsemble v2.0 is released, with many new features and changes.", + "source_repository": "https://github.com/Libensemble/libensemble/", + "source_branch": "main", + "source_directory": "docs/", + "light_logo": "libE_logo.png", + "dark_logo": "libE_logo_white.png", } # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -239,22 +234,6 @@ def setup(app): app.connect("autodoc-process-docstring", remove_noqa) -# Custom sidebar templates, must be a dictionary that maps document names -# to template names. -# -# This is required for the alabaster theme -# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars -# html_sidebars = { -# '**': [ -# 'about.html', -# 'navigation.html', -# 'relations.html', # needs 'show_related': True theme option to display -# 'searchbox.html', -# 'donate.html', -# ] -# } - - # -- Options for HTMLHelp output ------------------------------------------ # Output file base name for HTML help builder. diff --git a/docs/data_structures/alloc_specs.rst b/docs/data_structures/alloc_specs.rst index 074c6a1528..f29c0e5a2d 100644 --- a/docs/data_structures/alloc_specs.rst +++ b/docs/data_structures/alloc_specs.rst @@ -19,7 +19,7 @@ Can be constructed and passed to libEnsemble as a Python class or a dictionary. * libEnsemble uses the following defaults if the user doesn't provide their own ``alloc_specs``: .. literalinclude:: ../../libensemble/specs.py - :start-at: alloc_f: Callable = give_sim_work_first + :start-at: alloc_f: object = only_persistent_gens :end-before: end_alloc_tag :caption: Default settings for alloc_specs @@ -31,14 +31,4 @@ Can be constructed and passed to libEnsemble as a Python class or a dictionary. my_new_alloc = AllocSpecs() my_new_alloc.alloc_f = another_function -.. seealso:: - - `test_uniform_sampling_one_residual_at_a_time.py`_ specifies fields - to be used by the allocation function ``give_sim_work_first`` from - fast_alloc_and_pausing.py_. - - .. literalinclude:: ../../libensemble/tests/functionality_tests/test_uniform_sampling_one_residual_at_a_time.py - :start-at: alloc_specs - :end-before: end_alloc_specs_rst_tag - -.. _fast_alloc_and_pausing.py: https://github.com/Libensemble/libensemble/blob/develop/libensemble/alloc_funcs/fast_alloc_and_pausing.py .. _test_uniform_sampling_one_residual_at_a_time.py: https://github.com/Libensemble/libensemble/blob/develop/libensemble/tests/functionality_tests/test_uniform_sampling_one_residual_at_a_time.py diff --git a/docs/data_structures/data_structures.rst b/docs/data_structures/data_structures.rst index 35a5ba0158..a5a71862e8 100644 --- a/docs/data_structures/data_structures.rst +++ b/docs/data_structures/data_structures.rst @@ -8,10 +8,10 @@ See :ref:`here` for instruction on constructing a complete workflow :maxdepth: 2 :caption: libEnsemble Specifications: - libE_specs + libE_specs/libE_specs gen_specs sim_specs + exit_criteria alloc_specs platform_specs persis_info - exit_criteria diff --git a/docs/data_structures/gen_specs.rst b/docs/data_structures/gen_specs.rst index 66b12b3cc0..e95950731e 100644 --- a/docs/data_structures/gen_specs.rst +++ b/docs/data_structures/gen_specs.rst @@ -3,89 +3,57 @@ Generator Specs =============== -Used to specify the generator function, its inputs and outputs, and user data. - -Can be constructed and passed to libEnsemble as a Python class or a dictionary. - -.. tab-set:: - - .. tab-item:: class - - .. code-block:: python - :linenos: - - ... - import numpy as np - from libensemble import GenSpecs - from generator import gen_random_sample - - ... - - gen_specs = GenSpecs( - gen_f=gen_random_sample, - outputs=[("x", float, (1,))], - user={ - "lower": np.array([-3]), - "upper": np.array([3]), - "gen_batch_size": 5, - }, - ) - ... - - .. autopydantic_model:: libensemble.specs.GenSpecs - :model-show-json: False - :model-show-config-member: False - :model-show-config-summary: False - :model-show-validator-members: False - :model-show-validator-summary: False - :field-list-validators: False - - .. tab-item:: dict - - .. code-block:: python - :linenos: - - ... - import numpy as np - from generator import gen_random_sample - - ... - - gen_specs = { - "gen_f": gen_random_sample, - "out": [("x", float, (1,))], - "user": { - "lower": np.array([-3]), - "upper": np.array([3]), - "gen_batch_size": 5, - }, - } - - .. seealso:: - - .. _gen-specs-example1: - - - test_uniform_sampling.py_: - the generator function ``uniform_random_sample`` in sampling.py_ will generate 500 random - points uniformly over the 2D domain defined by ``gen_specs["ub"]`` and - ``gen_specs["lb"]``. - - .. literalinclude:: ../../libensemble/tests/functionality_tests/test_uniform_sampling.py - :start-at: gen_specs - :end-before: end_gen_specs_rst_tag - - .. seealso:: - - - test_persistent_aposmm_nlopt.py_ shows an example where ``gen_specs["in"]`` is empty, but - ``gen_specs["persis_in"]`` specifies values to return to the persistent generator. - - - test_persistent_aposmm_with_grad.py_ shows a similar example where an ``H0`` is used to - provide points from a previous run. In this case, ``gen_specs["in"]`` is populated to provide - the generator with data for the initial points. - - - In some cases you might be able to give different (perhaps fewer) fields in ``"persis_in"`` - than ``"in"``; you may not need to give ``x`` for example, as the persistent generator - already has ``x`` for those points. See `more example uses`_ of ``persis_in``. +Used to specify the generator, its inputs and outputs, and user data. + +Standardized (gest-api) +----------------------- + +.. code-block:: python + :linenos: + + from libensemble import GenSpecs + from libensemble.gen_classes import UniformSample + from gest_api.vocs import VOCS + + vocs = VOCS( + variables={"x": [-3.0, 3.0]}, + objectives={"y": "MINIMIZE"}, + ) + + gen_specs = GenSpecs( + generator=UniformSample(vocs), + vocs=vocs, + ) + ... + +Classic (gen_f) +--------------- + +.. code-block:: python + :linenos: + + import numpy as np + from libensemble import GenSpecs + from generator import gen_random_sample + + gen_specs = GenSpecs( + gen_f=gen_random_sample, + outputs=[("x", float, (1,))], + user={ + "lower": np.array([-3]), + "upper": np.array([3]), + "gen_batch_size": 5, + }, + ) + ... + +.. autopydantic_model:: libensemble.specs.GenSpecs + :model-show-json: False + :model-show-config-member: False + :model-show-config-summary: False + :model-show-validator-members: False + :model-show-validator-summary: False + :field-list-validators: False .. note:: diff --git a/docs/data_structures/libE_specs.rst b/docs/data_structures/libE_specs.rst deleted file mode 100644 index caa7b2eda8..0000000000 --- a/docs/data_structures/libE_specs.rst +++ /dev/null @@ -1,373 +0,0 @@ -.. _datastruct-libe-specs: - -LibE Specs -========== - -libEnsemble is primarily customized by setting options within a ``LibeSpecs`` class or dictionary. - -.. code-block:: python - - from libensemble.specs import LibeSpecs - - specs = LibeSpecs( - gen_on_manager=True, - save_every_k_gens=100, - sim_dirs_make=True, - nworkers=4 - ) - -.. dropdown:: Settings by Category - :open: - - .. tab-set:: - - .. tab-item:: General - - **comms** [str] = ``"mpi"``: - Manager/Worker communications mode: ``'mpi'``, ``'local'``, or ``'tcp'``. - If ``nworkers`` is specified, then ``local`` comms will be used unless a - parallel MPI environment is detected. - - **nworkers** [int]: - Number of worker processes in ``"local"``, ``"threads"``, or ``"tcp"``. - - **gen_on_manager** [bool] = False - Instructs Manager process to run generator functions. - This generator function can access/modify user objects by reference. - - **mpi_comm** [MPI communicator] = ``MPI.COMM_WORLD``: - libEnsemble MPI communicator. - - **dry_run** [bool] = ``False``: - Whether libEnsemble should immediately exit after validating all inputs. - - **abort_on_exception** [bool] = ``True``: - In MPI mode, whether to call ``MPI_ABORT`` on an exception. - If ``False``, an exception will be raised by the manager. - - **worker_timeout** [int] = ``1``: - On libEnsemble shutdown, number of seconds after which workers considered timed out, - then terminated. - - **kill_canceled_sims** [bool] = ``False``: - Try to kill sims with ``cancel_requested`` set to ``True``. - If ``False``, the manager avoids this moderate overhead. - - **disable_log_files** [bool] = ``False``: - Disable ``ensemble.log`` and ``libE_stats.txt`` log files. - - **gen_workers** [list of ints]: - List of workers that should run only generators. All other workers will run - only simulator functions. - - .. tab-item:: Directories - - .. tab-set:: - - .. tab-item:: General - - **use_workflow_dir** [bool] = ``False``: - Whether to place *all* log files, dumped arrays, and default ensemble-directories in a - separate ``workflow`` directory. Each run is suffixed with a hash. - If copying back an ensemble directory from another location, the copy is placed here. - - **workflow_dir_path** [str]: - Optional path to the workflow directory. - - **ensemble_dir_path** [str] = ``"./ensemble"``: - Path to main ensemble directory. Can serve - as single working directory for workers, or contain calculation directories. - - .. code-block:: python - - LibeSpecs.ensemble_dir_path = "/scratch/my_ensemble" - - **ensemble_copy_back** [bool] = ``False``: - Whether to copy back contents of ``ensemble_dir_path`` to launch - location. Useful if ``ensemble_dir_path`` is located on node-local storage. - - **reuse_output_dir** [bool] = ``False``: - Whether to allow overwrites and access to previous ensemble and workflow directories in subsequent runs. - ``False`` by default to protect results. - - **calc_dir_id_width** [int] = ``4``: - The width of the numerical ID component of a calculation directory name. Leading - zeros are padded to the sim/gen ID. - - **use_worker_dirs** [bool] = ``False``: - Whether to organize calculation directories under worker-specific directories: - - .. tab-set:: - - .. tab-item:: False - - .. code-block:: - - - /ensemble_dir - - /sim0000 - - /gen0001 - - /sim0001 - ... - - .. tab-item:: True - - .. code-block:: - - - /ensemble_dir - - /worker1 - - /sim0000 - - /gen0001 - - /sim0004 - ... - - /worker2 - ... - - .. tab-item:: Sims - - **sim_dirs_make** [bool] = ``False``: - Whether to make calculation directories for each simulation function call. - - **sim_dir_copy_files** [list]: - Paths to files or directories to copy into each sim directory, or ensemble directory. - List of strings or ``pathlib.Path`` objects. - - **sim_dir_symlink_files** [list]: - Paths to files or directories to symlink into each sim directory, or ensemble directory. - List of strings or ``pathlib.Path`` objects. - - **sim_input_dir** [str]: - Copy this directory's contents into the working directory upon calling the simulation function. - Forms the base of a simulation directory. - - .. tab-item:: Gens - - **gen_dirs_make** [bool] = ``False``: - Whether to make generator-specific calculation directories for each generator function call. - *Each persistent generator creates a single directory*. - - **gen_dir_copy_files** [list]: - Paths to copy into the working directory upon calling the generator function. - List of strings or ``pathlib.Path`` objects - - **gen_dir_symlink_files** [list]: - Paths to files or directories to symlink into each gen directory. - List of strings or ``pathlib.Path`` objects - - **gen_input_dir** [str]: - Copy this directory's contents into the working directory upon calling the generator function. - Forms the base of a generator directory. - - .. tab-item:: Profiling - - **profile** [bool] = ``False``: - Profile manager and worker logic using ``cProfile``. - - **safe_mode** [bool] = ``True``: - Prevents user functions from overwriting internal fields, but requires moderate overhead. - - **stats_fmt** [dict]: - A dictionary of options for formatting ``"libE_stats.txt"``. - See "Formatting Options for libE_stats.txt". - - **live_data** [LiveData] = None: - Add a live data capture object (e.g., for plotting). - - .. tab-item:: TCP - - **workers** [list]: - TCP Only: A list of worker hostnames. - - **ip** [str]: - TCP Only: IP address for Manager's system. - - **port** [int]: - TCP Only: Port number for Manager's system. - - **authkey** [str]: - TCP Only: Authkey for Manager's system. - - **workerID** [int]: - TCP Only: Worker ID number assigned to the new process. - - **worker_cmd** [list]: - TCP Only: Split string corresponding to worker/client Python process invocation. Contains - a local Python path, calling script, and manager/server format-fields for ``manager_ip``, - ``manager_port``, ``authkey``, and ``workerID``. ``nworkers`` is specified normally. - - .. tab-item:: History - - **save_every_k_sims** [int]: - Save history array to file after every k simulated points. - - **save_every_k_gens** [int]: - Save history array to file after every k generated points. - - **save_H_and_persis_on_abort** [bool] = ``True``: - Save states of ``H`` and ``persis_info`` to file on aborting after an exception. - - **save_H_on_completion** bool | None = ``False`` - Save state of ``H`` to file upon completing a workflow. Also enabled when either ``save_every_k_sims`` - or ``save_every_k_gens`` is set. - - **save_H_with_date** bool | None = ``False`` - Save ``H`` filename contains date and timestamp. - - **H_file_prefix** str | None = ``"libE_history"`` - Prefix for ``H`` filename. - - **use_persis_return_gen** [bool] = ``False``: - Adds persistent generator output fields to the History array on return. - - **use_persis_return_sim** [bool] = ``False``: - Adds persistent simulator output fields to the History array on return. - - **final_gen_send** [bool] = ``False``: - Send final simulation results to persistent generators before shutdown. - The results will be sent along with the ``PERSIS_STOP`` tag. - - .. tab-item:: Resources - - **disable_resource_manager** [bool] = ``False``: - Disable the built-in resource manager, including automatic resource detection - and/or assignment of resources to workers. ``"resource_info"`` will be ignored. - - **platform** [str]: - Name of a :ref:`known platform`, e.g., ``LibeSpecs.platform = "perlmutter_g"`` - Alternatively set the ``LIBE_PLATFORM`` environment variable. - - **platform_specs** [Platform|dict]: - A ``Platform`` object (or dictionary) specifying :ref:`settings for a platform.`. - Fields not provided will be auto-detected. Can be set to a :ref:`known platform object`. - - **num_resource_sets** [int]: - The total number of resource sets into which resources will be divided. - By default resources will be divided by workers (excluding - ``zero_resource_workers``). - - **gen_num_procs** [int] = ``0``: - The default number of processors (MPI ranks) required by generators. Unless - overridden by equivalent ``persis_info`` settings, generators will be allocated - this many processors for applications launched via the MPIExecutor. - - **gen_num_gpus** [int] = ``0``: - The default number of GPUs required by generators. Unless overridden by - the equivalent ``persis_info`` settings, generators will be allocated this - many GPUs. - - **gpus_per_group** [int]: - Number of GPUs for each group in the scheduler. This can be used when - running on nodes with different numbers of GPUs. In effect a - block of this many GPUs will be treated as a virtual node. - By default the GPUs on each node are treated as a group. - - **use_tiles_as_gpus** [bool] = ``False``: - If ``True`` then treat a GPU tile as one GPU, assuming - ``tiles_per_GPU`` is provided in ``platform_specs`` or detected. - - **enforce_worker_core_bounds** [bool] = ``False``: - Permit submission of tasks with a - higher processor count than the CPUs available to the worker. - Larger node counts are not allowed. Ignored when - ``disable_resource_manager`` is set. - - **dedicated_mode** [bool] = ``False``: - Instructs libEnsemble’s MPI executor not to run applications on nodes where - libEnsemble processes (manager and workers) are running. - - **zero_resource_workers** [list of ints]: - List of workers (by IDs) that require no resources. For when a fixed mapping of workers - to resources is required. Otherwise, use ``num_resource_sets``. - For use with supported allocation functions. - - **resource_info** [dict]: - Provide resource information that will override automatically detected resources. - The allowable fields are given below in "Overriding Resource Auto-Detection" - Ignored if ``disable_resource_manager`` is set. - - **scheduler_opts** [dict]: - Options for the resource scheduler. - See "Scheduler Options" for more options. - -.. dropdown:: Complete Class API - - .. autopydantic_model:: libensemble.specs.LibeSpecs - :model-show-json: False - :model-show-config-member: False - :model-show-config-summary: False - :model-show-validator-members: False - :model-show-validator-summary: False - :field-list-validators: False - :model-show-field-summary: False - -Scheduler Options ------------------ - -See options for :ref:`built-in scheduler`. - -.. _resource_info: - -Overriding Resource Auto-Detection ----------------------------------- - -Note that ``"cores_on_node"`` and ``"gpus_on_node"`` are supported for backward -compatibility, but use of :ref:`Platform specification` is -recommended for these settings. - -.. dropdown:: Resource Info Fields - - The allowable ``libE_specs["resource_info"]`` fields are:: - - "cores_on_node" [tuple (int, int)]: - Tuple (physical cores, logical cores) on nodes. - - "gpus_on_node" [int]: - Number of GPUs on each node. - - "node_file" [str]: - Name of file containing a node-list. Default is "node_list". - - "nodelist_env_slurm" [str]: - The environment variable giving a node list in Slurm format - (Default: Uses ``SLURM_NODELIST``). Queried only if - a ``node_list`` file is not provided and the resource manager is - enabled. - - "nodelist_env_cobalt" [str]: - The environment variable giving a node list in Cobalt format - (Default: Uses ``COBALT_PARTNAME``) Queried only - if a ``node_list`` file is not provided and the resource manager - is enabled. - - "nodelist_env_lsf" [str]: - The environment variable giving a node list in LSF format - (Default: Uses ``LSB_HOSTS``) Queried only - if a ``node_list`` file is not provided and the resource manager - is enabled. - - "nodelist_env_lsf_shortform" [str]: - The environment variable giving a node list in LSF short-form - format (Default: Uses ``LSB_MCPU_HOSTS``) Queried only - if a ``node_list`` file is not provided and the resource manager is - enabled. - - For example:: - - customizer = {cores_on_node": (16, 64), - "node_file": "libe_nodes"} - - libE_specs["resource_info"] = customizer - -Formatting Options for libE_stats File --------------------------------------- - -The allowable ``libE_specs["stats_fmt"]`` fields are:: - - "task_timing" [bool] = ``False``: - Outputs elapsed time for each task launched by the executor. - - "task_datetime" [bool] = ``False``: - Outputs the elapsed time and start and end time for each task launched by the executor. - Can be used with the ``"plot_libe_tasks_util_v_time.py"`` to give task utilization plots. - - "show_resource_sets" [bool] = ``False``: - Shows the resource set IDs assigned to each worker for each call of the user function. diff --git a/docs/data_structures/libE_specs/libE_specs.rst b/docs/data_structures/libE_specs/libE_specs.rst new file mode 100644 index 0000000000..a219109851 --- /dev/null +++ b/docs/data_structures/libE_specs/libE_specs.rst @@ -0,0 +1,108 @@ +.. _datastruct-libe-specs: + +**Introduction** \|\| `General `__ \|\| `Directories `__ \|\| `Profiling `__ \|\| `TCP `__ \|\| `History `__ \|\| `Resources `__ + +LibE Specs +========== + +libEnsemble is primarily customized by setting options within a ``LibeSpecs`` instance. + +.. code-block:: python + + from libensemble.specs import LibeSpecs + + specs = LibeSpecs(save_every_k_gens=100, sim_dirs_make=True, nworkers=4) + +.. toctree:: + :hidden: + + libE_specs_general + libE_specs_directories + libE_specs_profiling + libE_specs_tcp + libE_specs_history + libE_specs_resources + +.. dropdown:: Complete Class API + + .. autopydantic_model:: libensemble.specs.LibeSpecs + :model-show-json: False + :model-show-config-member: False + :model-show-config-summary: False + :model-show-validator-members: False + :model-show-validator-summary: False + :field-list-validators: False + :model-show-field-summary: False + +Scheduler Options +----------------- + +See options for :ref:`built-in scheduler`. + +.. _resource_info: + +Overriding Resource Auto-Detection +---------------------------------- + +Note that ``"cores_on_node"`` and ``"gpus_on_node"`` are supported for backward +compatibility, but use of :ref:`Platform specification` is +recommended for these settings. + +.. dropdown:: Resource Info Fields + + The allowable ``libE_specs["resource_info"]`` fields are:: + + "cores_on_node" [tuple (int, int)]: + Tuple (physical cores, logical cores) on nodes. + + "gpus_on_node" [int]: + Number of GPUs on each node. + + "node_file" [str]: + Name of file containing a node-list. Default is "node_list". + + "nodelist_env_slurm" [str]: + The environment variable giving a node list in Slurm format + (Default: Uses ``SLURM_NODELIST``). Queried only if + a ``node_list`` file is not provided and the resource manager is + enabled. + + "nodelist_env_cobalt" [str]: + The environment variable giving a node list in Cobalt format + (Default: Uses ``COBALT_PARTNAME``) Queried only + if a ``node_list`` file is not provided and the resource manager + is enabled. + + "nodelist_env_lsf" [str]: + The environment variable giving a node list in LSF format + (Default: Uses ``LSB_HOSTS``) Queried only + if a ``node_list`` file is not provided and the resource manager + is enabled. + + "nodelist_env_lsf_shortform" [str]: + The environment variable giving a node list in LSF short-form + format (Default: Uses ``LSB_MCPU_HOSTS``) Queried only + if a ``node_list`` file is not provided and the resource manager is + enabled. + + For example:: + + customizer = {cores_on_node": (16, 64), + "node_file": "libe_nodes"} + + libE_specs["resource_info"] = customizer + +Formatting Options for libE_stats File +-------------------------------------- + +The allowable ``libE_specs["stats_fmt"]`` fields are:: + + "task_timing" [bool] = ``False``: + Outputs elapsed time for each task launched by the executor. + + "task_datetime" [bool] = ``False``: + Outputs the elapsed time and start and end time for each task launched by the executor. + Can be used with the ``"plot_libe_tasks_util_v_time.py"`` to give task utilization plots. + + "show_resource_sets" [bool] = ``False``: + Shows the resource set IDs assigned to each worker for each call of the user function. diff --git a/docs/data_structures/libE_specs/libE_specs_directories.rst b/docs/data_structures/libE_specs/libE_specs_directories.rst new file mode 100644 index 0000000000..76c848da05 --- /dev/null +++ b/docs/data_structures/libE_specs/libE_specs_directories.rst @@ -0,0 +1,99 @@ +Directories +=========== + +`Introduction `__ \|\| `General `__ \|\| **Directories** \|\| `Profiling `__ \|\| `TCP `__ \|\| `History `__ \|\| `Resources `__ + +.. tab-set:: + + .. tab-item:: General + + **use_workflow_dir** [bool] = ``False``: + Whether to place *all* log files, dumped arrays, and default ensemble-directories in a + separate ``workflow`` directory. Each run is suffixed with a hash. + If copying back an ensemble directory from another location, the copy is placed here. + + **workflow_dir_path** [str]: + Optional path to the workflow directory. + + **ensemble_dir_path** [str] = ``"./ensemble"``: + Path to main ensemble directory. Can serve + as single working directory for workers, or contain calculation directories. + + .. code-block:: python + + LibeSpecs.ensemble_dir_path = "/scratch/my_ensemble" + + **ensemble_copy_back** [bool] = ``False``: + Whether to copy back contents of ``ensemble_dir_path`` to launch + location. Useful if ``ensemble_dir_path`` is located on node-local storage. + + **reuse_output_dir** [bool] = ``False``: + Whether to allow overwrites and access to previous ensemble and workflow directories in subsequent runs. + ``False`` by default to protect results. + + **calc_dir_id_width** [int] = ``4``: + The width of the numerical ID component of a calculation directory name. Leading + zeros are padded to the sim/gen ID. + + **use_worker_dirs** [bool] = ``False``: + Whether to organize calculation directories under worker-specific directories: + + .. tab-set:: + + .. tab-item:: False + + .. code-block:: + + - /ensemble_dir + - /sim0000 + - /gen0001 + - /sim0001 + ... + + .. tab-item:: True + + .. code-block:: + + - /ensemble_dir + - /worker1 + - /sim0000 + - /gen0001 + - /sim0004 + ... + - /worker2 + ... + + .. tab-item:: Sims + + **sim_dirs_make** [bool] = ``False``: + Whether to make calculation directories for each simulation function call. + + **sim_dir_copy_files** [list]: + Paths to files or directories to copy into each sim directory, or ensemble directory. + List of strings or ``pathlib.Path`` objects. + + **sim_dir_symlink_files** [list]: + Paths to files or directories to symlink into each sim directory, or ensemble directory. + List of strings or ``pathlib.Path`` objects. + + **sim_input_dir** [str]: + Copy this directory's contents into the working directory upon calling the simulation function. + Forms the base of a simulation directory. + + .. tab-item:: Gens + + **gen_dirs_make** [bool] = ``False``: + Whether to make generator-specific calculation directories for each generator function call. + *Each persistent generator creates a single directory*. + + **gen_dir_copy_files** [list]: + Paths to copy into the working directory upon calling the generator function. + List of strings or ``pathlib.Path`` objects + + **gen_dir_symlink_files** [list]: + Paths to files or directories to symlink into each gen directory. + List of strings or ``pathlib.Path`` objects + + **gen_input_dir** [str]: + Copy this directory's contents into the working directory upon calling the generator function. + Forms the base of a generator directory. diff --git a/docs/data_structures/libE_specs/libE_specs_general.rst b/docs/data_structures/libE_specs/libE_specs_general.rst new file mode 100644 index 0000000000..f7f07f75fa --- /dev/null +++ b/docs/data_structures/libE_specs/libE_specs_general.rst @@ -0,0 +1,40 @@ +General +======= + +`Introduction `__ \|\| **General** \|\| `Directories `__ \|\| `Profiling `__ \|\| `TCP `__ \|\| `History `__ \|\| `Resources `__ + +**comms** [str] = ``"mpi"``: + Manager/Worker communications mode: ``'mpi'``, ``'local'``, ``'threads'``, or ``'tcp'``. + If ``nworkers`` is specified, then ``local`` comms will be used unless a + parallel MPI environment is detected. + +**nworkers** [int]: + Number of worker processes in ``"local"``, ``"threads"``, or ``"tcp"``. + +**gen_on_worker** [bool] = False + Instructs Worker process to run generator instead of Manager. + +**mpi_comm** [MPI communicator] = ``MPI.COMM_WORLD``: + libEnsemble MPI communicator. + +**dry_run** [bool] = ``False``: + Whether libEnsemble should immediately exit after validating all inputs. + +**abort_on_exception** [bool] = ``True``: + In MPI mode, whether to call ``MPI_ABORT`` on an exception. + If ``False``, an exception will be raised by the manager. + +**worker_timeout** [int] = ``1``: + On libEnsemble shutdown, number of seconds after which workers considered timed out, + then terminated. + +**kill_canceled_sims** [bool] = ``False``: + Try to kill sims with ``cancel_requested`` set to ``True``. + If ``False``, the manager avoids this moderate overhead. + +**disable_log_files** [bool] = ``False``: + Disable ``ensemble.log`` and ``libE_stats.txt`` log files. + +**gen_workers** [list of ints]: + List of workers that should run only generators. All other workers will run + only simulator functions. diff --git a/docs/data_structures/libE_specs/libE_specs_history.rst b/docs/data_structures/libE_specs/libE_specs_history.rst new file mode 100644 index 0000000000..55e9089696 --- /dev/null +++ b/docs/data_structures/libE_specs/libE_specs_history.rst @@ -0,0 +1,27 @@ +History +======= + +`Introduction `__ \|\| `General `__ \|\| `Directories `__ \|\| `Profiling `__ \|\| `TCP `__ \|\| **History** \|\| `Resources `__ + +**save_every_k_sims** [int]: + Save history array to file after every k simulated points. + +**save_every_k_gens** [int]: + Save history array to file after every k generated points. + +**save_H_and_persis_on_abort** [bool] = ``True``: + Save states of ``H`` and ``persis_info`` to file on aborting after an exception. + +**save_H_on_completion** [bool] = ``False``: + Save state of ``H`` to file upon completing a workflow. Also enabled when either ``save_every_k_sims`` + or ``save_every_k_gens`` is set. + +**save_H_with_date** [bool] = ``False``: + ``H`` filename contains date and timestamp. + +**H_file_prefix** [str] = ``"libE_history"``: + Prefix for ``H`` filename. + +**final_gen_send** [bool] = ``False``: + Send final simulation results to persistent generators before shutdown. + The results will be sent along with the ``PERSIS_STOP`` tag. diff --git a/docs/data_structures/libE_specs/libE_specs_profiling.rst b/docs/data_structures/libE_specs/libE_specs_profiling.rst new file mode 100644 index 0000000000..6a855c8ce6 --- /dev/null +++ b/docs/data_structures/libE_specs/libE_specs_profiling.rst @@ -0,0 +1,17 @@ +Profiling +========= + +`Introduction `__ \|\| `General `__ \|\| `Directories `__ \|\| **Profiling** \|\| `TCP `__ \|\| `History `__ \|\| `Resources `__ + +**profile** [bool] = ``False``: + Profile manager and worker logic using ``cProfile``. + +**safe_mode** [bool] = ``False``: + Prevents user functions from overwriting protected History fields, but requires moderate overhead. + +**stats_fmt** [dict]: + A dictionary of options for formatting ``"libE_stats.txt"``. + See "Formatting Options for libE_stats.txt". + +**live_data** [LiveData] = None: + Add a live data capture object (e.g., for plotting). diff --git a/docs/data_structures/libE_specs/libE_specs_resources.rst b/docs/data_structures/libE_specs/libE_specs_resources.rst new file mode 100644 index 0000000000..6b6118d663 --- /dev/null +++ b/docs/data_structures/libE_specs/libE_specs_resources.rst @@ -0,0 +1,60 @@ +Resources +========= + +`Introduction `__ \|\| `General `__ \|\| `Directories `__ \|\| `Profiling `__ \|\| `TCP `__ \|\| `History `__ \|\| **Resources** + +**disable_resource_manager** [bool] = ``False``: + Disable the built-in resource manager, including automatic resource detection + and/or assignment of resources to workers. ``"resource_info"`` will be ignored. + +**platform** [str]: + Name of a :ref:`known platform`, e.g., ``LibeSpecs.platform = "perlmutter_g"`` + Alternatively set the ``LIBE_PLATFORM`` environment variable. + +**platform_specs** [Platform|dict]: + A ``Platform`` object (or dictionary) specifying :ref:`settings for a platform.`. + Fields not provided will be auto-detected. Can be set to a :ref:`known platform object`. + +**num_resource_sets** [int]: + The total number of resource sets into which resources will be divided. + By default resources will be divided by workers (excluding + ``zero_resource_workers``). + +**gen_num_procs** [int] = ``0``: + The default number of processors (MPI ranks) required by generators. Unless + overridden by equivalent ``persis_info`` settings, generators will be allocated + this many processors for applications launched via the MPIExecutor. + +**gen_num_gpus** [int] = ``0``: + The default number of GPUs required by generators. Unless overridden by + the equivalent ``persis_info`` settings, generators will be allocated this + many GPUs. + +**gpus_per_group** [int]: + Number of GPUs for each group in the scheduler. This can be used when + running on nodes with different numbers of GPUs. In effect a + block of this many GPUs will be treated as a virtual node. + By default the GPUs on each node are treated as a group. + +**use_tiles_as_gpus** [bool] = ``False``: + If ``True`` then treat a GPU tile as one GPU when GPU tiles + are provided in ``platform_specs`` or auto-detected. + +**enforce_worker_core_bounds** [bool] = ``False``: + Permit submission of tasks with a + higher processor count than the CPUs available to the worker. + Larger node counts are not allowed. Ignored when + ``disable_resource_manager`` is set. + +**dedicated_mode** [bool] = ``False``: + Instructs libEnsemble’s MPI executor not to run applications on nodes where + libEnsemble processes (manager and workers) are running. + +**resource_info** [dict]: + Provide resource information that will override automatically detected resources. + The allowable fields are given below in "Overriding Resource Auto-Detection" + Ignored if ``disable_resource_manager`` is set. + +**scheduler_opts** [dict]: + Options for the resource scheduler. + See "Scheduler Options" for more options. diff --git a/docs/data_structures/libE_specs/libE_specs_tcp.rst b/docs/data_structures/libE_specs/libE_specs_tcp.rst new file mode 100644 index 0000000000..d0d2a05655 --- /dev/null +++ b/docs/data_structures/libE_specs/libE_specs_tcp.rst @@ -0,0 +1,24 @@ +TCP +=== + +`Introduction `__ \|\| `General `__ \|\| `Directories `__ \|\| `Profiling `__ \|\| **TCP** \|\| `History `__ \|\| `Resources `__ + +**workers** [list]: + TCP Only: A list of worker hostnames. + +**ip** [str]: + TCP Only: IP address for Manager's system. + +**port** [int]: + TCP Only: Port number for Manager's system. + +**authkey** [str]: + TCP Only: Authkey for Manager's system. + +**workerID** [int]: + TCP Only: Worker ID number assigned to the new process. + +**worker_cmd** [list]: + TCP Only: Split string corresponding to worker/client Python process invocation. Contains + a local Python path, calling script, and manager/server format-fields for ``manager_ip``, + ``manager_port``, ``authkey``, and ``workerID``. ``nworkers`` is specified normally. diff --git a/docs/data_structures/persis_info.rst b/docs/data_structures/persis_info.rst index 987137f8ee..6e620f886c 100644 --- a/docs/data_structures/persis_info.rst +++ b/docs/data_structures/persis_info.rst @@ -13,50 +13,52 @@ and from the corresponding workers. These are received in the ``persis_info`` argument of user functions, and returned as the optional second return value. A typical example is a random number generator stream to be used in consecutive -calls to a generator (see -:meth:`add_unique_random_streams()`) +calls to a generator. Generators should initialize their own RNG using +:meth:`get_rng()`. All other entries persist on the manager and can be updated in the calling script between ensemble invocations, or in the allocation function. Examples: -.. tab-set:: - - .. tab-item:: RNG or reusable structures - - .. literalinclude:: ../../libensemble/gen_funcs/sampling.py - :linenos: - :start-at: def uniform_random_sample(_, persis_info, gen_specs): - :end-before: def uniform_random_sample_with_variable_resources(_, persis_info, gen_specs): - :emphasize-lines: 17 - :caption: libensemble/libensemble/gen_funcs/sampling.py - - .. tab-item:: Incrementing indexes or process counts - - .. literalinclude:: ../../libensemble/alloc_funcs/fast_alloc.py - :linenos: - :start-at: for wid in support.avail_worker_ids(gen_workers=False): - :end-before: # Give gen work if possible - :caption: libensemble/alloc_funcs/fast_alloc.py - - .. tab-item:: Tracking running generators - - .. literalinclude:: ../../libensemble/alloc_funcs/start_only_persistent.py - :linenos: - :start-at: avail_workers = support.avail_worker_ids(persistent=False, zero_resource_workers=True, gen_workers=True) - :end-before: return Work, persis_info, 0 - :emphasize-lines: 18 - :caption: libensemble/alloc_funcs/start_only_persistent.py - - .. tab-item:: Allocation function triggers shutdown - - .. literalinclude:: ../../libensemble/alloc_funcs/start_only_persistent.py - :linenos: - :start-at: if gen_count < persis_info.get("num_gens_started", 0): - :end-before: # Give evaluated results back to a running persistent gen - :emphasize-lines: 1 - :caption: libensemble/alloc_funcs/start_only_persistent.py +RNG or reusable structures +-------------------------- + +.. literalinclude:: ../../libensemble/gen_funcs/sampling.py + :linenos: + :start-at: def uniform_random_sample(_, persis_info, gen_specs, libE_info): + :end-before: def uniform_random_sample_with_variable_resources(_, persis_info, gen_specs, libE_info): + :emphasize-lines: 10 + :caption: libensemble/libensemble/gen_funcs/sampling.py + +Incrementing indexes or process counts +-------------------------------------- + +.. literalinclude:: ../../libensemble/alloc_funcs/fast_alloc.py + :linenos: + :start-at: for wid in support.avail_worker_ids(gen_workers=False): + :end-before: # Give gen work if possible + :caption: libensemble/alloc_funcs/fast_alloc.py + +Tracking running generators +--------------------------- + +.. literalinclude:: ../../libensemble/alloc_funcs/start_only_persistent.py + :linenos: + :start-at: avail_workers = support.avail_worker_ids(persistent=False, gen_workers=True) + :end-before: return Work, persis_info, 0 + :emphasize-lines: 18 + :caption: libensemble/alloc_funcs/start_only_persistent.py + +Allocation function triggers shutdown +------------------------------------- + +.. literalinclude:: ../../libensemble/alloc_funcs/start_only_persistent.py + :linenos: + :start-at: if gen_count < persis_info.get("num_gens_started", 0): + :end-before: # Give evaluated results back to a running persistent gen + :emphasize-lines: 1 + :caption: libensemble/alloc_funcs/start_only_persistent.py .. - Random number generators or other structures for use on consecutive calls .. - Incrementing array row indexes or process counts diff --git a/docs/data_structures/platform_specs.rst b/docs/data_structures/platform_specs.rst index 35198535f1..bfc4104059 100644 --- a/docs/data_structures/platform_specs.rst +++ b/docs/data_structures/platform_specs.rst @@ -15,37 +15,37 @@ A ``Platform`` object or dictionary specifying settings for a platform. To define a platform (in calling script): -.. tab-set:: +Platform Object +^^^^^^^^^^^^^^^ - .. tab-item:: Platform Object - - .. code-block:: python +.. code-block:: python - from libensemble.resources.platforms import Platform + from libensemble.resources.platforms import Platform - libE_specs["platform_specs"] = Platform( - mpi_runner="srun", - cores_per_node=64, - logical_cores_per_node=128, - gpus_per_node=8, - gpu_setting_type="runner_default", - gpu_env_fallback="ROCR_VISIBLE_DEVICES", - scheduler_match_slots=False, - ) + libE_specs["platform_specs"] = Platform( + mpi_runner="srun", + cores_per_node=64, + logical_cores_per_node=128, + gpus_per_node=8, + gpu_setting_type="runner_default", + gpu_env_fallback="ROCR_VISIBLE_DEVICES", + scheduler_match_slots=False, + ) - .. tab-item:: Dictionary +Dictionary +^^^^^^^^^^ - .. code-block:: python +.. code-block:: python - libE_specs["platform_specs"] = { - "mpi_runner": "srun", - "cores_per_node": 64, - "logical_cores_per_node": 128, - "gpus_per_node": 8, - "gpu_setting_type": "runner_default", - "gpu_env_fallback": "ROCR_VISIBLE_DEVICES", - "scheduler_match_slots": False, - } + libE_specs["platform_specs"] = { + "mpi_runner": "srun", + "cores_per_node": 64, + "logical_cores_per_node": 128, + "gpus_per_node": 8, + "gpu_setting_type": "runner_default", + "gpu_env_fallback": "ROCR_VISIBLE_DEVICES", + "scheduler_match_slots": False, + } The list of platform fields is given below. Any fields not given will be auto-detected by libEnsemble. diff --git a/docs/data_structures/sim_specs.rst b/docs/data_structures/sim_specs.rst index 856ab5a9fe..0c937c5e82 100644 --- a/docs/data_structures/sim_specs.rst +++ b/docs/data_structures/sim_specs.rst @@ -5,73 +5,51 @@ Simulation Specs Used to specify the simulation function, its inputs and outputs, and user data. -Can be constructed and passed to libEnsemble as a Python class or a dictionary. +Standardized (gest-api) +----------------------- -.. tab-set:: +.. code-block:: python + :linenos: - .. tab-item:: class + from libensemble import SimSpecs + from gest_api.vocs import VOCS + from my_package import my_sim_callable - .. code-block:: python - :linenos: + vocs = VOCS( + variables={"x": [-3.0, 3.0]}, + objectives={"y": "MINIMIZE"}, + ) - ... - from libensemble import SimSpecs - from simulator import sim_find_sine + sim_specs = SimSpecs( + simulator=my_sim_callable, + vocs=vocs, + ) + ... - ... +Classic (sim_f) +--------------- - sim_specs = SimSpecs( - sim_f=sim_find_sine, - inputs=["x"], - outputs=[("y", float)], - user={"batch": 1234}, - ) - ... +.. code-block:: python + :linenos: - .. autopydantic_model:: libensemble.specs.SimSpecs - :model-show-json: False - :model-show-config-member: False - :model-show-config-summary: False - :model-show-validator-members: False - :model-show-validator-summary: False - :field-list-validators: False + from libensemble import SimSpecs + from simulator import sim_find_sine - .. tab-item:: dict + sim_specs = SimSpecs( + sim_f=sim_find_sine, + inputs=["x"], + outputs=[("y", float)], + user={"batch": 1234}, + ) + ... - .. code-block:: python - :linenos: +.. autopydantic_model:: libensemble.specs.SimSpecs + :model-show-json: False + :model-show-config-member: False + :model-show-config-summary: False + :model-show-validator-members: False + :model-show-validator-summary: False + :field-list-validators: False - ... - from simulator import six_hump_camel - ... - - sim_specs = { - "sim_f": six_hump_camel, - "in": ["x"], - "out": [("y", float)], - "user": {"batch": 1234}, - } - ... - - - test_uniform_sampling.py_ has a :class:`sim_specs` that declares - the name of the ``"in"`` field variable, ``"x"`` (as specified by the - corresponding generator ``"out"`` field ``"x"`` from the :ref:`gen_specs - example`). Only the field name is required in - ``sim_specs["in"]``. - - .. literalinclude:: ../../libensemble/tests/functionality_tests/test_uniform_sampling.py - :start-at: sim_specs - :end-before: end_sim_specs_rst_tag - - - run_libe_forces.py_ has a longer :class:`sim_specs` declaration with a number of - user-specific fields. These are given to the corresponding sim_f, which - can be found at forces_simf.py_. - - .. literalinclude:: ../../libensemble/tests/scaling_tests/forces/forces_adv/run_libe_forces.py - :start-at: sim_f - :end-before: end_sim_specs_rst_tag - -.. _forces_simf.py: https://github.com/Libensemble/libensemble/blob/develop/libensemble/tests/scaling_tests/forces/forces_simple/forces_simf.py -.. _run_libe_forces.py: https://github.com/Libensemble/libensemble/blob/develop/libensemble/tests/scaling_tests/forces/forces_simple/run_libe_forces.py .. _test_uniform_sampling.py: https://github.com/Libensemble/libensemble/blob/develop/libensemble/tests/functionality_tests/test_uniform_sampling.py diff --git a/docs/dev_guide/dev_API/developer_API.rst b/docs/dev_guide/dev_API/developer_API.rst index c09647db46..6774cbd629 100644 --- a/docs/dev_guide/dev_API/developer_API.rst +++ b/docs/dev_guide/dev_API/developer_API.rst @@ -17,3 +17,5 @@ This section documents the internal modules of libEnsemble. node_resources_module mpi_resources_module scheduler_module + work_dict + worker_array diff --git a/docs/function_guides/work_dict.rst b/docs/dev_guide/dev_API/work_dict.rst similarity index 96% rename from docs/function_guides/work_dict.rst rename to docs/dev_guide/dev_API/work_dict.rst index 4252919de0..0afeebabfb 100644 --- a/docs/function_guides/work_dict.rst +++ b/docs/dev_guide/dev_API/work_dict.rst @@ -21,7 +21,7 @@ the data given to worker ``i``. Populated in the allocation function. ``Work[i]` "persistent" [bool]: True if worker i will enter persistent mode (Default: False) The work dictionary is typically set using the ``gen_work`` or ``sim_work`` -:doc:`helper functions<../function_guides/allocator>` in the allocation function. +:doc:`helper functions<../../function_guides/allocator>` in the allocation function. ``H_fields``, for example, is usually packed from either ``sim_specs["in"]``, ``gen_specs["in"]`` or the equivalent "persis_in" variants. diff --git a/docs/function_guides/worker_array.rst b/docs/dev_guide/dev_API/worker_array.rst similarity index 100% rename from docs/function_guides/worker_array.rst rename to docs/dev_guide/dev_API/worker_array.rst diff --git a/docs/dev_guide/release_management/release_platforms/rel_spack.rst b/docs/dev_guide/release_management/release_platforms/rel_spack.rst index 660ee2fdb0..a7688ed721 100644 --- a/docs/dev_guide/release_management/release_platforms/rel_spack.rst +++ b/docs/dev_guide/release_management/release_platforms/rel_spack.rst @@ -24,7 +24,7 @@ Do ONCE in your local checkout: To set upstream repo:: - git remote add upstream https://github.com/spack/spack.git + git remote add upstream https://github.com/spack/spack-packages.git git remote -v # check added (Optional) To prevent accidental pushes to upstream:: @@ -80,12 +80,15 @@ See the Spack packaging_ and contribution_ guides for more info. Quick example to update libEnsemble ----------------------------------- +The libEnsemble ``package.py`` file can be viewed online at: +https://github.com/spack/spack-packages/blob/develop/repos/spack_repo/builtin/packages/py_libensemble/package.py + This will open the libEnsemble ``package.py`` file in your editor (given by environment variable ``EDITOR``):: spack edit py-libensemble # SPACK_ROOT must be set (see above) (Python packages use "py-" prefix) -Or just open it manually: ``var/spack/repos/builtin/packages/py-libensemble/package.py``. +Or just open it manually: ``repos/spack_repo/builtin/packages/py_libensemble/package.py``. Now get the checksum for new lines: @@ -119,7 +122,7 @@ Express Summary: Make Fork Identical to Upstream Quick summary for bringing the develop branch on a forked repo up to speed with upstream (YOU WILL LOSE ANY CHANGES):: - git remote add upstream https://github.com/spack/spack.git + git remote add upstream https://github.com/spack/spack-packages.git git fetch upstream git checkout develop git reset --hard upstream/develop diff --git a/docs/examples/alloc_funcs.rst b/docs/examples/alloc_funcs.rst deleted file mode 100644 index f54f4bf3c4..0000000000 --- a/docs/examples/alloc_funcs.rst +++ /dev/null @@ -1,101 +0,0 @@ -.. _examples-alloc: - -Allocation Functions -==================== - -Below are example allocation functions available in libEnsemble. - -Many users use these unmodified. - -.. IMPORTANT:: - See the API for allocation functions :ref:`here`. - -.. note:: - The default allocation function (for non-persistent generators) is :ref:`give_sim_work_first`. - - The most commonly used (for persistent generators) is :ref:`start_only_persistent`. - -.. role:: underline - :class: underline - -.. _gswf_label: - -give_sim_work_first -------------------- -.. automodule:: give_sim_work_first - :members: - :undoc-members: - -.. dropdown:: :underline:`give_sim_work_first.py` - - .. literalinclude:: ../../libensemble/alloc_funcs/give_sim_work_first.py - :language: python - :linenos: - -fast_alloc ----------- -.. automodule:: fast_alloc - :members: - :undoc-members: - -.. dropdown:: :underline:`fast_alloc.py` - - .. literalinclude:: ../../libensemble/alloc_funcs/fast_alloc.py - :language: python - :linenos: - -.. _start_only_persistent_label: - -start_only_persistent ---------------------- -.. automodule:: start_only_persistent - :members: - :undoc-members: - -.. dropdown:: :underline:`start_only_persistent.py` - - .. literalinclude:: ../../libensemble/alloc_funcs/start_only_persistent.py - :language: python - :linenos: - -start_persistent_local_opt_gens -------------------------------- -.. automodule:: start_persistent_local_opt_gens - :members: - :undoc-members: - -fast_alloc_and_pausing ----------------------- -.. automodule:: fast_alloc_and_pausing - :members: - :undoc-members: - -only_one_gen_alloc ------------------- -.. automodule:: only_one_gen_alloc - :members: - :undoc-members: - -start_fd_persistent -------------------- -.. automodule:: start_fd_persistent - :members: - :undoc-members: - -persistent_aposmm_alloc ------------------------ -.. automodule:: persistent_aposmm_alloc - :members: - :undoc-members: - -give_pregenerated_work ----------------------- -.. automodule:: give_pregenerated_work - :members: - :undoc-members: - -inverse_bayes_allocf --------------------- -.. automodule:: inverse_bayes_allocf - :members: - :undoc-members: diff --git a/docs/examples/calling_scripts.rst b/docs/examples/calling_scripts.rst index 183128b6ed..7a58ad05e4 100644 --- a/docs/examples/calling_scripts.rst +++ b/docs/examples/calling_scripts.rst @@ -1,19 +1,12 @@ -Calling Scripts -=============== +Top-Level Scripts +================= -Below are example calling scripts used to populate specifications for each user -function and libEnsemble before initiating libEnsemble via the primary ``libE()`` -call. The primary libEnsemble-relevant portions have been highlighted in each -example. Non-highlighted portions may include setup routines, compilation steps -for user applications, or output processing. The first two scripts correspond to -random sampling calculations, while the third corresponds to an optimization routine. - -Many other examples of calling scripts can be found in libEnsemble's `regression tests`_. +Many other examples of top-level scripts can be found in libEnsemble's `regression tests`_. Local Sine Tutorial ------------------- -This example is from the Local Sine :doc:`Tutorial<../tutorials/local_sine_tutorial>`, +This example is from the Local Sine :doc:`Tutorial<../tutorials/local_sine_tutorial/local_sine_tutorial>`, meant to run with Python's multiprocessing as the primary ``comms`` method. .. literalinclude:: ../../examples/tutorials/simple_sine/test_local_sine_tutorial.py @@ -38,35 +31,22 @@ Run using five workers with:: python run_libe_forces.py -n 5 -One worker runs a persistent generator and the other four run the forces simulations. - .. literalinclude:: ../../libensemble/tests/scaling_tests/forces/forces_simple/run_libe_forces.py :language: python :caption: tests/scaling_tests/forces/forces_simple/run_libe_forces.py :linenos: -Object + yaml Version -~~~~~~~~~~~~~~~~~~~~~ - -.. literalinclude:: ../../libensemble/tests/scaling_tests/forces/forces_adv/run_libe_forces_from_yaml.py - :language: python - :caption: tests/scaling_tests/forces/forces_adv/run_libe_forces_from_yaml.py - :linenos: - -.. literalinclude:: ../../libensemble/tests/scaling_tests/forces/forces_adv/forces.yaml - :language: yaml - :caption: tests/scaling_tests/forces/forces_adv/forces.yaml - :linenos: - -Persistent APOSMM with Gradients --------------------------------- +gest-api APOSMM +--------------- -This example is also from the regression tests and demonstrates configuring a -persistent run via a custom allocation function. +This example from the regression tests demonstrates the gest-api interface with a +standardized ``APOSMM`` generator class parameterized by a ``VOCS`` object, and +paired with a gest-api ``simulator`` callable. -.. literalinclude:: ../../libensemble/tests/regression_tests/test_persistent_aposmm_with_grad.py +.. literalinclude:: ../../libensemble/tests/regression_tests/test_asktell_aposmm_nlopt.py :language: python - :caption: tests/regression_tests/test_persistent_aposmm_with_grad.py + :caption: tests/regression_tests/test_asktell_aposmm_nlopt.py :linenos: + :end-at: workflow.exit_criteria = ExitCriteria(sim_max=2000, wallclock_max=600) .. _regression tests: https://github.com/Libensemble/libensemble/tree/develop/libensemble/tests/regression_tests diff --git a/docs/examples/examples_index.rst b/docs/examples/examples_index.rst index 1e92e21c03..c1d6abfb28 100644 --- a/docs/examples/examples_index.rst +++ b/docs/examples/examples_index.rst @@ -2,7 +2,7 @@ Overview of Examples ==================== Here we give example generation, simulation, and allocation functions for -libEnsemble, as well as example calling scripts. +libEnsemble, as well as example top-level scripts. The examples come from the libEnsemble repository and the `libEnsemble Community Repository`_. @@ -12,7 +12,6 @@ The examples come from the libEnsemble repository and the `libEnsemble Community gen_funcs sim_funcs - alloc_funcs calling_scripts .. _libEnsemble Community Repository: https://github.com/Libensemble/libe-community-examples diff --git a/docs/examples/gen_funcs.rst b/docs/examples/gen_funcs.rst index 9bdaa311d9..0bae6f7642 100644 --- a/docs/examples/gen_funcs.rst +++ b/docs/examples/gen_funcs.rst @@ -4,7 +4,7 @@ Generator Functions Here we list many generator functions included with libEnsemble. .. IMPORTANT:: - See the API for generator functions :ref:`here`. + See the API for generator functions :ref:`here`. Sampling -------- @@ -84,7 +84,6 @@ Modeling and Approximation :hidden: gpcam - tasmanian fd_param_finder surmise @@ -100,7 +99,7 @@ Modeling and Approximation Modular Bayesian calibration/inference framework using Surmise_ (demonstration of cancelling previous issued simulations). -- :doc:`Tasmanian` +- :ref:`Tasmanian` Evaluates points generators by the Tasmanian_ sparse grid library diff --git a/docs/examples/gest_api.rst b/docs/examples/gest_api.rst new file mode 100644 index 0000000000..077355b775 --- /dev/null +++ b/docs/examples/gest_api.rst @@ -0,0 +1,113 @@ +============================= +(New) Standardized Generators +============================= + +libEnsemble also supports all generators that use the gest_api_ interface. + +.. code-block:: python + :linenos: + :emphasize-lines: 17 + + from gest_api.vocs import VOCS + from optimas.generators import GridSamplingGenerator + + from libensemble.specs import GenSpecs + + vocs = VOCS( + variables={ + "x0": [-3.0, 2.0], + "x1": [1.0, 5.0], + }, + objectives={"f": "MAXIMIZE"}, + ) + + generator = GridSamplingGenerator(vocs=vocs, n_steps=[7, 15]) + + gen_specs = GenSpecs( + generator=generator, + batch_size=4, + vocs=vocs, + ) + ... + +Included with libEnsemble +========================= + +Sampling +-------- + +.. toctree:: + :maxdepth: 1 + :caption: Sampling + :hidden: + + gest_api/sampling + +- :doc:`Basic sampling` + + Various generators for sampling a space. + +Optimization +------------ + +.. toctree:: + :maxdepth: 1 + :caption: Optimization + :hidden: + + gest_api/aposmm + +- :doc:`APOSMM` + + Asynchronously Parallel Optimization Solver for finding Multiple Minima (paper_). + +Modeling and Approximation +-------------------------- + +.. toctree:: + :maxdepth: 1 + :caption: Modeling and Approximation + :hidden: + + gest_api/gpcam + +- :doc:`gpCAM` + + Gaussian Process-based adaptive sampling using gpcam_. + +Verified Third Party Examples +============================= + +Generators that use the gest_api_ interface and are verified to work with libEnsemble. + +The standardized interface was developed in partnership with their authors. + +Xopt - https://github.com/xopt-org/Xopt +--------------------------------------- + +Examples: + +`Expected Improvement`_ + +`Nelder Mead`_ + +Optimas - https://github.com/optimas-org/optimas +------------------------------------------------ + +Examples: + +`Grid Sampling`_ + +`Ax Multi-fidelity`_ + +.. _gest_api: https://github.com/campa-consortium/gest-api +.. _gpcam: https://gpcam.lbl.gov/ +.. _paper: https://link.springer.com/article/10.1007/s12532-017-0131-4 + +.. _Expected Improvement: https://github.com/Libensemble/libensemble/blob/develop/libensemble/tests/regression_tests/test_xopt_EI.py + +.. _Nelder Mead: https://github.com/Libensemble/libensemble/blob/develop/libensemble/tests/regression_tests/test_xopt_nelder_mead.py + +.. _Grid Sampling: https://github.com/Libensemble/libensemble/blob/develop/libensemble/tests/regression_tests/test_optimas_grid_sample.py + +.. _Ax Multi-fidelity: https://github.com/Libensemble/libensemble/blob/develop/libensemble/tests/regression_tests/test_optimas_ax_mf.py diff --git a/docs/examples/gest_api/aposmm.rst b/docs/examples/gest_api/aposmm.rst new file mode 100644 index 0000000000..dbbd4f7ad1 --- /dev/null +++ b/docs/examples/gest_api/aposmm.rst @@ -0,0 +1,24 @@ +APOSMM +------ + +.. autoclass:: gen_classes.aposmm.APOSMM + :members: suggest, ingest, export, suggest_updates, finalize + :undoc-members: + :show-inheritance: + + +APOSMM with libEnsemble +^^^^^^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../libensemble/tests/regression_tests/test_asktell_aposmm_nlopt.py + :linenos: + :start-at: workflow = Ensemble(parse_args=True) + :end-before: # Perform the run + +APOSMM standalone +^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../libensemble/tests/unit_tests/test_persistent_aposmm.py + :linenos: + :start-at: def test_asktell_ingest_first(): + :end-before: assert persis_info.get("run_order"), "Standalone persistent_aposmm didn't do any localopt runs" diff --git a/docs/examples/gest_api/gpcam.rst b/docs/examples/gest_api/gpcam.rst new file mode 100644 index 0000000000..8060e29280 --- /dev/null +++ b/docs/examples/gest_api/gpcam.rst @@ -0,0 +1,21 @@ +gpCAM +------ + +.. autoclass:: gen_classes.gpCAM.GP_CAM + :members: suggest, ingest + :undoc-members: + :show-inheritance: + + +.. autoclass:: gen_classes.gpCAM.GP_CAM_Covar + :members: suggest, ingest + :undoc-members: + :show-inheritance: + + +.. seealso:: + + .. literalinclude:: ../../../libensemble/tests/regression_tests/test_asktell_gpCAM.py + :linenos: + :start-at: vocs = VOCS(variables={"x0": [-3, 3], "x1": [-2, 2], "x2": [-1, 1], "x3": [-1, 1]}, objectives={"f": "MINIMIZE"}) + :end-before: if is_manager: diff --git a/docs/examples/gest_api/sampling.rst b/docs/examples/gest_api/sampling.rst new file mode 100644 index 0000000000..9659dc6e08 --- /dev/null +++ b/docs/examples/gest_api/sampling.rst @@ -0,0 +1,10 @@ +sampling +-------- + +.. autoclass:: gen_classes.sampling.UniformSample + :members: suggest, ingest + :undoc-members: + +.. autoclass:: gen_classes.external.sampling.UniformSample + :members: suggest, ingest + :undoc-members: diff --git a/docs/examples/persistent_sampling.rst b/docs/examples/persistent_sampling.rst index 7f778a8e8c..cf33eaa554 100644 --- a/docs/examples/persistent_sampling.rst +++ b/docs/examples/persistent_sampling.rst @@ -1,6 +1,9 @@ persistent_sampling ------------------- +.. role:: underline + :class: underline + .. automodule:: persistent_sampling :members: :undoc-members: diff --git a/docs/examples/sim_funcs.rst b/docs/examples/sim_funcs.rst index be4374d884..37fb6ecf14 100644 --- a/docs/examples/sim_funcs.rst +++ b/docs/examples/sim_funcs.rst @@ -8,7 +8,7 @@ function launching tasks, see the :doc:`Electrostatic Forces tutorial <../tutorials/executor_forces_tutorial>`. .. IMPORTANT:: - See the API for simulation functions :ref:`here`. + See the API for simulation functions :ref:`here`. .. role:: underline :class: underline @@ -60,5 +60,6 @@ Special simulation functions :maxdepth: 1 sim_funcs/mock_sim + sim_funcs/surmise_test_function .. _build_forces.sh: https://github.com/Libensemble/libensemble/blob/main/libensemble/tests/scaling_tests/forces/forces_app/build_forces.sh diff --git a/docs/examples/tasmanian.rst b/docs/examples/tasmanian.rst deleted file mode 100644 index 0099616f32..0000000000 --- a/docs/examples/tasmanian.rst +++ /dev/null @@ -1,31 +0,0 @@ -persistent_tasmanian --------------------- - -Required: Tasmanian_, pypackaging_, scikit-build_ - -Example usage: batched_, async_ - -Note that Tasmanian can be pip installed, but currently must -use either *venv* or *--user* install. - -``E.g: pip install scikit-build packaging Tasmanian --user`` - -.. automodule:: persistent_tasmanian - :members: sparse_grid_batched, sparse_grid_async - :undoc-members: - -.. role:: underline - :class: underline - -.. dropdown:: :underline:`persistent_tasmanian.py` - - .. literalinclude:: ../../libensemble/gen_funcs/persistent_tasmanian.py - :language: python - :linenos: - -.. _pypackaging: https://pypi.org/project/pypackaging/ -.. _scikit-build: https://scikit-build.readthedocs.io/en/latest/index.html - -.. Caution - tasmanian_ is named in example docstring so must be named differently -.. _batched: https://github.com/Libensemble/libensemble/blob/main/libensemble/tests/regression_tests/test_persistent_tasmanian.py -.. _async: https://github.com/Libensemble/libensemble/blob/main/libensemble/tests/regression_tests/test_persistent_tasmanian_async.py diff --git a/docs/executor/ex_base.rst b/docs/executor/ex_base.rst new file mode 100644 index 0000000000..1a4d3cf31d --- /dev/null +++ b/docs/executor/ex_base.rst @@ -0,0 +1,62 @@ +Base Executor +============= + +`Overview `__ \|\| **Base Executor** \|\| `MPI Executor `__ + +.. automodule:: executor + :no-undoc-members: + +Only for running local serial-launched applications. +To run MPI applications and use detected resources, use the `MPI Executor `__ tab. + +.. tab-set:: + + .. tab-item:: Base Executor + + .. autoclass:: libensemble.executors.executor.Executor + :members: + :exclude-members: serial_setup, sim_default_app, gen_default_app, get_app, default_app, set_resources, get_task, set_workerID, set_worker_info, new_tasks_timing, add_platform_info, set_gen_procs_gpus, kill, poll + + .. automethod:: __init__ + + .. tab-item:: Task + + .. _task_tag: + + Tasks are created and returned by the Executor's ``submit()``. Tasks + can be polled, killed, and waited on with the respective ``poll``, ``kill``, and ``wait`` functions. + Task information can be queried through instance attributes and query functions. + + .. autoclass:: libensemble.executors.executor.Task + :members: + :exclude-members: calc_task_timing, check_poll + + .. tab-item:: Task Attributes + + .. note:: + These should not be set directly. Tasks are launched by the Executor, + and task information can be queried through the task attributes + below and the query functions. + + :task.state: (string) The task status. One of + ("UNKNOWN"|"CREATED"|"WAITING"|"RUNNING"|"FINISHED"|"USER_KILLED"|"FAILED"|"FAILED_TO_START") + + :task.process: (process obj) The process object used by the underlying process + manager (e.g., return value of subprocess.Popen). + :task.errcode: (int) The error code (or return code) used by the underlying process manager. + :task.finished: (boolean) True means task has finished running - not whether it was successful. + :task.success: (boolean) Did task complete successfully (e.g., the return code is zero)? + :task.runtime: (int) Time in seconds that task has been running. + :task.submit_time: (int) Time since epoch that task was submitted. + :task.total_time: (int) Total time from task submission to completion (only available when task is finished). + + Run configuration attributes - some will be autogenerated: + + :task.workdir: (string) Work directory for the task + :task.name: (string) Name of task - autogenerated + :task.app: (app obj) Use application/executable, registered using exctr.register_app + :task.app_args: (string) Application arguments as a string + :task.stdout: (string) Name of file where the standard output of the task is written (in task.workdir) + :task.stderr: (string) Name of file where the standard error of the task is written (in task.workdir) + :task.dry_run: (boolean) True if task corresponds to dry run (no actual submission) + :task.runline: (string) Complete, parameterized command to be subprocessed to launch app diff --git a/docs/executor/ex_index.rst b/docs/executor/ex_index.rst index ee4698c21f..a4f33cb39a 100644 --- a/docs/executor/ex_index.rst +++ b/docs/executor/ex_index.rst @@ -1,5 +1,7 @@ .. _executor_index: +**Overview** \|\| `Base Executor `__ \|\| `MPI Executor `__ + Executors ========= @@ -7,10 +9,13 @@ libEnsemble's Executors can be used within user functions to provide a simple, portable interface for running and managing user applications. .. toctree:: - :maxdepth: 2 - :titlesonly: - :caption: libEnsemble Executors: + :hidden: + + ex_overview + ex_base + ex_mpi + +The **Executor** provides a portable interface for running applications on any system and +any number of compute resources. - overview - executor - mpi_executor +Please select from the sections above or the sidebar navigation to read more. diff --git a/docs/executor/mpi_executor.rst b/docs/executor/ex_mpi.rst similarity index 60% rename from docs/executor/mpi_executor.rst rename to docs/executor/ex_mpi.rst index 13773f5ad5..59a36f9e52 100644 --- a/docs/executor/mpi_executor.rst +++ b/docs/executor/ex_mpi.rst @@ -1,30 +1,24 @@ -MPI Executor - MPI apps -======================= +MPI Executor +============ -.. automodule:: mpi_executor - :no-undoc-members: +`Overview `__ \|\| `Base Executor `__ \|\| **MPI Executor** -See this :doc:`example` for usage. +.. automodule:: mpi_executor + :no-undoc-members: .. autoclass:: libensemble.executors.mpi_executor.MPIExecutor - :show-inheritance: - :inherited-members: - :exclude-members: serial_setup, sim_default_app, gen_default_app, get_app, default_app, set_resources, get_task, set_workerID, set_worker_info, new_tasks_timing, add_platform_info, set_gen_procs_gpus, kill, poll - -.. .. automethod:: __init__ - -.. :member-order: bysource -.. :members: __init__, register_app, submit, manager_poll + :show-inheritance: + :inherited-members: + :exclude-members: serial_setup, sim_default_app, gen_default_app, get_app, default_app, set_resources, get_task, set_workerID, set_worker_info, new_tasks_timing, add_platform_info, set_gen_procs_gpus, kill, poll -Class-specific Attributes -------------------------- +**Class-specific Attributes** Class-specific attributes can be set directly to alter the behavior of the MPI Executor. However, they should be used with caution, because they may not be implemented in other executors. :max_submit_attempts: (int) Maximum number of launch attempts for a given - task. *Default: 5*. + task. *Default: 5*. :fail_time: (int or float) *Only if wait_on_start is set.* Maximum run time to failure in seconds that results in relaunch. *Default: 2*. :retry_delay_incr: (int or float) Delay increment between launch attempts in seconds. diff --git a/docs/executor/overview.rst b/docs/executor/ex_overview.rst similarity index 79% rename from docs/executor/overview.rst rename to docs/executor/ex_overview.rst index 196ba38b8b..f53510b733 100644 --- a/docs/executor/overview.rst +++ b/docs/executor/ex_overview.rst @@ -1,11 +1,10 @@ -Executor Overview -================= +Overview +======== -Most computationally expensive libEnsemble workflows involve launching applications -from a :ref:`sim_f` or :ref:`gen_f` running on a worker to the -compute nodes of a supercomputer, cluster, or other compute resource. +**Overview** \|\| `Base Executor `__ \|\| `MPI Executor `__ -The **Executor** provides a portable interface for running applications on any system. +The **Executor** provides a portable interface for running applications on any system and +any number of compute resources. .. dropdown:: Detailed description @@ -37,10 +36,7 @@ The **Executor** provides a portable interface for running applications on any s ``app_name`` from registration in the calling script alongside other optional parameters described in the API. -Basic usage ------------ - -**In calling script** +**Basic usage** To set up an MPI executor, register an MPI application, and add to the ensemble object. @@ -54,10 +50,6 @@ to the ensemble object. exctr.register_app(full_path="/path/to/my/exe", app_name="sim1") ensemble = Ensemble(executor=exctr) -If using the ``libE()`` call, the Executor in the calling script does **not** -have to be passed to the ``libE()`` function. It is transferred via the -``Executor.executor`` class variable. - **In user simulation function**:: def sim_func(H, persis_info, sim_specs, libE_info): @@ -82,15 +74,11 @@ Example use-cases: * :doc:`Forces example with GPUs <../tutorials/forces_gpu_tutorial>`: Auto-assigns GPUs via executor. -See the :doc:`Executor` or :doc:`MPIExecutor` interface -for the complete API. - See :doc:`Running on HPC Systems<../platforms/platforms_index>` for illustrations of how common options such as ``libE_specs["dedicated_mode"]`` affect the run configuration on clusters and supercomputers. -Advanced Features ------------------ +**Advanced Features** **Example of polling output and killing application:** @@ -136,16 +124,6 @@ In simulation function (sim_f). print(task.state) # state may be finished/failed/killed -.. The Executor can also be retrieved using Python's ``with`` context switching statement, -.. although this is effectively syntactical sugar to above:: -.. -.. from libensemble.executors import Executor -.. -.. with Executor.executor as exctr: -.. task = exctr.submit(app_name="sim1", num_procs=8, app_args="input.txt", -.. stdout="out.txt", stderr="err.txt") -.. ... - Users who wish to poll only for manager kill signals and timeouts don't necessarily need to construct a polling loop like above, but can instead use the ``Executor`` built-in ``polling_loop()`` method. An alternative to the above simulation function @@ -178,10 +156,4 @@ which partitions resources among workers, ensuring that runs utilize different resources (e.g., nodes). Furthermore, the ``MPIExecutor`` offers resilience via the feature of re-launching tasks that fail to start because of system factors. -Various back-end mechanisms may be used by the Executor to best interact -with each system, including proxy launchers or task management systems. -Currently, these Executors launch at the application level within -an existing resource pool. However, submissions to a batch scheduler may be -supported in future Executors. - .. _concurrent futures: https://docs.python.org/library/concurrent.futures.html diff --git a/docs/executor/executor.rst b/docs/executor/executor.rst deleted file mode 100644 index 6784134a05..0000000000 --- a/docs/executor/executor.rst +++ /dev/null @@ -1,62 +0,0 @@ -Base Executor - Local apps -========================== - -.. automodule:: executor - :no-undoc-members: - -See the Executor APIs for optional arguments. - -.. tab-set:: - - .. tab-item:: Base Executor - - Only for running local serial-launched applications. - To run MPI applications and use detected resources, use the :doc:`MPIExecutor<../executor/mpi_executor>` - - .. autoclass:: libensemble.executors.executor.Executor - :members: - :exclude-members: serial_setup, sim_default_app, gen_default_app, get_app, default_app, set_resources, get_task, set_workerID, set_worker_info, new_tasks_timing, add_platform_info, set_gen_procs_gpus, kill, poll - - .. automethod:: __init__ - - .. tab-item:: Task - - .. _task_tag: - - Tasks are created and returned by the Executor's ``submit()``. Tasks - can be polled, killed, and waited on with the respective ``poll``, ``kill``, and ``wait`` functions. - Task information can be queried through instance attributes and query functions. - - .. autoclass:: libensemble.executors.executor.Task - :members: - :exclude-members: calc_task_timing, check_poll - - .. tab-item:: Task Attributes - - .. note:: - These should not be set directly. Tasks are launched by the Executor, - and task information can be queried through the task attributes - below and the query functions. - - :task.state: (string) The task status. One of - ("UNKNOWN"|"CREATED"|"WAITING"|"RUNNING"|"FINISHED"|"USER_KILLED"|"FAILED"|"FAILED_TO_START") - - :task.process: (process obj) The process object used by the underlying process - manager (e.g., return value of subprocess.Popen). - :task.errcode: (int) The error code (or return code) used by the underlying process manager. - :task.finished: (boolean) True means task has finished running - not whether it was successful. - :task.success: (boolean) Did task complete successfully (e.g., the return code is zero)? - :task.runtime: (int) Time in seconds that task has been running. - :task.submit_time: (int) Time since epoch that task was submitted. - :task.total_time: (int) Total time from task submission to completion (only available when task is finished). - - Run configuration attributes - some will be autogenerated: - - :task.workdir: (string) Work directory for the task - :task.name: (string) Name of task - autogenerated - :task.app: (app obj) Use application/executable, registered using exctr.register_app - :task.app_args: (string) Application arguments as a string - :task.stdout: (string) Name of file where the standard output of the task is written (in task.workdir) - :task.stderr: (string) Name of file where the standard error of the task is written (in task.workdir) - :task.dry_run: (boolean) True if task corresponds to dry run (no actual submission) - :task.runline: (string) Complete, parameterized command to be subprocessed to launch app diff --git a/docs/function_guides/allocator.rst b/docs/function_guides/allocator.rst index a65c404ab9..7a04f2f783 100644 --- a/docs/function_guides/allocator.rst +++ b/docs/function_guides/allocator.rst @@ -4,23 +4,21 @@ Allocation Functions ==================== Although the included allocation functions are sufficient for -most users, those who want to fine-tune how data or resources are allocated to their generator or simulator can write their own. +most users, those who want to fine-tune how data or resources +may be allocated to their generator or simulator can write their own. -The ``alloc_f`` is unique since it is called by libEnsemble's manager instead of a worker. +We encourage experimenting with: -For allocation functions, as with the other user functions, the level of complexity can -vary widely. We encourage experimenting with: - - 1. Prioritization of simulations - 2. Sending results immediately or in batch - 3. Assigning varying resources to evaluations +1. Prioritization of simulations +2. Sending results immediately or in batch +3. Assigning varying resources to evaluations .. dropdown:: Example .. literalinclude:: ../../libensemble/alloc_funcs/fast_alloc.py :caption: libensemble.alloc_funcs.fast_alloc.give_sim_work_first -Most ``alloc_f`` function definitions written by users resemble:: +The ``alloc_f`` function definition resembles:: def my_allocator(W, H, sim_specs, gen_specs, alloc_specs, persis_info, libE_info): @@ -35,14 +33,14 @@ Most users first check that it is appropriate to allocate work:: if libE_info["sim_max_given"] or not libE_info["any_idle_workers"]: return {}, persis_info -If the allocation is to continue, a support class is instantiated and a -:ref:`Work dictionary` is initialized:: +If the allocation is to continue, instantiate a support class to assist with the +:ref:`Work dictionary` construction:: manage_resources = "resource_sets" in H.dtype.names or libE_info["use_resource_sets"] support = AllocSupport(W, manage_resources, persis_info, libE_info) Work = {} -This Work dictionary is populated with integer keys ``wid`` for each worker and +The Work dictionary is populated with integer keys ``wid`` for each worker and dictionary values to give to those workers: .. dropdown:: Example ``Work`` @@ -102,14 +100,20 @@ is returned as the third value, this instructs the ensemble to stop. Information from the manager describing the progress of the current libEnsemble routine can be found in ``libE_info``:: - libE_info = {"exit_criteria": dict, # Criteria for ending routine - "elapsed_time": float, # Time elapsed since start of routine - "manager_kill_canceled_sims": bool, # True if manager is to send kills to cancelled simulations - "sim_started_count": int, # Total number of points given for simulation function evaluation - "sim_ended_count": int, # Total number of points returned from simulation function evaluations - "gen_informed_count": int, # Total number of evaluated points given back to a generator function - "sim_max_given": bool, # True if `sim_max` simulations have been given out to workers - "use_resource_sets": bool} # True if num_resource_sets has been explicitly set. + libE_info = { + "any_idle_workers": bool, # True if there are any idle workers + "exit_criteria": {...}, # Criteria for ending routine + "elapsed_time": float, # Time elapsed since start of routine + "gen_informed_count": int, # Total number of evaluated points given back to a generator function + "manager_kill_canceled_sims": bool, # True if manager is to send kills to cancelled simulations + "scheduler_opts": {...}, # Options passed to the scheduler. "split2fit" and "match_slots" + "sim_started_count": int, # Total number of points given for simulation function evaluation + "sim_ended_count": int, # Total number of points returned from simulation function evaluations + "sim_max_given": bool, # True if `sim_max` simulations have been given out to workers + "use_resource_sets": bool, # True if num_resource_sets has been explicitly set. + "gen_num_procs": int, # Number of processes used for generator function evaluations + "gen_num_gpus": int, # Number of GPUs used for generator function evaluations + "gen_on_worker": bool} # True if generator function is running on a worker Most often, the allocation function will just return once ``sim_max_given`` is ``True``, but the user could choose to do something different, @@ -126,10 +130,110 @@ or mark points for cancellation. The remaining values above are useful for efficient filtering of H values (e.g., ``sim_ended_count`` saves filtering by an entire column of H.) -Descriptions of included allocation functions can be found :doc:`here<../examples/alloc_funcs>`. The default allocation function is -``give_sim_work_first``. During its worker ID loop, it checks if there's unallocated +``start_only_persistent``. During its worker ID loop, it checks if there's unallocated work and assigns simulations for that work. Otherwise, it initializes generators for up to ``"num_active_gens"`` instances. Other settings like ``batch_mode`` are also supported. See -:ref:`here` for more information about ``give_sim_work_first``. +:ref:`here` for more information. + +.. _examples-alloc: + +Examples +======== + +Below are example allocation functions available in libEnsemble. + +Many users use these unmodified. + +.. IMPORTANT:: + The default allocation function changed in libEnsemble v2.0 from ``give_sim_work_first`` to ``start_only_persistent``. + +.. note:: + + The most commonly used allocation function for non-persistent generators is :ref:`give_sim_work_first`. + +.. role:: underline + :class: underline + +.. _start_only_persistent_label: + +start_only_persistent +--------------------- +.. automodule:: start_only_persistent + :members: + :undoc-members: + +.. dropdown:: :underline:`start_only_persistent.py` + + .. literalinclude:: ../../libensemble/alloc_funcs/start_only_persistent.py + :language: python + :linenos: + +.. _gswf_label: + +give_sim_work_first +------------------- +.. automodule:: give_sim_work_first + :members: + :undoc-members: + +.. dropdown:: :underline:`give_sim_work_first.py` + + .. literalinclude:: ../../libensemble/alloc_funcs/give_sim_work_first.py + :language: python + :linenos: + +fast_alloc +---------- +.. automodule:: fast_alloc + :members: + :undoc-members: + +.. dropdown:: :underline:`fast_alloc.py` + + .. literalinclude:: ../../libensemble/alloc_funcs/fast_alloc.py + :language: python + :linenos: + +start_persistent_local_opt_gens +------------------------------- +.. automodule:: start_persistent_local_opt_gens + :members: + :undoc-members: + +fast_alloc_and_pausing +---------------------- +.. automodule:: fast_alloc_and_pausing + :members: + :undoc-members: + +only_one_gen_alloc +------------------ +.. automodule:: only_one_gen_alloc + :members: + :undoc-members: + +start_fd_persistent +------------------- +.. automodule:: start_fd_persistent + :members: + :undoc-members: + +persistent_aposmm_alloc +----------------------- +.. automodule:: persistent_aposmm_alloc + :members: + :undoc-members: + +give_pregenerated_work +---------------------- +.. automodule:: give_pregenerated_work + :members: + :undoc-members: + +inverse_bayes_allocf +-------------------- +.. automodule:: inverse_bayes_allocf + :members: + :undoc-members: diff --git a/docs/function_guides/calc_status.rst b/docs/function_guides/calc_status.rst index fc1038a36f..93384bc2ae 100644 --- a/docs/function_guides/calc_status.rst +++ b/docs/function_guides/calc_status.rst @@ -19,81 +19,81 @@ user-specified string. They are the third optional return value from a user func Built-in codes are available in the ``libensemble.message_numbers`` module, but users are also free to return any custom string. -.. tab-set:: - - .. tab-item:: calc_status with :ref:`Executor` - - .. code-block:: python - :linenos: - :emphasize-lines: 4,16,19,22,30 - - from libensemble.message_numbers import WORKER_DONE, WORKER_KILL, TASK_FAILED - - task = exctr.submit(calc_type="sim", num_procs=cores, wait_on_start=True) - calc_status = UNSET_TAG - poll_interval = 1 # secs - while not task.finished: - if task.runtime > time_limit: - task.kill() # Timeout - else: - time.sleep(poll_interval) - task.poll() - - if task.finished: - if task.state == "FINISHED": - print("Task {} completed".format(task.name)) - calc_status = WORKER_DONE - elif task.state == "FAILED": - print("Warning: Task {} failed: Error code {}".format(task.name, task.errcode)) - calc_status = TASK_FAILED - elif task.state == "USER_KILLED": - print("Warning: Task {} has been killed".format(task.name)) - calc_status = WORKER_KILL - else: - print("Warning: Task {} in unknown state {}. Error code {}".format(task.name, task.state, task.errcode)) - - outspecs = sim_specs["out"] - output = np.zeros(1, dtype=outspecs) - output["energy"][0] = final_energy - - return output, persis_info, calc_status - - .. tab-item:: Custom calc_status - - .. code-block:: python - :linenos: - - from libensemble.message_numbers import WORKER_DONE, TASK_FAILED - - task = exctr.submit(calc_type="sim", num_procs=cores, wait_on_start=True) - - task.wait(timeout=60) - - file_output = read_task_output(task) - if task.errcode == 0: - if "fail" in file_output: - calc_status = "Task failed successfully?" - else: - calc_status = WORKER_DONE - else: - calc_status = TASK_FAILED - - outspecs = sim_specs["out"] - output = np.zeros(1, dtype=outspecs) - output["energy"][0] = final_energy - - return output, persis_info, calc_status - -.. tab-set:: - - .. tab-item:: Available values - - .. literalinclude:: ../../libensemble/message_numbers.py - :start-after: first_calc_status_rst_tag - :end-before: last_calc_status_rst_tag - - .. tab-item:: Corresponding messages - - .. literalinclude:: ../../libensemble/message_numbers.py - :start-at: calc_status_strings - :end-before: last_calc_status_string_rst_tag +calc_status with Executor +--------------------------- + +.. code-block:: python + :linenos: + :emphasize-lines: 4,16,19,22,30 + + from libensemble.message_numbers import WORKER_DONE, WORKER_KILL, TASK_FAILED + + task = exctr.submit(calc_type="sim", num_procs=cores, wait_on_start=True) + calc_status = UNSET_TAG + poll_interval = 1 # secs + while not task.finished: + if task.runtime > time_limit: + task.kill() # Timeout + else: + time.sleep(poll_interval) + task.poll() + + if task.finished: + if task.state == "FINISHED": + print("Task {} completed".format(task.name)) + calc_status = WORKER_DONE + elif task.state == "FAILED": + print("Warning: Task {} failed: Error code {}".format(task.name, task.errcode)) + calc_status = TASK_FAILED + elif task.state == "USER_KILLED": + print("Warning: Task {} has been killed".format(task.name)) + calc_status = WORKER_KILL + else: + print("Warning: Task {} in unknown state {}. Error code {}".format(task.name, task.state, task.errcode)) + + outspecs = sim_specs["out"] + output = np.zeros(1, dtype=outspecs) + output["energy"][0] = final_energy + + return output, persis_info, calc_status + +Custom calc_status +------------------ + +.. code-block:: python + :linenos: + + from libensemble.message_numbers import WORKER_DONE, TASK_FAILED + + task = exctr.submit(calc_type="sim", num_procs=cores, wait_on_start=True) + + task.wait(timeout=60) + + file_output = read_task_output(task) + if task.errcode == 0: + if "fail" in file_output: + calc_status = "Task failed successfully?" + else: + calc_status = WORKER_DONE + else: + calc_status = TASK_FAILED + + outspecs = sim_specs["out"] + output = np.zeros(1, dtype=outspecs) + output["energy"][0] = final_energy + + return output, persis_info, calc_status + +Available values +---------------- + +.. literalinclude:: ../../libensemble/message_numbers.py + :start-after: first_calc_status_rst_tag + :end-before: last_calc_status_rst_tag + +Corresponding messages +---------------------- + +.. literalinclude:: ../../libensemble/message_numbers.py + :start-at: calc_status_strings + :end-before: last_calc_status_string_rst_tag diff --git a/docs/function_guides/function_guide_index.rst b/docs/function_guides/function_guide_index.rst index 621bf36d27..916a6fdd50 100644 --- a/docs/function_guides/function_guide_index.rst +++ b/docs/function_guides/function_guide_index.rst @@ -1,28 +1,19 @@ -====================== -Writing User Functions -====================== +===================== +Writing Gens and Sims +===================== -User functions typically require only some familiarity with NumPy_, but if they conform to -the :ref:`user function APIs`, they can incorporate methods from machine-learning, -mathematics, resource management, or other libraries/applications. - -These guides describe common development patterns and optional components: +These guides describe common development patterns and optional components +for users writing generators and simulators for libEnsemble. .. toctree:: :maxdepth: 2 - :caption: Writing User Functions + :caption: Writing Gens and Sims generator simulator - allocator - sim_gen_alloc_api .. toctree:: :maxdepth: 2 :caption: Useful Data Structures calc_status - work_dict - worker_array - -.. _NumPy: http://www.numpy.org diff --git a/docs/function_guides/generator.rst b/docs/function_guides/generator.rst index c0d530e72e..c560ce3934 100644 --- a/docs/function_guides/generator.rst +++ b/docs/function_guides/generator.rst @@ -1,251 +1,26 @@ .. _funcguides-gen: -Generator Functions -=================== +Generators +========== -Generator and :ref:`Simulator functions` have relatively similar interfaces. +**Introduction** \|\| `Standardized Generator (gest-api) `__ \|\| `Legacy Generator Function `__ Writing a Generator ------------------- -.. tab-set:: - - .. tab-item:: Non-decorated - :sync: nodecorate - - .. code-block:: python - - def my_generator(Input, persis_info, gen_specs, libE_info): - batch_size = gen_specs["user"]["batch_size"] - - Output = np.zeros(batch_size, gen_specs["out"]) - # ... - Output["x"], persis_info = generate_next_simulation_inputs(Input["f"], persis_info) - - return Output, persis_info - - .. tab-item:: Decorated - :sync: decorate - - .. code-block:: python - - from libensemble.specs import input_fields, output_data - - - @input_fields(["f"]) - @output_data([("x", float)]) - def my_generator(Input, persis_info, gen_specs, libE_info): - batch_size = gen_specs["user"]["batch_size"] - - Output = np.zeros(batch_size, gen_specs["out"]) - # ... - Output["x"], persis_info = generate_next_simulation_inputs(Input["f"], persis_info) - - return Output, persis_info - -Most ``gen_f`` function definitions written by users resemble:: - - def my_generator(Input, persis_info, gen_specs, libE_info): - -where: - - * ``Input`` is a selection of the :ref:`History array`, a NumPy structured array. - * :ref:`persis_info` is a dictionary containing state information. - * :ref:`gen_specs` is a dictionary of generator parameters. - * ``libE_info`` is a dictionary containing miscellaneous entries. - -Valid generator functions can accept a subset of the above parameters. So a very simple generator can start:: - - def my_generator(Input): - -If ``gen_specs`` was initially defined: - -.. tab-set:: - - .. tab-item:: Non-decorated function - :sync: nodecorate - - .. code-block:: python - - gen_specs = GenSpecs( - gen_f=my_generator, - inputs=["f"], - outputs=["x", float, (1,)], - user={"batch_size": 128}, - ) - - .. tab-item:: Decorated function - :sync: decorate - - .. code-block:: python - - gen_specs = GenSpecs( - gen_f=my_generator, - user={"batch_size": 128}, - ) - -Then user parameters and a *local* array of outputs may be obtained/initialized like:: - - batch_size = gen_specs["user"]["batch_size"] - Output = np.zeros(batch_size, dtype=gen_specs["out"]) - -This array should be populated by whatever values are generated within -the function:: - - Output["x"], persis_info = generate_next_simulation_inputs(Input["f"], persis_info) - -Then return the array and ``persis_info`` to libEnsemble:: - - return Output, persis_info - -Between the ``Output`` definition and the ``return``, any computation can be performed. -Users can try an :doc:`executor<../executor/overview>` to submit applications to parallel -resources, or plug in components from other libraries to serve their needs. - .. note:: + The `gest-api` generator interface is the recommended approach for new libEnsemble projects. + The "Legacy Generator Function" interface is supported for backward compatibility but may be deprecated in a future release. - State ``gen_f`` information like checkpointing should be - appended to ``persis_info``. - -.. _persistent-gens: - -Persistent Generators ---------------------- - -While non-persistent generators return after completing their calculation, persistent -generators do the following in a loop: - - 1. Receive simulation results and metadata; exit if metadata instructs. - 2. Perform analysis. - 3. Send subsequent simulation parameters. - -Persistent generators don't need to be re-initialized on each call, but are typically -more complicated. The persistent :doc:`APOSMM<../examples/aposmm>` -optimization generator function included with libEnsemble maintains -local optimization subprocesses based on results from complete simulations. - -Use ``GenSpecs.persis_in`` to specify fields to send back to the generator throughout the run. -``GenSpecs.inputs`` only describes the input fields when the function is **first called**. - -Functions for a persistent generator to communicate directly with the manager -are available in the :ref:`libensemble.tools.persistent_support` class. - -Sending/receiving data is supported by the :ref:`PersistentSupport` class:: - - from libensemble.tools import PersistentSupport - from libensemble.message_numbers import STOP_TAG, PERSIS_STOP, EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG - - my_support = PersistentSupport(libE_info, EVAL_GEN_TAG) - -Implementing functions from the above class is relatively simple: - -.. tab-set:: - - .. tab-item:: send - - .. currentmodule:: libensemble.tools.persistent_support.PersistentSupport - .. autofunction:: send - - This function call typically resembles:: - - my_support.send(local_H_out[selected_IDs]) - - Note that this function has no return. - - .. tab-item:: recv - - .. currentmodule:: libensemble.tools.persistent_support.PersistentSupport - .. autofunction:: recv - - This function call typically resembles:: - - tag, Work, calc_in = my_support.recv() - - if tag in [STOP_TAG, PERSIS_STOP]: - cleanup() - break - - The logic following the function call is typically used to break the persistent - generator's main loop and return. - - .. tab-item:: send_recv - - .. currentmodule:: libensemble.tools.persistent_support.PersistentSupport - .. autofunction:: send_recv - - This function performs both of the previous functions in a single statement. Its - usage typically resembles:: - - tag, Work, calc_in = my_support.send_recv(local_H_out[selected_IDs]) - if tag in [STOP_TAG, PERSIS_STOP]: - cleanup() - break - - Once the persistent generator's loop has been broken because of - the tag from the manager, it should return with an additional tag:: - - return local_H_out, persis_info, FINISHED_PERSISTENT_GEN_TAG - -See :ref:`calc_status` for more information about -the message tags. - -.. _gen_active_recv: - -Active receive mode -------------------- - -By default, a persistent worker is expected to -receive and send data in a *ping pong* fashion. Alternatively, -a worker can be initiated in *active receive* mode by the allocation -function (see :ref:`start_only_persistent`). -The persistent worker can then send and receive from the manager at any time. - -Ensure there are no communication deadlocks in this mode. In manager-worker message exchanges, only the worker-side -receive is blocking by default (a non-blocking option is available). - -Cancelling Simulations ----------------------- - -Previously submitted simulations can be cancelled by sending a message to the manager: - -.. currentmodule:: libensemble.tools.persistent_support.PersistentSupport -.. autofunction:: request_cancel_sim_ids - -- If a generated point is cancelled by the generator **before sending** to another worker for simulation, then it won't be sent. -- If that point has **already been evaluated** by a simulation, the ``cancel_requested`` field will remain ``True``. -- If that point is **currently being evaluated**, a kill signal will be sent to the corresponding worker; it must be manually processed in the simulation function. - -The :doc:`Borehole Calibration tutorial<../tutorials/calib_cancel_tutorial>` gives an example -of the capability to cancel pending simulations. - -Modification of existing points -------------------------------- - -To change existing fields of the History array, create a NumPy structured array where the ``dtype`` contains -the ``sim_id`` and the fields to be modified. Send this array with ``keep_state=True`` to the manager. -This will overwrite the manager's History array. - -For example, the cancellation function ``request_cancel_sim_ids`` could be replicated by -the following (where ``sim_ids_to_cancel`` is a list of integers): - -.. code-block:: python - - # Send only these fields to existing H rows and libEnsemble will slot in the change. - H_o = np.zeros(len(sim_ids_to_cancel), dtype=[("sim_id", int), ("cancel_requested", bool)]) - H_o["sim_id"] = sim_ids_to_cancel - H_o["cancel_requested"] = True - ps.send(H_o, keep_state=True) - -Generator initiated shutdown ----------------------------- +Tutorial sections +----------------- -If using a supporting allocation function, the generator can prompt the ensemble to shutdown -by simply exiting the function (e.g., on a test for a converged value). For example, the -allocation function :ref:`start_only_persistent` closes down -the ensemble as soon as a persistent generator returns. The usual return values should be given. +1. Introduction (this page) +2. :doc:`Standardized Generator (gest-api) ` +3. :doc:`Legacy Generator Function ` -Examples --------- +.. toctree:: + :hidden: -Examples of non-persistent and persistent generator functions -can be found :doc:`here<../examples/gen_funcs>`. + generator_standardized + generator_legacy diff --git a/docs/function_guides/generator_legacy.rst b/docs/function_guides/generator_legacy.rst new file mode 100644 index 0000000000..c8c155a363 --- /dev/null +++ b/docs/function_guides/generator_legacy.rst @@ -0,0 +1,202 @@ +Legacy Generator Function +========================= + +`Introduction `__ \|\| `Standardized Generator (gest-api) `__ \|\| **Legacy Generator Function** + +.. code-block:: python + + def my_generator(Input, persis_info, gen_specs, libE_info): + batch_size = gen_specs["user"]["batch_size"] + + Output = np.zeros(batch_size, gen_specs["out"]) + # ... + Output["x"], persis_info = generate_next_simulation_inputs(Input["f"], persis_info) + + return Output, persis_info + +Most ``gen_f`` function definitions written by users resemble:: + + def my_generator(Input, persis_info, gen_specs, libE_info): + +where: + + * ``Input`` is a selection of the :ref:`History array`, a NumPy structured array. + * :ref:`persis_info` is a dictionary containing state information. + * :ref:`gen_specs` is a dictionary of generator parameters. + * ``libE_info`` is a dictionary containing miscellaneous entries. + +Valid generator functions can accept a subset of the above parameters. So a very simple generator can start:: + + def my_generator(Input): + +If ``gen_specs`` was initially defined: + +.. code-block:: python + + gen_specs = GenSpecs( + gen_f=my_generator, + inputs=["f"], + outputs=["x", float, (1,)], + user={"batch_size": 128}, + ) + +Then user parameters and a *local* array of outputs may be obtained/initialized like:: + + batch_size = gen_specs["user"]["batch_size"] + Output = np.zeros(batch_size, dtype=gen_specs["out"]) + +This array should be populated by whatever values are generated within +the function:: + + Output["x"], persis_info = generate_next_simulation_inputs(Input["f"], persis_info) + +Then return the array and ``persis_info`` to libEnsemble:: + + return Output, persis_info + +Between the ``Output`` definition and the ``return``, any computation can be performed. +Users can try an :doc:`executor<../executor/ex_index>` to submit applications to parallel +resources, or plug in components from other libraries to serve their needs. + +.. note:: + + State ``gen_f`` information like checkpointing should be + appended to ``persis_info``. + +.. _persistent-gens: + +**Persistent Generators** + +While non-persistent generators return after completing their calculation, persistent +generators do the following in a loop: + + 1. Receive simulation results and metadata; exit if metadata instructs. + 2. Perform analysis. + 3. Send subsequent simulation parameters. + +Persistent generators don't need to be re-initialized on each call, but are typically +more complicated. The persistent :doc:`APOSMM<../examples/aposmm>` +optimization generator function included with libEnsemble maintains +local optimization subprocesses based on results from complete simulations. + +Use ``GenSpecs.persis_in`` to specify fields to send back to the generator throughout the run. +``GenSpecs.inputs`` only describes the input fields when the function is **first called**. + +Functions for a persistent generator to communicate directly with the manager +are available in the :ref:`libensemble.tools.persistent_support` class. + +Sending/receiving data is supported by the :ref:`PersistentSupport` class:: + + from libensemble.tools import PersistentSupport + from libensemble.message_numbers import STOP_TAG, PERSIS_STOP, EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG + + my_support = PersistentSupport(libE_info, EVAL_GEN_TAG) + +Implementing functions from the above class is relatively simple: + +send +^^^^ + +.. currentmodule:: libensemble.tools.persistent_support.PersistentSupport +.. autofunction:: send + +This function call typically resembles:: + + my_support.send(local_H_out[selected_IDs]) + +Note that this function has no return. + +recv +^^^^ + +.. currentmodule:: libensemble.tools.persistent_support.PersistentSupport +.. autofunction:: recv + +This function call typically resembles:: + + tag, Work, calc_in = my_support.recv() + + if tag in [STOP_TAG, PERSIS_STOP]: + cleanup() + break + +The logic following the function call is typically used to break the persistent +generator's main loop and return. + +send_recv +^^^^^^^^^ + +.. currentmodule:: libensemble.tools.persistent_support.PersistentSupport +.. autofunction:: send_recv + +This function performs both of the previous functions in a single statement. Its +usage typically resembles:: + + tag, Work, calc_in = my_support.send_recv(local_H_out[selected_IDs]) + if tag in [STOP_TAG, PERSIS_STOP]: + cleanup() + break + +Once the persistent generator's loop has been broken because of +the tag from the manager, it should return with an additional tag:: + + return local_H_out, persis_info, FINISHED_PERSISTENT_GEN_TAG + +See :ref:`calc_status` for more information about +the message tags. + +.. _gen_active_recv: + +**Active receive mode** + +By default, a persistent worker is expected to +receive and send data in a *ping pong* fashion. Alternatively, +a worker can be initiated in *active receive* mode by the allocation +function (see :ref:`start_only_persistent`). +The persistent worker can then send and receive from the manager at any time. + +Ensure there are no communication deadlocks in this mode. In manager-worker message exchanges, only the worker-side +receive is blocking by default (a non-blocking option is available). + +**Cancelling Simulations** + +Previously submitted simulations can be cancelled by sending a message to the manager: + +.. currentmodule:: libensemble.tools.persistent_support.PersistentSupport +.. autofunction:: request_cancel_sim_ids + +- If a generated point is cancelled by the generator **before sending** to another worker for simulation, then it won't be sent. +- If that point has **already been evaluated** by a simulation, the ``cancel_requested`` field will remain ``True``. +- If that point is **currently being evaluated**, a kill signal will be sent to the corresponding worker; it must be manually processed in the simulation function. + +The :doc:`Borehole Calibration tutorial<../tutorials/calib_cancel_tutorial>` gives an example +of the capability to cancel pending simulations. + +**Modification of existing points** + +To change existing fields of the History array, create a NumPy structured array where the ``dtype`` contains +the ``sim_id`` and the fields to be modified. Send this array with ``keep_state=True`` to the manager. +This will overwrite the manager's History array. + +For example, the cancellation function ``request_cancel_sim_ids`` could be replicated by +the following (where ``sim_ids_to_cancel`` is a list of integers): + +.. code-block:: python + + # Send only these fields to existing H rows and libEnsemble will slot in the change. + H_o = np.zeros(len(sim_ids_to_cancel), dtype=[("sim_id", int), ("cancel_requested", bool)]) + H_o["sim_id"] = sim_ids_to_cancel + H_o["cancel_requested"] = True + ps.send(H_o, keep_state=True) + +**Generator initiated shutdown** + +If using a supporting allocation function, the generator can prompt the ensemble to shutdown +by simply exiting the function (e.g., on a test for a converged value). For example, the +allocation function :ref:`start_only_persistent` closes down +the ensemble as soon as a persistent generator returns. The usual return values should be given. + +**Examples** + +Examples of non-persistent and persistent generator functions +can be found :doc:`here<../examples/gen_funcs>`. diff --git a/docs/function_guides/generator_standardized.rst b/docs/function_guides/generator_standardized.rst new file mode 100644 index 0000000000..d02c0619f7 --- /dev/null +++ b/docs/function_guides/generator_standardized.rst @@ -0,0 +1,60 @@ +Standardized Generator (gest-api) +================================= + +`Introduction `__ \|\| **Standardized Generator (gest-api)** \|\| `Legacy Generator Function `__ + +Standardized generators are classes that inherit from ``gest_api.Generator``. +They adhere to the ``gest-api`` standard and are parameterized by a ``VOCS`` +object defining the problem's variables and objectives. + +A basic generator implements the ``suggest()`` and ``ingest()`` methods, which +operate on lists of dictionaries: + +.. code-block:: python + :linenos: + + import numpy as np + from gest_api import Generator + from gest_api.vocs import VOCS + + + class UniformSample(Generator): + """Samples over the domain specified in the VOCS.""" + + def __init__(self, vocs: VOCS): + self.vocs = vocs + self.rng = np.random.default_rng(1) + super().__init__(vocs) + + def _validate_vocs(self, vocs): + assert len(self.vocs.variable_names), "VOCS must contain variables." + + def suggest(self, n_trials): + output = [] + for _ in range(n_trials): + trial = {} + for key in self.vocs.variables: + trial[key] = self.rng.uniform(self.vocs.variables[key].domain[0], self.vocs.variables[key].domain[1]) + output.append(trial) + return output + + def ingest(self, calc_in): + pass # random sample so nothing to ingest + +libEnsemble's handling of standardized generators is specified using ``GenSpecs``: + +.. code-block:: python + + gen_specs = GenSpecs( + generator=UniformSample(vocs), + inputs=["sim_id"], + persis_in=["x", "f"], + outputs=[("x", float, 2)], + vocs=vocs, + user={"batch_size": 128}, + ) + +.. note:: + Ensure that ``gen_specs.inputs`` or ``gen_specs.persis_in`` requests at least one field + (like ``"sim_id"`` or ``"f"``) to be sent back, even if the generator does not + process them. diff --git a/docs/function_guides/history_array.rst b/docs/function_guides/history_array.rst index 6820b6faec..03ded946d9 100644 --- a/docs/function_guides/history_array.rst +++ b/docs/function_guides/history_array.rst @@ -15,25 +15,25 @@ libEnsemble uses a NumPy structured array to store information about each point The manager maintains a global copy. Each row contains: - 1. Data generated by the :ref:`gen_f` - 2. Resultant output from the :ref:`sim_f` + 1. Data generated by the :ref:`generator` + 2. Resultant output from the :ref:`simulator function` 3. :ref:`Reserved fields` containing metadata -When the history array is initialized, it creates fields for each -``gen_specs["out"]`` and ``sim_specs["out"]`` entry. These entries may resemble:: +**Simulator functions** (``sim_f``) must return their data as arrays with the same +dtype as ``sim_specs["out"]``. Alternatively, a ``simulator`` +callable in gest-api format (accepting and returning a ``dict``) can be provided via +``SimSpecs.simulator``; libEnsemble wraps it automatically and handles the dtype +conversion. - gen_specs["out"] = [("x", float, 2), ("theta", int)] - sim_specs["out"] = [("f", float)] +**Generators** that adhere to the ``gest_api`` standard implement ``suggest()`` and +``ingest()`` methods that operate on lists of Python dictionaries. libEnsemble +automatically casts their ``dict`` outputs to NumPy for inclusion in the History array. -.. In this example, ``x`` is a two-dimensional coordinate, ``theta`` represents some -.. integer input parameter, and ``f`` is a scalar output of the simulation to be -.. run with the generated ``x`` and ``theta`` values. - -Therefore, the ``gen_f`` and ``sim_f`` must return output as NumPy -structured arrays for slotting into these fields. - -.. (The manager's history array will update any fields -.. returned to it.) +When using a ``VOCS`` object (from ``gest_api.vocs``) to parameterize ``GenSpecs`` or +``SimSpecs``, field names in the History array are derived automatically from the VOCS +variable, objective, and constraint keys. ``LibensembleGenerator`` subclasses optionally +collapse all VOCS variables into a single ``"x"`` array field (and objectives into +``"f"``) unless an explicit ``variables_mapping`` is provided. Ensure input/output field names for a function match each other or a :ref:`reserved field`:: @@ -48,45 +48,12 @@ Reserved Fields User fields and reserved fields are combined together in the final History array returned by libEnsemble. -.. Automatically tracked fields within the History array include: - -.. 1. ``sim_id``, to globally identify the point. Assigned by manager if the generator doesn't provide. -.. 2. ``cancel_requested``, - -.. The manager's history array also contains several reserved fields. These -.. include a ``sim_id`` to globally identify the point (on the manager this is -.. usually the same as the array index). The ``sim_id`` can be provided by the -.. user from the ``gen_f``, but is otherwise assigned by the manager as generated -.. points are received. - -.. The reserved boolean field ``cancel_requested`` can also be set in a user -.. function to request that libEnsemble cancels the evaluation of the point. - -.. The remaining reserved fields are protected (populated by libEnsemble), and -.. store information about each entry. These include boolean fields for the -.. current scheduling status of the point (``sim_started`` when the sim evaluation -.. has started out, ``sim_ended`` when sim evaluation has completed, and -.. ``gen_informed`` when the sim output has been passed back to the generator). -.. Timing fields give the time (since the epoch) corresponding to each state, and -.. when the point was generated. Other protected fields include the worker IDs on -.. which points were generated or evaluated. - -.. The user fields and the reserved fields together make up the final history array -.. returned by libEnsemble. - These reserved fields can be modified to adjust how/when a point is evaluated: * ``sim_id`` [int]: Each unit of work must have a ``sim_id``. This can be set by the generator or by the manager by default. Users should ensure these IDs are sequential and unique when running multiple generators. -.. * The generator can assign this, but users must be -.. careful to ensure that points are added in order. For example, if ``alloc_f`` -.. allows for two ``gen_f`` instances to be running simultaneously, ``alloc_f`` -.. should ensure that both don't generate points with the same ``sim_id``. -.. If the generator does not provide, then a ``sim_id`` will be assigned by the -.. manager as generated points are received. - * ``cancel_requested`` [bool]: Can be set ``True`` in a generator to request attempted cancellation of the corresponding simulation. @@ -114,11 +81,9 @@ The following fields are automatically populated by libEnsemble: ``kill_sent`` [bool]: ``True`` if a kill signal was sent to worker for this entry -Other than ``"sim_id"`` and ``cancel_requested``, these fields cannot be -overwritten by user functions unless ``libE_specs["safe_mode"]`` is set to ``False``. - -.. warning:: - Adjusting values in protected fields may crash libEnsemble. +Other than ``"sim_id"`` and ``"cancel_requested"``, these fields cannot be +overwritten by user functions when ``libE_specs["safe_mode"]`` is set to ``True`` +(protection is opt-in; the default value of ``safe_mode`` is ``False``). Example Workflow updating History --------------------------------- @@ -136,10 +101,13 @@ reserved fields: ``sim_id``, ``sim_started``, and ``sim_ended`` are shown for br | -:ref:`gen_f` and :ref:`sim_f` functions accept a local history -array as the first argument that contains only the rows and fields specified. -For new function calls these will be specified by either ``gen_specs["in"]`` or -``sim_specs["in"]``. For generators this may be empty. +For legacy generator functions (``gen_f``), the function accepts a local history +array slice as the first argument containing only the rows and fields specified by +``gen_specs["in"]`` (may be empty). It returns a NumPy structured array that +libEnsemble writes into H. + +For gest-api generators, ``suggest(n)`` returns a list of dicts and ``ingest(results)`` +receives a list of dicts; libEnsemble handles all conversions to and from NumPy. | diff --git a/docs/function_guides/sim_gen_alloc_api.rst b/docs/function_guides/sim_gen_alloc_api.rst deleted file mode 100644 index 546806edef..0000000000 --- a/docs/function_guides/sim_gen_alloc_api.rst +++ /dev/null @@ -1,140 +0,0 @@ -User Function API ------------------ -.. _user_api: - -libEnsemble requires functions for generation, simulation, and allocation. - -While libEnsemble provides a default allocation function, the simulator and generator functions -must be specified. The required API and example arguments are given here. -:doc:`Example sim and gen functions<../examples/examples_index>` are provided in the -libEnsemble package. - -:doc:`See here for more in-depth guides to writing user functions` - -As of v0.10.0, valid simulator and generator functions -can *accept and return a smaller subset of the listed parameters and return values*. For instance, -a ``def my_simulation(one_Input) -> one_Output`` function is now accepted, -as is ``def my_generator(Input, persis_info) -> Output, persis_info``. - -sim_f API -~~~~~~~~~ -.. _api_sim_f: - -The simulator function will be called by libEnsemble's workers with *up to* the following arguments and returns:: - - Out, persis_info, calc_status = sim_f(H[sim_specs["in"]][sim_ids_from_allocf], persis_info, sim_specs, libE_info) - -Parameters: -*********** - - **H**: ``numpy structured array`` - :ref:`(example)` - - **persis_info**: :obj:`dict` - :ref:`(example)` - - **sim_specs**: :obj:`dict` - :ref:`(example)` - - **libE_info**: :obj:`dict` - :ref:`(example)` - -Returns: -******** - - **H**: ``numpy structured array`` - with keys/value-sizes matching those in sim_specs["out"] - :ref:`(example)` - - **persis_info**: :obj:`dict` - :ref:`(example)` - - **calc_status**: :obj:`int`, optional - Provides a task status to the manager and the libE_stats.txt file - :ref:`(example)` - -gen_f API -~~~~~~~~~ -.. _api_gen_f: - -The generator function will be called by libEnsemble's workers with *up to* the following arguments and returns:: - - Out, persis_info, calc_status = gen_f(H[gen_specs["in"]][sim_ids_from_allocf], persis_info, gen_specs, libE_info) - -Parameters: -*********** - - **H**: ``numpy structured array`` - :ref:`(example)` - - **persis_info**: :obj:`dict` - :ref:`(example)` - - **gen_specs**: :obj:`dict` - :ref:`(example)` - - **libE_info**: :obj:`dict` - :ref:`(example)` - -Returns: -******** - - **H**: ``numpy structured array`` - with keys/value-sizes matching those in gen_specs["out"] - :ref:`(example)` - - **persis_info**: :obj:`dict` - :ref:`(example)` - - **calc_status**: :obj:`int`, optional - Provides a task status to the manager and the libE_stats.txt file - :ref:`(example)` - -alloc_f API -~~~~~~~~~~~ -.. _api_alloc_f: - -The allocation function will be called by libEnsemble's manager with the following API:: - - Work, persis_info, stop_flag = alloc_f(W, H, sim_specs, gen_specs, alloc_specs, persis_info, libE_info) - -Parameters: -*********** - - **W**: ``numpy structured array`` - :doc:`(example)` - - **H**: ``numpy structured array`` - :ref:`(example)` - - **sim_specs**: :obj:`dict` - :ref:`(example)` - - **gen_specs**: :obj:`dict` - :ref:`(example)` - - **alloc_specs**: :obj:`dict` - :ref:`(example)` - - **persis_info**: :obj:`dict` - :ref:`(example)` - - **libE_info**: :obj:`dict` - Various statistics useful to the allocation function for determining how much - work has been evaluated, or if the routine should prepare to complete. See - the :doc:`allocation function guide` for more - information. - -Returns: -******** - - **Work**: :obj:`dict` - Dictionary with integer keys ``i`` for work to be sent to worker ``i``. - :ref:`(example)` - - **persis_info**: :obj:`dict` - :doc:`(example)<../data_structures/persis_info>` - - **stop_flag**: :obj:`int`, optional - Set to 1 to request libEnsemble manager to stop giving additional work after - receiving existing work diff --git a/docs/function_guides/simulator.rst b/docs/function_guides/simulator.rst index 3b423fd7bf..5d69a4f79b 100644 --- a/docs/function_guides/simulator.rst +++ b/docs/function_guides/simulator.rst @@ -3,110 +3,30 @@ Simulator Functions =================== +**Introduction** \|\| `Standardized Simulator (gest-api) `__ \|\| `Legacy Simulator Function `__ + Simulator and :ref:`Generator functions` have relatively similar interfaces. Writing a Simulator ------------------- -.. tab-set:: - - .. tab-item:: Non-decorated - :sync: nodecorate - - .. code-block:: python - - def my_simulation(Input, persis_info, sim_specs, libE_info): - batch_size = sim_specs["user"]["batch_size"] - - Output = np.zeros(batch_size, sim_specs["out"]) - # ... - Output["f"], persis_info = do_a_simulation(Input["x"], persis_info) - - return Output, persis_info - - .. tab-item:: Decorated - :sync: decorate - - .. code-block:: python - - from libensemble.specs import input_fields, output_data - - - @input_fields(["x"]) - @output_data([("f", float)]) - def my_simulation(Input, persis_info, sim_specs, libE_info): - batch_size = sim_specs["user"]["batch_size"] - - Output = np.zeros(batch_size, sim_specs["out"]) - # ... - Output["f"], persis_info = do_a_simulation(Input["x"], persis_info) - - return Output, persis_info - -Most ``sim_f`` function definitions written by users resemble:: - - def my_simulation(Input, persis_info, sim_specs, libE_info): - -where: - - * ``Input`` is a selection of the :ref:`History array`, a NumPy structured array. - * :ref:`persis_info` is a dictionary containing state information. - * :ref:`sim_specs` is a dictionary of simulation parameters. - * ``libE_info`` is a dictionary containing libEnsemble-specific entries. - -Valid simulator functions can accept a subset of the above parameters. So a very simple simulator function can start:: - - def my_simulation(Input): - -If ``sim_specs`` was initially defined: - -.. tab-set:: - - .. tab-item:: Non-decorated function - :sync: nodecorate - - .. code-block:: python - - sim_specs = SimSpecs( - sim_f=my_simulation, - inputs=["x"], - outputs=["f", float, (1,)], - user={"batch_size": 128}, - ) - - .. tab-item:: Decorated function - :sync: decorate - - .. code-block:: python - - sim_specs = SimSpecs( - sim_f=my_simulation, - user={"batch_size": 128}, - ) - -Then user parameters and a *local* array of outputs may be obtained/initialized like:: - - batch_size = sim_specs["user"]["batch_size"] - Output = np.zeros(batch_size, dtype=sim_specs["out"]) - -This array should be populated with output values from the simulation:: - - Output["f"], persis_info = do_a_simulation(Input["x"], persis_info) - -Then return the array and ``persis_info`` to libEnsemble:: +.. note:: + The `gest-api` simulator interface is the recommended approach for new libEnsemble projects. + The "Legacy Simulator Function" interface is supported for backward compatibility but may be deprecated in a future release. - return Output, persis_info +Tutorial sections +----------------- -Between the ``Output`` definition and the ``return``, any computation can be performed. -Users can try an :doc:`executor<../executor/overview>` to submit applications to parallel -resources, or plug in components from other libraries to serve their needs. +1. Introduction (this page) +2. :doc:`Standardized Simulator (gest-api) ` +3. :doc:`Legacy Simulator Function ` Executor -------- libEnsemble's Executors are commonly used within simulator functions to launch and monitor applications. An excellent overview is already available -:doc:`here<../executor/overview>`. +:doc:`here<../executor/ex_index>`. See the :doc:`Ensemble with an MPI Application tutorial<../tutorials/executor_forces_tutorial>` for an additional example to try out. @@ -125,3 +45,9 @@ function returns. An example routine using a persistent simulator can be found in test_persistent_sim_uniform_sampling_. .. _test_persistent_sim_uniform_sampling: https://github.com/Libensemble/libensemble/blob/develop/libensemble/tests/functionality_tests/test_persistent_sim_uniform_sampling.py + +.. toctree:: + :hidden: + + simulator_standardized + simulator_legacy diff --git a/docs/function_guides/simulator_legacy.rst b/docs/function_guides/simulator_legacy.rst new file mode 100644 index 0000000000..3f65096abc --- /dev/null +++ b/docs/function_guides/simulator_legacy.rst @@ -0,0 +1,58 @@ +Legacy Simulator Function +========================= + +`Introduction `__ \|\| `Standardized Simulator (gest-api) `__ \|\| **Legacy Simulator Function** + +.. code-block:: python + + def my_simulation(Input, persis_info, sim_specs, libE_info): + batch_size = sim_specs["user"]["batch_size"] + + Output = np.zeros(batch_size, sim_specs["out"]) + # ... + Output["f"], persis_info = do_a_simulation(Input["x"], persis_info) + + return Output, persis_info + +Most ``sim_f`` function definitions written by users resemble:: + + def my_simulation(Input, persis_info, sim_specs, libE_info): + +where: + + * ``Input`` is a selection of the :ref:`History array`, a NumPy structured array. + * :ref:`persis_info` is a dictionary containing state information. + * :ref:`sim_specs` is a dictionary of simulation parameters. + * ``libE_info`` is a dictionary containing libEnsemble-specific entries. + +Valid simulator functions can accept a subset of the above parameters. So a very simple simulator function can start:: + + def my_simulation(Input): + +If ``sim_specs`` was initially defined: + +.. code-block:: python + + sim_specs = SimSpecs( + sim_f=my_simulation, + inputs=["x"], + outputs=["f", float, (1,)], + user={"batch_size": 128}, + ) + +Then user parameters and a *local* array of outputs may be obtained/initialized like:: + + batch_size = sim_specs["user"]["batch_size"] + Output = np.zeros(batch_size, dtype=sim_specs["out"]) + +This array should be populated with output values from the simulation:: + + Output["f"], persis_info = do_a_simulation(Input["x"], persis_info) + +Then return the array and ``persis_info`` to libEnsemble:: + + return Output, persis_info + +Between the ``Output`` definition and the ``return``, any computation can be performed. +Users can try an :doc:`executor<../executor/ex_index>` to submit applications to parallel +resources, or plug in components from other libraries to serve their needs. diff --git a/docs/function_guides/simulator_standardized.rst b/docs/function_guides/simulator_standardized.rst new file mode 100644 index 0000000000..27b72deb5f --- /dev/null +++ b/docs/function_guides/simulator_standardized.rst @@ -0,0 +1,43 @@ +Standardized Simulator (gest-api) +================================= + +`Introduction `__ \|\| **Standardized Simulator (gest-api)** \|\| `Legacy Simulator Function `__ + +Standardized simulators are plain callables — no base class required — with the signature:: + + def my_simulation(input_dict: dict, **kwargs) -> dict: + +They receive a single point as a Python dictionary (keyed by VOCS variable and constant +names) and return a dictionary of outputs (keyed by VOCS objective, observable, and +constraint names). + +.. code-block:: python + + def my_simulation(input_dict: dict, **kwargs) -> dict: + x1 = input_dict["x1"] + x2 = input_dict["x2"] + f = (x1 - 1) ** 2 + (x2 - 2) ** 2 + return {"f": f} + +Configure it with ``SimSpecs`` using a ``VOCS`` object. ``inputs`` and ``outputs`` +are derived automatically from the VOCS when not set explicitly: + +.. code-block:: python + + from gest_api.vocs import VOCS + from libensemble.specs import SimSpecs + + vocs = VOCS( + variables={"x1": [0, 1.0], "x2": [0, 10.0]}, + objectives={"f": "MINIMIZE"}, + ) + + sim_specs = SimSpecs( + simulator=my_simulation, + vocs=vocs, + ) + +If ``libE_info`` is needed (e.g., to access the :doc:`executor<../executor/ex_index>`), +declare it as a keyword argument and libEnsemble will pass it automatically:: + + def my_simulation(input_dict: dict, libE_info=None, **kwargs) -> dict: diff --git a/docs/history_output_logging.rst b/docs/history_output_logging.rst index 9ffa48ad44..4f29900739 100644 --- a/docs/history_output_logging.rst +++ b/docs/history_output_logging.rst @@ -1,6 +1,15 @@ Output Management ================= +Simulation Directories +~~~~~~~~~~~~~~~~~~~~~~ + +By default, libEnsemble places output files in the current working directory. + +See the ``Directories`` section of :ref:`libE_specs` for instructions +on how to separate simulation runs into separate directories and copy/symlink input files into these +locations. + Default Log Files ~~~~~~~~~~~~~~~~~ The history array :ref:`H` and diff --git a/docs/index.rst b/docs/index.rst index 9f7093ff10..49a06cdc6c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -10,7 +10,7 @@ :caption: User Guide: Quickstart - advanced_installation + advanced_installation/advanced_installation overview_usecases programming_libE running_libE @@ -20,20 +20,21 @@ :maxdepth: 1 :caption: Tutorials: - tutorials/local_sine_tutorial + tutorials/local_sine_tutorial/local_sine_tutorial tutorials/executor_forces_tutorial tutorials/forces_gpu_tutorial tutorials/gpcam_tutorial tutorials/aposmm_tutorial tutorials/calib_cancel_tutorial + tutorials/xopt_bayesian_gen .. toctree:: :maxdepth: 1 :caption: Examples: + examples/gest_api examples/gen_funcs examples/sim_funcs - examples/alloc_funcs examples/calling_scripts Submission Scripts @@ -41,11 +42,13 @@ :maxdepth: 1 :caption: Additional References: + function_guides/history_array + resource_manager/resources_index + function_guides/allocator FAQ known_issues release_notes contributing - posters .. toctree:: :maxdepth: 1 @@ -53,3 +56,4 @@ dev_guide/release_management/release_index dev_guide/dev_API/developer_API + bibliography diff --git a/docs/introduction.rst b/docs/introduction.rst index 4b36943398..0e1ebca6f9 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -1,7 +1,15 @@ .. include:: ../README.rst :start-after: after_badges_rst_tag + :end-before: before_script_creator_tag -See the :doc:`tutorial` for a step-by-step beginners guide. +.. include:: ../README.rst + :start-after: after_script_creator_tag + :end-before: before_colab_tag + +.. include:: ../README.rst + :start-after: after_colab_tag + +See the :doc:`tutorial` for a step-by-step beginners guide. See the `user guide`_ for more information. diff --git a/docs/introduction_latex.rst b/docs/introduction_latex.rst index e7750bac5f..512282dbfe 100644 --- a/docs/introduction_latex.rst +++ b/docs/introduction_latex.rst @@ -39,7 +39,6 @@ .. _pytest-timeout: https://pypi.org/project/pytest-timeout/ .. _pytest: https://pypi.org/project/pytest/ .. _Python: http://www.python.org -.. _pyyaml: https://pyyaml.org/ .. _Quickstart: https://libensemble.readthedocs.io/en/main/introduction.html .. _ReadtheDocs: http://libensemble.readthedocs.org/ .. _SciPy: http://www.scipy.org @@ -51,7 +50,6 @@ .. _SWIG: http://swig.org/ .. _tarball: https://github.com/Libensemble/libensemble/releases/latest .. _Tasmanian: https://github.com/ORNL/Tasmanian -.. _tomli: https://pypi.org/project/tomli/ .. _tqdm: https://tqdm.github.io/ .. _user guide: https://libensemble.readthedocs.io/en/latest/programming_libE.html .. _VTMOP: https://github.com/Libensemble/libe-community-examples#vtmop diff --git a/docs/known_issues.rst b/docs/known_issues.rst index 89c596aae5..a68f1bcf47 100644 --- a/docs/known_issues.rst +++ b/docs/known_issues.rst @@ -19,8 +19,6 @@ may occur when using libEnsemble. * Local comms mode (multiprocessing) may fail if MPI is initialized before forking processors. This is thought to be responsible for issues combining multiprocessing with PETSc on some platforms. -* Remote detection of logical cores via ``LSB_HOSTS`` (e.g., Summit) returns the - number of physical cores as SMT info not available. * TCP mode does not support (1) more than one libEnsemble call in a given script or (2) the auto-resources option to the Executor. diff --git a/docs/latex_index.rst b/docs/latex_index.rst index 556a421a24..e2fd0ffb90 100644 --- a/docs/latex_index.rst +++ b/docs/latex_index.rst @@ -34,7 +34,7 @@ other libEnsemble information. .. toctree:: :maxdepth: 3 - advanced_installation + advanced_installation/advanced_installation tutorials/tutorials FAQ known_issues diff --git a/docs/libe_module.rst b/docs/libe_module.rst index 6f60d633a6..caa957d13f 100644 --- a/docs/libe_module.rst +++ b/docs/libe_module.rst @@ -3,19 +3,6 @@ Running an Ensemble =================== -libEnsemble features two approaches to run an ensemble. We recommend the newer ``Ensemble`` class, -but will continue to support ``libE()`` for backward compatibility. - -.. tab-set:: - - .. tab-item:: Ensemble Class - - .. autoclass:: libensemble.ensemble.Ensemble() - :members: - :no-undoc-members: - - .. tab-item:: libE() - - .. automodule:: libensemble.libE - :members: - :no-undoc-members: +.. autoclass:: libensemble.ensemble.Ensemble() + :members: + :no-undoc-members: diff --git a/docs/nitpicky b/docs/nitpicky index e43a0760bb..5f46003f50 100644 --- a/docs/nitpicky +++ b/docs/nitpicky @@ -47,7 +47,6 @@ py:class libensemble.resources.platforms.Perlmutter py:class libensemble.resources.platforms.PerlmutterCPU py:class libensemble.resources.platforms.PerlmutterGPU py:class libensemble.resources.platforms.Polaris -py:class libensemble.resources.platforms.Summit py:class libensemble.resources.rset_resources.RSetResources py:class libensemble.resources.env_resources.EnvResources py:class libensemble.resources.resources.Resources @@ -57,3 +56,15 @@ py:meth libensemble.tools.save_libE_output # Types specifying objects that can dramatically vary py:class comm py:class communicator + +# Additional nitpicky targets from recent Sphinx warnings +py:class libensemble.resources.platforms.Lumi +py:class libensemble.resources.platforms.LumiGPU +py:class numpy._typing._array_like._ScalarT +py:class Comm +py:class npt.DTypeLike +py:class libensemble.generators.PersistentGenInterfacer +py:class gest_api.vocs.VOCS +py:class libensemble.generators.LibensembleGenerator +py:class ~_ScalarT +py:class numpy.random._generator.Generator diff --git a/docs/overview_usecases.rst b/docs/overview_usecases.rst index 6d77b197b0..04ebb5e14f 100644 --- a/docs/overview_usecases.rst +++ b/docs/overview_usecases.rst @@ -1,17 +1,15 @@ Understanding libEnsemble ========================= -Manager, Workers, and User Functions -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Manager, Workers, Generators, and Simulators +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. begin_overview_rst_tag -libEnsemble's **manager** allocates work to **workers**, -which perform computations via **user functions**: +libEnsemble's **manager** allocates work from **generators** to **workers**, +which perform computations via **simulators**: -* :ref:`generator`: Generates inputs to the *simulator* (``sim_f``) -* :ref:`simulator`: Performs an evaluation based on parameters from the *generator* (``gen_f``) -* :ref:`allocator`: Decides whether a simulator or generator should be - called (and with what inputs/resources) as workers become available +* :ref:`generator`: Generates inputs for the *simulator* +* :ref:`simulator`: Performs an evaluation using parameters from the *generator* .. figure:: images/adaptiveloop.png :alt: Adaptive loops @@ -20,132 +18,86 @@ which perform computations via **user functions**: | -The default allocator (``alloc_f``) instructs workers to run the simulator on the -highest priority work from the generator. If a worker is idle and there is -no work, that worker is instructed to call the generator. +An :doc:`executor` interface is available so generators and simulators +can launch and monitor external applications. -.. figure:: images/diagram_with_persis.png - :alt: libE component diagram - :align: center - :scale: 40 - -| - -An :doc:`executor` interface is available so user functions -can execute and monitor external applications. - -libEnsemble uses a NumPy structured array known as the :ref:`history array` -to keep a record of all simulations. The global history array is stored on the -manager, while selected rows and fields of this array are passed to and from user functions. +All simulations and generated values are recorded in a NumPy +structured array called the :ref:`history array`. Example Use Cases ~~~~~~~~~~~~~~~~~ .. begin_usecases_rst_tag -Below are some expected libEnsemble use cases that we support (or are working -to support): - .. dropdown:: **Click Here for Use-Cases** * A user wants to optimize a simulation calculation. The simulation may - already be using parallel resources but not a large fraction of some - computer. libEnsemble can coordinate the concurrent evaluation of the - simulation ``sim_f`` at various parameter values based on candidate parameter - values from ``gen_f`` (possibly after each ``sim_f`` output). + already be using parallel resources but not a large fraction of a + computer. libEnsemble can coordinate concurrent evaluations of the + simulator at multiple parameter values based on candidate parameter + values produced by the generator (possibly after each simulator output). - * A user has a ``gen_f`` that produces meshes for a - ``sim_f``. Given the ``sim_f`` output, the ``gen_f`` can refine a mesh or - produce a new mesh. libEnsemble can ensure that the calculated meshes can be - used by multiple simulations without requiring moving data. + * A user has a generator that produces meshes for a + simulator. Based on the simulator output, the generator can refine a mesh or + produce a new mesh. libEnsemble ensures that generated meshes can be + reused by multiple simulations without requiring data movement. - * A user wants to evaluate a simulation ``sim_f`` with different sets of + * A user wants to evaluate a simulation with different sets of parameters, each drawn from a set of possible values. Some parameter values are known to cause the simulation to fail. libEnsemble can stop unresponsive evaluations and recover computational resources for future - evaluations. The ``gen_f`` can possibly update the sampling after discovering - regions where evaluations of ``sim_f`` fail. + evaluations. The generator can update the sampling strategy after discovering + regions where evaluations of the simulator fail. - * A user has a simulation ``sim_f`` that requires calculating multiple - expensive quantities, some of which depend on other quantities. The ``sim_f`` - can observe intermediate quantities to stop related calculations and + * A user has a simulation that requires calculating multiple + expensive quantities, some of which depend on other quantities. The simulator + can monitor intermediate quantities to stop related calculations early and preempt future calculations associated with poor parameter values. - * A user has a ``sim_f`` with multiple fidelities, with the higher-fidelity - evaluations requiring more computational resources, and a - ``gen_f``/``alloc_f`` that decides which parameters should be evaluated and - at what fidelity level. libEnsemble can coordinate these evaluations without - requiring the user to know parallel programming. + * A user has a simulation with multiple fidelities, where higher-fidelity + evaluations require more computational resources. The generator and allocator + decide which parameters should be evaluated and at what fidelity level. libEnsemble + coordinates these evaluations without requiring the user to write parallel code. - * A user wishes to identify multiple local optima for a ``sim_f``. Furthermore, + * A user wishes to identify multiple local optima for a simulation. In addition, sensitivity analysis is desired at each identified optimum. libEnsemble can - use the points from the APOSMM ``gen_f`` to identify optima; and after a - point is ruled to be an optimum, a different ``gen_f`` can produce a - collection of parameters necessary for sensitivity analysis of ``sim_f``. + use points from the APOSMM generator to identify optima. After a point is + determined to be an optimum, a different generator can generate the + parameter sets required for sensitivity analysis of the simulation. - Combinations of these use cases are supported as well. An example of - such a combination is using libEnsemble to solve an optimization problem that - relies on simulations that fail frequently. + Combinations of these use cases are also supported. Glossary ~~~~~~~~ -Here we define some terms used throughout libEnsemble's code and documentation. -Although many of these terms seem straightforward, defining such terms assists -with keeping confusion to a minimum when communicating about libEnsemble and -its capabilities. - .. dropdown:: **Click Here for Glossary** :open: - * **Manager**: Single libEnsemble process facilitating communication between - other processes. Within libEnsemble, the *Manager* process configures and - passes work to and from the workers. + * **Manager**: A single libEnsemble process that facilitates communication between + other processes. The *Manager* configures and distributes work to + workers and collects their output. * **Worker**: libEnsemble processes responsible for performing units of work, - which may include submitting or executing tasks. *Worker* processes run - generation and simulation routines, submit additional tasks for execution, - and return results to the manager. - - * **Calling Script**: libEnsemble is typically imported, parameterized, and - initiated in a single Python file referred to as a *calling script*. ``sim_f`` - and ``gen_f`` functions are also commonly configured and parameterized here. - - * **User function**: A generator, simulator, or allocation function. These - are Python functions that govern the libEnsemble workflow. They - must conform to the libEnsemble API for each respective user function, but otherwise can - be created or modified by the user. libEnsemble comes with many examples of - each type of user function. - - * **Executor**: The executor can be used within user functions to provide a - simple, portable interface for running and managing user tasks (applications). - There are multiple executors including the base ``Executor`` and ``MPIExecutor``. - - * **Submit**: Enqueue or indicate that one or more jobs or tasks need to be - launched. When using the libEnsemble Executor, a *submitted* task is executed - immediately or queued for execution. + which may include executing tasks or submitting external jobs. Workers typically + run simulators and return results to the manager. - * **Tasks**: Sub-processes or independent units of work. Workers perform - *tasks* as directed by the manager; tasks may include submitting external - programs for execution using the Executor. + * **Executor**: A simple, portable interface for + launching and managing tasks (applications). Multiple executors are + available, including the base ``Executor`` and ``MPIExecutor``. + + * **Submit**: A *submitted* task is either executed + immediately or queued for execution. - * **Persistent**: Typically, a worker communicates with the manager - before and after initiating a user ``gen_f`` or ``sim_f`` calculation. However, user - functions may also be constructed to communicate directly with the manager, - for example, to efficiently maintain and update data structures instead of - communicating them between manager and worker. These calculations - and the workers assigned to them are referred to as *persistent*. + * **Tasks**: Subprocesses or independent units of work. Tasks result from + launching external programs for execution using the Executor. - * **Resource Manager** libEnsemble has a built-in resource manager that can detect - (or be provided with) a set of resources (e.g., a node-list). Resources are - divided up among workers (using *resource sets*) and can be dynamically - reassigned. + * **Resource Manager**: libEnsemble module that detects + (or is provided with) available resources (e.g., a list of nodes). *Resource sets* are + divided among workers and can be dynamically reassigned. * **Resource Set**: The smallest unit of resources that can be assigned (and - dynamically reassigned) to workers. By default it is the provisioned resources - divided by the number of workers (excluding any workers given in the - ``zero_resource_workers`` libE_specs option). However, it can also be set - directly by the ``num_resource_sets`` libE_specs option. + dynamically reassigned) to workers. By default this is the provisioned resources + divided by the number of workers. It can also be set explicitly using the ``num_resource_sets`` ``libE_specs`` option. - * **Slot**: The ``resource sets`` enumerated on a node (starting with zero). If - a resource set has more than one node, then each node is considered to have slot + * **Slot**: Resource sets enumerated on a node (starting from zero). If + a resource set spans multiple nodes, each node is considered to have slot zero. diff --git a/docs/platforms/aurora.rst b/docs/platforms/aurora.rst index af2e7cc160..c29ed0bc08 100644 --- a/docs/platforms/aurora.rst +++ b/docs/platforms/aurora.rst @@ -27,7 +27,7 @@ To obtain libEnsemble:: pip install libensemble -See :doc:`here<../advanced_installation>` for more information on advanced +See :doc:`here<../advanced_installation/advanced_installation>` for more information on advanced options for installing libEnsemble, including using Spack. Example @@ -57,7 +57,7 @@ simulations for each worker: .. code-block:: python # Instruct libEnsemble to exit after this many simulations - ensemble.exit_criteria = ExitCriteria(sim_max=nsim_workers*2) + ensemble.exit_criteria = ExitCriteria(sim_max=nsim_workers * 2) Now grab an interactive session on two nodes (or use the batch script at ``../submission_scripts/submit_pbs_aurora.sh``):: @@ -115,26 +115,6 @@ will use one GPU tile):: python run_libe_forces.py -n 25 -Running generator on the manager --------------------------------- - -An alternative is to run the generator on a thread on the manager. The -number of workers can then be set to the number of simulation workers. - -Change the ``libE_specs`` in **run_libe_forces.py** as follows: - -.. code-block:: python - - nsim_workers = ensemble.nworkers - - # Persistent gen does not need resources - ensemble.libE_specs = LibeSpecs( - gen_on_manager=True, - -then we can run with 12 (instead of 13) workers:: - - python run_libe_forces.py -n 12 - Dynamic resource assignment --------------------------- diff --git a/docs/platforms/bebop.rst b/docs/platforms/bebop.rst index e57172c1b3..2682a54863 100644 --- a/docs/platforms/bebop.rst +++ b/docs/platforms/bebop.rst @@ -46,7 +46,7 @@ To install via ``conda``: conda config --add channels conda-forge conda install -c conda-forge libensemble -See :doc:`here<../advanced_installation>` for more information on advanced options +See :doc:`here<../advanced_installation/advanced_installation>` for more information on advanced options for installing libEnsemble. Job Submission @@ -75,7 +75,7 @@ Now run your script with four workers (one for generator and three for simulatio **three** workers to one allocated compute node, with three nodes available for the workers to launch calculations with the Executor or a launch command. This is an example of running in :doc:`centralized` mode, and, -if using the :doc:`Executor<../executor/mpi_executor>`, libEnsemble should +if using the :doc:`Executor<../executor/ex_index>`, libEnsemble should be initiated with ``libE_specs["dedicated_mode"]=True`` .. note:: diff --git a/docs/platforms/example_scripts.rst b/docs/platforms/example_scripts.rst index d534f0c662..d6d7892abd 100644 --- a/docs/platforms/example_scripts.rst +++ b/docs/platforms/example_scripts.rst @@ -95,10 +95,3 @@ SLURM - MPI / Distributed Mode (co-locate workers & MPI applications) .. literalinclude:: ../../examples/libE_submission_scripts/submit_distrib_mpi4py.sh :caption: /examples/libE_submission_scripts/submit_distrib_mpi4py.sh :language: bash - -Summit (Decommissioned) - On Launch Nodes with Multiprocessing -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. literalinclude:: ../../examples/libE_submission_scripts/summit_submit_mproc.sh - :caption: /examples/libE_submission_scripts/summit_submit_mproc.sh - :language: bash diff --git a/docs/platforms/frontier.rst b/docs/platforms/frontier.rst index 4fdc7a0b36..a57ffadd97 100644 --- a/docs/platforms/frontier.rst +++ b/docs/platforms/frontier.rst @@ -33,7 +33,7 @@ libEnsemble can be installed via pip:: pip install libensemble -See :doc:`advanced installation<../advanced_installation>` for other installation options. +See :doc:`advanced installation<../advanced_installation/advanced_installation>` for other installation options. Example ------- diff --git a/docs/platforms/improv.rst b/docs/platforms/improv.rst index bdb2269a85..dfe40da138 100644 --- a/docs/platforms/improv.rst +++ b/docs/platforms/improv.rst @@ -15,7 +15,7 @@ To create a conda environment and install libEnsemble:: conda activate improv_libe_env pip install libensemble -See :doc:`here<../advanced_installation>` for more information on advanced +See :doc:`here<../advanced_installation/advanced_installation>` for more information on advanced options for installing libEnsemble, including using Spack. Job Submission diff --git a/docs/platforms/perlmutter.rst b/docs/platforms/perlmutter.rst index 88d3f808b2..a1c79703f8 100644 --- a/docs/platforms/perlmutter.rst +++ b/docs/platforms/perlmutter.rst @@ -50,7 +50,7 @@ by one of the following ways. conda config --add channels conda-forge conda install -c conda-forge libensemble -See :doc:`advanced installation<../advanced_installation>` for other installation options. +See :doc:`advanced installation<../advanced_installation/advanced_installation>` for other installation options. Job Submission -------------- @@ -105,26 +105,6 @@ To see GPU usage, ssh into the node you are on in another window and run:: watch -n 0.1 nvidia-smi -Running generator on the manager -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -An alternative is to run the generator on a thread on the manager. The -number of workers can then be set to the number of simulation workers. - -Change the ``libE_specs`` in **run_libe_forces.py** as follows. - - .. code-block:: python - - nsim_workers = ensemble.nworkers - - # Persistent gen does not need resources - ensemble.libE_specs = LibeSpecs( - gen_on_manager=True, - -and run with:: - - python run_libe_forces.py -n 4 - To watch video ^^^^^^^^^^^^^^ @@ -181,14 +161,14 @@ Some FAQs specific to Perlmutter. See more on the :doc:`FAQ<../FAQ>` page. #SBATCH --gpus-per-task=1 Instead provide these to sub-tasks via the ``extra_args`` option to - the :doc:`MPIExecutor<../executor/mpi_executor>` ``submit`` function. + the :doc:`MPIExecutor<../executor/ex_index>` ``submit`` function. .. dropdown:: **GTL_DEBUG: [0] cudaHostRegister: no CUDA-capable device is detected** If using the environment variable ``MPICH_GPU_SUPPORT_ENABLED``, then ``srun`` commands, at time of writing, expect an option for allocating GPUs (e.g.~ ``--gpus-per-task=1`` would allocate one GPU to each MPI task of the MPI run). It is recommended that tasks submitted - via the :doc:`MPIExecutor<../executor/mpi_executor>` specify this in the ``extra_args`` + via the :doc:`MPIExecutor<../executor/ex_index>` specify this in the ``extra_args`` option to the ``submit`` function (rather than using an ``#SBATCH`` command). This is needed even when using setting ``CUDA_VISIBLE_DEVICES`` or other options. diff --git a/docs/platforms/platforms_index.rst b/docs/platforms/platforms_index.rst index 79285aa7b0..d1de30c45d 100644 --- a/docs/platforms/platforms_index.rst +++ b/docs/platforms/platforms_index.rst @@ -19,40 +19,26 @@ Centralized Running ------------------- The default communications scheme places the manager and workers on the first node. -The :doc:`MPI Executor<../executor/mpi_executor>` can then be invoked by each +The :doc:`MPI Executor<../executor/ex_index>` can then be invoked by each simulation worker, and libEnsemble will distribute user applications across the node allocation. This is the **most common approach** where each simulation runs an MPI application. -The generator will run on a worker by default, but if running a single generator, -the :ref:`libE_specs` option **gen_on_manager** is recommended, -which runs the generator on the manager (using a thread) as below. +.. image:: ../images/centralized_gen_on_manager.png + :alt: centralized + :scale: 55 -.. list-table:: - :widths: 60 40 +A SLURM batch script may include: - * - .. image:: ../images/centralized_gen_on_manager.png - :alt: centralized - :scale: 55 +.. code-block:: bash - - In calling script: + #SBATCH --nodes 3 - .. code-block:: python - :linenos: + python run_libe_forces.py --nworkers 3 - ensemble.libE_specs = LibeSpecs( - gen_on_manager=True, - ) - - A SLURM batch script may include: - - .. code-block:: bash - - #SBATCH --nodes 3 - - python run_libe_forces.py --nworkers 3 - -When using **gen_on_manager**, set ``nworkers`` to the number of workers desired for running simulations. +If running multiple generator processes instead, then set the +:ref:`libE_specs` option **gen_on_worker** so that multiple +worker processes can run multiple generator instances. Dedicated Mode ^^^^^^^^^^^^^^ @@ -62,32 +48,29 @@ True, the MPI executor will not launch applications on nodes where libEnsemble P processes (manager and workers) are running. Workers launch applications onto the remaining nodes in the allocation. -.. list-table:: - :widths: 60 40 - * - .. image:: ../images/centralized_dedicated.png - :alt: centralized dedicated mode - :scale: 30 +.. image:: ../images/centralized_dedicated.png + :alt: centralized dedicated mode + :scale: 30 - - In calling script: +In calling script: - .. code-block:: python - :linenos: +.. code-block:: python + :linenos: - ensemble.libE_specs = LibeSpecs( - num_resource_sets=2, - dedicated_mode=True, - ) - - A SLURM batch script may include: + ensemble.libE_specs = LibeSpecs( + gen_on_worker=True, + num_resource_sets=2, + dedicated_mode=True, + ) - .. code-block:: bash +A SLURM batch script may include: - #SBATCH --nodes 3 +.. code-block:: bash - python run_libe_forces.py --nworkers 3 + #SBATCH --nodes 3 -Note that **gen_on_manager** is not set in the above example. + python run_libe_forces.py --nworkers 3 Distributed Running ------------------- @@ -120,7 +103,7 @@ the nodes within that allocation. *How does libEnsemble know where to run tasks (user applications)?* -The libEnsemble :doc:`MPI Executor<../executor/mpi_executor>` can be initialized from the user calling +The libEnsemble :doc:`MPI Executor<../executor/ex_index>` can be initialized from the user calling script, and then used by workers to run tasks. The Executor will automatically detect the nodes available on most systems. Alternatively, the user can provide a file called **node_list** in the run directory. By default, the Executor will divide up the nodes evenly to each worker. @@ -130,17 +113,9 @@ Mapping Tasks to Resources The :ref:`resource manager` detects node lists from :ref:`common batch schedulers`, -and partitions these to workers. The :doc:`MPI Executor<../executor/mpi_executor>` +and partitions these to workers. The :doc:`MPI Executor<../executor/ex_index>` accesses the resources available to the current worker when launching tasks. -Zero-resource workers ---------------------- - -Users with persistent ``gen_f`` functions may notice that the persistent workers -are still automatically assigned system resources. This can be resolved by using -the ``gen_on_manager`` option or by -:ref:`fixing the number of resource sets`. - Assigning GPUs -------------- @@ -163,7 +138,7 @@ System detection for resources can be overridden using the :ref:`resource_info` for more. +`custom_info` argument. See the :doc:`MPI Executor<../executor/ex_index>` for more. Systems with Launch/MOM Nodes ----------------------------- @@ -171,8 +146,7 @@ Systems with Launch/MOM Nodes Some large systems have a 3-tier node setup. That is, they have a separate set of launch nodes (known as MOM nodes on Cray Systems). User batch jobs or interactive sessions run on a launch node. Most such systems supply a special MPI runner that has some application-level scheduling -capability (e.g., ``aprun``, ``jsrun``). MPI applications can only be submitted from these nodes. Examples -of these systems include Summit and Sierra. +capability (e.g., ``aprun``, ``jsrun``). MPI applications can only be submitted from these nodes. There are two ways of running libEnsemble on these kinds of systems. The first, and simplest, is to run libEnsemble on the launch nodes. This is often sufficient if the worker's simulation @@ -225,7 +199,7 @@ accessible on the remote system:: exctr.register_app(full_path="/home/user/forces.x", app_name="forces") task = exctr.submit(app_name="forces", num_procs=64) -Specify a Globus Compute endpoint in either :class:`sim_specs` or :class:`gen_specs` via the ``globus_compute_endpoint`` +Specify a Globus Compute endpoint in :class:`sim_specs` via the ``globus_compute_endpoint`` argument. For example:: from libensemble.specs import SimSpecs @@ -256,7 +230,6 @@ libEnsemble on specific HPC systems. improv perlmutter polaris - summit srun example_scripts diff --git a/docs/platforms/polaris.rst b/docs/platforms/polaris.rst index 5fdf82aaae..21518ccf42 100644 --- a/docs/platforms/polaris.rst +++ b/docs/platforms/polaris.rst @@ -36,7 +36,7 @@ environment (if you need ``conda install``). More details at `Python for Polaris pip install libensemble -See :doc:`here<../advanced_installation>` for more information on advanced options +See :doc:`here<../advanced_installation/advanced_installation>` for more information on advanced options for installing libEnsemble, including using Spack. Job Submission diff --git a/docs/platforms/srun.rst b/docs/platforms/srun.rst index 5ec8a64839..101b441bc5 100644 --- a/docs/platforms/srun.rst +++ b/docs/platforms/srun.rst @@ -11,7 +11,7 @@ Example SLURM submission scripts for various systems are given in the :doc:`examples`. Further examples are given in some of the specific platform guides (e.g., :doc:`Perlmutter guide`) -By default, the :doc:`MPIExecutor<../executor/mpi_executor>` uses ``mpirun`` +By default, the :doc:`MPIExecutor<../executor/ex_index>` uses ``mpirun`` as a priority over ``srun`` as it works better in some cases. If ``mpirun`` does not work well, then try telling the MPIExecutor to use ``srun`` when it is initiated in the calling script:: @@ -45,14 +45,14 @@ when assigning more than one worker to any given node. #SBATCH --gpus-per-task=1 Instead provide these to sub-tasks via the ``extra_args`` option to the - :doc:`MPIExecutor<../executor/mpi_executor>` ``submit`` function. + :doc:`MPIExecutor<../executor/ex_index>` ``submit`` function. .. dropdown:: **GTL_DEBUG: [0] cudaHostRegister: no CUDA-capable device is detected** If using the environment variable ``MPICH_GPU_SUPPORT_ENABLED``, then ``srun`` commands may expect an option for allocating GPUs (e.g., ``--gpus-per-task=1`` would allocate one GPU to each MPI task of the MPI run). It is recommended that tasks submitted - via the :doc:`MPIExecutor<../executor/mpi_executor>` specify this in the ``extra_args`` + via the :doc:`MPIExecutor<../executor/ex_index>` specify this in the ``extra_args`` option to the ``submit`` function (rather than using an ``#SBATCH`` command). If running the libEnsemble calling script with ``srun``, then it is recommended that diff --git a/docs/platforms/summit.rst b/docs/platforms/summit.rst deleted file mode 100644 index 9a08a21eb5..0000000000 --- a/docs/platforms/summit.rst +++ /dev/null @@ -1,206 +0,0 @@ -======================= -Summit (Decommissioned) -======================= - -Summit_ was an IBM AC922 system located at the Oak Ridge Leadership Computing -Facility (OLCF). Each of the approximately 4,600 compute nodes on Summit contained two -IBM POWER9 processors and six NVIDIA Volta V100 accelerators. - -Summit featured three tiers of nodes: login, launch, and compute nodes. - -Users on login nodes submit batch runs to the launch nodes. -Batch scripts and interactive sessions run on the launch nodes. Only the launch -nodes can submit MPI runs to the compute nodes via ``jsrun``. - -These docs are maintained to guide libEnsemble's usage on three-tier systems and/or -`jsrun` systems similar to Summit. - -Configuring Python ------------------- - -Begin by loading the Python 3 Anaconda module:: - - $ module load python - -You can now create and activate your own custom conda_ environment:: - - conda create --name myenv python=3.10 - export PYTHONNOUSERSITE=1 # Make sure get python from conda env - . activate myenv - -If you are installing any packages with extensions, ensure that the correct compiler -module is loaded. If using mpi4py_, this must be installed from source, -referencing the compiler. Currently, mpi4py must be built with gcc:: - - module load gcc - -With your environment activated, run :: - - CC=mpicc MPICC=mpicc pip install mpi4py --no-binary mpi4py - -Installing libEnsemble ----------------------- - -Obtaining libEnsemble is now as simple as ``pip install libensemble``. -Your prompt should be similar to the following line: - -.. code-block:: console - - (my_env) user@login5:~$ pip install libensemble - -.. note:: - If you encounter pip errors, run ``python -m pip install --upgrade pip`` first - -Or, you can install via ``conda``: - -.. code-block:: console - - (my_env) user@login5:~$ conda config --add channels conda-forge - (my_env) user@login5:~$ conda install -c conda-forge libensemble - -See :doc:`here<../advanced_installation>` for more information on advanced options -for installing libEnsemble. -Special note on resource sets and Executor submit options - ---------------------------------------------------------- - -When using the portable MPI run configuration options (e.g., num_nodes) to the -:doc:`MPIExecutor<../executor/mpi_executor>` ``submit`` function, it is important -to note that, due to the resource sets used on Summit, the options refer to -resource sets as follows: - -- num_procs (int, optional) – The total number resource sets for this run. - -- num_nodes (int, optional) – The number of nodes on which to submit the run. - -- procs_per_node (int, optional) – The number of resource sets per node. - -It is recommended that the user defines a resource set as the minimal configuration -of CPU cores/processes and GPUs. These can be added to the ``extra_args`` option -of the *submit* function. Alternatively, the portable options can be ignored and -everything expressed in ``extra_args``. - -For example, the following *jsrun* line would run three resource sets, -each having one core (with one process), and one GPU, along with some extra options:: - - jsrun -n 3 -a 1 -g 1 -c 1 --bind=packed:1 --smpiargs="-gpu" - -To express this line in the ``submit`` function may look -something like the following:: - - exctr = Executor.executor - task = exctr.submit(app_name="mycode", - num_procs=3, - extra_args="-a 1 -g 1 -c 1 --bind=packed:1 --smpiargs="-gpu"" - app_args="-i input") - -This would be equivalent to:: - - exctr = Executor.executor - task = exctr.submit(app_name="mycode", - extra_args="-n 3 -a 1 -g 1 -c 1 --bind=packed:1 --smpiargs="-gpu"" - app_args="-i input") - -The libEnsemble resource manager works out the resources available to each worker, -but unlike some other systems, ``jsrun`` on Summit dynamically schedules runs to -available slots across and within nodes. It can also queue tasks. This allows variable -size runs to easily be handled on Summit. If oversubscription to the `jsrun` system -is desired, then libEnsemble's resource manager can be disabled in the -calling script via:: - - libE_specs["disable_resource_manager"] = True - -In the above example, the task being submitted used three GPUs, which is half those -available on a Summit node, and thus two such tasks may be allocated to each node -(from different workers), if they were running at the same time. - -Job Submission --------------- - -Summit used LSF_ for job management and submission. For libEnsemble, the most -important command is ``bsub`` for submitting batch scripts from the login nodes -to execute on the launch nodes. - -It is recommended to run libEnsemble on the launch nodes (assuming workers are -submitting MPI applications) using the ``local`` communications mode (multiprocessing). - -Interactive Runs -^^^^^^^^^^^^^^^^ - -You can run interactively with ``bsub`` by specifying the ``-Is`` flag, -similarly to the following:: - - $ bsub -W 30 -P [project] -nnodes 8 -Is - -This will place you on a launch node. - -.. note:: - You will need to reactivate your conda virtual environment. - -Batch Runs -^^^^^^^^^^ - -Batch scripts specify run settings using ``#BSUB`` statements. The following -simple example depicts configuring and launching libEnsemble to a launch node with -multiprocessing. This script also assumes the user is using the ``parse_args()`` -convenience function from libEnsemble's :doc:`tools module<../utilities>`. - -.. code-block:: bash - - #!/bin/bash -x - #BSUB -P - #BSUB -J libe_mproc - #BSUB -W 60 - #BSUB -nnodes 128 - #BSUB -alloc_flags "smt1" - - # --- Prepare Python --- - - # Load conda module and gcc. - module load python - module load gcc - - # Name of conda environment - export CONDA_ENV_NAME=my_env - - # Activate conda environment - export PYTHONNOUSERSITE=1 - source activate $CONDA_ENV_NAME - - # --- Prepare libEnsemble --- - - # Name of calling script - export EXE=calling_script.py - - # Communication Method - export COMMS="--comms local" - - # Number of workers. - export NWORKERS="--nworkers 128" - - hash -r # Check no commands hashed (pip/python...) - - # Launch libE - python $EXE $COMMS $NWORKERS > out.txt 2>&1 - -With this saved as ``myscript.sh``, allocating, configuring, and queueing -libEnsemble on Summit is achieved by running :: - - $ bsub myscript.sh - -Example submission scripts are also given in the :doc:`examples`. - -Launching User Applications from libEnsemble Workers ----------------------------------------------------- - -Only the launch nodes can submit MPI runs to the compute nodes via ``jsrun``. -This can be accomplished in user simulator functions directly. However, it is highly -recommended that the :doc:`Executor<../executor/ex_index>` interface -be used inside the simulator or generator, because this provides a portable interface -with many advantages including automatic resource detection, portability, -launch failure resilience, and ease of use. - -.. _conda: https://conda.io/en/latest/ -.. _LSF: https://www.olcf.ornl.gov/wp-content/uploads/2018/12/summit_workshop_fuson.pdf -.. _mpi4py: https://mpi4py.readthedocs.io/en/stable/ -.. _Summit: https://www.olcf.ornl.gov/olcf-resources/compute-systems/summit/ diff --git a/docs/posters.rst b/docs/posters.rst deleted file mode 100644 index 78c9af9117..0000000000 --- a/docs/posters.rst +++ /dev/null @@ -1,23 +0,0 @@ -Posters and Presentations -========================= - -Exascale Computing Project 2023 -------------------------------- - -.. raw:: html - - - -SciPy 2020 ----------- - -.. raw:: html - - - -CSE 2019 --------- - -.. raw:: html - - diff --git a/docs/programming_libE.rst b/docs/programming_libE.rst index f4ffaecac6..03e8e97fb6 100644 --- a/docs/programming_libE.rst +++ b/docs/programming_libE.rst @@ -1,8 +1,6 @@ Constructing Workflows ====================== -We now give greater detail in programming with libEnsemble. - .. toctree:: :maxdepth: 2 :caption: The Basics @@ -10,8 +8,6 @@ We now give greater detail in programming with libEnsemble. libe_module data_structures/data_structures history_output_logging - function_guides/history_array - resource_manager/resources_index .. toctree:: :caption: Writing User Functions: diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index 58efae7694..0000000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,8 +0,0 @@ -sphinx<9 -sphinxcontrib-bibtex -sphinxcontrib-spelling -autodoc_pydantic -sphinx-design -numpy -sphinx_rtd_theme>1 -sphinx-copybutton diff --git a/docs/resource_manager/overview.rst b/docs/resource_manager/overview.rst index 57231962b9..f980eca3b3 100644 --- a/docs/resource_manager/overview.rst +++ b/docs/resource_manager/overview.rst @@ -9,7 +9,7 @@ libEnsemble comes with built-in resource management. This entails the core counts, and GPUs), and the allocation of resources to workers. By default, the provisioned resources are divided by the number of workers. -libEnsemble's :doc:`MPI Executor<../executor/mpi_executor>` is aware of +libEnsemble's :doc:`MPI Executor<../executor/ex_index>` is aware of these supplied resources, and if not given any of ``num_nodes``, ``num_procs``, or ``procs_per_node`` in the submit function, it will try to use all nodes and CPU cores available to the worker. @@ -33,8 +33,7 @@ Variable resource assignment In slightly more detail, the resource manager divides resources into **resource sets**. One resource set is the smallest unit of resources that can be assigned (and dynamically reassigned) to workers. By default, the provisioned resources are -divided by the number of workers (excluding any workers given in the -``zero_resource_workers`` :class:`libE_specs` option). +divided by the number of workers. However, it can also be set directly by the ``num_resource_sets`` :class:`libE_specs` option. If the latter is set, the dynamic resource assignment algorithm will always be used. @@ -120,7 +119,7 @@ Accessing resources from the simulation function In the user's simulation function, the resources supplied to the worker can be :doc:`interrogated directly via the resources class attribute`. -libEnsemble's executors (e.g., the :doc:`MPI Executor<../executor/mpi_executor>`) are +libEnsemble's executors (e.g., the :doc:`MPI Executor<../executor/ex_index>`) are aware of these supplied resources, and if not given any of ``num_nodes``, ``num_procs``, or ``procs_per_node`` in the submit function, it will try to use all nodes and CPU cores available. @@ -217,20 +216,12 @@ Persistent generator You have *one* persistent generator and want *eight* workers to run concurrent simulations. In this case you can run with *nine* workers. -Either explicitly set eight resource sets (recommended): +Explicitly set eight resource sets (recommended): .. code-block:: python libE_specs["num_resource_sets"] = 8 -Or if the generator should always be the same worker, use one zero-resource worker: - -.. code-block:: python - - libE_specs["zero_resource_workers"] = [1] - -For the second option, an allocation function supporting zero-resource workers must be used. - Using the two-node example above, the initial worker mapping in this example will be: .. image:: ../images/variable_resources_persis_gen1.png diff --git a/docs/resource_manager/resource_detection.rst b/docs/resource_manager/resource_detection.rst index 2048eb2793..e294b82b9f 100644 --- a/docs/resource_manager/resource_detection.rst +++ b/docs/resource_manager/resource_detection.rst @@ -4,7 +4,7 @@ Resource Detection ================== The resource manager can detect system resources, and partition -these to workers. The :doc:`MPI Executor<../executor/mpi_executor>` +these to workers. The :doc:`MPI Executor<../executor/ex_index>` accesses the resources available to the current worker when launching tasks. Node-lists are detected by an environment variable on the following systems: diff --git a/docs/resource_manager/resources_index.rst b/docs/resource_manager/resources_index.rst index 72b48061d3..1802d13872 100644 --- a/docs/resource_manager/resources_index.rst +++ b/docs/resource_manager/resources_index.rst @@ -7,17 +7,14 @@ libEnsemble comes with built-in resource management. This entails the detection of available resources (e.g., nodelists, core counts, and GPUs), and the allocation of resources to workers. -Resource management can be disabled by setting -``libE_specs["disable_resource_manager"] = True``. This will prevent libEnsemble -from doing any resource detection or management. +It can be disabled by setting ``libE_specs["disable_resource_manager"] = True``. .. toctree:: :maxdepth: 2 :titlesonly: :caption: Resource Manager: - Zero-resource workers (e.g., Persistent gen does not need resources) overview resource_detection scheduler_module - Worker Resources Module (query resources for current worker) + worker_resources diff --git a/docs/resource_manager/zero_resource_workers.rst b/docs/resource_manager/zero_resource_workers.rst deleted file mode 100644 index 4c72cf5d7b..0000000000 --- a/docs/resource_manager/zero_resource_workers.rst +++ /dev/null @@ -1,89 +0,0 @@ -.. _zero_resource_workers: - -Zero-resource workers -~~~~~~~~~~~~~~~~~~~~~ - -Users with persistent ``gen_f`` functions may notice that the persistent workers -are still automatically assigned resources. This can be wasteful if those workers -only run ``gen_f`` functions in-place (i.e., they do not use the Executor -to submit applications to allocated nodes). Suppose the user is using the -:meth:`parse_args()` function and runs:: - - python run_ensemble_persistent_gen.py --nworkers 3 - -If three nodes are available in the node allocation, the result may look like the -following. - - .. image:: ../images/persis_wasted_node.png - :alt: persis_wasted_node - :scale: 40 - :align: center - -To avoid the the wasted node above, add an extra worker:: - - python run_ensemble_persistent_gen.py --nworkers 4 - -and in the calling script (*run_ensemble_persistent_gen.py*), explicitly set the -number of resource sets to the number of workers that will be running simulations. - -.. code-block:: python - - nworkers, is_manager, libE_specs, _ = parse_args() - libE_specs["num_resource_sets"] = nworkers - 1 - -When the ``num_resource_sets`` option is used, libEnsemble will use the dynamic -resource scheduler, and any worker may assign work to any node. This works well -for most users. - - .. image:: ../images/persis_add_worker.png - :alt: persis_add_worker - :scale: 40 - :align: center - -**Optional**: An alternative way to express the above would be to use the command -line:: - - python run_ensemble_persistent_gen.py --comms local --nsim_workers 3 - -This would automatically set the ``num_resource_sets`` option and add a single -worker for the persistent generator - a common use-case. - -In general, the number of resource sets should be set to enable the maximum -concurrency desired by the ensemble, taking into account generators and simulators. - -Users can set generator resources using the *libE_specs* options -``gen_num_procs`` and/or ``gen_num_gpus``, which take integer values. -If only ``gen_num_gpus`` is set, then the number of processors is set to match. - -To vary generator resources, ``persis_info`` settings can be used in allocation -functions before calling the ``gen_work`` support function. This takes the -same options (``gen_num_procs`` and ``gen_num_gpus``). - -Alternatively, the setting ``persis_info["gen_resources"]`` can also be set to -a number of resource sets. - -The available nodes are always divided by the number of resource sets, and there -may be multiple nodes or a partition of a node in each resource set. If the split -is uneven, resource sets are not split between nodes. For example, if there are -two nodes and five resource sets, one node will have three resource sets, and -the other will have two. - -Placing zero-resource functions on a fixed worker -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -If the generator must always be on worker one, then instead of using -``num_resource_sets``, use the ``zero_resource_workers`` *libE_specs* option: - -.. code-block:: python - - libE_specs["zero_resource_workers"] = [1] - -in the calling script and worker one will not be allocated resources. In general, -set the parameter ``zero_resource_workers`` to a list of worker IDs that should not -have resources assigned. - -This approach can be useful if running in -:doc:`distributed mode<../platforms/platforms_index>`. - -The use of the ``zero_resource_workers`` *libE_specs* option must be supported by -the allocation function, see :ref:`start_only_persistent`) diff --git a/docs/running_libE.rst b/docs/running_libE.rst index 7b8b0532d8..6329e13e27 100644 --- a/docs/running_libE.rst +++ b/docs/running_libE.rst @@ -3,131 +3,97 @@ Running libEnsemble =================== -Introduction ------------- - -libEnsemble runs with one manager and multiple workers. Each worker may run either -a generator or simulator function (both are Python scripts). Generators -determine the parameters/inputs for simulations. Simulator functions run and -manage simulations, which often involve running a user application (see -:doc:`Executor`). - -.. note:: - As of version 1.3.0, the generator can be run as a thread on the manager, - using the :ref:`libE_specs` option **gen_on_manager**. - When using this option, set the number of workers desired for running - simulations. See :ref:`Running generator on the manager` - for more details. - -To use libEnsemble, you will need a calling script, which in turn will specify -generator and simulator functions. Many :doc:`examples` -are available. - -There are currently three communication options for libEnsemble (determining how -the Manager and Workers communicate). These are ``local``, ``mpi``, ``tcp``. -The default is ``local`` if ``nworkers`` is specified, otherwise ``mpi``. - -Note that ``local`` comms can be used on multi-node systems, where -the :doc:`MPI executor` is used to distribute MPI applications -across the nodes. Indeed, this is the most commonly used option, even on large -supercomputers. - .. note:: You do not need the ``mpi`` communication mode to use the - :doc:`MPI Executor`. The communication modes described + :doc:`MPI Executor`. The communication modes described here only refer to how the libEnsemble manager and workers communicate. -.. tab-set:: +Local Comms +----------- - .. tab-item:: Local Comms +Uses Python's built-in multiprocessing_ module. +The ``comms`` type ``local`` and number of workers ``nworkers`` for running simulators +may be provided in :ref:`libE_specs`. - Uses Python's built-in multiprocessing_ module. - The ``comms`` type ``local`` and number of workers ``nworkers`` may - be provided in :ref:`libE_specs`. +Run: - Then run:: + python myscript.py - python myscript.py +Or, if the script uses the :meth:`parse_args` function +or an :class:`Ensemble` object with ``Ensemble(parse_args=True)``, +this can be specified on the command line: - Or, if the script uses the :meth:`parse_args` function - or an :class:`Ensemble` object with ``Ensemble(parse_args=True)``, - you can specify these on the command line:: + python myscript.py -n N - python myscript.py --nworkers N +libEnsemble will run on **one node** in this scenario. To +:doc:`disallow this node` +from app-launches (if running libEnsemble on a compute node), +set ``libE_specs["dedicated_mode"] = True``. - This will launch one manager and ``N`` workers. +This mode can also be used to run on a **launch** node of a three-tier +system, ensuring the whole compute-node allocation is available for +launching apps. Make sure there are no imports of ``mpi4py`` in your Python scripts. - The following abbreviated line is equivalent to the above:: +Note that on macOS and Windows, the default multiprocessing method is ``"spawn"`` +instead of ``"fork"``; to resolve many related issues, we recommend placing +calling script code in an ``if __name__ == "__main__":`` block. - python myscript.py -n N +**Limitations of local mode** - libEnsemble will run on **one node** in this scenario. To - :doc:`disallow this node` - from app-launches (if running libEnsemble on a compute node), - set ``libE_specs["dedicated_mode"] = True``. +- Workers cannot be :doc:`distributed` across nodes. +- In some scenarios, any import of ``mpi4py`` will cause this to break. +- Does not have the potential scaling of MPI mode, but is sufficient for most users. - This mode can also be used to run on a **launch** node of a three-tier - system (e.g., Summit), ensuring the whole compute-node allocation is available for - launching apps. Make sure there are no imports of ``mpi4py`` in your Python scripts. +MPI Comms +--------- - Note that on macOS (since Python 3.8) and Windows, the default multiprocessing method - is ``"spawn"`` instead of ``"fork"``; to resolve many related issues, we recommend placing - calling script code in an ``if __name__ == "__main__":`` block. +This option uses mpi4py_ for the Manager/Worker communication. It is used automatically if +you run your libEnsemble calling script with an MPI runner such as:: - **Limitations of local mode** + mpirun -np N python myscript.py - - Workers cannot be :doc:`distributed` across nodes. - - In some scenarios, any import of ``mpi4py`` will cause this to break. - - Does not have the potential scaling of MPI mode, but is sufficient for most users. +where ``N`` is the number of processes. This will launch one manager and +``N-1`` simulator workers. - .. tab-item:: MPI Comms +This option requires ``mpi4py`` to be installed to interface with the MPI on your system. +It works on a standalone system, and with both +:doc:`central and distributed modes` of running libEnsemble on +multi-node systems. - This option uses mpi4py_ for the Manager/Worker communication. It is used automatically if - you run your libEnsemble calling script with an MPI runner such as:: +It also potentially scales the best when running with many workers on HPC systems. - mpirun -np N python myscript.py +**Limitations of MPI mode** - where ``N`` is the number of processes. This will launch one manager and - ``N-1`` workers. +If launching MPI applications from workers, then MPI is nested. **This is not +supported with Open MPI**. This can be overcome by using a proxy launcher. +This nesting does work with MPICH_ and its derivative MPI implementations. - This option requires ``mpi4py`` to be installed to interface with the MPI on your system. - It works on a standalone system, and with both - :doc:`central and distributed modes` of running libEnsemble on - multi-node systems. +It is also unsuitable to use this mode when running on the **launch** nodes of +three-tier systems. In that case ``local`` mode is recommended. - It also potentially scales the best when running with many workers on HPC systems. +TCP Comms +--------- - **Limitations of MPI mode** +Run the Manager on one system and launch workers to remote +systems or nodes over TCP. Configure through +:class:`libE_specs`, or on the command line +if using an :class:`Ensemble` object with +``Ensemble(parse_args=True)``, - If launching MPI applications from workers, then MPI is nested. **This is not - supported with Open MPI**. This can be overcome by using a proxy launcher. - This nesting does work with MPICH_ and its derivative MPI implementations. +**Reverse-ssh interface** - It is also unsuitable to use this mode when running on the **launch** nodes of - three-tier systems (e.g., Summit). In that case ``local`` mode is recommended. +Set ``comms`` to ``ssh`` to launch workers on remote ssh-accessible systems. This +co-locates workers, functions, and any applications. User +functions can also be persistent, unlike when launching remote functions via +:ref:`Globus Compute`. - .. tab-item:: TCP Comms +The remote working directory and Python need to be specified. This may resemble:: - Run the Manager on one system and launch workers to remote - systems or nodes over TCP. Configure through - :class:`libE_specs`, or on the command line - if using an :class:`Ensemble` object with - ``Ensemble(parse_args=True)``, + python myscript.py --comms ssh --workers machine1 machine2 --worker_pwd /home/workers --worker_python /home/.conda/.../python - **Reverse-ssh interface** +**Limitations of TCP mode** - Set ``comms`` to ``ssh`` to launch workers on remote ssh-accessible systems. This - co-locates workers, functions, and any applications. User - functions can also be persistent, unlike when launching remote functions via - :ref:`Globus Compute`. - - The remote working directory and Python need to be specified. This may resemble:: - - python myscript.py --comms ssh --workers machine1 machine2 --worker_pwd /home/workers --worker_python /home/.conda/.../python - - **Limitations of TCP mode** - - - There cannot be two calls to ``libE()`` or ``Ensemble.run()`` in the same script. +- There cannot be two calls to ``Ensemble.run()`` or ``libE()`` in the same script. Further Command Line Options ---------------------------- @@ -135,55 +101,6 @@ Further Command Line Options See the :meth:`parse_args` function in :doc:`Convenience Tools` for further command line options. -Persistent Workers ------------------- -.. _persis_worker: - -In a regular (non-persistent) worker, the user's generator or simulation function is called -whenever the worker receives work. A persistent worker is one that continues to run the -generator or simulation function between work units, maintaining the local data environment. - -A common use-case consists of a persistent generator (such as :doc:`persistent_aposmm`) -that maintains optimization data while generating new simulation inputs. The persistent generator runs -on a dedicated worker while in persistent mode. This requires an appropriate -:doc:`allocation function` that will run the generator as persistent. - -When running with a persistent generator, it is important to remember that a worker will be dedicated -to the generator and cannot run simulations. For example, the following run:: - - mpirun -np 3 python my_script.py - -starts one manager, one worker with a persistent generator, and one worker for running simulations. - -If this example was run as:: - - mpirun -np 2 python my_script.py - -No simulations will be able to run. - -.. _gen-on-manager: - -Running generator on the manager --------------------------------- - -The majority of libEnsemble use cases run a single generator. The -:ref:`libE_specs` option **gen_on_manager** will cause -the generator function to run on a thread on the manager. This can run -persistent user functions, sharing data structures with the manager, and avoids -additional communication to a generator running on a worker. When using this -option, the number of workers specified should be the (maximum) number of -concurrent simulations. - -If modifying a workflow to use ``gen_on_manager`` consider the following. - -* Set ``nworkers`` to the number of workers desired for running simulations. -* If using :meth:`add_unique_random_streams()` - to seed random streams, the default generator seed will be zero. -* If you have a line like ``libE_specs["nresource_sets"] = nworkers -1``, this - line should be removed. -* If the generator does use resources, ``nresource_sets`` can be increased as needed - so that the generator and all simulations are resourced. - Environment Variables --------------------- @@ -194,10 +111,10 @@ For example:: set in your simulation script before the Executor *submit* command will export the setting to your run. For running a bash script in a sub environment when using the Executor, see -the ``env_script`` option to the :doc:`MPI Executor`. +the ``env_script`` option to the :doc:`MPI Executor`. -Further Run Information ------------------------ +Running on Multi-Node Systems +----------------------------- For running on multi-node platforms and supercomputers, there are alternative ways to configure libEnsemble to resources. See the :doc:`Running on HPC Systems` @@ -206,4 +123,3 @@ guide for more information, including some examples for specific systems. .. _mpi4py: https://mpi4py.readthedocs.io/en/stable/ .. _MPICH: https://www.mpich.org/ .. _multiprocessing: https://docs.python.org/3/library/multiprocessing.html -.. _PSI/J: https://exaworks.org/psij diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index f7fcae81db..92c7a56011 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -66,7 +66,7 @@ init instantiation intercommunications interoperable -intitializing +initializing intranode ints invocable diff --git a/docs/tutorials/aposmm_tutorial.rst b/docs/tutorials/aposmm_tutorial.rst index ecfd4de592..7cc0d7b9fd 100644 --- a/docs/tutorials/aposmm_tutorial.rst +++ b/docs/tutorials/aposmm_tutorial.rst @@ -5,12 +5,14 @@ Optimization with APOSMM This tutorial demonstrates libEnsemble's capability to identify multiple minima of simulation output using the built-in :doc:`APOSMM<../examples/aposmm>` (Asynchronously Parallel Optimization Solver for finding Multiple Minima) -:ref:`gen_f`. In this tutorial, we'll create a simple -simulation :ref:`sim_f` that defines a function with +:ref:`gen_f`. In this tutorial, we'll create a simple +simulation :ref:`sim_f` that defines a function with multiple minima, then write a libEnsemble calling script that imports APOSMM and parameterizes it to check for minima over a domain of outputs from our ``sim_f``. -|Open in Colab| +.. only:: html + + |Open in Colab| Six-Hump Camel Simulation Function ---------------------------------- @@ -26,35 +28,20 @@ below: :align: center Create a new Python file named ``six_hump_camel.py``. This will be our -``sim_f``, incorporating the above function. Write the following: +simulator callable, incorporating the above function. Write the following: .. code-block:: python :linenos: - import numpy as np - - - def six_hump_camel(H, _, sim_specs): - """Six-Hump Camel sim_f.""" - - batch = len(H["x"]) # Num evaluations each sim_f call. - H_o = np.zeros(batch, dtype=sim_specs["out"]) # Define output array H - - for i, x in enumerate(H["x"]): - H_o["f"][i] = six_hump_camel_func(x) # Function evaluations placed into H - - return H_o - - def six_hump_camel_func(x): """Six-Hump Camel function definition""" - x1 = x[0] - x2 = x[1] + x1 = x["x1"] + x2 = x["x2"] term1 = (4 - 2.1 * x1**2 + (x1**4) / 3) * x1**2 term2 = x1 * x2 term3 = (-4 + 4 * x2**2) * x2**2 - return term1 + term2 + term3 + return {"f": term1 + term2 + term3} APOSMM Operations ----------------- @@ -100,160 +87,83 @@ Throughout, generated and evaluated points are appended to the ``"local_pt"`` being ``True`` if the point is part of a local optimization run, and ``"local_min"`` being ``True`` if the point has been ruled a local minimum. -APOSMM Persistence ------------------- - -APOSMM is implemented as a Persistent generator. A single worker process initiates -APOSMM so that it "persists" the course of a given libEnsemble run. - -APOSMM begins its own concurrent optimization runs, each of which independently -produces a linear sequence of points trying to find a local minimum. These -points are given to workers and evaluated by simulation routines. - -If there are more workers than optimization runs at any iteration of the -generator, additional random sample points are generated to keep the workers -busy. - -In practice, since a single worker becomes "persistent" for APOSMM, users -should initiate one more worker than the number of parallel simulations:: - - python my_aposmm_routine.py --nworkers 4 - -results in three workers running simulations and one running APSOMM. - -If running libEnsemble using `mpi4py` communications, enough MPI ranks should be -given to support libEnsemble's manager, a persistent worker to run APOSMM, and -simulation routines. The following:: - - mpiexec -n 3 python my_aposmm_routine.py - -results in only one worker process to perform simulation evaluations. - Calling Script -------------- -Create a new Python file named ``my_first_aposmm.py``. Start by importing NumPy, -libEnsemble routines, APOSMM, our ``sim_f``, and a specialized allocation -function: +Create a new Python file named ``my_first_aposmm.py``. Start by importing +libEnsemble classes, APOSMM, and our simulator callable: .. code-block:: python :linenos: - import numpy as np + from six_hump_camel import six_hump_camel_func - from six_hump_camel import six_hump_camel + import libensemble.gen_funcs + + libensemble.gen_funcs.rc.aposmm_optimizers = "scipy" - from libensemble.libE import libE - from libensemble.gen_funcs.persistent_aposmm import aposmm - from libensemble.alloc_funcs.persistent_aposmm_alloc import persistent_aposmm_alloc - from libensemble.tools import parse_args, add_unique_random_streams + from libensemble import Ensemble + from libensemble.gen_classes import APOSMM + from gest_api.vocs import VOCS + from libensemble.specs import SimSpecs, GenSpecs, ExitCriteria -This allocation function starts a single Persistent APOSMM routine and provides -``sim_f`` output for points requested by APOSMM. Points can be sampled points -or points from local optimization runs. +APOSMM supports a wide variety of external optimizers. The ``rc.aposmm_optimizers`` +statement above indicates to APOSMM which optimization method package to use, +helping prevent unnecessary imports or package installations. -APOSMM supports a wide variety of external optimizers. The following statements -set optimizer settings to ``"scipy"`` to indicate to APOSMM which optimization -method to use, and help prevent unnecessary imports or package installations: +Next, initialize the ``Ensemble`` and define our variables and objectives using +a ``VOCS`` object: .. code-block:: python :linenos: - import libensemble.gen_funcs + if __name__ == "__main__": + workflow = Ensemble(parse_args=True) - libensemble.gen_funcs.rc.aposmm_optimizers = "scipy" + vocs = VOCS( + variables={"x1": [-2, 2], "x2": [-1, 1], "x1_on_cube": [-2, 2], "x2_on_cube": [-1, 1]}, + objectives={"f": "MINIMIZE"}, + ) + +Notice the addition of ``x1_on_cube`` and ``x2_on_cube``. APOSMM requires variables scaled to the unit cube internally. By defining both sets of variables, APOSMM can translate between our actual domain and its internal domain. -Set up :doc:`parse_args()<../utilities>`, -our :doc:`sim_specs<../data_structures/sim_specs>`, -:doc:`gen_specs<../data_structures/gen_specs>`, -and :doc:`alloc_specs<../data_structures/alloc_specs>`: +Now, configure APOSMM. Because APOSMM internally uses variables named ``x``, ``x_on_cube``, and an objective named ``f``, we must map our ``VOCS`` fields to these internal names using ``variables_mapping``: .. code-block:: python :linenos: - nworkers, is_manager, libE_specs, _ = parse_args() - - sim_specs = { - "sim_f": six_hump_camel, # Simulation function - "in": ["x"], # Accepts "x" values - "out": [("f", float)], # Returns f(x) values - } - - gen_out = [ - ("x", float, 2), # Produces "x" values - ("x_on_cube", float, 2), # "x" values scaled to unit cube - ("sim_id", int), # Produces sim_id's for History array indexing - ("local_min", bool), # Is a point a local minimum? - ("local_pt", bool), # Is a point from a local opt run? - ] - - gen_specs = { - "gen_f": aposmm, # APOSMM generator function - "persis_in": ["f"] + [n[0] for n in gen_out], - "out": gen_out, # Output defined like above dict - "user": { - "initial_sample_size": 100, # Random sample 100 points to start - "localopt_method": "scipy_Nelder-Mead", - "opt_return_codes": [0], # Status integers specific to localopt_method - "max_active_runs": 6, # Occur in parallel - "lb": np.array([-2, -1]), # Lower bound of search domain - "ub": np.array([2, 1]), # Upper bound of search domain - }, - } - - alloc_specs = {"alloc_f": persistent_aposmm_alloc} - -``gen_specs["user"]`` fields above that are required for APOSMM are: - - * ``"lb"`` - Search domain lower bound - * ``"ub"`` - Search domain upper bound - * ``"localopt_method"`` - Chosen local optimization method - * ``"initial_sample_size"`` - Number of uniformly sampled points generated - before local optimization runs. - * ``"opt_return_codes"`` - A list of integers that local optimization - methods return when a minimum is detected. SciPy's Nelder-Mead returns 0, - but other methods (not used in this tutorial) return 1. - -Also note the following: - - * ``gen_specs["in"]`` is empty. For other ``gen_f``'s this defines what - fields to give to the ``gen_f`` when called, but here APOSMM's - ``alloc_f`` defines those fields. - * ``"x_on_cube"`` in ``gen_specs["out"]``. APOSMM works internally on - ``"x"`` values scaled to the unit cube. To avoid back-and-forth scaling - issues, both types of ``"x"``'s are communicated back, even though the - simulation will likely use ``"x"`` values. (APOSMM performs handshake to - ensure that the ``x_on_cube`` that was given to be evaluated is the same - the one that is given back.) - * ``"sim_id"`` in ``gen_specs["out"]``. APOSMM produces points in its - local History array that it will need to update later, and can best - reference those points (and avoid a search) if APOSMM produces the IDs - itself, instead of libEnsemble. - -Other options and configurations for APOSMM can be found in the -APOSMM :doc:`API reference<../examples/aposmm>`. - -Set :ref:`exit_criteria` so libEnsemble knows -when to complete, and :ref:`persis_info` for -random sampling seeding: + aposmm = APOSMM( + vocs, + max_active_runs=workflow.nworkers, + variables_mapping={"x": ["x1", "x2"], "x_on_cube": ["x1_on_cube", "x2_on_cube"], "f": ["f"]}, + initial_sample_size=100, + localopt_method="scipy_Nelder-Mead", + opt_return_codes=[0], + ) -.. code-block:: python - :linenos: + workflow.gen_specs = GenSpecs( + generator=aposmm, + vocs=vocs, + batch_size=5, + initial_batch_size=10, + ) - exit_criteria = {"sim_max": 2000} - persis_info = add_unique_random_streams({}, nworkers + 1) +APOSMM is instantiated directly as a standardized generator. It handles its own required fields, simplifying our configurations. ``opt_return_codes`` is a list of integers that local optimization methods return when a minimum is detected. SciPy's Nelder-Mead returns 0. -Finally, add statements to :doc:`initiate libEnsemble<../libe_module>`, and quickly -check calculated minima: +Finally, we configure the simulation function, exit criteria, and run the workflow. We can also print out any points that APOSMM identified as local minima: .. code-block:: python :linenos: - if __name__ == "__main__": # required by multiprocessing on macOS and windows - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) + workflow.sim_specs = SimSpecs(simulator=six_hump_camel_func, vocs=vocs) + workflow.exit_criteria = ExitCriteria(sim_max=2000) + + H, _, _ = workflow.run() - if is_manager: - print("Minima:", H[np.where(H["local_min"])]["x"]) + if workflow.is_manager: + # We can map our variables back to an array for easy printing + minima = [[row["x1"], row["x2"]] for row in H if row["local_min"]] + print("Minima:", minima) Final Setup, Run, and Output ---------------------------- @@ -272,27 +182,10 @@ the routine. After a couple seconds, the output should resemble the following:: - [0] libensemble.libE (MANAGER_WARNING): - ******************************************************************************* - User generator script will be creating sim_id. - Take care to do this sequentially. - Also, any information given back for existing sim_id values will be overwritten! - So everything in gen_specs["out"] should be in gen_specs["in"]! - ******************************************************************************* - - Minima: [[ 0.08993295 -0.71265804] - [ 1.70360676 -0.79614982] - [-1.70368421 0.79606073] - [-0.08988064 0.71270945] - [-1.60699361 -0.56859108] - [ 1.60713962 0.56869567]] - -The first section labeled ``MANAGER_WARNING`` is a default libEnsemble warning -for generator functions that create ``sim_id``'s, like APOSMM. It does not -indicate a failure. + Minima: [[0.08988580227184285, -0.7126604246830723], [-0.08983226938927827, 0.7126622830878125], [-1.7036480556534283, 0.7960787201083437], [1.7035677028481488, -0.7961234727197022], [1.607106093246473, 0.5686524941018596], [-1.607102046898864, -0.568650772274404]] The local minima for the Six-Hump Camel simulation function as evaluated by -APOSMM with libEnsemble should be listed directly below the warning. +APOSMM with libEnsemble should be listed directly above. Please see the API reference :doc:`here<../examples/aposmm>` for more APOSMM configuration options and other information. @@ -304,7 +197,7 @@ Applications APOSMM is not limited to evaluating minima from pure Python simulation functions. Many common libEnsemble use-cases involve using -libEnsemble's :doc:`MPI Executor<../executor/overview>` to launch user +libEnsemble's :doc:`MPI Executor<../executor/ex_index>` to launch user applications with parameters requested by APOSMM, then evaluate their output using APOSMM, and repeat until minima are identified. A currently supported example can be found in libEnsemble's `WarpX Scaling Test`_. diff --git a/docs/tutorials/calib_cancel_tutorial.rst b/docs/tutorials/calib_cancel_tutorial.rst index 1c0fcff3c8..316e56ba1d 100644 --- a/docs/tutorials/calib_cancel_tutorial.rst +++ b/docs/tutorials/calib_cancel_tutorial.rst @@ -12,7 +12,7 @@ compute resources may then be more effectively applied toward critical evaluatio For a somewhat different approach than libEnsemble's :doc:`other tutorials`, we'll emphasize the settings, functions, and data fields within the calling script, -:ref:`persistent generator`, manager, and :ref:`sim_f` +:ref:`persistent generator`, manager, and :ref:`sim_f` that make this capability possible, rather than outlining a step-by-step process. The libEnsemble regression test ``test_persistent_surmise_calib.py`` demonstrates @@ -36,7 +36,7 @@ gravitational constant, and the corresponding computer model could be the set of differential equations that govern the drop. In a case where the computation of the computer model is relatively expensive, we employ a fast surrogate model to approximate the model and to inform good parameters to test next. Here the computer -model :math:`f(\theta, x)` is accessible only through performing :ref:`sim_f` +model :math:`f(\theta, x)` is accessible only through performing :ref:`sim_f` evaluations. As a convenience for testing, the ``observed`` data values are modelled by calling the ``sim_f`` @@ -151,11 +151,9 @@ The allocation function used in this example is the *only_persistent_gens* funct alloc_specs = { "alloc_f": alloc_f, - "user": { - "init_sample_size": init_sample_size, - "async_return": True, - "active_recv_gen": True, - }, + "initial_batch_size": init_sample_size, + "async_return": True, + "active_recv_gen": True, } **async_return** tells the allocation function to return results to the generator as soon @@ -215,8 +213,8 @@ by a user function, otherwise it will be ignored. To demonstrate this, the test captures and processes this signal from the manager. In order to do this, a compiled version of the borehole function is launched by ``sim_funcs/borehole_kills.py`` -via the :doc:`Executor<../executor/overview>`. As the borehole application used here is serial, we use the -:doc:`Executor base class<../executor/executor>` rather than the commonly used :doc:`MPIExecutor<../executor/mpi_executor>` +via the :doc:`Executor<../executor/ex_index>`. As the borehole application used here is serial, we use the +:doc:`Executor base class<../executor/ex_index>` rather than the commonly used :doc:`MPIExecutor<../executor/ex_index>` class. The base Executor submit routine simply sub-processes a serial application in-place. After the initial sample batch of evaluations has been processed, an artificial delay is added to the sub-processed borehole to allow time to receive the kill signal and terminate the application. Killed simulations will be reported at diff --git a/docs/tutorials/executor_forces_tutorial.rst b/docs/tutorials/executor_forces_tutorial.rst index e01496734b..e1f7d4b813 100644 --- a/docs/tutorials/executor_forces_tutorial.rst +++ b/docs/tutorials/executor_forces_tutorial.rst @@ -4,16 +4,18 @@ Ensemble with an MPI Application This tutorial highlights libEnsemble's capability to portably execute and monitor external scripts or user applications within simulation or generator -functions using the :doc:`executor<../executor/overview>`. +functions using the :doc:`executor<../executor/ex_index>`. -|Open in Colab| +.. only:: html + + |Open in Colab| The calling script registers a compiled executable that simulates electrostatic forces between a collection of particles. The simulator function launches instances of this executable and reads output files to determine the result. -This tutorial uses libEnsemble's :doc:`MPI Executor<../executor/mpi_executor>`, +This tutorial uses libEnsemble's :doc:`MPI Executor<../executor/ex_index>`, which automatically detects available MPI runners and resources. This example also uses a persistent generator. This generator runs on a @@ -49,7 +51,7 @@ generation functions and call libEnsemble. Create a Python file called :linenos: :end-at: ensemble = Ensemble -We first instantiate our :doc:`MPI Executor<../executor/mpi_executor>`. +We first instantiate our :doc:`MPI Executor<../executor/ex_index>`. Registering an application is as easy as providing the full file-path and giving it a memorable name. This Executor will later be used within our simulation function to launch the registered app. @@ -82,32 +84,15 @@ expect, and also to parameterize user functions: :end-at: gen_specs_end_tag :lineno-start: 37 -Next, configure an allocation function, which starts the one persistent -generator and farms out the simulations. We also tell it to wait for all -simulations to return their results, before generating more parameters. - -.. literalinclude:: ../../libensemble/tests/functionality_tests/test_executor_forces_tutorial.py - :language: python - :linenos: - :start-at: ensemble.alloc_specs = AllocSpecs - :end-at: ) - :lineno-start: 55 - -Now we set :ref:`exit_criteria` to -exit after running eight simulations. - -We also give each worker a seeded random stream, via the -:ref:`persis_info` option. -These can be used for random number generation if required. - -Finally we :doc:`run<../libe_module>` the ensemble. +Next, we set :ref:`exit_criteria` to +exit after running eight simulations, and finally we :doc:`run<../libe_module>` the ensemble. .. literalinclude:: ../../libensemble/tests/functionality_tests/test_executor_forces_tutorial.py :language: python :linenos: :start-at: Instruct libEnsemble :end-at: ensemble.run() - :lineno-start: 62 + :lineno-start: 55 Exercise ^^^^^^^^ @@ -336,44 +321,6 @@ These may require additional browsing of the documentation to complete. ... -Running the generator on the manager ------------------------------------- - -As of version 1.3.0, the generator can be run on a thread on the manager, -using the :ref:`libE_specs` option **gen_on_manager**. - -Change the libE_specs as follows. - - .. code-block:: python - :linenos: - :lineno-start: 28 - - nsim_workers = ensemble.nworkers - - # Persistent gen does not need resources - ensemble.libE_specs = LibeSpecs( - gen_on_manager=True, - sim_dirs_make=True, - ensemble_dir_path="./test_executor_forces_tutorial", - ) - -When running set ``nworkers`` to the number of workers desired for running simulations. -E.g., Instead of: - -.. code-block:: bash - - python run_libe_forces.py --nworkers 5 - -use: - -.. code-block:: bash - - python run_libe_forces.py --nworkers 4 - -Note that as the generator random number seed will be zero instead of one, the checksum will change. - -For more information see :ref:`Running generator on the manager`. - Running forces application with input file ------------------------------------------ diff --git a/docs/tutorials/forces_gpu_tutorial.rst b/docs/tutorials/forces_gpu_tutorial.rst index be487f33cc..dc74ad215b 100644 --- a/docs/tutorials/forces_gpu_tutorial.rst +++ b/docs/tutorials/forces_gpu_tutorial.rst @@ -35,6 +35,7 @@ from the simple forces example are highlighted: # Optional - to print GPU settings from libensemble.tools.test_support import check_gpu_setting + def run_forces(H, persis_info, sim_specs, libE_info): """Launches the forces MPI app and auto-assigns ranks and GPU resources. @@ -153,6 +154,7 @@ and use this information however you want. output = np.zeros(1, dtype=sim_specs["out"]) output["energy"][0] = final_energy + return output The above code will assign a GPU to each worker on CUDA-capable systems, @@ -214,7 +216,6 @@ For example:: python run_libe_forces.py --nworkers 9 -See :ref:`zero-resource workers` for more ways to express this. Changing the number of GPUs per worker -------------------------------------- diff --git a/docs/tutorials/gpcam_tutorial.rst b/docs/tutorials/gpcam_tutorial.rst index a013c1b67e..190697374b 100644 --- a/docs/tutorials/gpcam_tutorial.rst +++ b/docs/tutorials/gpcam_tutorial.rst @@ -5,7 +5,9 @@ This example uses gpCAM_ to construct a global surrogate of ``f`` values using a In each iteration, a batch of points is produced for concurrent evaluation, maximizing uncertainty reduction. -|Open in Colab| +.. only:: html + + |Open in Colab| Ensure that libEnsemble, and gpCAM are installed via: ``pip install libensemble gpcam`` @@ -30,6 +32,7 @@ This version (and others) of the gpCAM generator can be found at `libensemble/ge from libensemble.message_numbers import EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG from libensemble.tools.persistent_support import PersistentSupport + def persistent_gpCAM(H_in, persis_info, gen_specs, libE_info): """Run a batched gpCAM model to create a surrogate""" @@ -156,6 +159,7 @@ For running applications using parallel resources in the simulator see the `forc # Define our simulation function import numpy as np + def six_hump_camel(H, persis_info, sim_specs, _): """Six-Hump Camel sim_f.""" @@ -189,6 +193,8 @@ First we will create a cleanup script so we can easily re-run. # To rerun this notebook, we need to delete the ensemble directory. import shutil + + def cleanup(): try: shutil.rmtree("ensemble") @@ -218,31 +224,30 @@ If you wish to make your own functions based on the above, those can be imported nworkers = 4 - # When using gen_on_manager, nworkers is number of concurrent sims. # final_gen_send means the last evaluated points are returned to the generator to update the model. - libE_specs = LibeSpecs(nworkers=nworkers, gen_on_manager=True, final_gen_send=True) + libE_specs = LibeSpecs(nworkers=nworkers, final_gen_send=True) n = 2 # Input dimensions batch_size = 4 num_batches = 6 gen_specs = GenSpecs( - gen_f=persistent_gpCAM, # Generator function - persis_in=["f"], # Objective, defined in sim, is returned to gen + gen_f=persistent_gpCAM, # Generator function + persis_in=["f"], # Objective, defined in sim, is returned to gen outputs=[("x", float, (n,))], # Parameters (name, type, size) user={ "batch_size": batch_size, "lb": np.array([-2, -1]), # lower boundaries for n dimensions - "ub": np.array([2, 1]), # upper boundaries for n dimensions - "ask_max_iter": 5, # Number of iterations for ask (default 20) + "ub": np.array([2, 1]), # upper boundaries for n dimensions + "ask_max_iter": 5, # Number of iterations for ask (default 20) "rng_seed": 0, }, ) sim_specs = SimSpecs( - sim_f=six_hump_camel, # Simulator function - inputs=["x"], # Input field names. "x" defined in gen - outputs=[("f", float)], # Objective + sim_f=six_hump_camel, # Simulator function + inputs=["x"], # Input field names. "x" defined in gen + outputs=[("f", float)], # Objective ) # Starts one persistent generator. Simulated values are returned in batch. @@ -251,7 +256,7 @@ If you wish to make your own functions based on the above, those can be imported user={"async_return": False}, # False = batch returns ) - exit_criteria = ExitCriteria(sim_max=num_batches*batch_size) + exit_criteria = ExitCriteria(sim_max=num_batches * batch_size) # Initialize and run the ensemble. ensemble = Ensemble( @@ -272,7 +277,7 @@ At the end of our calling script we run the ensemble. H, persis_info, flag = ensemble.run() # Start the ensemble. Blocks until completion. ensemble.save_output("H_array", append_attrs=False) # Save H (history of all evaluated points) to file - pprint(H[["sim_id", "x", "f"]][:16]) # See first 16 results + pprint(H[["sim_id", "x", "f"]][:16]) # See first 16 results Rerun and test model at known points ------------------------------------ @@ -312,15 +317,21 @@ values at the test points. markersize = 10 plt.figure(figsize=(10, 5)) plt.plot( - num_sims, mse, marker="^", markeredgecolor="black", markeredgewidth=2, - markersize=markersize, linewidth=2, label="Mean squared error" + num_sims, + mse, + marker="^", + markeredgecolor="black", + markeredgewidth=2, + markersize=markersize, + linewidth=2, + label="Mean squared error", ) plt.xticks(num_sims) # Labeling the axes and the legend - plt.title('Mean Squared Error at test points') + plt.title("Mean Squared Error at test points") plt.xlabel("Number of simulations") - plt.ylabel('Mean squared error (rad$^2$)') + plt.ylabel("Mean squared error (rad$^2$)") legend = plt.legend(framealpha=1, edgecolor="black") # Increase edge width here plt.grid(True) plt.show() diff --git a/docs/tutorials/local_sine_tutorial.rst b/docs/tutorials/local_sine_tutorial.rst deleted file mode 100644 index 0509824750..0000000000 --- a/docs/tutorials/local_sine_tutorial.rst +++ /dev/null @@ -1,283 +0,0 @@ -=================== -Simple Introduction -=================== - -This tutorial demonstrates the capability to perform ensembles of -calculations in parallel using :doc:`libEnsemble<../introduction>`. - -We recommend reading this brief :doc:`Overview<../overview_usecases>`. - -|Open in Colab| - -For this tutorial, our generator will produce uniform randomly sampled -values, and our simulator will calculate the sine of each. By default we don't -need to write a new allocation function. - -.. tab-set:: - - .. tab-item:: 1. Getting started - - libEnsemble is written entirely in Python_. Let's make sure - the correct version is installed. - - .. code-block:: bash - - python --version # This should be >= 3.10 - - .. _Python: https://www.python.org/ - - For this tutorial, you need NumPy_ and (optionally) - Matplotlib_ to visualize your results. Install libEnsemble and these other - libraries with - - .. code-block:: bash - - pip install libensemble - pip install matplotlib # Optional - - If your system doesn't allow you to perform these installations, try adding - ``--user`` to the end of each command. - - .. tab-item:: 2. Generator - - Let's begin the coding portion of this tutorial by writing our generator function, - or :ref:`gen_f`. - - An available libEnsemble worker will call this generator function with the - following parameters: - - * :ref:`InputArray`: A selection of the :ref:`History array` (*H*), - passed to the generator function in case the user wants to generate - new values based on simulation outputs. Since our generator produces random - numbers, it'll be ignored this time. - - * :ref:`persis_info`: Dictionary with worker-specific - information. In our case, this dictionary contains NumPy Random Stream objects - for generating random numbers. - - * :ref:`gen_specs`: Dictionary with user-defined static fields and - parameters. Customizable parameters such as lower and upper bounds and batch - sizes are placed within the ``gen_specs["user"]`` dictionary. - - Later on, we'll populate :class:`gen_specs` and ``persis_info`` when we initialize libEnsemble. - - For now, create a new Python file named ``sine_gen.py``. Write the following: - - .. literalinclude:: ../../libensemble/tests/functionality_tests/sine_gen.py - :language: python - :linenos: - :caption: examples/tutorials/simple_sine/sine_gen.py - - Our function creates ``batch_size`` random numbers uniformly distributed - between the ``lower`` and ``upper`` bounds. A random stream - from ``persis_info`` is used to generate these values, which are then placed - into an output NumPy array that matches the dtype from ``gen_specs["out"]``. - - .. tab-item:: 3. Simulator - - Next, we'll write our simulator function or :ref:`sim_f`. Simulator - functions perform calculations based on values from the generator function. - The only new parameter here is :ref:`sim_specs`, which - serves a purpose similar to the :class:`gen_specs` dictionary. - - Create a new Python file named ``sine_sim.py``. Write the following: - - .. literalinclude:: ../../libensemble/tests/functionality_tests/sine_sim.py - :language: python - :linenos: - :caption: examples/tutorials/simple_sine/sine_sim.py - - Our simulator function is called by a worker for every work item produced by - the generator function. This function calculates the sine of the passed value, - and then returns it so the worker can store the result. - - .. tab-item:: 4. Script - - Now lets write the script that configures our generator and simulator - functions and starts libEnsemble. - - Create an empty Python file named ``calling.py``. - In this file, we'll start by importing NumPy, libEnsemble's setup classes, - and the generator and simulator functions we just created. - - In a class called :ref:`LibeSpecs` we'll - specify the number of workers and the manager/worker intercommunication method. - ``"local"``, refers to Python's multiprocessing. - - .. literalinclude:: ../../libensemble/tests/functionality_tests/test_local_sine_tutorial.py - :language: python - :linenos: - :end-at: libE_specs = LibeSpecs - - We configure the settings and specifications for our ``sim_f`` and ``gen_f`` - functions in the :ref:`GenSpecs` and - :ref:`SimSpecs` classes, which we saw previously - being passed to our functions *as dictionaries*. - These classes also describe to libEnsemble what inputs and outputs from those - functions to expect. - - .. literalinclude:: ../../libensemble/tests/functionality_tests/test_local_sine_tutorial.py - :language: python - :linenos: - :lineno-start: 10 - :start-at: gen_specs = GenSpecs - :end-at: sim_specs_end_tag - - We then specify the circumstances where - libEnsemble should stop execution in :ref:`ExitCriteria`. - - .. literalinclude:: ../../libensemble/tests/functionality_tests/test_local_sine_tutorial.py - :language: python - :linenos: - :lineno-start: 26 - :start-at: exit_criteria = ExitCriteria - :end-at: exit_criteria = ExitCriteria - - Now we're ready to write our libEnsemble :doc:`libE<../programming_libE>` - function call. :ref:`ensemble.H` is the final version of - the history array. ``ensemble.flag`` should be zero if no errors occur. - - .. literalinclude:: ../../libensemble/tests/functionality_tests/test_local_sine_tutorial.py - :language: python - :linenos: - :lineno-start: 28 - :start-at: ensemble = Ensemble - :end-at: print(history) - - That's it! Now that these files are complete, we can run our simulation. - - .. code-block:: bash - - python calling.py - - If everything ran perfectly and you included the above print statements, you - should get something similar to the following output (although the - columns might be rearranged). - - .. code-block:: - - ["y", "sim_started_time", "gen_worker", "sim_worker", "sim_started", "sim_ended", "x", "allocated", "sim_id", "gen_ended_time"] - [(-0.37466051, 1.559+09, 2, 2, True, True, [-0.38403059], True, 0, 1.559+09) - (-0.29279634, 1.559+09, 2, 3, True, True, [-2.84444261], True, 1, 1.559+09) - ( 0.29358492, 1.559+09, 2, 4, True, True, [ 0.29797487], True, 2, 1.559+09) - (-0.3783986, 1.559+09, 2, 1, True, True, [-0.38806564], True, 3, 1.559+09) - (-0.45982062, 1.559+09, 2, 2, True, True, [-0.47779319], True, 4, 1.559+09) - ... - - In this arrangement, our output values are listed on the far left with the - generated values being the fourth column from the right. - - Two additional log files should also have been created. - ``ensemble.log`` contains debugging or informational logging output from - libEnsemble, while ``libE_stats.txt`` contains a quick summary of all - calculations performed. - - Here is graphed output using ``Matplotlib``, with entries colored by which - worker performed the simulation: - - .. image:: ../images/sinex.png - :alt: sine - :align: center - - If you want to verify your results through plotting and installed Matplotlib - earlier, copy and paste the following code into the bottom of your calling - script and run ``python calling.py`` again - - .. literalinclude:: ../../libensemble/tests/functionality_tests/test_local_sine_tutorial.py - :language: python - :linenos: - :lineno-start: 37 - :start-at: import matplotlib - :end-at: plt.savefig("tutorial_sines.png") - - Each of these example files can be found in the repository in `examples/tutorials/simple_sine`_. - - **Exercise** - - Write a Calling Script with the following specifications: - - 1. Set the generator function's lower and upper bounds to -6 and 6, respectively - 2. Increase the generator batch size to 10 - 3. Set libEnsemble to stop execution after 160 *generations* using the ``gen_max`` option - 4. Print an error message if any errors occurred while libEnsemble was running - - .. dropdown:: **Click Here for Solution** - - .. literalinclude:: ../../libensemble/tests/functionality_tests/test_local_sine_tutorial_2.py - :language: python - :linenos: - :emphasize-lines: 15,16,17,27,33,34 - - .. tab-item:: 5. Next steps - - **libEnsemble with MPI** - - MPI_ is a standard interface for parallel computing, implemented in libraries - such as MPICH_ and used at extreme scales. MPI potentially allows libEnsemble's - processes to be distributed over multiple nodes and works in some - circumstances where Python's multiprocessing does not. In this section, we'll - explore modifying the above code to use MPI instead of multiprocessing. - - We recommend the MPI distribution MPICH_ for this tutorial, which can be found - for a variety of systems here_. You also need mpi4py_, which can be installed - with ``pip install mpi4py``. If you'd like to use a specific version or - distribution of MPI instead of MPICH, configure mpi4py with that MPI at - installation with ``MPICC= pip install mpi4py`` If this - doesn't work, try appending ``--user`` to the end of the command. See the - mpi4py_ docs for more information. - - Verify that MPI has been installed correctly with ``mpirun --version``. - - **Modifying the script** - - Only a few changes are necessary to make our code MPI-compatible. For starters, - comment out the ``libE_specs`` definition: - - .. literalinclude:: ../../libensemble/tests/functionality_tests/test_local_sine_tutorial_3.py - :language: python - :start-at: # libE_specs = LibeSpecs - :end-at: # libE_specs = LibeSpecs - - We'll be parameterizing our MPI runtime with a ``parse_args=True`` argument to - the ``Ensemble`` class instead of ``libE_specs``. We'll also use an ``ensemble.is_manager`` - attribute so only the first MPI rank runs the data-processing code. - - The bottom of your calling script should now resemble: - - .. literalinclude:: ../../libensemble/tests/functionality_tests/test_local_sine_tutorial_3.py - :linenos: - :lineno-start: 28 - :language: python - :start-at: # replace libE_specs - - With these changes in place, our libEnsemble code can be run with MPI by - - .. code-block:: bash - - mpirun -n 5 python calling.py - - where ``-n 5`` tells ``mpirun`` to produce five processes, one of which will be - the manager process with the libEnsemble manager and the other four will run - libEnsemble workers. - - This tutorial is only a tiny demonstration of the parallelism capabilities of - libEnsemble. libEnsemble has been developed primarily to support research on - High-Performance computers, with potentially hundreds of workers performing - calculations simultaneously. Please read our - :doc:`platform guides <../platforms/platforms_index>` for introductions to using - libEnsemble on many such machines. - - libEnsemble's Executors can launch non-Python user applications and simulations across - allocated compute resources. Try out this feature with a more-complicated - libEnsemble use-case within our - :doc:`Electrostatic Forces tutorial <./executor_forces_tutorial>`. - -.. _Matplotlib: https://matplotlib.org/ -.. _MPI: https://en.wikipedia.org/wiki/Message_Passing_Interface -.. _MPICH: https://www.mpich.org/ -.. _mpi4py: https://mpi4py.readthedocs.io/en/stable/install.html -.. _NumPy: https://www.numpy.org/ -.. _here: https://www.mpich.org/downloads/ -.. _examples/tutorials/simple_sine: https://github.com/Libensemble/libensemble/tree/develop/examples/tutorials/simple_sine -.. |Open in Colab| image:: https://colab.research.google.com/assets/colab-badge.svg - :target: http://colab.research.google.com/github/Libensemble/libensemble/blob/develop/examples/tutorials/simple_sine/sine_tutorial_notebook.ipynb diff --git a/docs/tutorials/local_sine_tutorial/local_sine_tutorial.rst b/docs/tutorials/local_sine_tutorial/local_sine_tutorial.rst new file mode 100644 index 0000000000..8413c3de7e --- /dev/null +++ b/docs/tutorials/local_sine_tutorial/local_sine_tutorial.rst @@ -0,0 +1,31 @@ +=================== +Simple Introduction +=================== + +**Introduction** \|\| `1. Getting started `__ \|\| `2. Generator `__ \|\| `3. Simulator `__ \|\| `4. Script `__ \|\| `5. Next steps `__ + +This tutorial demonstrates the capability to perform ensembles of +calculations in parallel using :doc:`libEnsemble<../../introduction>`. + +We recommend reading this brief :doc:`Overview<../../overview_usecases>`. + +.. only:: html + + |Open in Colab| + + +For this tutorial, our generator will produce uniform randomly sampled +values, and our simulator will calculate the sine of each. By default we don't +need to write a new allocation function. + +.. toctree:: + :hidden: + + local_sine_tutorial_1 + local_sine_tutorial_2 + local_sine_tutorial_3 + local_sine_tutorial_4 + local_sine_tutorial_5 + +.. |Open in Colab| image:: https://colab.research.google.com/assets/colab-badge.svg + :target: http://colab.research.google.com/github/Libensemble/libensemble/blob/develop/examples/tutorials/simple_sine/sine_tutorial_notebook.ipynb diff --git a/docs/tutorials/local_sine_tutorial/local_sine_tutorial_1.rst b/docs/tutorials/local_sine_tutorial/local_sine_tutorial_1.rst new file mode 100644 index 0000000000..5c5db2ec82 --- /dev/null +++ b/docs/tutorials/local_sine_tutorial/local_sine_tutorial_1.rst @@ -0,0 +1,28 @@ +1. Getting started +================== + +`Introduction `__ \|\| **1. Getting started** \|\| `2. Generator `__ \|\| `3. Simulator `__ \|\| `4. Script `__ \|\| `5. Next steps `__ + +libEnsemble is written entirely in Python_. Let's make sure +the correct version is installed. + +.. code-block:: bash + + python --version # This should be >= 3.11 + +.. _Python: https://www.python.org/ + +For this tutorial, you need NumPy_ and (optionally) +Matplotlib_ to visualize your results. Install libEnsemble and these other +libraries with + +.. code-block:: bash + + pip install libensemble + pip install matplotlib # Optional + +If your system doesn't allow you to perform these installations, try adding +``--user`` to the end of each command. + +.. _Matplotlib: https://matplotlib.org/ +.. _NumPy: https://www.numpy.org/ diff --git a/docs/tutorials/local_sine_tutorial/local_sine_tutorial_2.rst b/docs/tutorials/local_sine_tutorial/local_sine_tutorial_2.rst new file mode 100644 index 0000000000..024bb52d14 --- /dev/null +++ b/docs/tutorials/local_sine_tutorial/local_sine_tutorial_2.rst @@ -0,0 +1,30 @@ +2. Generator +============ + +`Introduction `__ \|\| `1. Getting started `__ \|\| **2. Generator** \|\| `3. Simulator `__ \|\| `4. Script `__ \|\| `5. Next steps `__ + +Let's begin the coding portion of this tutorial by writing our generator. + +An available libEnsemble worker will call this generator's ``.suggest()`` method to obtain +new values to evaluate. + +For now, create a new Python file named ``sine_gen.py``. Write the following: + +.. literalinclude:: ../../../libensemble/tests/functionality_tests/sine_gen_std.py + :language: python + :linenos: + :caption: examples/tutorials/simple_sine/sine_gen_std.py + +libEnsemble accepts generators that implement the gest-api_ interface. These generators +accept a ``gest_api.VOCS`` object for configuration, and contain a ``.suggest(num_points)`` +method that returns ``num_points`` points. Points consist of a list of dictionaries +with keys that match the variable names from the ``gest_api.VOCS`` object. + +Our generator's ``suggest()`` method creates ``num_points`` dictionaries. For each key in +the generator's ``self.variables``, it creates a random number uniformly distributed +between the corresponding ``lower`` and ``upper`` bounds of its domain. + +Our generator must implement a ``_validate_vocs()`` method. Here, we implement a simple +check that ensures the ``VOCS`` object has at least one variable. + +.. _gest-api: https://github.com/campa-consortium/gest-api diff --git a/docs/tutorials/local_sine_tutorial/local_sine_tutorial_3.rst b/docs/tutorials/local_sine_tutorial/local_sine_tutorial_3.rst new file mode 100644 index 0000000000..05836abf32 --- /dev/null +++ b/docs/tutorials/local_sine_tutorial/local_sine_tutorial_3.rst @@ -0,0 +1,20 @@ +3. Simulator +============ + +`Introduction `__ \|\| `1. Getting started `__ \|\| `2. Generator `__ \|\| **3. Simulator** \|\| `4. Script `__ \|\| `5. Next steps `__ + +Next, we'll write our simulator function or :ref:`sim_f`. Simulator +functions perform calculations based on values from the generator. +:ref:`sim_specs` is a dictionary containing user-defined fields +and parameters. + +Create a new Python file named ``sine_sim.py``. Write the following: + +.. literalinclude:: ../../../libensemble/tests/functionality_tests/sine_sim.py + :language: python + :linenos: + :caption: examples/tutorials/simple_sine/sine_sim.py + +Our simulator function is called by a worker for every work item produced by +the generator. This function calculates the sine of the passed value, +and then returns it so the worker can store the result. diff --git a/docs/tutorials/local_sine_tutorial/local_sine_tutorial_4.rst b/docs/tutorials/local_sine_tutorial/local_sine_tutorial_4.rst new file mode 100644 index 0000000000..92a5c8536b --- /dev/null +++ b/docs/tutorials/local_sine_tutorial/local_sine_tutorial_4.rst @@ -0,0 +1,121 @@ +4. Script +========= + +`Introduction `__ \|\| `1. Getting started `__ \|\| `2. Generator `__ \|\| `3. Simulator `__ \|\| **4. Script** \|\| `5. Next steps `__ + +Now lets write the script that configures our generator and simulator +functions and starts libEnsemble. + +Create an empty Python file named ``calling.py``. +In this file, we'll start by importing NumPy, libEnsemble's setup classes, the generator, +and simulator function. + +In a class called :ref:`LibeSpecs` we'll +specify the number of workers and the manager/worker intercommunication method. +``"local"``, refers to Python's multiprocessing. + +.. literalinclude:: ../../../libensemble/tests/functionality_tests/test_local_sine_tutorial.py + :language: python + :linenos: + :end-at: libE_specs = LibeSpecs + +We configure the settings and specifications for our ``sim_f`` and ``gen_f`` +functions in the :ref:`GenSpecs` and +:ref:`SimSpecs` classes, which we saw previously +being passed to our functions *as dictionaries*. +These classes also describe to libEnsemble what inputs and outputs from those +functions to expect. + +.. literalinclude:: ../../../libensemble/tests/functionality_tests/test_local_sine_tutorial.py + :language: python + :linenos: + :lineno-start: 10 + :start-at: gen_specs = GenSpecs + :end-at: sim_specs_end_tag + +We then specify the circumstances where +libEnsemble should stop execution in :ref:`ExitCriteria`. + +.. literalinclude:: ../../../libensemble/tests/functionality_tests/test_local_sine_tutorial.py + :language: python + :linenos: + :lineno-start: 26 + :start-at: exit_criteria = ExitCriteria + :end-at: exit_criteria = ExitCriteria + +Now we're ready to write our libEnsemble :doc:`libE<../../programming_libE>` +function call. :ref:`ensemble.H` is the final version of +the history array. ``ensemble.flag`` should be zero if no errors occur. + +.. literalinclude:: ../../../libensemble/tests/functionality_tests/test_local_sine_tutorial.py + :language: python + :linenos: + :lineno-start: 28 + :start-at: ensemble = Ensemble + :end-at: print(history) + +That's it! Now that these files are complete, we can run our simulation. + +.. code-block:: bash + + python calling.py + +If everything ran perfectly and you included the above print statements, you +should get something similar to the following output (although the +columns might be rearranged). + +.. code-block:: + + ["y", "sim_started_time", "gen_worker", "sim_worker", "sim_started", "sim_ended", "x", "allocated", "sim_id", "gen_ended_time"] + [(-0.37466051, 1.559+09, 2, 2, True, True, [-0.38403059], True, 0, 1.559+09) + (-0.29279634, 1.559+09, 2, 3, True, True, [-2.84444261], True, 1, 1.559+09) + ( 0.29358492, 1.559+09, 2, 4, True, True, [ 0.29797487], True, 2, 1.559+09) + (-0.3783986, 1.559+09, 2, 1, True, True, [-0.38806564], True, 3, 1.559+09) + (-0.45982062, 1.559+09, 2, 2, True, True, [-0.47779319], True, 4, 1.559+09) + ... + +In this arrangement, our output values are listed on the far left with the +generated values being the fourth column from the right. + +Two additional log files should also have been created. +``ensemble.log`` contains debugging or informational logging output from +libEnsemble, while ``libE_stats.txt`` contains a quick summary of all +calculations performed. + +Here is graphed output using ``Matplotlib``, with entries colored by which +worker performed the simulation: + +.. image:: ../../images/sinex.png + :alt: sine + :align: center + +If you want to verify your results through plotting and installed Matplotlib +earlier, copy and paste the following code into the bottom of your calling +script and run ``python calling.py`` again + +.. literalinclude:: ../../../libensemble/tests/functionality_tests/test_local_sine_tutorial.py + :language: python + :linenos: + :lineno-start: 37 + :start-at: import matplotlib + :end-at: plt.savefig("tutorial_sines.png") + +Each of these example files can be found in the repository in `examples/tutorials/simple_sine`_. + +**Exercise** + +Write a Calling Script with the following specifications: + +1. Set the generator function's lower and upper bounds to -6 and 6, respectively +2. Increase the generator batch size to 10 +3. Set libEnsemble to stop execution after 160 *generations* using the ``gen_max`` option +4. Print an error message if any errors occurred while libEnsemble was running + +.. dropdown:: **Click Here for Solution** + + .. literalinclude:: ../../../libensemble/tests/functionality_tests/test_local_sine_tutorial_2.py + :language: python + :linenos: + :emphasize-lines: 15,16,17,27,33,34 + +.. _examples/tutorials/simple_sine: https://github.com/Libensemble/libensemble/tree/develop/examples/tutorials/simple_sine diff --git a/docs/tutorials/local_sine_tutorial/local_sine_tutorial_5.rst b/docs/tutorials/local_sine_tutorial/local_sine_tutorial_5.rst new file mode 100644 index 0000000000..e5045cf0ec --- /dev/null +++ b/docs/tutorials/local_sine_tutorial/local_sine_tutorial_5.rst @@ -0,0 +1,70 @@ +5. Next steps +============= + +`Introduction `__ \|\| `1. Getting started `__ \|\| `2. Generator `__ \|\| `3. Simulator `__ \|\| `4. Script `__ \|\| **5. Next steps** + +**libEnsemble with MPI** + +MPI_ is a standard interface for parallel computing, implemented in libraries +such as MPICH_ and used at extreme scales. MPI potentially allows libEnsemble's +processes to be distributed over multiple nodes and works in some +circumstances where Python's multiprocessing does not. In this section, we'll +explore modifying the above code to use MPI instead of multiprocessing. + +We recommend the MPI distribution MPICH_ for this tutorial, which can be found +for a variety of systems here_. You also need mpi4py_, which can be installed +with ``pip install mpi4py``. If you'd like to use a specific version or +distribution of MPI instead of MPICH, configure mpi4py with that MPI at +installation with ``MPICC= pip install mpi4py`` If this +doesn't work, try appending ``--user`` to the end of the command. See the +mpi4py_ docs for more information. + +Verify that MPI has been installed correctly with ``mpirun --version``. + +**Modifying the script** + +Only a few changes are necessary to make our code MPI-compatible. For starters, +comment out the ``libE_specs`` definition: + +.. literalinclude:: ../../../libensemble/tests/functionality_tests/test_local_sine_tutorial_3.py + :language: python + :start-at: # libE_specs = LibeSpecs + :end-at: # libE_specs = LibeSpecs + +We'll be parameterizing our MPI runtime with a ``parse_args=True`` argument to +the ``Ensemble`` class instead of ``libE_specs``. We'll also use an ``ensemble.is_manager`` +attribute so only the first MPI rank runs the data-processing code. + +The bottom of your calling script should now resemble: + +.. literalinclude:: ../../../libensemble/tests/functionality_tests/test_local_sine_tutorial_3.py + :linenos: + :lineno-start: 28 + :language: python + :start-at: # replace libE_specs + +With these changes in place, our libEnsemble code can be run with MPI by + +.. code-block:: bash + + mpirun -n 5 python calling.py + +where ``-n 5`` tells ``mpirun`` to produce five processes, one of which will be +the libEnsemble manager process and the others will run libEnsemble workers. + +This tutorial is only a tiny demonstration of the parallelism capabilities of +libEnsemble. libEnsemble has been developed primarily to support research on +High-Performance computers, with potentially hundreds of workers performing +calculations simultaneously. Please read our +:doc:`platform guides <../../platforms/platforms_index>` for introductions to using +libEnsemble on many such machines. + +libEnsemble's Executors can launch non-Python user applications and simulations across +allocated compute resources. Try out this feature with a more-complicated +libEnsemble use-case within our +:doc:`Electrostatic Forces tutorial <../executor_forces_tutorial>`. + +.. _MPI: https://en.wikipedia.org/wiki/Message_Passing_Interface +.. _MPICH: https://www.mpich.org/ +.. _here: https://www.mpich.org/downloads/ +.. _mpi4py: https://mpi4py.readthedocs.io/en/stable/install.html diff --git a/docs/tutorials/tutorials.rst b/docs/tutorials/tutorials.rst index 6b40e4b658..1ea0edc10e 100644 --- a/docs/tutorials/tutorials.rst +++ b/docs/tutorials/tutorials.rst @@ -3,9 +3,10 @@ Tutorials .. toctree:: - local_sine_tutorial + local_sine_tutorial/local_sine_tutorial executor_forces_tutorial forces_gpu_tutorial gpcam_tutorial aposmm_tutorial calib_cancel_tutorial + xopt_bayesian_gen diff --git a/docs/tutorials/xopt_bayesian_gen.rst b/docs/tutorials/xopt_bayesian_gen.rst new file mode 100644 index 0000000000..9227ac8ce1 --- /dev/null +++ b/docs/tutorials/xopt_bayesian_gen.rst @@ -0,0 +1,173 @@ +Bayesian Optimization with Xopt +=============================== + +**Requires**: libensemble, xopt, gest-api + +This tutorial demonstrates using Xopt's Bayesian **ExpectedImprovementGenerator** with libEnsemble. + +We'll show two approaches: + +1. Using an xopt-style simulator (callable function) +2. Using a libEnsemble-style simulator function + +.. only:: html + + |Open in Colab| + +Imports +------- + +.. code-block:: python + + import numpy as np + from gest_api.vocs import VOCS + from xopt.generators.bayesian.expected_improvement import ExpectedImprovementGenerator + + from libensemble import Ensemble + from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f + from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, LibeSpecs, SimSpecs + +Simulator Function +------------------ + +First, we define the xopt-style simulator function. + +This is a basic function just to show how it works. + +.. code-block:: python + + def test_callable(input_dict: dict) -> dict: + """Single-objective callable test function""" + assert isinstance(input_dict, dict) + x1 = input_dict["x1"] + x2 = input_dict["x2"] + y1 = x2 + c1 = x1 + return {"y1": y1, "c1": c1} + +Setup +----- + +Define the VOCS specification and set up the generator. + +.. code-block:: python + + libE_specs = LibeSpecs(gen_on_manager=True, nworkers=4) + + vocs = VOCS( + variables={"x1": [0, 1.0], "x2": [0, 10.0]}, + objectives={"y1": "MINIMIZE"}, + constraints={"c1": ["GREATER_THAN", 0.5]}, + constants={"constant1": 1.0}, + ) + + gen = ExpectedImprovementGenerator(vocs=vocs) + + # Create 4 initial points and ingest them + initial_points = [ + {"x1": 0.2, "x2": 2.0, "y1": 2.0, "c1": 0.2}, + {"x1": 0.5, "x2": 5.0, "y1": 5.0, "c1": 0.5}, + {"x1": 0.7, "x2": 7.0, "y1": 7.0, "c1": 0.7}, + {"x1": 0.9, "x2": 9.0, "y1": 9.0, "c1": 0.9}, + ] + gen.ingest(initial_points) + +Define libEnsemble specifications. Note the gen_specs and sim_specs are set using vocs. + +Approach 1: Using Xopt-style Simulator (Callable Function) +----------------------------------------------------------- + +The simulator is a simple callable function that takes a dictionary of inputs and returns a dictionary of outputs. + +.. code-block:: python + + gen_specs = GenSpecs( + generator=gen, + vocs=vocs, + ) + + # Note: using 'simulator' parameter for xopt-style callable + sim_specs = SimSpecs( + simulator=test_callable, + vocs=vocs, + ) + + alloc_specs = AllocSpecs(alloc_f=alloc_f) + exit_criteria = ExitCriteria(sim_max=12) + + workflow = Ensemble( + libE_specs=libE_specs, + sim_specs=sim_specs, + alloc_specs=alloc_specs, + gen_specs=gen_specs, + exit_criteria=exit_criteria, + ) + + H, _, _ = workflow.run() + + if workflow.is_manager: + print(f"Completed {len(H)} simulations") + print(H[["x1", "x2", "y1", "c1"]]) + assert np.array_equal(H["y1"], H["x2"]) + assert np.array_equal(H["c1"], H["x1"]) + +Approach 2: Using libEnsemble-style Simulator Function +------------------------------------------------------- + +Now we define the libEnsemble-style simulator function and use it in the workflow. + +.. code-block:: python + + def test_sim(H, persis_info, sim_specs, _): + """ + Simple sim function that takes x1, x2, constant1 from H and returns y1, c1. + Logic: y1 = x2, c1 = x1 + """ + batch = len(H) + H_o = np.zeros(batch, dtype=sim_specs["out"]) + + for i in range(batch): + x1 = H["x1"][i] + x2 = H["x2"][i] + H_o["y1"][i] = x2 + H_o["c1"][i] = x1 + + return H_o, persis_info + +Reset generator and change to libEnsemble-style simulator: + +.. code-block:: python + + # Reset generator and change to libEnsemble-style simulator + gen = ExpectedImprovementGenerator(vocs=vocs) + gen.ingest(initial_points) + + gen_specs = GenSpecs( + generator=gen, + vocs=vocs, + ) + + # Note: using 'sim_f' parameter for libEnsemble-style function + sim_specs = SimSpecs( + sim_f=test_sim, + vocs=vocs, + ) + + workflow = Ensemble( + libE_specs=libE_specs, + sim_specs=sim_specs, + alloc_specs=alloc_specs, + gen_specs=gen_specs, + exit_criteria=exit_criteria, + ) + + H, _, _ = workflow.run() + + if workflow.is_manager: + print(f"Completed {len(H)} simulations") + print(H[["x1", "x2", "y1", "c1"]]) + assert np.array_equal(H["y1"], H["x2"]) + assert np.array_equal(H["c1"], H["x1"]) + +.. |Open in Colab| image:: https://colab.research.google.com/assets/colab-badge.svg + :target: https://colab.research.google.com/github/Libensemble/libensemble/blob/develop/examples/tutorials/xopt_bayesian_gen/xopt_EI_example.ipynb diff --git a/docs/utilities.rst b/docs/utilities.rst index 3c75dc9703..dbdc2dcb22 100644 --- a/docs/utilities.rst +++ b/docs/utilities.rst @@ -1,47 +1,49 @@ Convenience Tools and Functions =============================== -.. tab-set:: +Setup Helpers +------------- - .. tab-item:: Setup Helpers +.. automodule:: tools + :members: + :no-undoc-members: - .. automodule:: tools - :members: - :no-undoc-members: +Persistent Helpers +------------------ - .. tab-item:: Persistent Helpers +.. _p_gen_routines: - .. _p_gen_routines: +These routines are commonly used within persistent generator functions +such as ``persistent_aposmm`` in ``libensemble/gen_funcs/`` for intermediate +communication with the manager. Persistent simulator functions are also supported. - These routines are commonly used within persistent generator functions - such as ``persistent_aposmm`` in ``libensemble/gen_funcs/`` for intermediate - communication with the manager. Persistent simulator functions are also supported. +.. automodule:: persistent_support + :members: + :no-undoc-members: - .. automodule:: persistent_support - :members: - :no-undoc-members: +Allocation Helpers +------------------ - .. tab-item:: Allocation Helpers +These routines are used within custom allocation functions to help prepare ``Work`` +structures for workers. See the routines within ``libensemble/alloc_funcs/`` for +examples. - These routines are used within custom allocation functions to help prepare ``Work`` - structures for workers. See the routines within ``libensemble/alloc_funcs/`` for - examples. +.. automodule:: alloc_support + :members: + :no-undoc-members: - .. automodule:: alloc_support - :members: - :no-undoc-members: +Live Data +--------- - .. tab-item:: Live Data +These classes provide a means to capture and display data during a workflow run. +Users may provide an initialized object via ``libE_specs["live_data"]``. For example:: - These classes provide a means to capture and display data during a workflow run. - Users may provide an initialized object via ``libE_specs["live_data"]``. For example:: + from libensemble.tools.live_data.plot2n import Plot2N + libE_specs["live_data"] = Plot2N(plot_type='2d') - from libensemble.tools.live_data.plot2n import Plot2N - libE_specs["live_data"] = Plot2N(plot_type='2d') +.. automodule:: libensemble.tools.live_data.live_data + :members: - .. automodule:: libensemble.tools.live_data.live_data - :members: - - .. automodule:: plot2n - :members: Plot2N - :show-inheritance: +.. automodule:: plot2n + :members: Plot2N + :show-inheritance: diff --git a/docs/welcome.rst b/docs/welcome.rst index 9498fab1ac..01fdef4425 100644 --- a/docs/welcome.rst +++ b/docs/welcome.rst @@ -40,7 +40,7 @@ libEnsemble A complete toolkit for dynamic ensembles of calculations - New to libEnsemble? :doc:`Start here`. - - Try out libEnsemble with a :doc:`tutorial`. + - Try out libEnsemble with a :doc:`tutorial`. - Go in depth by reading the :doc:`full overview`. - See the :doc:`FAQ` for common questions and answers, errors, and resolutions. - Check us out on `GitHub`_. diff --git a/docs/xSDK_policy_compatibility.md b/docs/xSDK_policy_compatibility.md deleted file mode 100644 index 0dc83520d0..0000000000 --- a/docs/xSDK_policy_compatibility.md +++ /dev/null @@ -1,82 +0,0 @@ -# xSDK Community Policy Compatibility for libEnsemble - -This document summarizes the efforts of libEnsemble -to achieve compatibility with the xSDK community policies. - -**Website:** https://github.com/Libensemble/libensemble - -### Mandatory Policies - -[General libEnsemble Note](#liben-note) - -| Policy |Support| Notes | -|------------------------|-------|-------------------------| -|**M1.** Support xSDK community GNU Autoconf or CMake options. |N/A| libEnsemble is a Python package and provides a `setup.py` file for installation. This is compatible with Python's built-in installation feature (`python setup.py install`) and with the ubiquitous `pip` installer. libEnsemble is also in the Spack repository and can be installed with `spack install py-libensemble`. GNU Autoconf or CMake are unsuitable for a Python package. -|**M2.** Provide a comprehensive test suite for correctness of installation verification. |Full| libEnsemble has a test suite that includes both unit tests and regression tests that are run on every push to GitHub via [Travis CI](https://travis-ci.org/Libensemble/libensemble). In addition to this test suite, further scaling tests are manually run on HPC platforms including Cori, Theta, and Summit. -|**M3.** Employ user-provided MPI communicator (no MPI_COMM_WORLD). |Full|libEnsemble takes an MPI communicator as an option; if libEnsemble is configured for MPI mode, this provided communicator will be employed. If no communicator is given, a duplicate of MPI_COMM_WORLD is taken as a default. | -|**M4.** Give best effort at portability to key architectures (standard Linux distributions, GNU, Clang, vendor compilers, and target machines at ALCF, NERSC, OLCF). |Full| libEnsemble is tested regularly, including prior to every release, on ALCF (Theta), OLCF (Summit) and NERSC (Cori) platforms. [M4 details](#m4-details)| -|**M5.** Provide a documented, reliable way to contact the development team. |Full| The libEnsemble team can be contacted through: 1) The public [issues page on GitHub](https://github.com/Libensemble/libensemble/issues). 2) [Slack](https://libensemble.slack.com). 3) The public email list libensemble@mcs.anl.gov. | -|**M6.** Respect system resources and settings made by other previously called packages (e.g., signal handling). |Full| libEnsemble does not modify system resources or settings. | -|**M7.** Come with an open source (BSD style) license. |Full| libEnsemble uses a 3-clause BSD license stated in the `LICENSE` file in the top level of the GitHub repository. | -|**M8.** Provide a runtime API to return the current version number of the software. |Full| The version can be returned within Python via: `libensemble.__version__`| -|**M9.** Use a limited and well-defined symbol, macro, library, and include file name space. |Full| All libEnsemble symbols (e.g., functions, variables, modules, packages) begin with the prefix `libensemble.`. This prevents any namespace conflicts.| -|**M10.** Provide an xSDK team accessible repository (not necessarily publicly available). |Full| The libEnsemble repository is public and can be found at https://github.com/Libensemble/libensemble. Gitflow is used, along with pull requests, whereby only those with administrator privileges can accept pull requests into the master or develop branches. The workflow guidelines are provided in a `CONTRIBUTING.rst` file at the top level of the repository and a release process is given in the documentation. | -|**M11.** Have no hardwired print or IO statements that cannot be turned off. |Full| All output from the libEnsemble core package, except for the raising of exceptions, is routed through a libEnsemble logger, which is isolated from the Python root logger. Log messages of type `MANAGER_WARNING` or above are duplicated to standard error by default to ensure they are not missed. This can be turned off through the API. The API also allows the user to change the logging verbosity level and the name of the log file. This would allow a user, for example, to append logging to an existing log file, or to keep it separate. libEnsemble contains no interactive input. libEnsemble creates the files `ensemble.log` and `libE_stats.txt`, but the creation of these files can be preempted. [M11 details](#m11-details)| -|**M12.** For external dependencies, allow installing, building, and linking against an outside copy of external software. |Full| libEnsemble does not contain any other package's source code within. Note that Python packages are imported using the conventional `sys.path` system. Alternative instances of a package can be used by, for example, including in the `PYTHONPATH` environment variable.| -|**M13.** Install headers and libraries under \/include and \/lib. |Full| The standard Python installation is used for Python dependencies. This installs external Python packages under `/lib/python/site-packages/` When installed through Spack, the `` is specific to each Python package. This is added to `PYTHONPATH` when the Spack module for that library is loaded.| -|**M14.** Be buildable using 64 bit pointers. 32 bit is optional. |Full| There is no explicit use of pointers in libEnsemble, as Python handles pointers internally and depends on the install of Python (e.g., CPython), which will generally be 64-bit on supported systems. | -|**M15.** All xSDK compatibility changes should be sustainable. |Full| The xSDK-compatible package is in the standard release path. All the changes here should be sustainable. | -|**M16.** The package must support production-quality installation compatible with the xSDK install tool and xSDK metapackage. |Full|libEnsemble configure and install has full support from Spack. | - -M4 details : libEnsemble is a Python code and so does -not directly use compilers. It does, however, use NumPy, SciPy and mpi4py which -use compiled extensions. The current CI tests of libEnsemble use the standard -CPython compatible builds of these extensions (which are built using the GNU -compilers). libEnsemble is also regularly tested using the Intel distribution -for Python. - -libEnsemble is supported on Linux platforms and macOS. Windows platforms are -currently not supported. - -M11 details : Note: The sub-packages in the libensemble -directory structure such as `sim_specs` and `gen_specs` may contain print -statements. These are considered examples for users, rather than core -libEnsemble packages. - -A special exception exists in the `node_resources.py` module; part of -libEnsemble's resource detection infrastructure. The routine -`_print_local_cpu_resources()` can be launched by libEnsemble to probe -resources on a target node, and the output of this independent program is -captured by libEnsemble. - -### Recommended Policies - -| Policy |Support| Notes | -|------------------------|-------|-------------------------| -|**R1.** Have a public repository. |Full| Yes (see M10 above). | -|**R2.** Possible to run test suite under valgrind in order to test for memory corruption issues. |Full| It is possible to run the test suite under Valgrind. While libEnsemble is Python code, this may be useful for compiled extensions that are imported. PYTHONMALLOC=malloc must be set on the run line. CPython also provides a suppression file.| -|**R3.** Adopt and document consistent system for error conditions/exceptions. |Full| libEnsemble defines and raises exceptions according to module. All exceptions on workers are passed to the manager for processing. Warnings are handled by the logger. [R3 details](#r3-details)|| -|**R4.** Free all system resources acquired as soon as they are no longer needed. |Full| Python has built-in garbage collection that frees memory when it becomes unreferenced. When opening files, wherever possible, `with` expressions or `try/finally` blocks are used to ensure file handles are closed, even in the case of an error.| -|**R5.** Provide a mechanism to export ordered list of library dependencies. |Full| The dependencies for libEnsemble are given in `setup.py` and when pip install or pip setup.py egg_info are run, a file is created `libensemble.egg-info/requires.txt` containing the list of required and optional dependencies. If installing through pip, these will automatically be installed if they do not exist (`pip install libensemble` installs req. dependencies, while `pip install libensemble[extras]` installs both required and optional dependencies.| -|**R6.** Document versions of packages that it works with or depends upon, preferably in machine-readable form. |Full| Dependencies are given in the documentation. In some cases, this includes a lower bound on the version number. These dependencies are also specified in the Spack package, and automatically resolved during installation.| -|**R7.** Have README, SUPPORT, LICENSE, and CHANGELOG files in top directory. |Full| These files are present in the top directory.| - -R3 details : libEnsemble catches all exceptions -(explicitly raised and unexpected) from the manager and worker processes at the -libEnsemble level, resulting in libEnsemble dumping the key ensemble state to -files. In `mpi4py` mode, the default is to then call MPI_ABORT to prevent a -hang. However, this can be turned off (via the `libE_specs` argument). In the -case it is turned off, or if other communication modes are used, the exception -is then raised. The user can in turn catch these exceptions from their calling -script. - -libEnsemble Note : The nature of libEnsemble's -interoperability with other libraries is different from typical xSDK libraries. -libEnsemble is a Python code and interaction with other libraries may take -several forms. These include: libEnsemble calling other libraries through -Python bindings, libEnsemble launching applications (possibly providing a -sub-communicator), libEnsemble being called from a Python level infrastructure, -libEnsemble being launched as part of a campaign level workflow, or libEnsemble -potentially being activated via a system call or embedded interpreter; a more -unconventional approach. This is, therefore, a good opportunity to consider -interoperability from a Python and broader workflow perspective. diff --git a/examples/libE_submission_scripts/summit_submit_mproc.sh b/examples/libE_submission_scripts/summit_submit_mproc.sh deleted file mode 100644 index ba565f6c82..0000000000 --- a/examples/libE_submission_scripts/summit_submit_mproc.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/bash -x -#BSUB -P -#BSUB -J libe_mproc -#BSUB -W 30 -#BSUB -nnodes 4 -#BSUB -alloc_flags "smt1" - -# Script to run libEnsemble using multiprocessing on launch nodes. -# Assumes Conda environment is set up. - -# To be run with central job management -# - Manager and workers run on launch node. -# - Workers submit tasks to the compute nodes in the allocation. - -# Name of calling script- -export EXE=libE_calling_script.py - -# Communication Method -export COMMS="--comms local" - -# Number of workers. -export NWORKERS="--nworkers 4" - -# Wallclock for libE. (allow clean shutdown) -export LIBE_WALLCLOCK=25 # Optional if pass to script - -# Name of Conda environment -export CONDA_ENV_NAME= - -# Need these if not already loaded -# module load python -# module load gcc/4.8.5 - -# Activate conda environment -export PYTHONNOUSERSITE=1 -. activate $CONDA_ENV_NAME - -# hash -d python # Check pick up python in conda env -hash -r # Check no commands hashed (pip/python...) - -# Launch libE -# python $EXE $NUM_WORKERS > out.txt 2>&1 # No args. All defined in calling script -# python $EXE $COMMS $NWORKERS > out.txt 2>&1 # If calling script is using parse_args() -python $EXE $LIBE_WALLCLOCK $COMMS $NWORKERS > out.txt 2>&1 # If calling script takes wall-clock as positional arg. diff --git a/examples/readme_notebook.ipynb b/examples/readme_notebook.ipynb index 20a76707f4..1b22fba94e 100644 --- a/examples/readme_notebook.ipynb +++ b/examples/readme_notebook.ipynb @@ -76,7 +76,6 @@ " exit_criteria=exit_criteria,\n", " )\n", "\n", - " sampling.add_random_streams()\n", " H, persis_info, flag = sampling.run()\n", "\n", " # Print first 10 lines of input/output values\n", diff --git a/examples/tutorials/aposmm/aposmm_tutorial_notebook.ipynb b/examples/tutorials/aposmm/aposmm_tutorial_notebook.ipynb index 3f8f242990..a8d1400c60 100644 --- a/examples/tutorials/aposmm/aposmm_tutorial_notebook.ipynb +++ b/examples/tutorials/aposmm/aposmm_tutorial_notebook.ipynb @@ -114,7 +114,7 @@ "from libensemble.libE import libE\n", "from libensemble.gen_funcs.persistent_aposmm import aposmm\n", "from libensemble.alloc_funcs.persistent_aposmm_alloc import persistent_aposmm_alloc\n", - "from libensemble.tools import parse_args, add_unique_random_streams" + "from libensemble.tools import parse_args" ] }, { @@ -235,7 +235,7 @@ "metadata": {}, "outputs": [], "source": [ - "persis_info = add_unique_random_streams({}, nworkers + 1)\n", + "persis_info = {}\n", "\n", "H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs)" ] @@ -369,8 +369,7 @@ "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.13.1" + "pygments_lexer": "ipython3" } }, "nbformat": 4, diff --git a/examples/tutorials/aposmm/tutorial_aposmm.py b/examples/tutorials/aposmm/tutorial_aposmm.py index 7260a39c26..7dc6b398b1 100644 --- a/examples/tutorials/aposmm/tutorial_aposmm.py +++ b/examples/tutorials/aposmm/tutorial_aposmm.py @@ -5,7 +5,7 @@ from libensemble.alloc_funcs.persistent_aposmm_alloc import persistent_aposmm_alloc from libensemble.gen_funcs.persistent_aposmm import aposmm from libensemble.libE import libE -from libensemble.tools import add_unique_random_streams, parse_args +from libensemble.tools import parse_args libensemble.gen_funcs.rc.aposmm_optimizers = "scipy" @@ -42,8 +42,7 @@ alloc_specs = {"alloc_f": persistent_aposmm_alloc} exit_criteria = {"sim_max": 2000} -persis_info = add_unique_random_streams({}, nworkers + 1) -H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) +H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) if is_manager: print("Minima:", H[np.where(H["local_min"])]["x"]) diff --git a/examples/tutorials/forces_with_executor/forces_tutorial_notebook.ipynb b/examples/tutorials/forces_with_executor/forces_tutorial_notebook.ipynb index b85222e445..f6f157f8e1 100644 --- a/examples/tutorials/forces_with_executor/forces_tutorial_notebook.ipynb +++ b/examples/tutorials/forces_with_executor/forces_tutorial_notebook.ipynb @@ -312,10 +312,7 @@ " gen_specs=gen_specs,\n", " sim_specs=sim_specs,\n", " exit_criteria=exit_criteria,\n", - ")\n", - "\n", - "# Seed random streams for each worker, particularly for gen_f\n", - "ensemble.add_random_streams()" + ")\n" ] }, { @@ -562,9 +559,6 @@ " user={\"input_filename\": input_file, \"input_names\": [\"particles\"]},\n", ")\n", "\n", - "# To reset random number seed in the generator\n", - "ensemble.add_random_streams()\n", - "\n", "# Clean up any previous outputs and launch libEnsemble\n", "cleanup()\n", "H, persis_info, flag = ensemble.run()\n", diff --git a/examples/tutorials/simple_sine/sine_gen_std.py b/examples/tutorials/simple_sine/sine_gen_std.py new file mode 100644 index 0000000000..43bafbf842 --- /dev/null +++ b/examples/tutorials/simple_sine/sine_gen_std.py @@ -0,0 +1,26 @@ +import numpy as np +from gest_api import Generator + + +class RandomSample(Generator): + """ + This sampler accepts a gest-api VOCS object for configuration and returns random samples. + """ + + def __init__(self, vocs): + self.variables = vocs.variables + self.rng = np.random.default_rng(1) + self._validate_vocs(vocs) + + def _validate_vocs(self, vocs): + if not len(vocs.variables) > 0: + raise ValueError("vocs must have at least one variable") + + def suggest(self, num_points): + output = [] + for _ in range(num_points): + trial = {} + for key in self.variables.keys(): + trial[key] = self.rng.uniform(self.variables[key].domain[0], self.variables[key].domain[1]) + output.append(trial) + return output diff --git a/examples/tutorials/simple_sine/sine_tutorial_notebook.ipynb b/examples/tutorials/simple_sine/sine_tutorial_notebook.ipynb index 0ebc6173f6..bfdab66a37 100644 --- a/examples/tutorials/simple_sine/sine_tutorial_notebook.ipynb +++ b/examples/tutorials/simple_sine/sine_tutorial_notebook.ipynb @@ -186,7 +186,6 @@ "\n", "# Initialize and run the ensemble.\n", "ensemble = Ensemble(sim_specs, gen_specs, exit_criteria, libE_specs)\n", - "ensemble.add_random_streams() # setup the random streams unique to each worker\n", "H, persis_info, flag = ensemble.run() # start the ensemble. Blocks until completion." ] }, diff --git a/examples/tutorials/xopt_bayesian_gen/xopt_EI_example.ipynb b/examples/tutorials/xopt_bayesian_gen/xopt_EI_example.ipynb new file mode 100644 index 0000000000..bc27f41a2d --- /dev/null +++ b/examples/tutorials/xopt_bayesian_gen/xopt_EI_example.ipynb @@ -0,0 +1,264 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Xopt Expected Improvement Generator Example\n", + "\n", + "**Requires**: libensemble, xopt, gest-api\n", + "\n", + "This notebook demonstrates using Xopt's Bayeisan **ExpectedImprovementGenerator** with libEnsemble.\n", + "We'll show two approaches:\n", + "1. Using an xopt-style simulator (callable function)\n", + "2. Using a libEnsemble-style simulator function\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Check installed packages" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "if 'google.colab' in sys.modules:\n", + " !pip install gest-api\n", + " !pip install xopt\n", + " !pip install libensemble" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Imports\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from gest_api.vocs import VOCS\n", + "from xopt.generators.bayesian.expected_improvement import ExpectedImprovementGenerator\n", + "\n", + "from libensemble import Ensemble\n", + "from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f\n", + "from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, LibeSpecs, SimSpecs\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Simulator Function\n", + "\n", + "First, we define the xopt-style simulator function.\n", + "\n", + "This is a basic function just to show how it works.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def test_callable(input_dict: dict) -> dict:\n", + " \"\"\"Single-objective callable test function\"\"\"\n", + " assert isinstance(input_dict, dict)\n", + " x1 = input_dict[\"x1\"]\n", + " x2 = input_dict[\"x2\"]\n", + " y1 = x2\n", + " c1 = x1\n", + " return {\"y1\": y1, \"c1\": c1}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "Define the VOCS specification and set up the generator.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "libE_specs = LibeSpecs(gen_on_manager=True, nworkers=4)\n", + "\n", + "vocs = VOCS(\n", + " variables={\"x1\": [0, 1.0], \"x2\": [0, 10.0]},\n", + " objectives={\"y1\": \"MINIMIZE\"},\n", + " constraints={\"c1\": [\"GREATER_THAN\", 0.5]},\n", + " constants={\"constant1\": 1.0},\n", + ")\n", + "\n", + "gen = ExpectedImprovementGenerator(vocs=vocs)\n", + "\n", + "# Create 4 initial points and ingest them\n", + "initial_points = [\n", + " {\"x1\": 0.2, \"x2\": 2.0, \"y1\": 2.0, \"c1\": 0.2},\n", + " {\"x1\": 0.5, \"x2\": 5.0, \"y1\": 5.0, \"c1\": 0.5},\n", + " {\"x1\": 0.7, \"x2\": 7.0, \"y1\": 7.0, \"c1\": 0.7},\n", + " {\"x1\": 0.9, \"x2\": 9.0, \"y1\": 9.0, \"c1\": 0.9},\n", + "]\n", + "gen.ingest(initial_points)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Define libEnsemble specifications. Note the gen_specs and sim_specs are set using vocs." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "gen_specs = GenSpecs(\n", + " generator=gen,\n", + " vocs=vocs,\n", + ")\n", + "\n", + "# Note: using 'simulator' parameter for xopt-style callable\n", + "sim_specs = SimSpecs(\n", + " simulator=test_callable,\n", + " vocs=vocs,\n", + ")\n", + "\n", + "alloc_specs = AllocSpecs(alloc_f=alloc_f)\n", + "exit_criteria = ExitCriteria(sim_max=12)\n", + "\n", + "workflow = Ensemble(\n", + " libE_specs=libE_specs,\n", + " sim_specs=sim_specs,\n", + " alloc_specs=alloc_specs,\n", + " gen_specs=gen_specs,\n", + " exit_criteria=exit_criteria,\n", + ")\n", + "\n", + "H, _, _ = workflow.run()\n", + "\n", + "if workflow.is_manager:\n", + " print(f\"Completed {len(H)} simulations\")\n", + " print(H[[\"x1\", \"x2\", \"y1\", \"c1\"]])\n", + " assert np.array_equal(H[\"y1\"], H[\"x2\"])\n", + " assert np.array_equal(H[\"c1\"], H[\"x1\"])\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Approach 2: Using libEnsemble-style Simulator Function\n", + "\n", + "Now we define the libEnsemble-style simulator function and use it in the workflow.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def test_sim(H, persis_info, sim_specs, _):\n", + " \"\"\"\n", + " Simple sim function that takes x1, x2, constant1 from H and returns y1, c1.\n", + " Logic: y1 = x2, c1 = x1\n", + " \"\"\"\n", + " batch = len(H)\n", + " H_o = np.zeros(batch, dtype=sim_specs[\"out\"])\n", + "\n", + " for i in range(batch):\n", + " x1 = H[\"x1\"][i]\n", + " x2 = H[\"x2\"][i]\n", + " H_o[\"y1\"][i] = x2\n", + " H_o[\"c1\"][i] = x1\n", + "\n", + " return H_o, persis_info" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Reset generator and change to libEnsemble-style simulator\n", + "gen = ExpectedImprovementGenerator(vocs=vocs)\n", + "gen.ingest(initial_points)\n", + "\n", + "gen_specs = GenSpecs(\n", + " generator=gen,\n", + " vocs=vocs,\n", + ")\n", + "\n", + "# Note: using 'sim_f' parameter for libEnsemble-style function\n", + "sim_specs = SimSpecs(\n", + " sim_f=test_sim,\n", + " vocs=vocs,\n", + ")\n", + "\n", + "workflow = Ensemble(\n", + " libE_specs=libE_specs,\n", + " sim_specs=sim_specs,\n", + " alloc_specs=alloc_specs,\n", + " gen_specs=gen_specs,\n", + " exit_criteria=exit_criteria,\n", + ")\n", + "\n", + "H, _, _ = workflow.run()\n", + "\n", + "if workflow.is_manager:\n", + " print(f\"Completed {len(H)} simulations\")\n", + " print(H[[\"x1\", \"x2\", \"y1\", \"c1\"]])\n", + " assert np.array_equal(H[\"y1\"], H[\"x2\"])\n", + " assert np.array_equal(H[\"c1\"], H[\"x1\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/install/find_mpi.py b/install/find_mpi.py deleted file mode 100644 index bdec0458ca..0000000000 --- a/install/find_mpi.py +++ /dev/null @@ -1,32 +0,0 @@ -import os - -import mpi4py -from mpi4py import MPI - -path = mpi4py.__path__[0] -print("\nmpi4py path found is:", path) - -configfile = os.path.join(path, "mpi.cfg") -print("\nShowing config file: ", configfile, "\n") - -with open(configfile, "r") as confile_handle: - print(confile_handle.read()) - -with open(configfile, "r") as infile: - for line in infile: - if line.startswith("mpicc ="): - mpi4py_mpicc = line[8:-1] - cmd_line = str(mpi4py_mpicc) + " -v" - print(cmd_line, ":\n") - os.system(cmd_line) - break - -size = MPI.COMM_WORLD.Get_size() -rank = MPI.COMM_WORLD.Get_rank() -name = MPI.Get_processor_name() - -assert size == 1 -assert rank == 0 -assert len(name) - -print("Passed") diff --git a/install/gen_deps_environment.yml b/install/gen_deps_environment.yml deleted file mode 100644 index 50b88b6b5a..0000000000 --- a/install/gen_deps_environment.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: condaenv - -channels: - - conda-forge - -dependencies: - - pip - - numpy>=2 - - scipy - - superlu_dist - - hypre - - mumps-mpi - - DFO-LS - - mpmath - - ax-platform==0.5.0 - - petsc - - petsc4py diff --git a/install/install_ibcdfo.sh b/install/install_ibcdfo.sh deleted file mode 100644 index 0ed790f01a..0000000000 --- a/install/install_ibcdfo.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash - -git clone --recurse-submodules -b main https://github.com/POptUS/IBCDFO.git -pushd IBCDFO/minq/py/minq5/ -export PYTHONPATH="$PYTHONPATH:$(pwd)" -echo "PYTHONPATH=$PYTHONPATH" >> $GITHUB_ENV -popd -pushd IBCDFO/ibcdfo_pypkg/ -pip install -e . -popd diff --git a/install/install_minq.sh b/install/install_minq.sh new file mode 100644 index 0000000000..cdc325ccd4 --- /dev/null +++ b/install/install_minq.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +git clone https://github.com/POptUS/MINQ +pushd MINQ/py/minq5/ +export PYTHONPATH="$PYTHONPATH:$(pwd)" +echo "PYTHONPATH=$PYTHONPATH" >> $GITHUB_ENV +popd diff --git a/install/misc_feature_requirements.txt b/install/misc_feature_requirements.txt deleted file mode 100644 index 9c08c835e5..0000000000 --- a/install/misc_feature_requirements.txt +++ /dev/null @@ -1 +0,0 @@ -globus-compute-sdk==3.16.1 diff --git a/install/testing_requirements.txt b/install/testing_requirements.txt deleted file mode 100644 index 0d6a40fa2c..0000000000 --- a/install/testing_requirements.txt +++ /dev/null @@ -1,11 +0,0 @@ -flake8==7.3.0 -coverage>=7.5 -pytest==8.4.2 -pytest-cov==7.0.0 -pytest-timeout==2.4.0 -mock==5.2.0 -python-dateutil==2.9.0.post0 -anyio==4.11.0 -matplotlib==3.10.7 -mpmath==1.3.0 -rich==14.2.0 diff --git a/libensemble/__init__.py b/libensemble/__init__.py index 6053368219..8df3af2073 100644 --- a/libensemble/__init__.py +++ b/libensemble/__init__.py @@ -12,3 +12,4 @@ from libensemble import logger from .ensemble import Ensemble +from .generators import Generator diff --git a/libensemble/alloc_funcs/fast_alloc.py b/libensemble/alloc_funcs/fast_alloc.py index bb009740c8..e2027da1c0 100644 --- a/libensemble/alloc_funcs/fast_alloc.py +++ b/libensemble/alloc_funcs/fast_alloc.py @@ -7,7 +7,7 @@ def give_sim_work_first(W, H, sim_specs, gen_specs, alloc_specs, persis_info, li to evaluate in the simulation function. The fields in ``sim_specs["in"]`` are given. If all entries in `H` have been given a be evaluated, a worker is told to call the generator function, provided this wouldn't result in - more than ``alloc_specs["user"]["num_active_gen"]`` active generators. + more than ``gen_specs["num_active_gens"]`` or ``alloc_specs["user"]["num_active_gens"]`` active generators. This fast_alloc variation of give_sim_work_first is useful for cases that simply iterate through H, issuing evaluations in order and, in particular, @@ -23,7 +23,8 @@ def give_sim_work_first(W, H, sim_specs, gen_specs, alloc_specs, persis_info, li if libE_info["sim_max_given"] or not libE_info["any_idle_workers"]: return {}, persis_info - user = alloc_specs.get("user", {}) + user = {**gen_specs, **alloc_specs.get("user", {})} + manage_resources = libE_info["use_resource_sets"] support = AllocSupport(W, manage_resources, persis_info, libE_info) diff --git a/libensemble/alloc_funcs/fast_alloc_and_pausing.py b/libensemble/alloc_funcs/fast_alloc_and_pausing.py index f26b5c0780..fd162a6623 100644 --- a/libensemble/alloc_funcs/fast_alloc_and_pausing.py +++ b/libensemble/alloc_funcs/fast_alloc_and_pausing.py @@ -28,13 +28,16 @@ def give_sim_work_first(W, H, sim_specs, gen_specs, alloc_specs, persis_info, li if libE_info["sim_max_given"] or not libE_info["any_idle_workers"]: return {}, persis_info + user = {**gen_specs, **alloc_specs.get("user", {})} manage_resources = libE_info["use_resource_sets"] support = AllocSupport(W, manage_resources, persis_info, libE_info) Work = {} gen_count = support.count_gens() if gen_specs["user"].get("single_component_at_a_time"): - assert alloc_specs["user"]["batch_mode"], "Must be in batch mode when using 'single_component_at_a_time'" + assert alloc_specs["user"].get("batch_mode", False) or gen_specs.get( + "batch_mode", False + ), "Must be in batch mode when using 'single_component_at_a_time'" if len(H) != persis_info["H_len"]: # Something new is in the history. persis_info["need_to_give"].update(H["sim_id"][persis_info["H_len"] :].tolist()) @@ -119,13 +122,13 @@ def give_sim_work_first(W, H, sim_specs, gen_specs, alloc_specs, persis_info, li break while len(idle_gen_workers): - if gen_count < alloc_specs["user"].get("num_active_gens", gen_count + 1): + if gen_count < user.get("num_active_gens", gen_count + 1): lw = persis_info["last_worker"] last_size = persis_info.get("last_size") if len(H): # Don't give gen instances in batch mode if points are unfinished - if alloc_specs["user"].get("batch_mode") and not all( + if (alloc_specs["user"].get("batch_mode") or gen_specs.get("batch_mode")) and not all( np.logical_or(H["sim_ended"][last_size:], H["paused"][last_size:]) ): break @@ -142,7 +145,7 @@ def give_sim_work_first(W, H, sim_specs, gen_specs, alloc_specs, persis_info, li persis_info["last_worker"] = i persis_info["last_size"] = len(H) - elif gen_count >= alloc_specs["user"].get("num_active_gens", gen_count + 1): + elif gen_count >= user.get("num_active_gens", gen_count + 1): idle_gen_workers = [] return Work, persis_info diff --git a/libensemble/alloc_funcs/give_sim_work_first.py b/libensemble/alloc_funcs/give_sim_work_first.py index 7ac4d75e5e..96245f7a98 100644 --- a/libensemble/alloc_funcs/give_sim_work_first.py +++ b/libensemble/alloc_funcs/give_sim_work_first.py @@ -14,18 +14,18 @@ def give_sim_work_first( alloc_specs: dict, persis_info: dict, libE_info: dict, -) -> tuple[dict]: +) -> tuple[dict, dict]: """ Decide what should be given to workers. This allocation function gives any available simulation work first, and only when all simulations are - completed or running does it start (at most ``alloc_specs["user"]["num_active_gens"]``) + completed or running does it start (at most ``gen_specs["num_active_gens"]`` or ``alloc_specs["user"]["num_active_gens"]``) generator instances. - Allows for a ``alloc_specs["user"]["batch_mode"]`` where no generation + Allows for a ``gen_specs["batch_mode"]`` or ``alloc_specs["user"]["batch_mode"]`` where no generation work is given out unless all entries in ``H`` are returned. Can give points in highest priority, if ``"priority"`` is a field in ``H``. - If ``alloc_specs["user"]["give_all_with_same_priority"]`` is set to True, then + If ``gen_specs["batch_evaluate_same_priority"]`` or ``alloc_specs["user"]["batch_evaluate_same_priority"]`` is set to True, then all points with the same priority value are given as a batch to the sim. Workers performing sims will be assigned resources given in H["resource_sets"] @@ -40,7 +40,7 @@ def give_sim_work_first( `test_uniform_sampling.py `_ # noqa """ - user = alloc_specs.get("user", {}) + user = {**gen_specs, **alloc_specs.get("user", {})} if "cancel_sims_time" in user: # Cancel simulations that are taking too long @@ -54,7 +54,7 @@ def give_sim_work_first( return {}, persis_info # Initialize alloc_specs["user"] as user. - batch_give = user.get("give_all_with_same_priority", False) + batch_give = user.get("batch_evaluate_same_priority", False) gen_in = gen_specs.get("in", []) manage_resources = libE_info["use_resource_sets"] diff --git a/libensemble/alloc_funcs/persistent_aposmm_alloc.py b/libensemble/alloc_funcs/persistent_aposmm_alloc.py index 3b87d5b5b9..9ab6c99ee2 100644 --- a/libensemble/alloc_funcs/persistent_aposmm_alloc.py +++ b/libensemble/alloc_funcs/persistent_aposmm_alloc.py @@ -21,7 +21,10 @@ def persistent_aposmm_alloc(W, H, sim_specs, gen_specs, alloc_specs, persis_info if libE_info["sim_max_given"] or not libE_info["any_idle_workers"]: return {}, persis_info - init_sample_size = gen_specs["user"]["initial_sample_size"] + if not persis_info: + persis_info = {i: {} for i in range(len(W))} + user = {**gen_specs, **alloc_specs.get("user", {})} + init_sample_size = user["initial_batch_size"] manage_resources = libE_info["use_resource_sets"] support = AllocSupport(W, manage_resources, persis_info, libE_info) gen_count = support.count_persis_gens() @@ -69,7 +72,7 @@ def persistent_aposmm_alloc(W, H, sim_specs, gen_specs, alloc_specs, persis_info if persis_info.get("gen_started") is None: for wid in support.avail_worker_ids(persistent=False, gen_workers=True): # Finally, call a persistent generator as there is nothing else to do. - persis_info.get(wid)["nworkers"] = len(W) + persis_info[wid]["nworkers"] = len(W) try: Work[wid] = support.gen_work( wid, gen_specs.get("in", []), range(len(H)), persis_info.get(wid), persistent=True diff --git a/libensemble/alloc_funcs/start_only_persistent.py b/libensemble/alloc_funcs/start_only_persistent.py index 7781ed3b5f..6b02b4c60c 100644 --- a/libensemble/alloc_funcs/start_only_persistent.py +++ b/libensemble/alloc_funcs/start_only_persistent.py @@ -7,16 +7,16 @@ def only_persistent_gens(W, H, sim_specs, gen_specs, alloc_specs, persis_info, libE_info): """ This allocation function will give simulation work if possible, but - otherwise start up to ``alloc_specs["user"]["num_active_gens"]`` + otherwise start up to ``gen_specs["num_active_gens"]`` or ``alloc_specs["user"]["num_active_gens"]`` persistent generators (defaulting to one). By default, evaluation results are given back to the generator once all generated points have been returned from the simulation evaluation. - If ``alloc_specs["user"]["async_return"]`` is set to True, then any + If ``gen_specs["async_return"]`` or ``alloc_specs["user"]["async_return"]`` is set to True, then any returned points are given back to the generator. - If any workers are marked as zero_resource_workers, then these will only - be used for generators. + "" or ``alloc_specs["user"]["num_active_gens"]`` + persistent generators (defaulting to one). If any of the persistent generators has exited, then ensemble shutdown is triggered. @@ -34,7 +34,7 @@ def only_persistent_gens(W, H, sim_specs, gen_specs, alloc_specs, persis_info, l async_return: Boolean, optional Return results to gen as they come in (after sample). Default: False (batch return). - give_all_with_same_priority: Boolean, optional + batch_evaluate_same_priority: Boolean, optional If True, then all points with the same priority value are given as a batch to the sim. Default is False @@ -56,19 +56,20 @@ def only_persistent_gens(W, H, sim_specs, gen_specs, alloc_specs, persis_info, l return {}, persis_info # Initialize alloc_specs["user"] as user. - user = alloc_specs.get("user", {}) + user = {**gen_specs, **alloc_specs.get("user", {})} + manage_resources = libE_info["use_resource_sets"] active_recv_gen = user.get("active_recv_gen", False) # Persistent gen can handle irregular communications - init_sample_size = user.get("init_sample_size", 0) # Always batch return until this many evals complete - batch_give = user.get("give_all_with_same_priority", False) + initial_batch_size = user.get("initial_batch_size", 0) # Always batch return until this many evals complete + batch_give = user.get("batch_evaluate_same_priority", False) support = AllocSupport(W, manage_resources, persis_info, libE_info) gen_count = support.count_persis_gens() Work = {} # Asynchronous return to generator - async_return = user.get("async_return", False) and sum(H["sim_ended"]) >= init_sample_size + async_return = user.get("async_return", False) and sum(H["sim_ended"]) >= initial_batch_size if gen_count < persis_info.get("num_gens_started", 0): # When a persistent worker is done, trigger a shutdown (returning exit condition of 1) @@ -93,11 +94,10 @@ def only_persistent_gens(W, H, sim_specs, gen_specs, alloc_specs, persis_info, l # Now the give_sim_work_first part points_to_evaluate = ~H["sim_started"] & ~H["cancel_requested"] - avail_workers = support.avail_worker_ids(persistent=False, zero_resource_workers=False, gen_workers=False) + avail_workers = support.avail_worker_ids(persistent=False, gen_workers=False) if user.get("alt_type"): avail_workers = list( - set(support.avail_worker_ids(persistent=False, zero_resource_workers=False)) - | set(support.avail_worker_ids(persistent=EVAL_SIM_TAG, zero_resource_workers=False)) + set(support.avail_worker_ids(persistent=False)) | set(support.avail_worker_ids(persistent=EVAL_SIM_TAG)) ) for wid in avail_workers: if not np.any(points_to_evaluate): @@ -117,9 +117,9 @@ def only_persistent_gens(W, H, sim_specs, gen_specs, alloc_specs, persis_info, l points_to_evaluate[sim_ids_to_send] = False - # Start persistent gens if no worker to give out. Uses zero_resource_workers if defined. + # Start persistent gens if no worker to give out. if not np.any(points_to_evaluate): - avail_workers = support.avail_worker_ids(persistent=False, zero_resource_workers=True, gen_workers=True) + avail_workers = support.avail_worker_ids(persistent=False, gen_workers=True) for wid in avail_workers: if gen_count < user.get("num_active_gens", 1): diff --git a/libensemble/alloc_funcs/start_persistent_local_opt_gens.py b/libensemble/alloc_funcs/start_persistent_local_opt_gens.py index 918ebbb755..9f0537b8e3 100644 --- a/libensemble/alloc_funcs/start_persistent_local_opt_gens.py +++ b/libensemble/alloc_funcs/start_persistent_local_opt_gens.py @@ -27,6 +27,9 @@ def start_persistent_local_opt_gens(W, H, sim_specs, gen_specs, alloc_specs, per if libE_info["sim_max_given"] or not libE_info["any_idle_workers"]: return {}, persis_info + if not persis_info: + persis_info = {i: {} for i in range(len(W))} + manage_resources = libE_info["use_resource_sets"] support = AllocSupport(W, manage_resources, persis_info, libE_info) Work = {} @@ -42,6 +45,7 @@ def start_persistent_local_opt_gens(W, H, sim_specs, gen_specs, alloc_specs, per opt_ind = np.all(H["x"] == persis_info[i]["x_opt"], axis=1) assert sum(opt_ind) == 1, "There must be just one optimum" H["local_min"][opt_ind] = True + if "rand_stream" in persis_info[i]: persis_info[i] = {"rand_stream": persis_info[i]["rand_stream"]} # If wid is idle, but in persistent mode, and its calculated values have diff --git a/libensemble/comms/comms.py b/libensemble/comms/comms.py index 51042c4637..52f71dad97 100644 --- a/libensemble/comms/comms.py +++ b/libensemble/comms/comms.py @@ -264,8 +264,8 @@ def __init__(self, main, nworkers, *args, **kwargs): self.inbox = Queue() self.outbox = Queue() super().__init__(self, main, *args, **kwargs) - comm = QComm(self.inbox, self.outbox, nworkers) - self.handle = Process(target=_qcomm_main, args=(comm, main) + args, kwargs=kwargs) + self.comm = QComm(self.inbox, self.outbox, nworkers) + self.handle = Process(target=_qcomm_main, args=(self.comm, main) + args, kwargs=kwargs) def terminate(self, timeout=None): """Terminate the process.""" diff --git a/libensemble/ensemble.py b/libensemble/ensemble.py index 0ceba2ab33..24b47d72b0 100644 --- a/libensemble/ensemble.py +++ b/libensemble/ensemble.py @@ -1,15 +1,10 @@ -import importlib -import json import logging import numpy.typing as npt -import tomli -import yaml from libensemble.executors import Executor from libensemble.libE import libE from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, LibeSpecs, SimSpecs -from libensemble.tools import add_unique_random_streams from libensemble.tools import parse_args as parse_args_f from libensemble.tools import save_libE_output from libensemble.tools.parse_args import mpi_init @@ -36,7 +31,7 @@ class Ensemble: """ The primary object for a libEnsemble workflow. - Parses and validates settings, sets up logging, and maintains output. + Parses and validates settings and maintains output. .. dropdown:: Example :open: @@ -44,44 +39,39 @@ class Ensemble: .. code-block:: python :linenos: - import numpy as np + from gest_api.vocs import VOCS from libensemble import Ensemble - from libensemble.gen_funcs.sampling import latin_hypercube_sample + from libensemble.gen_classes.sampling import UniformSample from libensemble.sim_funcs.simple_sim import norm_eval - from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs + from libensemble.specs import ExitCriteria, GenSpecs, SimSpecs + + sampling = Ensemble(parse_args=True) - libE_specs = LibeSpecs(nworkers=4) - sampling = Ensemble(libE_specs=libE_specs) sampling.sim_specs = SimSpecs( sim_f=norm_eval, inputs=["x"], outputs=[("f", float)], ) + + vocs = VOCS( + variables={"x": [-3, 3]}, + objectives={"f": "EXPLORE"}, + ) + + generator = UniformSample(vocs=vocs) + sampling.gen_specs = GenSpecs( - gen_f=latin_hypercube_sample, - outputs=[("x", float, (1,))], - user={ - "gen_batch_size": 50, - "lb": np.array([-3]), - "ub": np.array([3]), - }, + generator=generator, + batch_size=50, ) - sampling.add_random_streams() sampling.exit_criteria = ExitCriteria(sim_max=100) if __name__ == "__main__": sampling.run() sampling.save_output(__file__) - - Run the above example via ``python this_file.py``. - - Instead of using the libE_specs line, you can also use ``sampling = Ensemble(parse_args=True)`` - and run via ``python this_file.py -n 4`` (4 workers). The ``parse_args=True`` parameter - instructs the Ensemble class to read command-line arguments. - Configure by: .. dropdown:: Option 1: Providing parameters on instantiation @@ -90,13 +80,14 @@ class Ensemble: :linenos: from libensemble import Ensemble + from libensemble.specs import SimSpecs from my_simulator import sim_find_energy - sim_specs = { - "sim_f": sim_find_energy, - "in": ["x"], - "out": [("y", float)], - } + sim_specs = SimSpecs( + sim_f=sim_find_energy, + inputs=["x"], + outputs=[("y", float)], + ) experiment = Ensemble(sim_specs=sim_specs) @@ -105,7 +96,8 @@ class Ensemble: .. code-block:: python :linenos: - from libensemble import Ensemble, SimSpecs + from libensemble import Ensemble + from libensemble.specs import SimSpecs from my_simulator import sim_find_energy sim_specs = SimSpecs( @@ -117,133 +109,29 @@ class Ensemble: experiment = Ensemble() experiment.sim_specs = sim_specs - .. dropdown:: Option 3: Loading parameters from files - - .. code-block:: python - :linenos: - - from libensemble import Ensemble - - experiment = Ensemble() - - my_experiment.from_yaml("my_parameters.yaml") - # or... - my_experiment.from_toml("my_parameters.toml") - # or... - my_experiment.from_json("my_parameters.json") - - .. tab-set:: - - .. tab-item:: my_parameters.yaml - - .. code-block:: yaml - :linenos: - - libE_specs: - save_every_k_gens: 20 - - exit_criteria: - sim_max: 80 - - gen_specs: - gen_f: generator.gen_random_sample - outputs: - x: - type: float - size: 1 - user: - gen_batch_size: 5 - - sim_specs: - sim_f: simulator.sim_find_sine - inputs: - - x - outputs: - y: - type: float - - .. tab-item:: my_parameters.toml - - .. code-block:: toml - :linenos: - - [libE_specs] - save_every_k_gens = 300 - - [exit_criteria] - sim_max = 80 - - [gen_specs] - gen_f = "generator.gen_random_sample" - [gen_specs.outputs] - [gen_specs.outputs.x] - type = "float" - size = 1 - [gen_specs.user] - gen_batch_size = 5 - - [sim_specs] - sim_f = "simulator.sim_find_sine" - inputs = ["x"] - [sim_specs.outputs] - [sim_specs.outputs.y] - type = "float" - - .. tab-item:: my_parameters.json - - .. code-block:: json - :linenos: - - { - "libE_specs": { - "save_every_k_gens": 300, - }, - "exit_criteria": { - "sim_max": 80 - }, - "gen_specs": { - "gen_f": "generator.gen_random_sample", - "outputs": { - "x": { - "type": "float", - "size": 1 - } - }, - "user": { - "gen_batch_size": 5 - } - }, - "sim_specs": { - "sim_f": "simulator.sim_find_sine", - "inputs": ["x"], - "outputs": { - "f": {"type": "float"} - } - } - } Parameters ---------- - sim_specs: :obj:`dict` or :class:`SimSpecs` + sim_specs: class:`SimSpecs` - Specifications for the simulation function + Specifications for the simulator function. - gen_specs: :obj:`dict` or :class:`GenSpecs`, Optional + gen_specs: class:`GenSpecs`, Optional - Specifications for the generator function + Specifications for the generator. - exit_criteria: :obj:`dict` or :class:`ExitCriteria`, Optional + exit_criteria: class:`ExitCriteria` - Tell libEnsemble when to stop a run + Tell libEnsemble when to stop a run. - libE_specs: :obj:`dict` or :class:`LibeSpecs`, Optional + libE_specs: class:`LibeSpecs`, Optional - Specifications for libEnsemble + Specifications for libEnsemble. - alloc_specs: :obj:`dict` or :class:`AllocSpecs`, Optional + alloc_specs: class:`AllocSpecs`, Optional - Specifications for the allocation function + Specifications for the allocation function. persis_info: :obj:`dict`, Optional @@ -252,12 +140,12 @@ class Ensemble: executor: :class:`Executor`, Optional - libEnsemble Executor instance for use within simulation or generator functions + libEnsemble Executor instance for use within simulator functions or generators. H0: `NumPy structured array `_, Optional A libEnsemble history to be prepended to this run's history - :ref:`(example)` + :ref:`(example)`. parse_args: bool, Optional @@ -269,20 +157,20 @@ class Ensemble: def __init__( self, - sim_specs: SimSpecs | None = SimSpecs(), - gen_specs: GenSpecs | None = GenSpecs(), - exit_criteria: ExitCriteria | None = {}, - libE_specs: LibeSpecs | None = LibeSpecs(), - alloc_specs: AllocSpecs | None = AllocSpecs(), - persis_info: dict | None = {}, + sim_specs: SimSpecs = SimSpecs(), + gen_specs: GenSpecs = GenSpecs(), + exit_criteria: ExitCriteria = ExitCriteria(), + libE_specs: LibeSpecs = LibeSpecs(), + alloc_specs: AllocSpecs = AllocSpecs(), + persis_info: dict = {}, executor: Executor | None = None, H0: npt.NDArray | None = None, - parse_args: bool | None = False, + parse_args: bool = False, ): self.sim_specs = sim_specs self.gen_specs = gen_specs self.exit_criteria = exit_criteria - self._libE_specs = libE_specs + self._libE_specs: LibeSpecs = libE_specs self.alloc_specs = alloc_specs self.persis_info = persis_info self.executor = executor @@ -291,17 +179,14 @@ def __init__( self._nworkers = 0 self.is_manager = False self.parsed = False - self._known_comms = None + self._known_comms: str = "" if parse_args: self._parse_args() self.parsed = True - self._known_comms = self._libE_specs.comms - if not self._known_comms and self._libE_specs is not None: - if isinstance(self._libE_specs, dict): - self._libE_specs = LibeSpecs(**self._libE_specs) - self._known_comms = self._libE_specs.comms + if self._libE_specs: + self._known_comms = getattr(self._libE_specs, "comms", "") if self._known_comms == "local": self.is_manager = True @@ -310,9 +195,10 @@ def __init__( elif self._known_comms == "mpi" and not parse_args: # Set internal _nworkers - not libE_specs (avoid "nworkers will be ignored" warning) - self._nworkers, self.is_manager = mpi_init(self._libE_specs.mpi_comm) + if self._libE_specs: + self._nworkers, self.is_manager = mpi_init(getattr(self._libE_specs, "mpi_comm", None)) - def _parse_args(self) -> (int, bool, LibeSpecs): + def _parse_args(self) -> tuple[int, bool, LibeSpecs]: # Set internal _nworkers - not libE_specs (avoid "nworkers will be ignored" warning) self._nworkers, self.is_manager, libE_specs_parsed, self.extra_args = parse_args_f() @@ -323,9 +209,76 @@ def _parse_args(self) -> (int, bool, LibeSpecs): return self.nworkers, self.is_manager, self._libE_specs - def ready(self) -> bool: - """Quickly verify that all necessary data has been provided""" - return all([i for i in [self.exit_criteria, self._libE_specs, self.sim_specs]]) + def ready(self) -> tuple[bool, list[str]]: + """Verify that all necessary data has been provided before calling :meth:`run`. + + Performs a pre-flight check on the ensemble configuration, covering: + + - A simulation callable (``sim_f`` or ``simulator``) is set on ``sim_specs``. + - At least one exit condition is configured on ``exit_criteria``. + - Workers are available (``nworkers > 0`` for local/threads/tcp comms, + or MPI comms is set, which infers workers from the MPI communicator). + - If both ``gen_specs`` and ``sim_specs`` use the classic field-name interface, + the generator output field names are a superset of the simulator input field names. + + Returns + ------- + tuple[bool, list[str]] + A 2-tuple of ``(is_ready, issues)``. + ``is_ready`` is ``True`` when all checks pass. + ``issues`` is a list of human-readable strings describing each problem found; + it is empty when ``is_ready`` is ``True``. + + Example + ------- + .. code-block:: python + + ok, issues = sampling.ready() + if not ok: + for issue in issues: + print(f" - {issue}") + """ + issues: list[str] = [] + + # --- sim_specs: a callable must be set --- + sim_callable = getattr(self.sim_specs, "sim_f", None) or getattr(self.sim_specs, "simulator", None) + if not sim_callable: + issues.append( + "sim_specs is missing a callable: set 'sim_f' (a function) or 'simulator' (a gest-api object)." + ) + + # --- exit_criteria: at least one stop condition must be set --- + ec = self.exit_criteria + if ec is None or not any( + getattr(ec, field, None) is not None for field in ("sim_max", "gen_max", "wallclock_max", "stop_val") + ): + issues.append( + "exit_criteria has no stop condition: set at least one of " + "'sim_max', 'gen_max', 'wallclock_max', or 'stop_val'." + ) + + # --- workers: must be determinable --- + comms = getattr(self._libE_specs, "comms", "mpi") + if comms in ("local", "threads", "tcp"): + if not self.nworkers: + issues.append( + f"libE_specs.comms is '{comms}' but 'nworkers' is not set. " + "Set 'libE_specs.nworkers' or pass '--nworkers N' on the command line." + ) + # For 'mpi', worker count is derived from the MPI communicator at runtime; no check needed here. + + # --- cross-spec field consistency (classic interface only) --- + gen_outputs = [f[0] for f in (getattr(self.gen_specs, "outputs", None) or [])] + sim_inputs = getattr(self.sim_specs, "inputs", None) or [] + if gen_outputs and sim_inputs: + missing = [field for field in sim_inputs if field not in gen_outputs] + if missing: + issues.append( + f"sim_specs.inputs requests field(s) {missing} that are not produced " + f"by gen_specs.outputs {gen_outputs}. Check that field names match." + ) + + return not issues, issues @property def libE_specs(self) -> LibeSpecs: @@ -333,28 +286,21 @@ def libE_specs(self) -> LibeSpecs: @libE_specs.setter def libE_specs(self, new_specs): - # We need to deal with libE_specs being specified as dict or class, and - # "not" overwrite the internal libE_specs["comms"]. - # Respect everything if libE_specs isn't set if not hasattr(self, "_libE_specs") or not self._libE_specs: - if isinstance(new_specs, dict): - self._libE_specs = LibeSpecs(**new_specs) - else: - self._libE_specs = new_specs + self._libE_specs = new_specs return # Cast new libE_specs temporarily to dict - if not isinstance(new_specs, dict): # exclude_defaults should only be enabled with Pydantic v2 - if new_specs.comms != "mpi" and new_specs.comms != self._libE_specs.comms: # passing in a non-default comms - raise ValueError(OVERWRITE_COMMS_WARN) - platform_specs_set = False - if new_specs.platform_specs != {}: # bugginess across Pydantic versions for recursively casting to dict - platform_specs_set = True - platform_specs = new_specs.platform_specs - new_specs = specs_dump(new_specs, exclude_none=True, exclude_defaults=True) - if platform_specs_set: - new_specs["platform_specs"] = specs_dump(platform_specs, exclude_none=True) + if new_specs.comms != "mpi" and new_specs.comms != self._libE_specs.comms: # passing in a non-default comms + raise ValueError(OVERWRITE_COMMS_WARN) + platform_specs_set = False + if new_specs.platform_specs != {}: # bugginess across Pydantic versions for recursively casting to dict + platform_specs_set = True + platform_specs = new_specs.platform_specs + new_specs = specs_dump(new_specs, exclude_none=True, exclude_defaults=True) + if platform_specs_set: + new_specs["platform_specs"] = specs_dump(platform_specs, exclude_none=True) # Unset "comms" if we already have a libE_specs that contains that field, that came from parse_args if new_specs.get("comms") and hasattr(self._libE_specs, "comms"): @@ -365,7 +311,7 @@ def libE_specs(self, new_specs): def _refresh_executor(self): Executor.executor = self.executor or Executor.executor - def run(self) -> (npt.NDArray, dict, int): + def run(self) -> tuple[npt.NDArray, dict, int]: """ Initializes libEnsemble. @@ -373,10 +319,10 @@ def run(self) -> (npt.NDArray, dict, int): Manager--worker intercommunications are parsed from the ``comms`` key of :ref:`libE_specs`. An MPI runtime is assumed by default - if ``--comms local`` wasn't specified on the command-line or in ``libE_specs``. + if ``-n N`` wasn't specified on the command-line or ``comms="local"`` in ``libE_specs``. If a MPI communicator was provided in ``libE_specs``, then each ``.run()`` call - will initiate intercommunications on a **duplicate** of that communicator. + will initiate on a **duplicate** of that communicator. Otherwise, a duplicate of ``COMM_WORLD`` will be used. Returns @@ -405,10 +351,10 @@ def run(self) -> (npt.NDArray, dict, int): """ self._refresh_executor() - - if self._libE_specs.comms != self._known_comms: + if self._libE_specs and getattr(self._libE_specs, "comms", "") != self._known_comms: raise ValueError(CHANGED_COMMS_WARN) + assert self._libE_specs is not None self.H, self.persis_info, self.flag = libE( self.sim_specs, self.gen_specs, @@ -431,153 +377,22 @@ def nworkers(self, value): if self._libE_specs: self._libE_specs.nworkers = value - def _get_func(self, loaded): - """Extracts user function specified in loaded dict""" - func_path_split = loaded.rsplit(".", 1) - func_name = func_path_split[-1] - try: - return getattr(importlib.import_module(func_path_split[0]), func_name) - except AttributeError: - self._util_logger.manager_warning(ATTR_ERR_MSG.format(func_name)) - raise - except ModuleNotFoundError: - self._util_logger.manager_warning(NOTFOUND_ERR_MSG.format(func_name)) - raise - - @staticmethod - def _get_outputs(loaded): - """Extracts output parameters from loaded dict""" - if not loaded: - return [] - fields = [i for i in loaded] - field_params = [i for i in loaded.values()] - results = [] - for i in range(len(fields)): - field_type = field_params[i]["type"] - built_in_type = __builtins__.get(field_type, field_type) - try: - if field_params[i]["size"] == 1: - size = (1,) # formatting how size=1 is typically preferred - else: - size = field_params[i]["size"] - results.append((fields[i], built_in_type, size)) - except KeyError: - results.append((fields[i], built_in_type)) - return results - - @staticmethod - def _get_normal(loaded): - return loaded - - def _get_option(self, specs, name): - """Gets a specs value, underlying spec is either a dict or a class""" - attr = getattr(self, specs) - if isinstance(attr, dict): - return attr.get(name) - else: - return getattr(attr, name) - - def _parse_spec(self, loaded_spec): - """Parses and creates traditional libEnsemble dictionary from loaded dict info""" - - field_f = { - "sim_f": self._get_func, - "gen_f": self._get_func, - "alloc_f": self._get_func, - "inputs": self._get_normal, - "persis_in": self._get_normal, - "outputs": self._get_outputs, - "globus_compute_endpoint": self._get_normal, - "user": self._get_normal, - } - - userf_fields = [f for f in loaded_spec if f in field_f.keys()] - - if len(userf_fields): - for f in userf_fields: - loaded_spec[f] = field_f[f](loaded_spec[f]) - - return loaded_spec - - def _parameterize(self, loaded): - """Updates and sets attributes from specs loaded from file""" - for f in loaded: - loaded_spec = self._parse_spec(loaded[f]) - old_spec = getattr(self, f) - ClassType = CORRESPONDING_CLASSES[f] - if isinstance(old_spec, dict): - old_spec.update(loaded_spec) - if old_spec.get("in") and old_spec.get("inputs"): - old_spec.pop("inputs") # avoid clashes - elif old_spec.get("out") and old_spec.get("outputs"): - old_spec.pop("outputs") # avoid clashes - setattr(self, f, ClassType(**old_spec)) - else: # None. attribute not set yet - setattr(self, f, ClassType(**loaded_spec)) - - def from_yaml(self, file_path: str): - """Parameterizes libEnsemble from ``yaml`` file""" - with open(file_path, "r") as f: - loaded = yaml.full_load(f) - - self._parameterize(loaded) - - def from_toml(self, file_path: str): - """Parameterizes libEnsemble from ``toml`` file""" - with open(file_path, "rb") as f: - loaded = tomli.load(f) - - self._parameterize(loaded) - - def from_json(self, file_path: str): - """Parameterizes libEnsemble from ``json`` file""" - with open(file_path, "rb") as f: - loaded = json.load(f) - - self._parameterize(loaded) - - def add_random_streams(self, num_streams: int = 0, seed: str = ""): - """ - - Adds ``np.random`` generators for each worker ID to ``self.persis_info``. - - Parameters - ---------- - - num_streams: int, Optional - - Number of matching worker ID and random stream entries to create. Defaults to - ``self.nworkers``. - - seed: str, Optional - - Seed for NumPy's RNG. - - """ - if num_streams: - nstreams = num_streams - else: - nstreams = self.nworkers - - self.persis_info = add_unique_random_streams(self.persis_info, nstreams + 1, seed=seed) - return self.persis_info - def save_output(self, basename: str, append_attrs: bool = True): """ Writes out History array and persis_info to files. - If using a workflow_dir, will place with specified filename in that directory. + If using a ``workflow_dir_path`` in ``libE_specs``, will place with specified filename in that directory. Parameters ---------- Format: ``_results_History_length=_evals=_ranks=`` - To have the filename be only the basename, set append_attrs=False + To have the filename be only the basename, set ``append_attrs=False`` Format: ``_results_History_length=_evals=_ranks=`` """ if self.is_manager: - if self._get_option("libE_specs", "workflow_dir_path"): + if getattr(self.libE_specs, "workflow_dir_path", False): save_libE_output( self.H, self.persis_info, diff --git a/libensemble/executors/executor.py b/libensemble/executors/executor.py index bd7e50704f..369308ada7 100644 --- a/libensemble/executors/executor.py +++ b/libensemble/executors/executor.py @@ -30,6 +30,9 @@ # To change logging level for just this module # logger.setLevel(logging.DEBUG) +# Placeholder for container support - replaced with simulation directory at runtime +LIBE_SIM_DIR_PLACEHOLDER = "%LIBENSEMBLE_SIM_DIR%" + STATES = """ UNKNOWN CREATED @@ -60,7 +63,7 @@ class ExecutorException(Exception): class TimeoutExpired(Exception): """Timeout exception raised when Timeout expires""" - def __init__(self, task: str, timeout: float) -> None: + def __init__(self, task: str, timeout: float | None) -> None: self.task = task self.timeout = timeout @@ -148,9 +151,9 @@ def __init__( self.stderr = stderr or self.name + ".err" self.workdir = workdir self.dry_run = dry_run - self.runline = None + self.runline: str | None = None self.run_attempts = 0 - self.env = {} + self.env: dict[str, str] = {} self.ngpus_req = 0 def reset(self) -> None: @@ -175,11 +178,11 @@ def workdir_exists(self) -> bool | None: def file_exists_in_workdir(self, filename: str) -> bool: """Returns true if the named file exists in the task's workdir""" - return self.workdir and os.path.exists(os.path.join(self.workdir, filename)) + return self.workdir and os.path.exists(Path(self.workdir) / filename) def read_file_in_workdir(self, filename: str) -> str: """Opens and reads the named file in the task's workdir""" - path = os.path.join(self.workdir, filename) + path = Path(self.workdir) / filename if not os.path.exists(path): raise ValueError(f"{filename} not found in working directory") with open(path) as f: @@ -236,6 +239,7 @@ def _set_complete(self) -> None: self.state = "FINISHED" else: self.calc_task_timing() + assert self.process is not None self.errcode = self.process.returncode self.success = self.errcode == 0 self.state = "FINISHED" if self.success else "FAILED" @@ -251,6 +255,7 @@ def poll(self) -> None: return # Poll the task + assert self.process is not None poll = self.process.poll() if poll is None: self.state = "RUNNING" @@ -327,7 +332,7 @@ def done(self) -> bool: self.poll() return self.finished - def kill(self, wait_time: int = 60) -> None: + def kill(self, wait_time: int | None = 60) -> None: """Kills or cancels the supplied task Parameters @@ -423,14 +428,15 @@ def __init__(self) -> None: """ self.manager_signal = None - self.default_apps = {"sim": None, "gen": None} - self.apps = {} + self.default_apps: dict[str, Application | None] = {"sim": None, "gen": None} + self.apps: dict[str, Application] = {} self.wait_time = 60 - self.list_of_tasks = [] + self.list_of_tasks: list[Task] = [] self.workerID = None self.comm = None self.last_task = 0 + self.base_dir = os.getcwd() Executor.executor = self def __enter__(self): @@ -444,12 +450,12 @@ def serial_setup(self): pass # To be overloaded @property - def sim_default_app(self) -> Application: + def sim_default_app(self) -> Application | None: """Returns the default simulation app""" return self.default_apps["sim"] @property - def gen_default_app(self) -> Application: + def gen_default_app(self) -> Application | None: """Returns the default generator app""" return self.default_apps["gen"] @@ -464,7 +470,7 @@ def get_app(self, app_name: str) -> Application: ) return app - def default_app(self, calc_type: str) -> Application: + def default_app(self, calc_type: str) -> Application | None: """Gets the default app for a given calc type""" app = self.default_apps.get(calc_type) jassert(calc_type in ["sim", "gen"], "Unrecognized calculation type", calc_type) @@ -522,6 +528,10 @@ def register_app( precedent: str, Optional Any str that should directly precede the application full path. + Supports the placeholder ``%LIBENSEMBLE_SIM_DIR%`` which is replaced + at runtime with the simulation directory as a relative path from + where the executor was created. This is useful for container exec + commands. """ if not app_name: @@ -533,10 +543,8 @@ def register_app( jassert(calc_type in self.default_apps, "Unrecognized calculation type", calc_type) self.default_apps[calc_type] = self.apps[app_name] - def manager_poll(self) -> int: + def manager_poll(self) -> int | None: """ - .. _manager_poll_label: - Polls for a manager signal The executor manager_signal attribute will be updated. @@ -544,12 +552,13 @@ def manager_poll(self) -> int: self.manager_signal = None # Reset + assert self.comm is not None # Check for messages; disregard anything but a stop signal if not self.comm.mail_flag(): - return + return None mtag, man_signal = self.comm.recv() if mtag != STOP_TAG: - return + return None # Process the signal and push back on comm (for now) self.manager_signal = man_signal @@ -572,8 +581,8 @@ def manager_kill_received(self) -> bool: def polling_loop( self, task: Task, timeout: int | None = None, delay: float = 0.1, poll_manager: bool = False ) -> int: - """Optional, blocking, generic task status polling loop. Operates until the task - finishes, times out, or is optionally killed via a manager signal. On completion, returns a + """Blocking, generic task status polling loop. Operates until the task + finishes, times out, or is killed via a manager signal. On completion, returns a presumptive :ref:`calc_status` integer. Useful for running an application via the Executor until it stops without monitoring its intermediate output. @@ -673,10 +682,26 @@ def set_worker_info(self, comm=None, workerid=None) -> None: self.workerID = workerid self.comm = comm - def _check_app_exists(self, full_path: str) -> None: + def _check_app_exists(self, app: Application) -> None: """Allows submit function to check if app exists and error if not""" - if not os.path.isfile(full_path): - raise ExecutorException(f"Application does not exist {full_path}") + if app.precedent: + # Could be a container call in precedent. In that case, + # the executable is not available on the host system and + # we just forward what the user provided. + return + + if not os.path.isfile(app.full_path): + raise ExecutorException(f"Application does not exist {app.full_path}") + + def _set_sim_dir_env(self, task: Task, run_cmd: list[str]) -> list[str]: + """Replace simulation directory placeholder in run command if present. + + Supports container-based execution where the simulation directory needs to be + passed to container exec commands (e.g., podman-hpc exec --workdir). + """ + sim_dir = os.path.relpath(task.workdir, self.base_dir) + task._add_to_env("LIBENSEMBLE_SIM_DIR", sim_dir) + return [arg.replace(LIBE_SIM_DIR_PLACEHOLDER, sim_dir) for arg in run_cmd] def submit( self, @@ -685,13 +710,13 @@ def submit( app_args: str | None = None, stdout: str | None = None, stderr: str | None = None, - dry_run: bool | None = False, - wait_on_start: bool | None = False, + dry_run: bool = False, + wait_on_start: bool = False, env_script: str | None = None, ) -> Task: """Create a new task and run as a local serial subprocess. - The created :class:`task` object is returned. + Returns :class:`task` object. Parameters ---------- @@ -734,6 +759,7 @@ def submit( The launched task object """ + app: Application | None = None if app_name is not None: app = self.get_app(app_name) elif calc_type is not None: @@ -741,16 +767,21 @@ def submit( else: raise ExecutorException("Either app_name or calc_type must be set") + assert app is not None + default_workdir = os.getcwd() task = Task(app, app_args, default_workdir, stdout, stderr, self.workerID, dry_run) if not dry_run: - self._check_app_exists(task.app.full_path) + self._check_app_exists(task.app) runline = task.app.app_cmd.split() if task.app_args is not None: runline.extend(task.app_args.split()) + runline = self._set_sim_dir_env(task, runline) + task.runline = " ".join(runline) + if dry_run: logger.info(f"Test (No submit) Runline: {' '.join(runline)}") else: diff --git a/libensemble/executors/mpi_executor.py b/libensemble/executors/mpi_executor.py index 9b167ddaa1..5a0190d5c4 100644 --- a/libensemble/executors/mpi_executor.py +++ b/libensemble/executors/mpi_executor.py @@ -1,9 +1,9 @@ """ This module launches and controls the running of MPI applications. -In order to create an MPI executor, the calling script should contain: +In order to create an MPI executor, the script should contain:: -.. code-block:: python + from libensemble.executors.mpi_executor import MPIExecutor exctr = MPIExecutor() @@ -17,7 +17,7 @@ import time import libensemble.utils.launcher as launcher -from libensemble.executors.executor import Executor, ExecutorException, Task +from libensemble.executors.executor import Application, Executor, ExecutorException, Task from libensemble.executors.mpi_runner import MPIRunner from libensemble.resources.mpi_resources import get_MPI_variant @@ -71,8 +71,6 @@ class MPIExecutor(Executor): from libensemble.executors.mpi_executor import MPIExecutor exctr = MPIExecutor(custom_info=customizer) - - """ def __init__(self, custom_info: dict = {}) -> None: @@ -185,7 +183,7 @@ def _launch_with_retries( else: break - def submit( + def submit( # type: ignore[override] self, calc_type: str | None = None, app_name: str | None = None, @@ -198,18 +196,18 @@ def submit( stdout: str | None = None, stderr: str | None = None, stage_inout: str | None = None, - hyperthreads: bool | None = False, - dry_run: bool | None = False, - wait_on_start: bool | None = False, + hyperthreads: bool = False, + dry_run: bool = False, + wait_on_start: bool = False, extra_args: str | None = None, - auto_assign_gpus: bool | None = False, - match_procs_to_gpus: bool | None = False, + auto_assign_gpus: bool = False, + match_procs_to_gpus: bool = False, env_script: str | None = None, mpi_runner_type: str | dict | None = None, ) -> Task: """Creates a new task, and either executes or schedules execution. - The created :class:`task` object is returned. + Returns :class:`task` object. The user must supply either the app_name or calc_type arguments (app_name is recommended). All other arguments are optional. @@ -306,6 +304,7 @@ def submit( then the available resources will be divided among workers. """ + app: Application | None = None if app_name is not None: app = self.get_app(app_name) elif calc_type is not None: @@ -313,11 +312,13 @@ def submit( else: raise ExecutorException("Either app_name or calc_type must be set") + assert app is not None + default_workdir = os.getcwd() task = Task(app, app_args, default_workdir, stdout, stderr, self.workerID, dry_run) if not dry_run: - self._check_app_exists(task.app.full_path) + self._check_app_exists(task.app) if stage_inout is not None: logger.warning("stage_inout option ignored in this " "executor - runs in-place") @@ -363,7 +364,8 @@ def submit( if task.app_args is not None: runline.extend(task.app_args.split()) - task.runline = " ".join(runline) # Allow to be queried + runline = self._set_sim_dir_env(task, runline) + task.runline = " ".join(runline) if env_script is not None: run_cmd = Executor._process_env_script(task, runline, env_script) diff --git a/libensemble/gen_classes/__init__.py b/libensemble/gen_classes/__init__.py new file mode 100644 index 0000000000..d0524159da --- /dev/null +++ b/libensemble/gen_classes/__init__.py @@ -0,0 +1,2 @@ +from .aposmm import APOSMM # noqa: F401 +from .sampling import UniformSample # noqa: F401 diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py new file mode 100644 index 0000000000..0ecad8968e --- /dev/null +++ b/libensemble/gen_classes/aposmm.py @@ -0,0 +1,384 @@ +import copy +import warnings +from math import gamma, pi, sqrt +from typing import Any, Dict, List, Optional + +import numpy as np +from gest_api.vocs import VOCS +from numpy import typing as npt + +from libensemble.generators import PersistentGenInterfacer +from libensemble.message_numbers import EVAL_GEN_TAG, PERSIS_STOP + + +class APOSMM(PersistentGenInterfacer): + """ + APOSMM coordinates multiple local optimization runs, dramatically reducing time for + discovering multiple minima on parallel systems. + + This *generator* adheres to the `Generator Standard `_. + + .. seealso:: + + `https://doi.org/10.1007/s12532-017-0131-4 `_ + + VOCS variables must include both regular and ``*_on_cube`` versions. E.g.,: + + .. code-block:: python + + vars_std = { + "var1": [-10.0, 10.0], + "var2": [0.0, 100.0], + "var3": [1.0, 50.0], + "var1_on_cube": [0, 1.0], + "var2_on_cube": [0, 1.0], + "var3_on_cube": [0, 1.0], + } + variables_mapping = { + "x": ["var1", "var2", "var3"], + "x_on_cube": ["var1_on_cube", "var2_on_cube", "var3_on_cube"], + } + gen = APOSMM(vocs, 3, 3, variables_mapping=variables_mapping, ...) + + Getting started + --------------- + + APOSMM requires a minimal sample before starting optimization. A random sample across the domain + can either be retrieved via a ``suggest()`` call right after initialization, or the user can ingest + a set of sample points via ``ingest()``. The minimal sample size is specified via the ``initial_sample_size`` + parameter. This many evaluated sample points *must* be provided to APOSMM before it will provide any + local optimization points. + + .. code-block:: python + + # Approach 1: Retrieve sample points via suggest() + gen = APOSMM(vocs, max_active_runs=2, initial_sample_size=10) + + # ask APOSMM for some sample points + initial_sample = gen.suggest(10) + for point in initial_sample: + point["f"] = func(point["x"]) + gen.ingest(initial_sample) + + # APOSMM will now provide local-optimization points. + points = gen.suggest(10) + + # ---------------- + + # Approach 2: Ingest pre-computed sample points via ingest() + gen = APOSMM(vocs, max_active_runs=2, initial_sample_size=10) + + initial_sample = create_initial_sample() + for point in initial_sample: + point["f"] = func(point["x"]) + + # provide APOSMM with sample points + gen.ingest(initial_sample) + + # APOSMM will now provide local-optimization points. + points = gen.suggest(10) + + ... + + + .. important:: + After the initial sample phase, APOSMM cannot accept additional "arbitrary" + sample points that are not associated with local optimization runs. + + + .. code-block:: python + + gen = APOSMM(vocs, max_active_runs=2, initial_sample_size=10) + + # ask APOSMM for some sample points + initial_sample = gen.suggest(10) + for point in initial_sample: + point["f"] = func(point["x"]) + gen.ingest(initial_sample) + + # APOSMM will now provide local-optimization points. + points_from_aposmm = gen.suggest(10) + for point in points_from_aposmm: + point["f"] = func(point["x"]) + gen.ingest(points_from_aposmm) + + gen.ingest(another_sample) # THIS CRASHES + + Parameters + ---------- + vocs: ``VOCS`` + The VOCS object, adhering to the VOCS interface from the Generator Standard. + + max_active_runs: ``int`` + Bound on number of runs APOSMM is *concurrently* advancing. + + initial_sample_size: ``int`` + + Minimal sample points required before starting optimization. + + If ``suggest(N)`` is called first, APOSMM produces this many random sample points across the domain, + with ``N <= initial_sample_size``. + + If ``ingest(sample)`` is called first, multiple calls like ``ingest(sample)`` are required until + the total number of points ingested is ``>= initial_sample_size``. + + History: ``npt.NDArray`` = ``[]`` + An optional history of previously evaluated points. + + sample_points: ``npt.NDArray`` = ``None`` + Included for compatibility with the underlying algorithm. + Points to be sampled (original domain). + If more sample points are needed by APOSMM during the course of the + optimization, points will be drawn uniformly over the domain. + + localopt_method: ``str`` = "scipy_Nelder-Mead" (scipy) or "LN_BOBYQA" (nlopt) + The local optimization method to use. Others being added over time. + + mu: ``float`` = ``1e-8`` + Distance from the boundary that all localopt starting points must satisfy + + nu: ``float`` = ``1e-8`` + Distance from identified minima that all starting points must satisfy + + rk_const: ``float`` = ``None`` + Multiplier in front of the ``r_k`` value. + If not provided, it will be set to ``0.5 * ((gamma(1 + (n / 2)) * 5) ** (1 / n)) / sqrt(pi)`` + + xtol_abs: ``float`` = ``1e-6`` + Localopt method's convergence tolerance. + + ftol_abs: ``float`` = ``1e-6`` + Localopt method's convergence tolerance. + + opt_return_codes: ``list[int]`` = ``[0]`` + scipy only: List of return codes that determine if a point should be ruled a local minimum. + + dist_to_bound_multiple: ``float`` = ``0.5`` + What fraction of the distance to the nearest boundary should the initial + step size be in localopt runs. + + random_seed: ``int`` = ``1`` + Seed for the random number generator. + """ + + returns_id = True + + def _validate_vocs(self, vocs: VOCS): + if len(vocs.constraints): + warnings.warn("APOSMM does not support constraints in VOCS. Ignoring.") + if len(vocs.constants): + warnings.warn("APOSMM does not support constants in VOCS. Ignoring.") + + def __init__( + self, + vocs: VOCS, + max_active_runs: int, + initial_sample_size: int, + History: npt.NDArray = [], + sample_points: Optional[npt.NDArray] = None, + localopt_method: str = "scipy_Nelder-Mead", + rk_const: Optional[float] = None, + xtol_abs: float = 1e-6, + ftol_abs: float = 1e-6, + opt_return_codes: list[int] = [0], + mu: float = 1e-8, + nu: float = 1e-8, + dist_to_bound_multiple: float = 0.05, + random_seed: int = 1, + **kwargs, + ) -> None: + + from libensemble.gen_funcs.persistent_aposmm import aposmm + + self.vocs = vocs + + gen_specs: Dict[str, Any] = {} + gen_specs["user"] = {} + libE_info: Dict[str, Any] = {} + gen_specs["gen_f"] = aposmm + n = len(list(vocs.variables.keys())) + + if not rk_const: + rk_const = 0.5 * ((gamma(1 + (n / 2)) * 5) ** (1 / n)) / sqrt(pi) + + FIELDS = [ + "initial_sample_size", + "sample_points", + "localopt_method", + "rk_const", + "xtol_abs", + "ftol_abs", + "mu", + "nu", + "opt_return_codes", + "dist_to_bound_multiple", + "max_active_runs", + "random_seed", + ] + + for k in FIELDS: + val = locals().get(k) + if val is not None: + gen_specs["user"][k] = val + + super().__init__(vocs, History, {}, gen_specs, libE_info, **kwargs) + + # Set bounds using the correct x mapping + x_mapping = self.variables_mapping["x"] + self.gen_specs["user"]["lb"] = np.array([vocs.variables[var].domain[0] for var in x_mapping]) + self.gen_specs["user"]["ub"] = np.array([vocs.variables[var].domain[1] for var in x_mapping]) + + x_size = len(self.variables_mapping.get("x", [])) + x_on_cube_size = len(self.variables_mapping.get("x_on_cube", [])) + + try: + assert x_size > 0 and x_on_cube_size > 0 + except AssertionError: + raise ValueError( + """ User must provide a variables_mapping dictionary in the following format: + + variables = {"core": [-3, 3], "edge": [-2, 2], "core_on_cube": [0, 1], "edge_on_cube": [0, 1]} + objectives = {"energy": "MINIMIZE"} + + variables_mapping = { + "x": ["core", "edge"], + "x_on_cube": ["core_on_cube", "edge_on_cube"], + "f": ["energy"], + } + """ + ) + try: + assert x_size == x_on_cube_size + except AssertionError: + raise ValueError( + "Within the variables_mapping dictionary, x and x_on_cube " + + f"must have same length but got {x_size} and {x_on_cube_size}" + ) + + gen_specs["out"] = [ + ("x", float, x_size), + ("x_on_cube", float, x_on_cube_size), + ("sim_id", int), + ("local_min", bool), + ("local_pt", bool), + ] + + gen_specs["persis_in"] = ["sim_id", "x", "x_on_cube", "f", "sim_ended"] + if "components" in kwargs or "components" in gen_specs.get("user", {}): + gen_specs["persis_in"].append("fvec") + + # SH - Need to know if this is gen_on_manager or not. + self.persis_info["nworkers"] = gen_specs["user"].get("max_active_runs") + self.all_local_minima: List[npt.NDArray] = [] + self._suggest_idx = 0 + self._last_suggest: Optional[npt.NDArray] = None + self._ingest_buf: Optional[npt.NDArray] = None + self._n_buffd_results = 0 + self._told_initial_sample = False + self._first_called_method: Optional[str] = None + self._last_call: Optional[str] = None + self._last_num_points = 0 + + def _slot_in_data(self, results): + """Slot in libE_calc_in and trial data into corresponding array fields. *Initial sample only!!*""" + for name in results.dtype.names: + if name == "_id": + self._ingest_buf["sim_id"][self._n_buffd_results : self._n_buffd_results + len(results)] = results[ + "_id" + ] + else: + self._ingest_buf[name][self._n_buffd_results : self._n_buffd_results + len(results)] = results[name] + + def _enough_initial_sample(self): + return ( + self._n_buffd_results >= int(self.gen_specs["user"]["initial_sample_size"]) + ) or self._told_initial_sample + + def _ready_to_suggest_genf(self): + """ + We're presumably ready to be suggested IF: + - When we're working on the initial sample: + - We have no _last_suggest cached + - all points given out have returned AND we've been suggested *at least* as many points as we cached + - When we're done with the initial sample: + - we've been suggested *at least* as many points as we cached + - we've just ingested some results + """ + if not self._told_initial_sample and self._last_suggest is not None: + cond = all([i in self._ingest_buf["sim_id"] for i in self._last_suggest["sim_id"]]) + else: + cond = True + return self._last_suggest is None or (cond and (self._suggest_idx >= len(self._last_suggest))) + + def suggest_numpy(self, num_points: int = 0) -> npt.NDArray: + """Request the next set of points to evaluate, as a NumPy array.""" + + if self._first_called_method is None: + self._first_called_method = "suggest" + self.gen_specs["user"]["generate_sample_points"] = True + + if self._ready_to_suggest_genf(): + self._suggest_idx = 0 + if self._last_call == "suggest" and num_points == 0 and self._last_num_points == 0: + self.finalize() + raise RuntimeError("Cannot suggest points since APOSMM is currently expecting to receive a sample") + self._last_suggest = super().suggest_numpy(num_points) + assert self._last_suggest is not None + + if self._last_suggest["local_min"].any(): # filter out local minima rows + min_idxs = self._last_suggest["local_min"] + self.all_local_minima.append(self._last_suggest[min_idxs]) + self._last_suggest = self._last_suggest[~min_idxs] + + if num_points > 0: # we've been suggested for a selection of the last suggest + assert self._last_suggest is not None + results = np.copy(self._last_suggest[self._suggest_idx : self._suggest_idx + num_points]) + self._suggest_idx += num_points + + else: + results = np.copy(self._last_suggest) + self._last_suggest = None + + self._last_call = "suggest" + self._last_num_points = num_points + return results + + def ingest_numpy(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: + + if self._first_called_method is None: + self._first_called_method = "ingest" + self.gen_specs["user"]["generate_sample_points"] = False + + if (results is None and tag == PERSIS_STOP) or self._told_initial_sample: + super().ingest_numpy(results, tag) + self._last_call = "ingest" + return + + # Initial sample buffering here: + + if self._n_buffd_results == 0: + # Create a dtype that includes sim_id but excludes _id + descr = [d for d in results.dtype.descr if d[0] != "_id"] + if "sim_id" not in [d[0] for d in descr]: + descr.append(("sim_id", int)) + self._ingest_buf = np.zeros(self.gen_specs["user"]["initial_sample_size"], dtype=descr) + + if not self._enough_initial_sample(): + self._slot_in_data(np.copy(results)) + self._n_buffd_results += len(results) + + if self._enough_initial_sample(): + assert self._ingest_buf is not None + if "sim_id" in results.dtype.names and not self._told_initial_sample: + self._ingest_buf["sim_id"] = range(len(self._ingest_buf)) + super().ingest_numpy(self._ingest_buf, tag) + self._told_initial_sample = True + self._n_buffd_results = 0 + + self._last_call = "ingest" + + def suggest_updates(self) -> List[npt.NDArray]: + """Request a list of NumPy arrays containing entries that have been identified as minima.""" + minima = copy.deepcopy(self.all_local_minima) + self.all_local_minima = [] + return minima diff --git a/libensemble/gen_classes/botorch_mfkg.py b/libensemble/gen_classes/botorch_mfkg.py new file mode 100644 index 0000000000..af321bc82b --- /dev/null +++ b/libensemble/gen_classes/botorch_mfkg.py @@ -0,0 +1,291 @@ +""" +Generator class for multi-fidelity Bayesian optimization using BoTorch's +Multi-Fidelity Knowledge Gradient (MFKG) acquisition function. + +Conforms to the gest-api ``Generator`` interface (``suggest``/``ingest``). +""" + +import torch +from botorch import fit_gpytorch_mll +from botorch.acquisition import PosteriorMean +from botorch.acquisition.cost_aware import InverseCostWeightedUtility +from botorch.acquisition.fixed_feature import FixedFeatureAcquisitionFunction +from botorch.acquisition.knowledge_gradient import qMultiFidelityKnowledgeGradient +from botorch.acquisition.utils import project_to_target_fidelity +from botorch.models.cost import AffineFidelityCostModel +from botorch.models.gp_regression_fidelity import SingleTaskMultiFidelityGP +from botorch.models.transforms.outcome import Standardize +from botorch.optim.optimize import optimize_acqf, optimize_acqf_mixed +from gest_api import Generator +from gest_api.vocs import VOCS +from gpytorch.mlls.exact_marginal_log_likelihood import ExactMarginalLogLikelihood + +__all__ = ["BoTorchMFKG"] + + +class BoTorchMFKG(Generator): + """ + Multi-fidelity Bayesian optimization using BoTorch's MFKG acquisition function. + + The VOCS must contain: + - At least two design variables (any names). + - A variable named ``fidelity`` with domain ``[0, 1]``. + - Exactly one objective. + + On the first call to ``suggest``, ``2 * n_init_samples`` points are returned: + each of the ``n_init_samples`` random design points is evaluated at both low + (fidelity=0) and high (fidelity=1) fidelity. After that, each ``suggest`` + call fits the GP, builds the MFKG acquisition function, and returns ``q`` + candidates chosen by ``optimize_acqf_mixed``. + + Because the internal optimiser always proposes exactly ``q`` candidates, + ``suggest(n_trials)`` returns ``min(n_trials, q)`` of them. Set + ``GenSpecs.batch_size = q`` so libEnsemble always requests the right number. + + Args: + vocs: VOCS object defining variables (must include ``fidelity``), objectives. + n_init_samples: Number of random design points for the initial batch. + Each is evaluated at both fidelities, so ``2 * n_init_samples`` + simulations are submitted on the first ``suggest`` call. + q: Number of MFKG candidates to propose per subsequent ``suggest`` call. + fidelity_weights: Mapping of fidelity-dimension index to weight, passed to + ``AffineFidelityCostModel``. Defaults to ``{fidelity_dim: 0.9}``. + fixed_cost: Fixed cost term for ``AffineFidelityCostModel``. + num_fantasies: Number of fantasy samples for MFKG. + num_restarts: ``num_restarts`` for BoTorch optimisation routines. + raw_samples: ``raw_samples`` for BoTorch optimisation routines. + seed: Random seed for reproducibility. + fidelity_variable: Name of the fidelity variable in VOCS. Defaults to + ``"fidelity"``. + """ + + def __init__( + self, + vocs: VOCS, + n_init_samples: int = 4, + q: int = 2, + fidelity_weights: dict | None = None, + fixed_cost: float = 0.1, + num_fantasies: int = 128, + num_restarts: int = 1, + raw_samples: int = 10, + seed: int = 42, + fidelity_variable: str = "fidelity", + *args, + **kwargs, + ): + self.vocs = vocs + self.n_init_samples = n_init_samples + self.q = q + self.num_fantasies = num_fantasies + self.num_restarts = num_restarts + self.raw_samples = raw_samples + self.fidelity_variable = fidelity_variable + self._initialized = False + + # Torch device / dtype settings + self._tkwargs = { + "dtype": torch.double, + "device": torch.device("cuda" if torch.cuda.is_available() else "cpu"), + } + + torch.manual_seed(seed) + + # Derive variable ordering from VOCS (preserves insertion order) + self._var_names = list(vocs.variables.keys()) + self._fidelity_dim = self._var_names.index(fidelity_variable) + self._design_var_names = [v for v in self._var_names if v != fidelity_variable] + self._n_design = len(self._design_var_names) + + # Build bounds tensor: shape (2, n_vars) + lowers = [vocs.variables[v].domain[0] for v in self._var_names] + uppers = [vocs.variables[v].domain[1] for v in self._var_names] + self._bounds = torch.tensor([lowers, uppers], **self._tkwargs) + + # Target fidelity maps fidelity_dim → 1.0 + self._target_fidelities = {self._fidelity_dim: 1.0} + + # Cost model and cost-aware utility + if fidelity_weights is None: + fidelity_weights = {self._fidelity_dim: 0.9} + self._cost_model = AffineFidelityCostModel(fidelity_weights=fidelity_weights, fixed_cost=fixed_cost) + self._cost_aware_utility = InverseCostWeightedUtility(self._cost_model) + + # Accumulated training data (populated by ingest) + self._train_x: torch.Tensor | None = None + self._train_obj: torch.Tensor | None = None + + # Pending candidates proposed in the most recent suggest call, waiting + # for ingest to receive their objective values. + self._pending_x: torch.Tensor | None = None + + super().__init__(vocs, *args, **kwargs) + + # ------------------------------------------------------------------ + # gest-api interface + # ------------------------------------------------------------------ + + def _validate_vocs(self, vocs: VOCS) -> None: + assert len(vocs.variable_names) >= 2, "VOCS must have at least two variables." + assert ( + self.fidelity_variable in vocs.variables + ), f"VOCS must contain a variable named '{self.fidelity_variable}'." + assert len(vocs.objective_names) == 1, "VOCS must contain exactly one objective." + + def suggest(self, n_trials: int) -> list[dict]: + """ + Return up to ``n_trials`` candidate points. + + On the first call, returns ``2 * n_init_samples`` initial points + (random design coordinates evaluated at both fidelities). On + subsequent calls, fits the MFKG model and returns ``q`` candidates, + capped at ``n_trials``. + """ + if not self._initialized: + candidates = self._initial_candidates() + self._initialized = True + else: + candidates = self._mfkg_candidates() + + # Respect n_trials: never return more than requested + candidates = candidates[:n_trials] + self._pending_x = candidates + return self._tensor_to_dicts(candidates) + + def ingest(self, results: list[dict]) -> None: + """ + Receive evaluated objective values and append to training data. + + Args: + results: List of dicts, each containing variable names and the + objective name(s) as keys. + """ + if not results: + return + + obj_name = self.vocs.objective_names[0] + + # Reconstruct x tensor in the same variable order used for suggest + x_rows = [] + obj_rows = [] + for r in results: + x_row = [r[v] for v in self._var_names] + x_rows.append(x_row) + obj_rows.append([r[obj_name]]) + + new_x = torch.tensor(x_rows, **self._tkwargs) + new_obj = torch.tensor(obj_rows, **self._tkwargs) + + if self._train_x is None: + self._train_x = new_x + self._train_obj = new_obj + else: + self._train_x = torch.cat([self._train_x, new_x]) + self._train_obj = torch.cat([self._train_obj, new_obj]) + + # ------------------------------------------------------------------ + # Internal helpers + # ------------------------------------------------------------------ + + def _initial_candidates(self) -> torch.Tensor: + """ + Generate 2 * n_init_samples initial candidates: each of n_init_samples + random design points is duplicated at low (0) and high (1) fidelity. + """ + design_lowers = [self.vocs.variables[v].domain[0] for v in self._design_var_names] + design_uppers = [self.vocs.variables[v].domain[1] for v in self._design_var_names] + lb = torch.tensor(design_lowers, **self._tkwargs) + ub = torch.tensor(design_uppers, **self._tkwargs) + + design_pts = lb + (ub - lb) * torch.rand(self.n_init_samples, self._n_design, **self._tkwargs) + + lf = torch.zeros(self.n_init_samples, 1, **self._tkwargs) + hf = torch.ones(self.n_init_samples, 1, **self._tkwargs) + + # Insert fidelity column at the correct position + lf_pts = self._insert_fidelity_col(design_pts, lf) + hf_pts = self._insert_fidelity_col(design_pts, hf) + + return torch.cat([lf_pts, hf_pts], dim=0) + + def _mfkg_candidates(self) -> torch.Tensor: + """Fit the GP, build MFKG, and optimise to get q candidates.""" + mll, model = self._initialize_model(self._train_x, self._train_obj) + fit_gpytorch_mll(mll) + mfkg_acqf = self._get_mfkg(model) + candidates = self._optimize_mfkg(mfkg_acqf) + return candidates.detach() + + def _insert_fidelity_col(self, design: torch.Tensor, fidelity: torch.Tensor) -> torch.Tensor: + """Reassemble full variable tensor inserting fidelity at the correct column.""" + n = design.shape[0] + full = torch.empty(n, len(self._var_names), **self._tkwargs) + design_col = 0 + for col, name in enumerate(self._var_names): + if name == self.fidelity_variable: + full[:, col] = fidelity.squeeze(-1) + else: + full[:, col] = design[:, design_col] + design_col += 1 + return full + + def _initialize_model(self, train_x: torch.Tensor, train_obj: torch.Tensor) -> tuple: + model = SingleTaskMultiFidelityGP( + train_x, + train_obj, + outcome_transform=Standardize(m=1), + data_fidelities=[self._fidelity_dim], + ) + mll = ExactMarginalLogLikelihood(model.likelihood, model) + return mll, model + + def _get_mfkg(self, model) -> qMultiFidelityKnowledgeGradient: + """Build the MFKG acquisition function.""" + # Estimate current value at target fidelity + curr_val_acqf = FixedFeatureAcquisitionFunction( + acq_function=PosteriorMean(model), + d=len(self._var_names), + columns=[self._fidelity_dim], + values=[1], + ) + _, current_value = optimize_acqf( + acq_function=curr_val_acqf, + bounds=self._bounds[:, [i for i in range(len(self._var_names)) if i != self._fidelity_dim]], + q=1, + num_restarts=self.num_restarts, + raw_samples=self.raw_samples, + options={"batch_limit": 10, "maxiter": 10}, + ) + + def _project(X): + return project_to_target_fidelity(X=X, target_fidelities=self._target_fidelities) + + return qMultiFidelityKnowledgeGradient( + model=model, + num_fantasies=self.num_fantasies, + current_value=current_value, + cost_aware_utility=self._cost_aware_utility, + project=_project, + ) + + def _optimize_mfkg(self, mfkg_acqf) -> torch.Tensor: + """Run optimize_acqf_mixed to propose q candidates.""" + candidates, _ = optimize_acqf_mixed( + acq_function=mfkg_acqf, + bounds=self._bounds, + fixed_features_list=[{self._fidelity_dim: 0.0}, {self._fidelity_dim: 1.0}], + q=self.q, + num_restarts=self.num_restarts, + raw_samples=self.raw_samples, + options={"batch_limit": 10, "maxiter": 10}, + ) + return candidates + + def _tensor_to_dicts(self, candidates: torch.Tensor) -> list[dict]: + """Convert a (n, n_vars) tensor to a list of variable-name dicts.""" + result = [] + candidates_np = candidates.cpu().numpy() + for row in candidates_np: + d = {name: float(row[i]) for i, name in enumerate(self._var_names)} + result.append(d) + return result diff --git a/libensemble/gen_classes/external/sampling.py b/libensemble/gen_classes/external/sampling.py new file mode 100644 index 0000000000..a62b1ed8f4 --- /dev/null +++ b/libensemble/gen_classes/external/sampling.py @@ -0,0 +1,64 @@ +import numpy as np +from gest_api import Generator +from gest_api.vocs import VOCS + +__all__ = [ + "UniformSample", + "UniformSampleArray", +] + + +class UniformSample(Generator): + """ + This sampler adheres to the gest-api VOCS interface and data structures (no numpy). + + Each variable is a scalar. + """ + + def __init__(self, vocs: VOCS): + self.vocs = vocs + self.rng = np.random.default_rng(1) + super().__init__(vocs) + + def _validate_vocs(self, vocs): + assert len(self.vocs.variable_names), "VOCS must contain variables." + + def suggest(self, n_trials): + output = [] + for _ in range(n_trials): + trial = {} + for key in self.vocs.variables: + trial[key] = self.rng.uniform(self.vocs.variables[key].domain[0], self.vocs.variables[key].domain[1]) + output.append(trial) + return output + + def ingest(self, calc_in): + pass # random sample so nothing to tell + + +class UniformSampleArray(Generator): + """ + This sampler adheres to the gest-api VOCS interface and data structures. + + Uses one array variable of any dimension. Array is a numpy array. + """ + + def __init__(self, vocs: VOCS): + self.vocs = vocs + self.rng = np.random.default_rng(1) + super().__init__(vocs) + + def _validate_vocs(self, vocs): + assert len(self.vocs.variables) == 1, "VOCS must contain exactly one variable." + + def suggest(self, n_trials): + output = [] + key = list(self.vocs.variables.keys())[0] + var = self.vocs.variables[key] + for _ in range(n_trials): + trial = {key: np.array([self.rng.uniform(bounds[0], bounds[1]) for bounds in var.domain])} + output.append(trial) + return output + + def ingest(self, calc_in): + pass # random sample so nothing to tell diff --git a/libensemble/gen_classes/gpCAM.py b/libensemble/gen_classes/gpCAM.py new file mode 100644 index 0000000000..5e419fb6f3 --- /dev/null +++ b/libensemble/gen_classes/gpCAM.py @@ -0,0 +1,153 @@ +"""Generator class exposing gpCAM functionality""" + +import time +from typing import List + +import numpy as np +from gest_api.vocs import VOCS +from gpcam import GPOptimizer as GP +from numpy import typing as npt + +# While there are class / func duplicates - re-use functions. +from libensemble.gen_funcs.persistent_gpCAM import ( + _calculate_grid_distances, + _eval_var, + _find_eligible_points, + _generate_mesh, + _read_testpoints, +) +from libensemble.generators import LibensembleGenerator + +__all__ = [ + "GP_CAM", + "GP_CAM_Covar", +] + + +# Equivalent to function persistent_gpCAM_ask_tell +class GP_CAM(LibensembleGenerator): + """ + This generation function constructs a global surrogate of `f` values. + + It is a batched method that produces a first batch uniformly random from + (lb, ub). On subsequent iterations, it calls an optimization method to + produce the next batch of points. This optimization might be too slow + (relative to the simulation evaluation time) for some use cases. + """ + + def __init__(self, vocs: VOCS, ask_max_iter: int = 10, random_seed: int = 1, *args, **kwargs): + + super().__init__(vocs, *args, **kwargs) + self.rng = np.random.default_rng(random_seed) + + self.lb = np.array([vocs.variables[i].domain[0] for i in vocs.variables]) + self.ub = np.array([vocs.variables[i].domain[1] for i in vocs.variables]) + self.n = len(self.lb) # dimension + self.all_x = np.empty((0, self.n)) + self.all_y = np.empty((0, 1)) + assert isinstance(self.n, int), "Dimension must be an integer" + assert isinstance(self.lb, np.ndarray), "lb must be a numpy array" + assert isinstance(self.ub, np.ndarray), "ub must be a numpy array" + + self.dtype = [("x", float, (self.n))] + + self.my_gp = None + self.noise = 1e-8 # 1e-12 + self.ask_max_iter = ask_max_iter + + def _validate_vocs(self, vocs): + assert len(vocs.variable_names), "VOCS must contain variables." + assert len(vocs.objective_names), "VOCS must contain at least one objective." + + def suggest_numpy(self, n_trials: int) -> npt.NDArray: + if self.all_x.shape[0] == 0: + self.x_new = self.rng.uniform(self.lb, self.ub, (n_trials, self.n)) + else: + start = time.time() + self.x_new = self.my_gp.ask( + input_set=np.column_stack((self.lb, self.ub)), + n=n_trials, + pop_size=n_trials, + acquisition_function="total correlation", + max_iter=self.ask_max_iter, # Larger takes longer. gpCAM default is 20. + )["x"] + print(f"Ask time:{time.time() - start}") + H_o = np.zeros(n_trials, dtype=self.dtype) + H_o["x"] = self.x_new + return H_o + + def ingest_numpy(self, calc_in: npt.NDArray) -> None: + if calc_in is not None: + if "x" in calc_in.dtype.names: # SH should we require x in? + self.x_new = np.atleast_2d(calc_in["x"]) + self.y_new = np.atleast_2d(calc_in["f"]).T + nan_indices = [i for i, fval in enumerate(self.y_new) if np.isnan(fval[0])] + self.x_new = np.delete(self.x_new, nan_indices, axis=0) + self.y_new = np.delete(self.y_new, nan_indices, axis=0) + + self.all_x = np.vstack((self.all_x, self.x_new)) + self.all_y = np.vstack((self.all_y, self.y_new)) + + noise_var = self.noise * np.ones(len(self.all_y)) + if self.my_gp is None: + self.my_gp = GP(self.all_x, self.all_y.flatten(), noise_variances=noise_var) + else: + self.my_gp.tell(self.all_x, self.all_y.flatten(), noise_variances=noise_var) + self.my_gp.train() + + +class GP_CAM_Covar(GP_CAM): + """ + This generation function constructs a global surrogate of `f` values. + + It is a batched method that produces a first batch uniformly random from + (lb, ub) and on following iterations samples the GP posterior covariance + function to find sample points. + """ + + def __init__(self, vocs: VOCS, test_points_file: str = None, use_grid: bool = False, *args, **kwargs): + super().__init__(vocs, *args, **kwargs) + self.test_points = _read_testpoints({"test_points_file": test_points_file}) + self.x_for_var = None + self.var_vals = None + self.use_grid = use_grid + self.persis_info = {} + if self.use_grid: + self.num_points = 10 + self.x_for_var = _generate_mesh(self.lb, self.ub, self.num_points) + self.r_low_init, self.r_high_init = _calculate_grid_distances(self.lb, self.ub, self.num_points) + + def suggest_numpy(self, n_trials: int) -> List[dict]: + if self.all_x.shape[0] == 0: + x_new = self.rng.uniform(self.lb, self.ub, (n_trials, self.n)) + else: + if not self.use_grid: + x_new = self.x_for_var[np.argsort(self.var_vals)[-n_trials:]] + else: + r_high = self.r_high_init + r_low = self.r_low_init + x_new = [] + r_cand = r_high # Let's start with a large radius and stop when we have batchsize points + + sorted_indices = np.argsort(-self.var_vals) + while len(x_new) < n_trials: + x_new = _find_eligible_points(self.x_for_var, sorted_indices, r_cand, n_trials) + if len(x_new) < n_trials: + r_high = r_cand + r_cand = (r_high + r_low) / 2.0 + + self.x_new = x_new + H_o = np.zeros(n_trials, dtype=self.dtype) + H_o["x"] = self.x_new + return H_o + + def ingest_numpy(self, calc_in: npt.NDArray): + if calc_in is not None: + super().ingest_numpy(calc_in) + if not self.use_grid: + n_trials = len(self.y_new) + self.x_for_var = self.rng.uniform(self.lb, self.ub, (10 * n_trials, self.n)) + + self.var_vals = _eval_var( + self.my_gp, self.all_x, self.all_y, self.x_for_var, self.test_points, self.persis_info + ) diff --git a/libensemble/gen_classes/sampling.py b/libensemble/gen_classes/sampling.py new file mode 100644 index 0000000000..0b06624480 --- /dev/null +++ b/libensemble/gen_classes/sampling.py @@ -0,0 +1,281 @@ +"""Generator classes providing points using sampling""" + +import numpy as np +from gest_api.vocs import VOCS + +from libensemble.generators import LibensembleGenerator + +__all__ = [ + "UniformSample", + "LatinHypercubeSample", + "UniformSampleObjComponents", + "UniformSampleWithVariableResources", + "UniformSampleWithVarPrioritiesAndResources", + "UniformSampleCancel", +] + + +class UniformSample(LibensembleGenerator): + """ + Samples over the domain specified in the VOCS. + + Implements ``suggest()`` and ``ingest()`` identically to the other generators. + """ + + def __init__(self, vocs: VOCS, random_seed: int = 1, *args, **kwargs): + super().__init__(vocs, *args, **kwargs) + self.rng = np.random.default_rng(random_seed) + + self.n = len(list(self.vocs.variables.keys())) + self.np_dtype = [(name, float) for name in self.vocs.variables.keys()] + self.lb = np.array([vocs.variables[i].domain[0] for i in vocs.variables]) + self.ub = np.array([vocs.variables[i].domain[1] for i in vocs.variables]) + + def suggest_numpy(self, n_trials): + out = np.zeros(n_trials, dtype=self.np_dtype) + + for i in range(n_trials): + vals = self.rng.uniform(self.lb, self.ub, (self.n)) + for j, name in enumerate(self.vocs.variables.keys()): + out[i][name] = vals[j] + + return out + + def ingest_numpy(self, calc_in): + pass # random sample so nothing to tell + + +def _lhs_unit_cube(n, k, rng): + """Generate ``k`` points in [0,1]^n using Latin hypercube sampling.""" + intervals = np.linspace(0, 1, k + 1) + rand_source = rng.uniform(0, 1, (k, n)) + rand_pts = np.zeros((k, n)) + sample = np.zeros((k, n)) + + a = intervals[:k] + b = intervals[1:] + for j in range(n): + rand_pts[:, j] = rand_source[:, j] * (b - a) + a + + for j in range(n): + sample[:, j] = rand_pts[rng.permutation(k), j] + + return sample + + +class LatinHypercubeSample(LibensembleGenerator): + """ + Latin hypercube sample over the domain specified in the VOCS. + + All ``n_trials`` points are drawn at once from a single LHS design, so + consecutive ``suggest()`` calls return new LHS designs (each independently + space-filling, but not stratified together). + """ + + def __init__(self, vocs: VOCS, random_seed: int = 1, *args, **kwargs): + super().__init__(vocs, *args, **kwargs) + self.rng = np.random.default_rng(random_seed) + + self.n = len(list(self.vocs.variables.keys())) + self.np_dtype = [(name, float) for name in self.vocs.variables.keys()] + self.lb = np.array([vocs.variables[i].domain[0] for i in vocs.variables]) + self.ub = np.array([vocs.variables[i].domain[1] for i in vocs.variables]) + + def suggest_numpy(self, n_trials): + out = np.zeros(n_trials, dtype=self.np_dtype) + + sample = _lhs_unit_cube(self.n, n_trials, self.rng) + scaled = sample * (self.ub - self.lb) + self.lb + for j, name in enumerate(self.vocs.variables.keys()): + out[name] = scaled[:, j] + + return out + + def ingest_numpy(self, calc_in): + pass + + +class UniformSampleObjComponents(LibensembleGenerator): + """ + Uniform random sample where each suggested point is replicated ``components`` + times so each objective component is evaluated separately. Each replicated row + carries the same ``x`` plus an ``obj_component`` index, a shared ``pt_id``, + and an independent random ``priority``. + + Used by component-aware solvers (e.g. POUNDERS, where each residual is its + own evaluation). The ``obj_component``, ``pt_id``, and ``priority`` fields are + libEnsemble H-array fields rather than VOCS objectives — downstream sim_f + is expected to read ``obj_component`` and return the matching residual. + """ + + def __init__(self, vocs: VOCS, components: int, random_seed: int = 1, *args, **kwargs): + super().__init__(vocs, *args, **kwargs) + self.rng = np.random.default_rng(random_seed) + self.components = components + + self.n = len(list(self.vocs.variables.keys())) + self.np_dtype = [(name, float) for name in self.vocs.variables.keys()] + [ + ("priority", float), + ("obj_component", int), + ("pt_id", int), + ] + self.lb = np.array([vocs.variables[i].domain[0] for i in vocs.variables]) + self.ub = np.array([vocs.variables[i].domain[1] for i in vocs.variables]) + self._pt_id_offset = 0 + + def suggest_numpy(self, n_trials): + m = self.components + out = np.zeros(n_trials * m, dtype=self.np_dtype) + + for i in range(n_trials): + x = self.rng.uniform(self.lb, self.ub, (1, self.n)) + slc = slice(i * m, (i + 1) * m) + for j, name in enumerate(self.vocs.variables.keys()): + out[name][slc] = x[0, j] + out["priority"][slc] = self.rng.uniform(0, 1, m) + out["obj_component"][slc] = np.arange(m) + out["pt_id"][slc] = self._pt_id_offset + i + + self._pt_id_offset += n_trials + return out + + def ingest_numpy(self, calc_in): + pass + + +class UniformSampleWithVariableResources(LibensembleGenerator): + """ + Uniform random sample that also requests a random number of resource sets per + evaluation (1 to ``max_resource_sets``). For testing/demonstrating variable + resource allocation. + + .. note:: + ``resource_sets`` is a libEnsemble manager-side H-array field, not a + VOCS variable. Whether the downstream libE manager honors it via this + new generator-class path depends on alloc_specs; the classic gen_funcs + path was tested with the default alloc. + """ + + def __init__( + self, vocs: VOCS, max_resource_sets: int, random_seed: int = 1, *args, **kwargs + ): + super().__init__(vocs, *args, **kwargs) + self.rng = np.random.default_rng(random_seed) + self.max_rsets = max_resource_sets + + self.n = len(list(self.vocs.variables.keys())) + self.np_dtype = [(name, float) for name in self.vocs.variables.keys()] + [ + ("resource_sets", int), + ] + self.lb = np.array([vocs.variables[i].domain[0] for i in vocs.variables]) + self.ub = np.array([vocs.variables[i].domain[1] for i in vocs.variables]) + + def suggest_numpy(self, n_trials): + out = np.zeros(n_trials, dtype=self.np_dtype) + + vals = self.rng.uniform(self.lb, self.ub, (n_trials, self.n)) + for j, name in enumerate(self.vocs.variables.keys()): + out[name] = vals[:, j] + out["resource_sets"] = self.rng.integers(1, self.max_rsets + 1, n_trials) + + return out + + def ingest_numpy(self, calc_in): + pass + + +class UniformSampleWithVarPrioritiesAndResources(LibensembleGenerator): + """ + Uniform random sample that emits an initial batch of ``initial_batch_size`` + points (each with one resource set and uniform priority), then on subsequent + calls emits one point at a time with a random number of resource sets (1 to + ``max_resource_sets``) and priority scaled by that count. + + .. note:: + Same caveat as ``UniformSampleWithVariableResources`` re: ``resource_sets`` + and ``priority`` being libEnsemble H-array fields rather than VOCS items. + """ + + def __init__( + self, + vocs: VOCS, + max_resource_sets: int, + initial_batch_size: int, + random_seed: int = 1, + *args, + **kwargs, + ): + super().__init__(vocs, *args, **kwargs) + self.rng = np.random.default_rng(random_seed) + self.max_rsets = max_resource_sets + self.initial_batch_size = initial_batch_size + self._initial_emitted = False + + self.n = len(list(self.vocs.variables.keys())) + self.np_dtype = [(name, float) for name in self.vocs.variables.keys()] + [ + ("resource_sets", int), + ("priority", float), + ] + self.lb = np.array([vocs.variables[i].domain[0] for i in vocs.variables]) + self.ub = np.array([vocs.variables[i].domain[1] for i in vocs.variables]) + + def suggest_numpy(self, n_trials): + if not self._initial_emitted: + b = self.initial_batch_size + out = np.zeros(b, dtype=self.np_dtype) + for i in range(b): + x = self.rng.uniform(self.lb, self.ub, (1, self.n)) + for j, name in enumerate(self.vocs.variables.keys()): + out[name][i] = x[0, j] + out["resource_sets"] = 1 + out["priority"] = 1.0 + self._initial_emitted = True + return out + + out = np.zeros(1, dtype=self.np_dtype) + x = self.rng.uniform(self.lb, self.ub) + for j, name in enumerate(self.vocs.variables.keys()): + out[name][0] = x[j] + out["resource_sets"][0] = self.rng.integers(1, self.max_rsets + 1) + out["priority"][0] = 10 * out["resource_sets"][0] + return out + + def ingest_numpy(self, calc_in): + pass + + +class UniformSampleCancel(LibensembleGenerator): + """ + Uniform random sample but every 10th point in each batch is emitted with + ``cancel_requested=True``. For testing immediate-cancellation paths. + + .. note:: + ``cancel_requested`` is a libEnsemble H-array field, not a VOCS variable. + Same caveat as the resource samplers. + """ + + def __init__(self, vocs: VOCS, random_seed: int = 1, *args, **kwargs): + super().__init__(vocs, *args, **kwargs) + self.rng = np.random.default_rng(random_seed) + + self.n = len(list(self.vocs.variables.keys())) + self.np_dtype = [(name, float) for name in self.vocs.variables.keys()] + [ + ("cancel_requested", bool), + ] + self.lb = np.array([vocs.variables[i].domain[0] for i in vocs.variables]) + self.ub = np.array([vocs.variables[i].domain[1] for i in vocs.variables]) + + def suggest_numpy(self, n_trials): + out = np.zeros(n_trials, dtype=self.np_dtype) + + vals = self.rng.uniform(self.lb, self.ub, (n_trials, self.n)) + for j, name in enumerate(self.vocs.variables.keys()): + out[name] = vals[:, j] + for i in range(n_trials): + if i % 10 == 0: + out["cancel_requested"][i] = True + + return out + + def ingest_numpy(self, calc_in): + pass diff --git a/libensemble/gen_funcs/aposmm_localopt_support.py b/libensemble/gen_funcs/aposmm_localopt_support.py index 2de29c8707..6d823e10fc 100644 --- a/libensemble/gen_funcs/aposmm_localopt_support.py +++ b/libensemble/gen_funcs/aposmm_localopt_support.py @@ -14,6 +14,7 @@ "run_external_localopt", ] +import traceback from multiprocessing import Event, Process, Queue import numpy as np @@ -44,9 +45,9 @@ class APOSMMException(Exception): if "dfols" in optimizers: import dfols # noqa: F401 if "ibcdfo_pounders" in optimizers: - from ibcdfo.pounders import pounders # noqa: F401 + from ibcdfo import run_pounders # noqa: F401 if "ibcdfo_manifold_sampling" in optimizers: - from ibcdfo.manifold_sampling import manifold_sampling_primal # noqa: F401 + from ibcdfo import run_MSP # noqa: F401 if "scipy" in optimizers: from scipy import optimize as sp_opt # noqa: F401 if "external_localopt" in optimizers: @@ -432,6 +433,8 @@ def run_local_ibcdfo_manifold_sampling(user_specs, comm_queue, x0, f0, child_can support that, so APOSMM assumes the first point will be re-evaluated (but not be sent back to the manager). """ + from ibcdfo import run_MSP # noqa: F811 + n = len(x0) # Define bound constraints (lower <= x <= upper) lb = np.zeros(n) @@ -449,7 +452,7 @@ def run_local_ibcdfo_manifold_sampling(user_specs, comm_queue, x0, f0, child_can # m = len(f0) subprob_switch = "linprog" - [X, F, hF, xkin, flag] = manifold_sampling_primal( + [X, F, hF, xkin, flag] = run_MSP( user_specs["hfun"], lambda x: scipy_dfols_callback_fun(x, comm_queue, child_can_read, parent_can_read, user_specs), x0, @@ -486,6 +489,8 @@ def run_local_ibcdfo_pounders(user_specs, comm_queue, x0, f0, child_can_read, pa support that, so APOSMM assumes the first point will be re-evaluated (but not be sent back to the manager). """ + from ibcdfo import run_pounders # noqa: F811 + n = len(x0) # Define bound constraints (lower <= x <= upper) lb = np.zeros(n) @@ -507,7 +512,7 @@ def run_local_ibcdfo_pounders(user_specs, comm_queue, x0, f0, child_can_read, pa else: Options = None - [X, F, hF, flag, xkin] = pounders( + [X, F, hF, flag, xkin] = run_pounders( lambda x: scipy_dfols_callback_fun(x, comm_queue, child_can_read, parent_can_read, user_specs), x0, n, @@ -645,8 +650,8 @@ def run_local_tao(user_specs, comm_queue, x0, f0, child_can_read, parent_can_rea def opt_runner(run_local_opt, user_specs, comm_queue, x0, f0, child_can_read, parent_can_read): try: run_local_opt(user_specs, comm_queue, x0, f0, child_can_read, parent_can_read) - except Exception as e: - comm_queue.put(ErrorMsg(e)) + except Exception: + comm_queue.put(ErrorMsg(traceback.format_exc())) parent_can_read.set() @@ -743,7 +748,7 @@ def put_set_wait_get(x, comm_queue, parent_can_read, child_can_read, user_specs) if user_specs.get("periodic"): assert np.allclose(x % 1, values[0] % 1, rtol=1e-15, atol=1e-15), "The point I gave is not the point I got back" else: - assert np.allclose(x, values[0], rtol=1e-15, atol=1e-15), "The point I gave is not the point I got back" + assert np.allclose(x, values[0], rtol=1e-8, atol=1e-8), "The point I gave is not the point I got back" return values diff --git a/libensemble/gen_funcs/persistent_aposmm.py b/libensemble/gen_funcs/persistent_aposmm.py index 685fa4021d..ac9a152ecb 100644 --- a/libensemble/gen_funcs/persistent_aposmm.py +++ b/libensemble/gen_funcs/persistent_aposmm.py @@ -16,6 +16,7 @@ from libensemble.gen_funcs.aposmm_localopt_support import ConvergedMsg, LocalOptInterfacer, simulate_recv_from_manager from libensemble.message_numbers import EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG +from libensemble.tools import get_rng from libensemble.tools.persistent_support import PersistentSupport # from scipy.spatial.distance import cdist @@ -163,6 +164,7 @@ def aposmm(H, persis_info, gen_specs, libE_info): try: user_specs = gen_specs["user"] ps = PersistentSupport(libE_info, EVAL_GEN_TAG) + rng = get_rng(gen_specs, libE_info) n, n_s, rk_const, ld, mu, nu, comm, local_H = initialize_APOSMM(H, user_specs, libE_info) ( local_opters, @@ -177,12 +179,31 @@ def aposmm(H, persis_info, gen_specs, libE_info): if user_specs["initial_sample_size"] != 0: # Send our initial sample. We don't need to check that n_s is large enough: # the alloc_func only returns when the initial sample has function values. - persis_info = add_k_sample_points_to_local_H( - user_specs["initial_sample_size"], user_specs, persis_info, n, comm, local_H, sim_id_to_child_inds - ) - if not user_specs.get("standalone"): + + if not user_specs.get("generate_sample_points", True): # add an extra receive for the sample points + # gonna loop here while the user suggests/ingests sample points until we reach the desired sample size + while n_s < user_specs["initial_sample_size"]: + tag, Work, presumptive_user_sample = ps.recv() + if presumptive_user_sample is not None: + n_s, n_r = update_local_H_after_receiving( + local_H, n, n_s, user_specs, Work, presumptive_user_sample, fields_to_pass, init=True + ) + something_sent = False + else: + persis_info = add_k_sample_points_to_local_H( + user_specs["initial_sample_size"], + user_specs, + persis_info, + n, + comm, + local_H, + sim_id_to_child_inds, + rng, + ) + something_sent = True + if not user_specs.get("standalone") and user_specs.get("generate_sample_points", True): ps.send(local_H[-user_specs["initial_sample_size"] :][[i[0] for i in gen_specs["out"]]]) - something_sent = True + something_sent = True else: something_sent = False @@ -275,7 +296,7 @@ def aposmm(H, persis_info, gen_specs, libE_info): if num_samples > 0: persis_info = add_k_sample_points_to_local_H( - num_samples, user_specs, persis_info, n, comm, local_H, sim_id_to_child_inds + num_samples, user_specs, persis_info, n, comm, local_H, sim_id_to_child_inds, rng ) new_inds_to_send_mgr = new_inds_to_send_mgr + list(range(len(local_H) - num_samples, len(local_H))) @@ -291,13 +312,17 @@ def aposmm(H, persis_info, gen_specs, libE_info): pass -def update_local_H_after_receiving(local_H, n, n_s, user_specs, Work, calc_in, fields_to_pass): +def update_local_H_after_receiving(local_H, n, n_s, user_specs, Work, calc_in, fields_to_pass, init=False): for name in ["f", "x_on_cube", "grad", "fvec"]: if name in fields_to_pass: assert name in calc_in.dtype.names, ( name + " must be returned to persistent_aposmm for localopt_method: " + user_specs["localopt_method"] ) + if init: + local_H.resize(len(calc_in), refcheck=False) + initialize_dists_and_inds(local_H, len(calc_in)) + for name in calc_in.dtype.names: local_H[name][Work["libE_info"]["H_rows"]] = calc_in[name] @@ -747,7 +772,7 @@ def initialize_children(user_specs): return local_opters, sim_id_to_child_inds, run_order, run_pts, total_runs, ended_runs, fields_to_pass -def add_k_sample_points_to_local_H(k, user_specs, persis_info, n, comm, local_H, sim_id_to_child_inds): +def add_k_sample_points_to_local_H(k, user_specs, persis_info, n, comm, local_H, sim_id_to_child_inds, rng): if "sample_points" in user_specs: v = np.sum(~local_H["local_pt"]) # Number of sample points so far sampled_points = user_specs["sample_points"][v : v + k] @@ -757,7 +782,7 @@ def add_k_sample_points_to_local_H(k, user_specs, persis_info, n, comm, local_H, k = k - len(sampled_points) if k > 0: - sampled_points = persis_info["rand_stream"].uniform(0, 1, (k, n)) + sampled_points = rng.uniform(0, 1, (k, n)) add_to_local_H(local_H, sampled_points, user_specs, on_cube=True) return persis_info diff --git a/libensemble/gen_funcs/persistent_botorch_mfkg_branin.py b/libensemble/gen_funcs/persistent_botorch_mfkg_branin.py deleted file mode 100644 index 5391fea12a..0000000000 --- a/libensemble/gen_funcs/persistent_botorch_mfkg_branin.py +++ /dev/null @@ -1,187 +0,0 @@ -""" -This file defines the persistent generator function for multi-fidelity Bayesian -optimization using BoTorch's Multi-Fidelity Knowledge Gradient (MFKG) acquisition function. - -This gen_f is meant to be used with the alloc_f function `only_persistent_gens`. -""" - -import numpy as np -import torch -from botorch import fit_gpytorch_mll -from botorch.acquisition import PosteriorMean -from botorch.acquisition.cost_aware import InverseCostWeightedUtility -from botorch.acquisition.fixed_feature import FixedFeatureAcquisitionFunction -from botorch.acquisition.knowledge_gradient import qMultiFidelityKnowledgeGradient -from botorch.acquisition.utils import project_to_target_fidelity -from botorch.models.cost import AffineFidelityCostModel -from botorch.models.gp_regression_fidelity import SingleTaskMultiFidelityGP -from botorch.models.transforms.outcome import Standardize -from botorch.optim.optimize import optimize_acqf, optimize_acqf_mixed -from gpytorch.mlls.exact_marginal_log_likelihood import ExactMarginalLogLikelihood - -from libensemble.message_numbers import EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG -from libensemble.tools.persistent_support import PersistentSupport - -__all__ = ["persistent_botorch_mfkg"] - -# Set seeds for reproducibility -np.random.seed(42) -torch.manual_seed(42) - -# Torch settings -tkwargs = { - "dtype": torch.double, - "device": torch.device("cuda" if torch.cuda.is_available() else "cpu"), -} - -# Specify bounds -dim = 3 -bounds = torch.tensor([[0.0] * dim, [1.0] * dim], **tkwargs) - -# Specify target fidelity -target_fidelities = {2: 1.0} - -# Specify cost model -cost_model = AffineFidelityCostModel(fidelity_weights={2: 0.9}, fixed_cost=0.1) -cost_aware_utility = InverseCostWeightedUtility(cost_model=cost_model) - - -# Custom function to project posterior to target fidelity (defer to default) -def project(X): - return project_to_target_fidelity(X=X, target_fidelities=target_fidelities) - - -# Wrapper function for compatibility with existing code -def problem(X, ps, gen_specs): - """ - Wrapper to convert tensor input to numpy and send to libE for evaluation. - - Args: - X: tensor of shape (n, 3) where columns are [x0, x1, fidelity] - ps: PersistentSupport object for communication - gen_specs: Generator specifications - - Returns: - tensor of shape (n,) with objective values - """ - # Send points to be evaluated - X_np = X.cpu().numpy() - H_o = np.zeros(len(X), dtype=gen_specs["out"]) - H_o["x"] = X_np[:, :2] - H_o["fidelity"] = X_np[:, 2] - - tag, Work, calc_in = ps.send_recv(H_o) - - # Convert results back to tensor - if calc_in is None or len(calc_in) == 0: - return None, tag - - train_obj = torch.tensor(calc_in["f"], **tkwargs).unsqueeze(-1) - - return train_obj, tag - - -# Function to generate training data -def generate_initial_data(n, ps, gen_specs): # Jeff: Initial sample size is twice this value of n - train_x = torch.rand(n, 2, **tkwargs) - train_lf = torch.zeros(n, 1) - train_hf = torch.ones(n, 1) - train_x_full_lf = torch.cat((train_x, train_lf), dim=1) - train_x_full_hf = torch.cat((train_x, train_hf), dim=1) - train_x_full = torch.cat((train_x_full_lf, train_x_full_hf), dim=0) - train_obj, tag = problem(train_x_full, ps, gen_specs) - return train_x_full, train_obj, tag - - -# Function to initialize a botorch model -def initialize_model(train_x, train_obj): - model = SingleTaskMultiFidelityGP(train_x, train_obj, outcome_transform=Standardize(m=1), data_fidelities=[2]) - mll = ExactMarginalLogLikelihood(model.likelihood, model) - return mll, model - - -# Multifidelity Knowledge Gradient acquisition function -def get_mfkg(model): - - curr_val_acqf = FixedFeatureAcquisitionFunction( - acq_function=PosteriorMean(model), - d=3, - columns=[2], - values=[1], - ) - - _, current_value = optimize_acqf( - acq_function=curr_val_acqf, - bounds=bounds[:, :-1], - q=1, # Jeff: Don't adjust this for some reason - num_restarts=1, # Jeff: I decreased this to make libE development faster - raw_samples=10, # Jeff: I decreased this to make libE development faster - options={"batch_limit": 10, "maxiter": 10}, # Jeff: I decreased this to make libE development faster - ) - - return qMultiFidelityKnowledgeGradient( - model=model, - num_fantasies=128, - current_value=current_value, - cost_aware_utility=cost_aware_utility, - project=project, - ) - - -# Optimization step -def optimize_mfkg_and_get_observation(mfkg_acqf, q, ps, gen_specs): - # Generate new candidates - candidates, _ = optimize_acqf_mixed( - acq_function=mfkg_acqf, - bounds=bounds, - fixed_features_list=[{2: 0.0}, {2: 1.0}], - q=q, # Jeff: This is the number of new samples to make - num_restarts=1, # Jeff: I decreased this to make libE development faster - raw_samples=10, # Jeff: I decreased this to make libE development faster - options={"batch_limit": 10, "maxiter": 10}, # Jeff: I decreased this to make libE development faster - ) - - # Observe new values - cost = cost_model(candidates).sum() - new_x = candidates.detach() - new_obj, tag = problem(new_x, ps, gen_specs) - return new_x, new_obj, cost, tag - - -# Function to perform a single iteration -def do_iteration(train_x, train_obj, q, ps, gen_specs): - mll, model = initialize_model(train_x, train_obj) - fit_gpytorch_mll(mll) - mfkg_acqf = get_mfkg(model) - new_x, new_obj, _, tag = optimize_mfkg_and_get_observation(mfkg_acqf, q, ps, gen_specs) - - if new_obj is None: - return model, train_x, train_obj, tag - - train_x = torch.cat([train_x, new_x]) - train_obj = torch.cat([train_obj, new_obj]) # Jeff: This is where the "sim" evaluation happens, and needs to be communicated back to the manager - - return model, train_x, train_obj, tag - - -def persistent_botorch_mfkg(H, persis_info, gen_specs, libE_info): - """ - Persistent generator function for multi-fidelity Bayesian optimization using BoTorch's MFKG. - """ - ps = PersistentSupport(libE_info, EVAL_GEN_TAG) - - # Extract user parameters - ub = gen_specs["user"]["ub"] - lb = gen_specs["user"]["lb"] - n_init_samples = gen_specs["user"]["n_init_samples"] - q = gen_specs["user"]["q"] - - # ## Perform Multifidelity Bayesian Optimization - # Generate initial data - train_x, train_obj, tag = generate_initial_data(n_init_samples, ps, gen_specs) - - # Step - while tag not in [STOP_TAG, PERSIS_STOP]: - model, train_x, train_obj, tag = do_iteration(train_x, train_obj, q, ps, gen_specs) - - return None, persis_info, FINISHED_PERSISTENT_GEN_TAG diff --git a/libensemble/gen_funcs/persistent_fd_param_finder.py b/libensemble/gen_funcs/persistent_fd_param_finder.py index d72f64537a..2f45327a76 100644 --- a/libensemble/gen_funcs/persistent_fd_param_finder.py +++ b/libensemble/gen_funcs/persistent_fd_param_finder.py @@ -119,4 +119,4 @@ def fd_param_finder(H, persis_info, gen_specs, libE_info): tag, Work, calc_in = ps.send_recv(H0) persis_info["Fnoise"] = Fnoise - return H0, persis_info, FINISHED_PERSISTENT_GEN_TAG + return None, persis_info, FINISHED_PERSISTENT_GEN_TAG diff --git a/libensemble/gen_funcs/persistent_gpCAM.py b/libensemble/gen_funcs/persistent_gpCAM.py index 262ca2d6b0..62e460dfe0 100644 --- a/libensemble/gen_funcs/persistent_gpCAM.py +++ b/libensemble/gen_funcs/persistent_gpCAM.py @@ -7,6 +7,7 @@ from numpy.lib.recfunctions import repack_fields from libensemble.message_numbers import EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG +from libensemble.tools import get_rng from libensemble.tools.persistent_support import PersistentSupport __all__ = [ @@ -15,10 +16,9 @@ ] -def _initialize_gpcAM(user_specs, libE_info, persis_info): +def _initialize_gpcAM(user_specs, libE_info, persis_info, gen_specs): """Extract user params""" - rng_seed = user_specs.get("rng_seed") # Will default to None - rng = persis_info.get("rand_stream") or np.random.default_rng(rng_seed) + rng = get_rng(gen_specs, libE_info) b = user_specs["batch_size"] lb = np.array(user_specs["lb"]) ub = np.array(user_specs["ub"]) @@ -166,7 +166,9 @@ def persistent_gpCAM(H_in, persis_info, gen_specs, libE_info): `test_gpCAM.py `_ """ # noqa - rng, batch_size, n, lb, ub, x_new, y_new, ps = _initialize_gpcAM(gen_specs["user"], libE_info, persis_info) + rng, batch_size, n, lb, ub, x_new, y_new, ps = _initialize_gpcAM( + gen_specs["user"], libE_info, persis_info, gen_specs + ) ask_max_iter = gen_specs["user"].get("ask_max_iter") or 10 test_points = _read_testpoints(gen_specs["user"]) noise = 1e-8 # Initializes noise @@ -230,7 +232,9 @@ def persistent_gpCAM_covar(H_in, persis_info, gen_specs, libE_info): noise = 1e-12 test_points = _read_testpoints(U) - rng, batch_size, n, lb, ub, x_new, y_new, ps = _initialize_gpcAM(gen_specs["user"], libE_info, persis_info) + rng, batch_size, n, lb, ub, x_new, y_new, ps = _initialize_gpcAM( + gen_specs["user"], libE_info, persis_info, gen_specs + ) # Send batches until manager sends stop tag tag = None diff --git a/libensemble/gen_funcs/persistent_inverse_bayes.py b/libensemble/gen_funcs/persistent_inverse_bayes.py index 2f677902b0..ad0b1acaa2 100644 --- a/libensemble/gen_funcs/persistent_inverse_bayes.py +++ b/libensemble/gen_funcs/persistent_inverse_bayes.py @@ -1,6 +1,7 @@ import numpy as np from libensemble.message_numbers import EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG +from libensemble.tools import get_rng from libensemble.tools.persistent_support import PersistentSupport __all__ = [ @@ -10,6 +11,7 @@ def persistent_updater_after_likelihood(H, persis_info, gen_specs, libE_info): """ """ + rng = get_rng(gen_specs, libE_info) ub = gen_specs["user"]["ub"] lb = gen_specs["user"]["lb"] n = len(lb) @@ -29,11 +31,11 @@ def persistent_updater_after_likelihood(H, persis_info, gen_specs, libE_info): for j in range(num_subbatches): for i in range(subbatch_size): row = subbatch_size * j + i - H_o["x"][row] = persis_info["rand_stream"].uniform(lb, ub, (1, n)) + H_o["x"][row] = rng.uniform(lb, ub, (1, n)) H_o["subbatch"][row] = j H_o["batch"][row] = batch - H_o["prior"][row] = np.random.randn() - H_o["prop"][row] = np.random.randn() + H_o["prior"][row] = rng.standard_normal() + H_o["prop"][row] = rng.standard_normal() # Send data and get next assignment tag, Work, calc_in = ps.send_recv(H_o) diff --git a/libensemble/gen_funcs/persistent_sampling.py b/libensemble/gen_funcs/persistent_sampling.py index 375d7f4387..a12002e5c0 100644 --- a/libensemble/gen_funcs/persistent_sampling.py +++ b/libensemble/gen_funcs/persistent_sampling.py @@ -3,7 +3,7 @@ import numpy as np from libensemble.message_numbers import EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG -from libensemble.specs import output_data, persistent_input_fields +from libensemble.tools import get_rng from libensemble.tools.persistent_support import PersistentSupport __all__ = [ @@ -16,9 +16,9 @@ ] -def _get_user_params(user_specs): +def _get_user_params(user_specs, gen_specs): """Extract user params""" - b = user_specs["initial_batch_size"] + b = gen_specs.get("initial_batch_size") or user_specs.get("initial_batch_size") or gen_specs.get("init_sample_size") ub = user_specs["ub"] lb = user_specs["lb"] n = len(lb) # dimension @@ -29,8 +29,6 @@ def _get_user_params(user_specs): return b, n, lb, ub -@persistent_input_fields(["sim_id"]) -@output_data([("x", float, (2,))]) # The dimension of 2 is a default and can be overwritten def persistent_uniform(_, persis_info, gen_specs, libE_info): """ This generation function always enters into persistent mode and returns @@ -43,19 +41,18 @@ def persistent_uniform(_, persis_info, gen_specs, libE_info): `test_persistent_uniform_sampling.py `_ `test_persistent_uniform_sampling_async.py `_ """ # noqa + rng = get_rng(gen_specs, libE_info) - b, n, lb, ub = _get_user_params(gen_specs["user"]) + b, n, lb, ub = _get_user_params(gen_specs.get("user", {}), gen_specs) ps = PersistentSupport(libE_info, EVAL_GEN_TAG) # Send batches until manager sends stop tag tag = None while tag not in [STOP_TAG, PERSIS_STOP]: H_o = np.zeros(b, dtype=gen_specs["out"]) - H_o["x"] = persis_info["rand_stream"].uniform(lb, ub, (b, n)) + H_o["x"] = rng.uniform(lb, ub, (b, n)) if "obj_component" in H_o.dtype.fields: - H_o["obj_component"] = persis_info["rand_stream"].integers( - low=0, high=gen_specs["user"]["num_components"], size=b - ) + H_o["obj_component"] = rng.integers(low=0, high=gen_specs["user"]["num_components"], size=b) tag, Work, calc_in = ps.send_recv(H_o) if hasattr(calc_in, "__len__"): b = len(calc_in) @@ -73,7 +70,7 @@ def persistent_uniform_final_update(_, persis_info, gen_specs, libE_info): `test_persistent_uniform_sampling_running_mean.py `_ """ # noqa - b, n, lb, ub = _get_user_params(gen_specs["user"]) + b, n, lb, ub = _get_user_params(gen_specs.get("user", {}), gen_specs) ps = PersistentSupport(libE_info, EVAL_GEN_TAG) def generate_corners(x, y): @@ -124,7 +121,6 @@ def sample_corners_with_probability(corners, p, b): running_total[row["corner_id"]] += row["f"] # Having received a PERSIS_STOP, update f_est field for all points and return - # For manager to honor final H_o return, must have set libE_specs["use_persis_return_gen"] = True f_est = running_total / number_of_samples H_o = np.zeros(len(sent), dtype=[("sim_id", int), ("corner_id", int), ("f_est", float)]) for count, i in enumerate(sent): @@ -145,7 +141,8 @@ def persistent_request_shutdown(_, persis_info, gen_specs, libE_info): .. seealso:: `test_persistent_uniform_gen_decides_stop.py `_ """ # noqa - b, n, lb, ub = _get_user_params(gen_specs["user"]) + rng = get_rng(gen_specs, libE_info) + b, n, lb, ub = _get_user_params(gen_specs.get("user", {}), gen_specs) shutdown_limit = gen_specs["user"]["shutdown_limit"] f_count = 0 ps = PersistentSupport(libE_info, EVAL_GEN_TAG) @@ -154,7 +151,7 @@ def persistent_request_shutdown(_, persis_info, gen_specs, libE_info): tag = None while tag not in [STOP_TAG, PERSIS_STOP]: H_o = np.zeros(b, dtype=gen_specs["out"]) - H_o["x"] = persis_info["rand_stream"].uniform(lb, ub, (b, n)) + H_o["x"] = rng.uniform(lb, ub, (b, n)) tag, Work, calc_in = ps.send_recv(H_o) if hasattr(calc_in, "__len__"): b = len(calc_in) @@ -173,14 +170,15 @@ def uniform_nonblocking(_, persis_info, gen_specs, libE_info): .. seealso:: `test_persistent_uniform_sampling.py `_ """ # noqa - b, n, lb, ub = _get_user_params(gen_specs["user"]) + rng = get_rng(gen_specs, libE_info) + b, n, lb, ub = _get_user_params(gen_specs.get("user", {}), gen_specs) ps = PersistentSupport(libE_info, EVAL_GEN_TAG) # Send batches until manager sends stop tag tag = None while tag not in [STOP_TAG, PERSIS_STOP]: H_o = np.zeros(b, dtype=gen_specs["out"]) - H_o["x"] = persis_info["rand_stream"].uniform(lb, ub, (b, n)) + H_o["x"] = rng.uniform(lb, ub, (b, n)) ps.send(H_o) received = False @@ -220,10 +218,15 @@ def batched_history_matching(_, persis_info, gen_specs, libE_info): .. seealso:: `test_persistent_uniform_sampling.py `_ """ # noqa + rng = get_rng(gen_specs, libE_info) lb = gen_specs["user"]["lb"] n = len(lb) - b = gen_specs["user"]["initial_batch_size"] + b = ( + gen_specs.get("initial_batch_size") + or gen_specs["user"].get("initial_batch_size") + or gen_specs.get("init_sample_size") + ) q = gen_specs["user"]["num_best_vals"] ps = PersistentSupport(libE_info, EVAL_GEN_TAG) @@ -233,7 +236,7 @@ def batched_history_matching(_, persis_info, gen_specs, libE_info): while tag not in [STOP_TAG, PERSIS_STOP]: H_o = np.zeros(b, dtype=gen_specs["out"]) - H_o["x"] = persis_info["rand_stream"].multivariate_normal(mu, Sigma, b) + H_o["x"] = rng.multivariate_normal(mu, Sigma, b) # Send data and get next assignment tag, Work, calc_in = ps.send_recv(H_o) @@ -247,10 +250,15 @@ def batched_history_matching(_, persis_info, gen_specs, libE_info): def persistent_uniform_with_cancellations(_, persis_info, gen_specs, libE_info): + rng = get_rng(gen_specs, libE_info) ub = gen_specs["user"]["ub"] lb = gen_specs["user"]["lb"] n = len(lb) - b = gen_specs["user"]["initial_batch_size"] + b = ( + gen_specs.get("initial_batch_size") + or gen_specs["user"].get("initial_batch_size") + or gen_specs.get("init_sample_size") + ) # Start cancelling points from half initial batch onward cancel_from = b // 2 # Should get at least this many points back @@ -261,7 +269,7 @@ def persistent_uniform_with_cancellations(_, persis_info, gen_specs, libE_info): tag = None while tag not in [STOP_TAG, PERSIS_STOP]: H_o = np.zeros(b, dtype=gen_specs["out"]) - H_o["x"] = persis_info["rand_stream"].uniform(lb, ub, (b, n)) + H_o["x"] = rng.uniform(lb, ub, (b, n)) tag, Work, calc_in = ps.send_recv(H_o) if hasattr(calc_in, "__len__"): diff --git a/libensemble/gen_funcs/persistent_sampling_var_resources.py b/libensemble/gen_funcs/persistent_sampling_var_resources.py index 394ef75815..efa227bad2 100644 --- a/libensemble/gen_funcs/persistent_sampling_var_resources.py +++ b/libensemble/gen_funcs/persistent_sampling_var_resources.py @@ -12,6 +12,7 @@ from libensemble.executors.executor import Executor from libensemble.message_numbers import EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG +from libensemble.tools import get_rng from libensemble.tools.persistent_support import PersistentSupport from libensemble.tools.test_support import check_gpu_setting @@ -25,11 +26,11 @@ ] -def _get_user_params(user_specs): +def _get_user_params(gen_specs): """Extract user params""" - b = user_specs["initial_batch_size"] - ub = user_specs["ub"] - lb = user_specs["lb"] + b = gen_specs["initial_batch_size"] + ub = gen_specs["user"]["ub"] + lb = gen_specs["user"]["lb"] n = len(lb) # dimension return b, n, lb, ub @@ -43,8 +44,8 @@ def uniform_sample(_, persis_info, gen_specs, libE_info): `test_uniform_sampling_with_variable_resources.py `_ """ # noqa - b, n, lb, ub = _get_user_params(gen_specs["user"]) - rng = persis_info["rand_stream"] + b, n, lb, ub = _get_user_params(gen_specs) + rng = get_rng(gen_specs, libE_info) ps = PersistentSupport(libE_info, EVAL_GEN_TAG) tag = None @@ -76,8 +77,8 @@ def uniform_sample_with_var_gpus(_, persis_info, gen_specs, libE_info): `test_GPU_variable_resources.py `_ """ # noqa - b, n, lb, ub = _get_user_params(gen_specs["user"]) - rng = persis_info["rand_stream"] + b, n, lb, ub = _get_user_params(gen_specs) + rng = get_rng(gen_specs, libE_info) ps = PersistentSupport(libE_info, EVAL_GEN_TAG) tag = None max_gpus = gen_specs["user"]["max_gpus"] @@ -111,8 +112,8 @@ def uniform_sample_with_procs_gpus(_, persis_info, gen_specs, libE_info): `test_GPU_variable_resources.py `_ """ # noqa - b, n, lb, ub = _get_user_params(gen_specs["user"]) - rng = persis_info["rand_stream"] + b, n, lb, ub = _get_user_params(gen_specs) + rng = get_rng(gen_specs, libE_info) ps = PersistentSupport(libE_info, EVAL_GEN_TAG) tag = None @@ -137,8 +138,8 @@ def uniform_sample_with_var_priorities(_, persis_info, gen_specs, libE_info): resource sets and priorities are requested for each point. """ - b, n, lb, ub = _get_user_params(gen_specs["user"]) - rng = persis_info["rand_stream"] + b, n, lb, ub = _get_user_params(gen_specs) + rng = get_rng(gen_specs, libE_info) ps = PersistentSupport(libE_info, EVAL_GEN_TAG) H_o = np.zeros(b, dtype=gen_specs["out"]) @@ -175,8 +176,8 @@ def uniform_sample_diff_simulations(_, persis_info, gen_specs, libE_info): `test_GPU_variable_resources_multi_task.py `_ """ # noqa - b, n, lb, ub = _get_user_params(gen_specs["user"]) - rng = persis_info["rand_stream"] + b, n, lb, ub = _get_user_params(gen_specs) + rng = get_rng(gen_specs, libE_info) ps = PersistentSupport(libE_info, EVAL_GEN_TAG) tag = None @@ -209,8 +210,8 @@ def uniform_sample_with_sim_gen_resources(_, persis_info, gen_specs, libE_info): `test_GPU_variable_resources.py `_ """ # noqa - b, n, lb, ub = _get_user_params(gen_specs["user"]) - rng = persis_info["rand_stream"] + b, n, lb, ub = _get_user_params(gen_specs) + rng = get_rng(gen_specs, libE_info) ps = PersistentSupport(libE_info, EVAL_GEN_TAG) tag = None diff --git a/libensemble/gen_funcs/persistent_surmise_calib.py b/libensemble/gen_funcs/persistent_surmise_calib.py index b28f6cd36b..70a362db21 100644 --- a/libensemble/gen_funcs/persistent_surmise_calib.py +++ b/libensemble/gen_funcs/persistent_surmise_calib.py @@ -17,6 +17,7 @@ thetaprior, ) from libensemble.message_numbers import EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG +from libensemble.tools import get_rng from libensemble.tools.persistent_support import PersistentSupport @@ -101,15 +102,15 @@ def cancel_columns(obs_offset, c, n_x, pending, ps): ps.request_cancel_sim_ids(sim_ids_to_cancel) -def assign_priority(n_x, n_thetas): +def assign_priority(n_x, n_thetas, rng): """Assign priorities to points.""" # Arbitrary priorities priority = np.arange(n_x * n_thetas) - np.random.shuffle(priority) + rng.shuffle(priority) return priority -def load_H(H, xs, thetas, offset=0, set_priorities=False): +def load_H(H, xs, thetas, rng, offset=0, set_priorities=False): """Fill inputs into H0. There will be num_points x num_thetas entries @@ -122,7 +123,7 @@ def load_H(H, xs, thetas, offset=0, set_priorities=False): if set_priorities: n_x = len(xs) - H["priority"] = assign_priority(n_x, n_thetas) + H["priority"] = assign_priority(n_x, n_thetas, rng) def gen_truevals(x, gen_specs): @@ -139,7 +140,7 @@ def gen_truevals(x, gen_specs): def surmise_calib(H, persis_info, gen_specs, libE_info): """Generator to select and obviate parameters for calibration.""" - rand_stream = persis_info["rand_stream"] + rng = get_rng(gen_specs, libE_info) n_thetas = gen_specs["user"]["n_init_thetas"] n_x = gen_specs["user"]["num_x_vals"] # Num of x points step_add_theta = gen_specs["user"]["step_add_theta"] # No. of thetas to generate per step @@ -149,10 +150,10 @@ def surmise_calib(H, persis_info, gen_specs, libE_info): priorscale = gen_specs["user"]["priorscale"] ps = PersistentSupport(libE_info, EVAL_GEN_TAG) - prior = thetaprior(priorloc, priorscale) + prior = thetaprior(priorloc, priorscale, rng) # Create points at which to evaluate the sim - x = gen_xs(n_x, rand_stream) + x = gen_xs(n_x, rng) H_o = gen_truevals(x, gen_specs) obs_offset = len(H_o) @@ -163,12 +164,12 @@ def surmise_calib(H, persis_info, gen_specs, libE_info): returned_fevals = np.reshape(calc_in["f"], (1, n_x)) true_fevals = returned_fevals - obs, obsvar = gen_observations(true_fevals, obsvar_const, rand_stream) + obs, obsvar = gen_observations(true_fevals, obsvar_const, rng) # Generate a batch of inputs and load into H H_o = np.zeros(n_x * (n_thetas), dtype=gen_specs["out"]) theta = gen_thetas(prior, n_thetas) - load_H(H_o, x, theta, set_priorities=True) + load_H(H_o, x, theta, rng, set_priorities=True) tag, Work, calc_in = ps.send_recv(H_o) # ------------------------------------------------------------------------- @@ -233,7 +234,7 @@ def surmise_calib(H, persis_info, gen_specs, libE_info): # n_thetas = step_add_theta H_o = np.zeros(n_x * (len(new_theta)), dtype=gen_specs["out"]) - load_H(H_o, x, new_theta, set_priorities=True) + load_H(H_o, x, new_theta, rng, set_priorities=True) tag, Work, calc_in = ps.send_recv(H_o) # Determine evaluations to cancel diff --git a/libensemble/gen_funcs/persistent_tasmanian.py b/libensemble/gen_funcs/persistent_tasmanian.py deleted file mode 100644 index ac491cf6a0..0000000000 --- a/libensemble/gen_funcs/persistent_tasmanian.py +++ /dev/null @@ -1,362 +0,0 @@ -""" -A persistent generator using the uncertainty quantification capabilities in -`Tasmanian `_. -""" - -import numpy as np - -from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as allocf -from libensemble.message_numbers import EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG -from libensemble.tools import parse_args -from libensemble.tools.persistent_support import PersistentSupport - -__all__ = [ - "sparse_grid_batched", - "sparse_grid_async", -] - - -def lex_le(x, y, tol=1e-12): - """ - Returns True if x <= y lexicographically up to some tolerance. - """ - cmp = np.fabs(x - y) > tol - ind = np.argmax(cmp) - if not cmp[ind]: - return True - return x[ind] <= y[ind] - - -def get_2D_insert_indices(x, y, x_ord=np.empty(0, dtype="int"), y_ord=np.empty(0, dtype="int"), tol=1e-12): - """ - Finds the row indices in a 2D numpy array `x` for which the sorted values of `y` can be inserted - into. If `x_ord` (resp. `y_ord`) is empty, then `x` (resp. `y`) must be lexicographically - sorted. Otherwise, `x[x_ord]` (resp. `y[y_ord]`) must be lexicographically sorted. Complexity is - O(x.shape[0] + y.shape[0]). - """ - assert len(x.shape) == 2 - assert len(y.shape) == 2 - if x.size == 0: - return np.zeros(y.shape[0], dtype="int") - else: - if x_ord.size == 0: - x_ord = np.arange(x.shape[0], dtype="int") - if y_ord.size == 0: - y_ord = np.arange(y.shape[0], dtype="int") - x_ptr = 0 - y_ptr = 0 - out_ord = np.empty(0, dtype="int") - while y_ptr < y.shape[0]: - # The case where y[k] <= max of x[k:end, :] - xk = x[x_ord[x_ptr], :] - yk = y[y_ord[y_ptr], :] - if lex_le(yk, xk, tol=tol): - out_ord = np.append(out_ord, x_ord[x_ptr]) - y_ptr += 1 - else: - x_ptr += 1 - # The edge case where y[k] is the largest of all elements of x. - if x_ptr >= x_ord.shape[0]: - for i in range(y_ptr, y_ord.shape[0], 1): - out_ord = np.append(out_ord, x_ord.shape[0]) - y_ptr += 1 - break - return out_ord - - -def get_2D_duplicate_indices(x, y, x_ord=np.empty(0, dtype="int"), y_ord=np.empty(0, dtype="int"), tol=1e-12): - """ - Finds the row indices of a 2D numpy array `x` which overlap with `y`. If `x_ord` (resp. `y_ord`) - is empty, then `x` (resp. `y`) must be lexicographically sorted. Otherwise, `x[x_ord]` (resp. - `y[y_ord]`) must be lexicographically sorted.Complexity is O(x.shape[0] + y.shape[0]). - """ - assert len(x.shape) == 2 - assert len(y.shape) == 2 - if x.size == 0: - return np.empty(0, dtype="int") - else: - if x_ord.size == 0: - x_ord = np.arange(x.shape[0], dtype="int") - if y_ord.size == 0: - y_ord = np.arange(y.shape[0], dtype="int") - x_ptr = 0 - y_ptr = 0 - out_ord = np.empty(0, dtype="int") - while y_ptr < y.shape[0] and x_ptr < x.shape[0]: - # The case where y[k] <= max of x[k:end, :] - xk = x[x_ord[x_ptr], :] - yk = y[y_ord[y_ptr], :] - if all(np.fabs(yk - xk) <= tol): - out_ord = np.append(out_ord, x_ord[x_ptr]) - x_ptr += 1 - elif lex_le(xk, yk, tol=tol): - x_ptr += 1 - else: - y_ptr += 1 - return out_ord - - -def get_state(queued_pts, queued_ids, id_offset, new_points=np.array([]), completed_points=np.array([]), tol=1e-12): - """ - Creates the data to be sent and updates the state arrays and scalars if new information - (new_points or completed_points) arrives. Ensures that the output state arrays remain sorted if - the input state arrays are already sorted. - """ - if new_points.size > 0: - new_points_ord = np.lexsort(np.rot90(new_points)) - new_points_ids = id_offset + np.arange(new_points.shape[0]) - id_offset += new_points.shape[0] - insert_idx = get_2D_insert_indices(queued_pts, new_points, y_ord=new_points_ord, tol=tol) - queued_pts = np.insert(queued_pts, insert_idx, new_points[new_points_ord], axis=0) - queued_ids = np.insert(queued_ids, insert_idx, new_points_ids[new_points_ord], axis=0) - - if completed_points.size > 0: - completed_ord = np.lexsort(np.rot90(completed_points)) - delete_ind = get_2D_duplicate_indices(queued_pts, completed_points, y_ord=completed_ord, tol=tol) - queued_pts = np.delete(queued_pts, delete_ind, axis=0) - queued_ids = np.delete(queued_ids, delete_ind, axis=0) - - return queued_pts, queued_ids, id_offset - - -def get_H0(gen_specs, refined_pts, refined_ord, queued_pts, queued_ids, tol=1e-12): - """ - For runs following the first one, get the history array H0 based on the ordering in `refined_pts` - """ - - def approx_eq(x, y): - return np.argmax(np.fabs(x - y)) <= tol - - num_ids = queued_ids.shape[0] - H0 = np.zeros(num_ids, dtype=gen_specs["out"]) - refined_priority = np.flip(np.arange(refined_pts.shape[0], dtype="int")) - rptr = 0 - for qptr in range(num_ids): - while not approx_eq(refined_pts[refined_ord[rptr]], queued_pts[qptr]): - rptr += 1 - assert rptr <= refined_pts.shape[0] - H0["x"][qptr] = queued_pts[qptr] - H0["sim_id"][qptr] = queued_ids[qptr] - H0["priority"][qptr] = refined_priority[refined_ord[rptr]] - return H0 - - -# ======================== -# Main generator functions -# ======================== - - -def sparse_grid_batched(H, persis_info, gen_specs, libE_info): - """ - Implements batched construction for a Tasmanian sparse grid, - using the loop described in Tasmanian Example 09: - `sparse grid example `_ - - """ - U = gen_specs["user"] - ps = PersistentSupport(libE_info, EVAL_GEN_TAG) - grid = U["tasmanian_init"]() # initialize the grid - allowed_refinements = [ - "setAnisotropicRefinement", - "getAnisotropicRefinement", - "setSurplusRefinement", - "getSurplusRefinement", - "none", - ] - assert ( - "refinement" in U and U["refinement"] in allowed_refinements - ), f"Must provide a gen_specs['user']['refinement'] in: {allowed_refinements}" - - while grid.getNumNeeded() > 0: - aPoints = grid.getNeededPoints() - - H0 = np.zeros(len(aPoints), dtype=gen_specs["out"]) - H0["x"] = aPoints - - # Receive values from manager - tag, Work, calc_in = ps.send_recv(H0) - if tag in [STOP_TAG, PERSIS_STOP]: - break - aModelValues = calc_in["f"] - - # Update surrogate on grid - t = aModelValues.reshape((aModelValues.shape[0], grid.getNumOutputs())) - t = t.flatten() - t = np.atleast_2d(t).T - grid.loadNeededPoints(t) - - if "tasmanian_checkpoint_file" in U: - grid.write(U["tasmanian_checkpoint_file"]) - - # set refinement, using user["refinement"] to pick the refinement strategy - if U["refinement"] in ["setAnisotropicRefinement", "getAnisotropicRefinement"]: - assert "sType" in U - assert "iMinGrowth" in U - assert "iOutput" in U - grid.setAnisotropicRefinement(U["sType"], U["iMinGrowth"], U["iOutput"]) - elif U["refinement"] in ["setSurplusRefinement", "getSurplusRefinement"]: - assert "fTolerance" in U - assert "iOutput" in U - assert "sCriteria" in U - grid.setSurplusRefinement(U["fTolerance"], U["iOutput"], U["sCriteria"]) - - return None, persis_info, FINISHED_PERSISTENT_GEN_TAG - - -def sparse_grid_async(H, persis_info, gen_specs, libE_info): - """ - Implements asynchronous construction for a Tasmanian sparse grid, - using the logic in the dynamic Tasmanian model construction function: - `sparse grid dynamic example `_ - - """ - U = gen_specs["user"] - ps = PersistentSupport(libE_info, EVAL_GEN_TAG) - grid = U["tasmanian_init"]() # initialize the grid - allowed_refinements = ["getCandidateConstructionPoints", "getCandidateConstructionPointsSurplus"] - assert ( - "refinement" in U and U["refinement"] in allowed_refinements - ), f"Must provide a gen_specs['user']['refinement'] in: {allowed_refinements}" - tol = U["_match_tolerance"] if "_match_tolerance" in U else 1.0e-12 - - # Choose the refinement function based on U["refinement"]. - if U["refinement"] == "getCandidateConstructionPoints": - assert "sType" in U - assert "liAnisotropicWeightsOrOutput" in U - if U["refinement"] == "getCandidateConstructionPointsSurplus": - assert "fTolerance" in U - assert "sRefinementType" in U - - def get_refined_points(g, U): - if U["refinement"] == "getCandidateConstructionPoints": - return g.getCandidateConstructionPoints(U["sType"], U["liAnisotropicWeightsOrOutput"]) - else: - assert U["refinement"] == "getCandidateConstructionPointsSurplus" - return g.getCandidateConstructionPointsSurplus(U["fTolerance"], U["sRefinementType"]) - # else: - # raise ValueError("Unknown refinement string") - - # Asynchronous helper and state variables. - num_dims = grid.getNumDimensions() - num_completed = 0 - offset = 0 - queued_pts = np.empty((0, num_dims), dtype="float") - queued_ids = np.empty(0, dtype="int") - - # First run. - grid.beginConstruction() - init_pts = get_refined_points(grid, U) - queued_pts, queued_ids, offset = get_state(queued_pts, queued_ids, offset, new_points=init_pts, tol=tol) - H0 = np.zeros(init_pts.shape[0], dtype=gen_specs["out"]) - H0["x"] = init_pts - H0["sim_id"] = np.arange(init_pts.shape[0], dtype="int") - H0["priority"] = np.flip(H0["sim_id"]) - tag, Work, calc_in = ps.send_recv(H0) - - # Subsequent runs. - while tag not in [STOP_TAG, PERSIS_STOP]: - # Parse the points returned by the allocator. - num_completed += calc_in["x"].shape[0] - queued_pts, queued_ids, offset = get_state( - queued_pts, queued_ids, offset, completed_points=calc_in["x"], tol=tol - ) - - # Compute the next batch of points (if they exist). - new_pts = np.empty((0, num_dims), dtype="float") - refined_pts = np.empty((0, num_dims), dtype="float") - refined_ord = np.empty(0, dtype="int") - if grid.getNumLoaded() < 1000 or num_completed > 0.2 * grid.getNumLoaded(): - # A copy is needed because the data in the calc_in arrays are not contiguous. - grid.loadConstructedPoint(np.copy(calc_in["x"]), np.copy(calc_in["f"])) - if "tasmanian_checkpoint_file" in U: - grid.write(U["tasmanian_checkpoint_file"]) - refined_pts = get_refined_points(grid, U) - # If the refined points are empty, then there is a stopping condition internal to the - # Tasmanian sparse grid that is being triggered by the loaded points. - if refined_pts.size == 0: - break - refined_ord = np.lexsort(np.rot90(refined_pts)) - delete_ind = get_2D_duplicate_indices(refined_pts, queued_pts, x_ord=refined_ord, tol=tol) - new_pts = np.delete(refined_pts, delete_ind, axis=0) - - if new_pts.shape[0] > 0: - # Update the state variables with the refined points and update the queue in the allocator. - num_completed = 0 - queued_pts, queued_ids, offset = get_state(queued_pts, queued_ids, offset, new_points=new_pts, tol=tol) - H0 = get_H0(gen_specs, refined_pts, refined_ord, queued_pts, queued_ids, tol=tol) - tag, Work, calc_in = ps.send_recv(H0) - else: - tag, Work, calc_in = ps.recv() - - return None, persis_info, FINISHED_PERSISTENT_GEN_TAG - - -def get_sparse_grid_specs(user_specs, sim_f, num_dims, num_outputs=1, mode="batched"): - """ - Helper function that generates the simulator, generator, and allocator specs as well as the - persis_info dictionary to ensure that they are compatible with the custom generators in this - script. The outputs should be used in the main libE() call. - - INPUTS: - user_specs (dict) : a dictionary of user specs that is needed in the generator specs; - expects the key "tasmanian_init" whose value is a 0-argument lambda - that initializes an appropriate Tasmanian sparse grid object. - - sim_f (func) : a lambda function that takes in generator outputs (simulator inputs) - and returns simulator outputs. - - num_dims (int) : number of model inputs. - - num_outputs (int) : number of model outputs. - - mode (string) : can either be "batched" or "async". - - OUTPUTS: - sim_specs (dict) : a dictionary of simulation specs and also one of the inputs of libE(). - - gen_specs (dict) : a dictionary of generator specs and also one of the inputs of libE(). - - alloc_specs (dict) : a dictionary of allocation specs and also one of the inputs of libE(). - - persis_info (dict) : a dictionary containing common information that is passed to all - workers and also one of the inputs of libE(). - - """ - - assert "tasmanian_init" in user_specs - assert mode in ["batched", "async"] - - sim_specs = { - "sim_f": sim_f, - "in": ["x"], - } - gen_out = [ - ("x", float, (num_dims,)), - ("sim_id", int), - ("priority", int), - ] - gen_specs = { - "persis_in": [t[0] for t in gen_out] + ["f"], - "out": gen_out, - "user": user_specs, - } - alloc_specs = { - "alloc_f": allocf, - "user": {}, - } - - if mode == "batched": - gen_specs["gen_f"] = sparse_grid_batched - sim_specs["out"] = [("f", float, (num_outputs,))] - if mode == "async": - gen_specs["gen_f"] = sparse_grid_async - sim_specs["out"] = [("x", float, (num_dims,)), ("f", float, (num_outputs,))] - alloc_specs["user"]["active_recv_gen"] = True - alloc_specs["user"]["async_return"] = True - - nworkers, _, _, _ = parse_args() - persis_info = {} - for i in range(nworkers + 1): - persis_info[i] = {"worker_num": i} - - return sim_specs, gen_specs, alloc_specs, persis_info diff --git a/libensemble/gen_funcs/sampling.py b/libensemble/gen_funcs/sampling.py index efe9eab407..e37d4d5343 100644 --- a/libensemble/gen_funcs/sampling.py +++ b/libensemble/gen_funcs/sampling.py @@ -4,7 +4,7 @@ import numpy as np -from libensemble.specs import output_data +from libensemble.tools import get_rng __all__ = [ "uniform_random_sample", @@ -16,31 +16,34 @@ ] -@output_data([("x", float, 2)]) # default: can be overwritten in gen_specs -def uniform_random_sample(_, persis_info, gen_specs): +def uniform_random_sample(_, persis_info, gen_specs, libE_info): """ - Generates ``gen_specs["user"]["gen_batch_size"]`` points uniformly over the domain + Generates ``gen_specs["batch_size"]`` points uniformly over the domain defined by ``gen_specs["user"]["ub"]`` and ``gen_specs["user"]["lb"]``. .. seealso:: `test_uniform_sampling.py `_ # noqa """ + if "rand_stream" not in persis_info: + persis_info["rand_stream"] = get_rng(gen_specs, libE_info) + rng = persis_info["rand_stream"] + ub = gen_specs["user"]["ub"] lb = gen_specs["user"]["lb"] n = len(lb) - b = gen_specs["user"]["gen_batch_size"] + b = gen_specs["batch_size"] H_o = np.zeros(b, dtype=gen_specs["out"]) - H_o["x"] = persis_info["rand_stream"].uniform(lb, ub, (b, n)) + H_o["x"] = rng.uniform(lb, ub, (b, n)) return H_o, persis_info -def uniform_random_sample_with_variable_resources(_, persis_info, gen_specs): +def uniform_random_sample_with_variable_resources(_, persis_info, gen_specs, libE_info): """ - Generates ``gen_specs["user"]["gen_batch_size"]`` points uniformly over the domain + Generates ``gen_specs["batch_size"]`` points uniformly over the domain defined by ``gen_specs["user"]["ub"]`` and ``gen_specs["user"]["lb"]``. Also randomly requests a different number of resource sets to be used in each evaluation. @@ -50,25 +53,28 @@ def uniform_random_sample_with_variable_resources(_, persis_info, gen_specs): .. seealso:: `test_uniform_sampling_with_variable_resources.py `_ # noqa """ + if "rand_stream" not in persis_info: + persis_info["rand_stream"] = get_rng(gen_specs, libE_info) + rng = persis_info["rand_stream"] ub = gen_specs["user"]["ub"] lb = gen_specs["user"]["lb"] max_rsets = gen_specs["user"]["max_resource_sets"] n = len(lb) - b = gen_specs["user"]["gen_batch_size"] + b = gen_specs["batch_size"] H_o = np.zeros(b, dtype=gen_specs["out"]) - H_o["x"] = persis_info["rand_stream"].uniform(lb, ub, (b, n)) - H_o["resource_sets"] = persis_info["rand_stream"].integers(1, max_rsets + 1, b) + H_o["x"] = rng.uniform(lb, ub, (b, n)) + H_o["resource_sets"] = rng.integers(1, max_rsets + 1, b) print(f'GEN: H rsets requested: {H_o["resource_sets"]}') return H_o, persis_info -def uniform_random_sample_with_var_priorities_and_resources(H, persis_info, gen_specs): +def uniform_random_sample_with_var_priorities_and_resources(H, persis_info, gen_specs, libE_info): """ Generates points uniformly over the domain defined by ``gen_specs["user"]["ub"]`` and ``gen_specs["user"]["lb"]``. Also, randomly requests a different priority and number of @@ -77,6 +83,10 @@ def uniform_random_sample_with_var_priorities_and_resources(H, persis_info, gen_ This generator is used to test/demonstrate setting of priorities and resource sets. """ + if "rand_stream" not in persis_info: + persis_info["rand_stream"] = get_rng(gen_specs, libE_info) + rng = persis_info["rand_stream"] + ub = gen_specs["user"]["ub"] lb = gen_specs["user"]["lb"] max_rsets = gen_specs["user"]["max_resource_sets"] @@ -84,12 +94,12 @@ def uniform_random_sample_with_var_priorities_and_resources(H, persis_info, gen_ n = len(lb) if len(H) == 0: - b = gen_specs["user"]["initial_batch_size"] + b = gen_specs["initial_batch_size"] H_o = np.zeros(b, dtype=gen_specs["out"]) for i in range(0, b): # x= i*np.ones(n) - x = persis_info["rand_stream"].uniform(lb, ub, (1, n)) + x = rng.uniform(lb, ub, (1, n)) H_o["x"][i] = x H_o["resource_sets"][i] = 1 H_o["priority"] = 1 @@ -97,15 +107,15 @@ def uniform_random_sample_with_var_priorities_and_resources(H, persis_info, gen_ else: H_o = np.zeros(1, dtype=gen_specs["out"]) # H_o["x"] = len(H)*np.ones(n) # Can use a simple count for testing. - H_o["x"] = persis_info["rand_stream"].uniform(lb, ub) - H_o["resource_sets"] = persis_info["rand_stream"].integers(1, max_rsets + 1) + H_o["x"] = rng.uniform(lb, ub) + H_o["resource_sets"] = rng.integers(1, max_rsets + 1) H_o["priority"] = 10 * H_o["resource_sets"] # print("Created sim for {} resource sets".format(H_o["resource_sets"]), flush=True) return H_o, persis_info -def uniform_random_sample_obj_components(H, persis_info, gen_specs): +def uniform_random_sample_obj_components(H, persis_info, gen_specs, libE_info): """ Generates points uniformly over the domain defined by ``gen_specs["user"]["ub"]`` and ``gen_specs["user"]["lb"]`` but requests each ``obj_component`` be evaluated @@ -114,18 +124,22 @@ def uniform_random_sample_obj_components(H, persis_info, gen_specs): .. seealso:: `test_uniform_sampling_one_residual_at_a_time.py `_ # noqa """ + if "rand_stream" not in persis_info: + persis_info["rand_stream"] = get_rng(gen_specs, libE_info) + rng = persis_info["rand_stream"] + ub = gen_specs["user"]["ub"] lb = gen_specs["user"]["lb"] n = len(lb) m = gen_specs["user"]["components"] - b = gen_specs["user"]["gen_batch_size"] + b = gen_specs["batch_size"] H_o = np.zeros(b * m, dtype=gen_specs["out"]) for i in range(0, b): - x = persis_info["rand_stream"].uniform(lb, ub, (1, n)) + x = rng.uniform(lb, ub, (1, n)) H_o["x"][i * m : (i + 1) * m, :] = np.tile(x, (m, 1)) - H_o["priority"][i * m : (i + 1) * m] = persis_info["rand_stream"].uniform(0, 1, m) + H_o["priority"][i * m : (i + 1) * m] = rng.uniform(0, 1, m) H_o["obj_component"][i * m : (i + 1) * m] = np.arange(0, m) H_o["pt_id"][i * m : (i + 1) * m] = len(H) // m + i @@ -133,48 +147,54 @@ def uniform_random_sample_obj_components(H, persis_info, gen_specs): return H_o, persis_info -def uniform_random_sample_cancel(_, persis_info, gen_specs): +def uniform_random_sample_cancel(_, persis_info, gen_specs, libE_info): """ Similar to uniform_random_sample but with immediate cancellation of selected points for testing. """ + if "rand_stream" not in persis_info: + persis_info["rand_stream"] = get_rng(gen_specs, libE_info) + rng = persis_info["rand_stream"] + ub = gen_specs["user"]["ub"] lb = gen_specs["user"]["lb"] n = len(lb) - b = gen_specs["user"]["gen_batch_size"] + b = gen_specs["batch_size"] H_o = np.zeros(b, dtype=gen_specs["out"]) for i in range(b): if i % 10 == 0: H_o[i]["cancel_requested"] = True - H_o["x"] = persis_info["rand_stream"].uniform(lb, ub, (b, n)) + H_o["x"] = rng.uniform(lb, ub, (b, n)) return H_o, persis_info -@output_data([("x", float, (1,))]) -def latin_hypercube_sample(_, persis_info, gen_specs): +def latin_hypercube_sample(_, persis_info, gen_specs, libE_info): """ - Generates ``gen_specs["user"]["gen_batch_size"]`` points in a Latin + Generates ``gen_specs["batch_size"]`` points in a Latin hypercube sample over the domain defined by ``gen_specs["user"]["ub"]`` and ``gen_specs["user"]["lb"]``. .. seealso:: `test_1d_sampling.py `_ # noqa """ + if "rand_stream" not in persis_info: + persis_info["rand_stream"] = get_rng(gen_specs, libE_info) + rng = persis_info["rand_stream"] ub = gen_specs["user"]["ub"] lb = gen_specs["user"]["lb"] n = len(lb) - b = gen_specs["user"]["gen_batch_size"] + b = gen_specs["batch_size"] H_o = np.zeros(b, dtype=gen_specs["out"]) - A = lhs_sample(n, b, persis_info["rand_stream"]) + A = lhs_sample(n, b, rng) H_o["x"] = A * (ub - lb) + lb diff --git a/libensemble/gen_funcs/surmise_calib_support.py b/libensemble/gen_funcs/surmise_calib_support.py index b75c8c3c62..40cb648aeb 100644 --- a/libensemble/gen_funcs/surmise_calib_support.py +++ b/libensemble/gen_funcs/surmise_calib_support.py @@ -7,9 +7,10 @@ class thetaprior: """Define the class instance of priors provided to the methods.""" - def __init__(self, loc, scale): + def __init__(self, loc, scale, rng): self._loc = loc self._scale = scale + self._rng = rng def lpdf(self, theta): """Return log prior density.""" @@ -21,7 +22,7 @@ def rnd(self, n): """Return random draws from prior.""" loc = self._loc scale = self._scale - return np.vstack((sps.norm.rvs(loc, scale, size=(n, 4)))) + return self._rng.normal(loc, scale, (n, 4)) def gen_true_theta(): diff --git a/libensemble/gen_funcs/uniform_or_localopt.py b/libensemble/gen_funcs/uniform_or_localopt.py index fca0e70bb6..916443c85f 100644 --- a/libensemble/gen_funcs/uniform_or_localopt.py +++ b/libensemble/gen_funcs/uniform_or_localopt.py @@ -10,12 +10,13 @@ import numpy as np from libensemble.message_numbers import EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG +from libensemble.tools import get_rng from libensemble.tools.persistent_support import PersistentSupport def uniform_or_localopt(H, persis_info, gen_specs, libE_info): """ - This generation function returns ``gen_specs["user"]["gen_batch_size"]`` uniformly + This generation function returns ``gen_specs["batch_size"]`` uniformly sampled points when called in nonpersistent mode (i.e., when ``libE_info["persistent"]`` isn't ``True``). Otherwise, the generation function starts a persistent nlopt local optimization run. @@ -24,18 +25,17 @@ def uniform_or_localopt(H, persis_info, gen_specs, libE_info): `test_uniform_sampling_then_persistent_localopt_runs.py `_ # noqa """ if libE_info.get("persistent"): - x_opt, persis_info_updates, tag_out = try_and_run_nlopt(H, gen_specs, libE_info) - H_o = [] - return H_o, persis_info_updates, tag_out + return try_and_run_nlopt(H, gen_specs, libE_info) else: + rng = get_rng(gen_specs, libE_info) ub = gen_specs["user"]["ub"] lb = gen_specs["user"]["lb"] n = len(lb) - b = gen_specs["user"]["gen_batch_size"] + b = gen_specs["batch_size"] H_o = np.zeros(b, dtype=gen_specs["out"]) for i in range(0, b): - x = persis_info["rand_stream"].uniform(lb, ub, (1, n)) + x = rng.uniform(lb, ub, (1, n)) H_o = add_to_Out(H_o, x, i, ub, lb) persis_info_updates = persis_info # Send this back so it is overwritten. @@ -107,10 +107,9 @@ def nlopt_obj_fun(x, grad): if exit_code > 0 and exit_code < 5: persis_info_updates["x_opt"] = x_opt except Exception: # Raised when manager sent PERSIS_STOP or STOP_TAG - x_opt = [] persis_info_updates = {} - return x_opt, persis_info_updates, FINISHED_PERSISTENT_GEN_TAG + return None, persis_info_updates, FINISHED_PERSISTENT_GEN_TAG def add_to_Out(H_o, x, i, ub, lb, local=False, active=False): diff --git a/libensemble/generators.py b/libensemble/generators.py new file mode 100644 index 0000000000..15ae0725e4 --- /dev/null +++ b/libensemble/generators.py @@ -0,0 +1,254 @@ +from abc import abstractmethod +from typing import List, Optional + +import numpy as np +from gest_api import Generator +from gest_api.vocs import VOCS +from numpy import typing as npt + +from libensemble.comms.comms import QCommProcess # , QCommThread +from libensemble.executors import Executor +from libensemble.message_numbers import EVAL_GEN_TAG, PERSIS_STOP +from libensemble.utils.misc import list_dicts_to_np, np_to_list_dicts, unmap_numpy_array + + +class GeneratorNotStartedException(Exception): + """Exception raised by a threaded/multiprocessed generator upon being suggested without having been started""" + + +class LibensembleGenerator(Generator): + """ + Generator interface that accepts the classic History, persis_info, gen_specs, libE_info parameters after VOCS. + + ``suggest/ingest`` methods communicate lists of dictionaries, like the standard. + ``suggest_numpy/ingest_numpy`` methods communicate numpy arrays containing the same data. + + .. note:: + Most LibensembleGenerator instances operate on "x" for variables and "f" for objectives internally. + By default we map "x" to the VOCS variables and "f" to the VOCS objectives, which works for most use cases. + If a given generator iterates internally over multiple, multi-dimensional variables or objectives, + then providing a custom ``variables_mapping`` is recommended. + + For instance: + ``variables_mapping = {"x": ["core", "edge"], + "y": ["mirror-x", "mirror-y"], + "f": ["energy"], + "grad": ["grad_x", "grad_y"]}``. + """ + + def __init__( + self, + vocs: VOCS, + History: npt.NDArray = [], + persis_info: dict = {}, + gen_specs: dict = {}, + libE_info: dict = {}, + variables_mapping: dict = {}, + **kwargs, + ): + self._validate_vocs(vocs) + self.vocs = vocs + self.History = History + self.gen_specs = gen_specs + self.libE_info = libE_info + + self.variables_mapping = variables_mapping + if not self.variables_mapping: + self.variables_mapping = {} + # Map variables to x if not already mapped + if "x" not in self.variables_mapping: + # SH TODO - is this check needed? + if len(list(self.vocs.variables.keys())) > 1 or list(self.vocs.variables.keys())[0] != "x": + self.variables_mapping["x"] = self._get_unmapped_keys(self.vocs.variables, "x") + # Map objectives to f if not already mapped + if "f" not in self.variables_mapping: + if ( + len(list(self.vocs.objectives.keys())) > 1 or list(self.vocs.objectives.keys())[0] != "f" + ): # e.g. {"f": ["f"]} doesn't need mapping + self.variables_mapping["f"] = self._get_unmapped_keys(self.vocs.objectives, "f") + # Map sim_id to _id + if self.returns_id: + self.variables_mapping["sim_id"] = ["_id"] + + if len(kwargs) > 0: # so user can specify gen-specific parameters as kwargs to constructor + if not self.gen_specs.get("user"): + self.gen_specs["user"] = {} + self.gen_specs["user"].update(kwargs) + if not persis_info.get("rand_stream"): + self.persis_info = {"rand_stream": np.random.default_rng(4321 + 1), "worker_num": 1} + else: + self.persis_info = persis_info + + def _validate_vocs(self, vocs) -> None: + pass + + def _get_unmapped_keys(self, vocs_dict, default_key): + """Get keys from vocs_dict that aren't already mapped to other keys in variables_mapping.""" + # Get all variables that aren't already mapped to other keys + mapped_vars = [] + for mapped_list in self.variables_mapping.values(): + mapped_vars.extend(mapped_list) + unmapped_vars = [v for v in list(vocs_dict.keys()) if v not in mapped_vars] + return unmapped_vars + + @abstractmethod + def suggest_numpy(self, num_points: Optional[int] = 0) -> npt.NDArray: + """Request the next set of points to evaluate, as a NumPy array.""" + + @abstractmethod + def ingest_numpy(self, results: npt.NDArray) -> None: + """Send the results, as a NumPy array, of evaluations to the generator.""" + + @staticmethod + def _convert_np_types(dict_list): + return [ + {key: (value.item() if isinstance(value, np.generic) else value) for key, value in item.items()} + for item in dict_list + ] + + def suggest(self, num_points: Optional[int] = 0) -> List[dict]: + """Request the next set of points to evaluate.""" + return LibensembleGenerator._convert_np_types( + np_to_list_dicts(self.suggest_numpy(num_points), mapping=self.variables_mapping) + ) + + def ingest(self, results: List[dict]) -> None: + """Send the results of evaluations to the generator.""" + self.ingest_numpy(list_dicts_to_np(results, mapping=self.variables_mapping)) + + +class PersistentGenInterfacer(LibensembleGenerator): + """Implement suggest/ingest for traditionally written libEnsemble persistent generator functions. + Still requires a handful of libEnsemble-specific data-structures on initialization. + """ + + def __init__( + self, + vocs: VOCS, + History: npt.NDArray = [], + persis_info: dict = {}, + gen_specs: dict = {}, + libE_info: dict = {}, + **kwargs, + ) -> None: + super().__init__(vocs, History, persis_info, gen_specs, libE_info, **kwargs) + self.gen_f = gen_specs["gen_f"] + self.History = History + self.libE_info = libE_info + self._running_gen_f: Optional[QCommProcess] = None + self.gen_result = None + + def setup(self) -> None: + """Must be called once before calling suggest/ingest. Initializes the background thread.""" + if self._running_gen_f is not None: + raise RuntimeError("Generator has already been started.") + # SH this contains the thread lock - removing.... wrong comm to pass on anyway. + if hasattr(Executor.executor, "comm"): + del Executor.executor.comm + self.libE_info["executor"] = Executor.executor + + self._running_gen_f = QCommProcess( + self.gen_f, + None, + self.History, + self.persis_info, + self.gen_specs, + self.libE_info, + user_function=True, + ) + + # This can be set here since the object isnt started until the first suggest + assert self._running_gen_f is not None + self.libE_info["comm"] = self._running_gen_f.comm + + def _prep_fields(self, results: npt.NDArray) -> npt.NDArray: + """Filter out fields that are not in persis_in and add sim_ended to the dtype""" + filtered_dtype = [ + (name, results.dtype[name]) for name in results.dtype.names if name in self.gen_specs["persis_in"] + ] + + new_dtype = filtered_dtype + [("sim_ended", bool)] + new_results = np.zeros(len(results), dtype=new_dtype) + + for field in new_results.dtype.names: + try: + new_results[field] = results[field] + except ValueError: + continue + + new_results["sim_ended"] = True + return new_results + + def ingest(self, results: List[dict], tag: int = EVAL_GEN_TAG) -> None: + """Send the results of evaluations to the generator.""" + self.ingest_numpy(list_dicts_to_np(results, mapping=self.variables_mapping), tag) + + def suggest_numpy(self, num_points: Optional[int] = 0) -> npt.NDArray: + """Request the next set of points to evaluate, as a NumPy array.""" + if self._running_gen_f is None: + self.setup() + assert self._running_gen_f is not None + self._running_gen_f.run() + _, suggest_full = self._running_gen_f.recv() + return suggest_full["calc_out"] + + def ingest_numpy(self, results: npt.NDArray, tag: int = EVAL_GEN_TAG) -> None: + """Send the results of evaluations to the generator, as a NumPy array.""" + if self._running_gen_f is None: + self.setup() + assert self._running_gen_f is not None + self._running_gen_f.run() + + if results is not None: + results = self._prep_fields(results) + Work = {"libE_info": {"H_rows": np.copy(results["sim_id"]), "persistent": True, "executor": None}} + assert self._running_gen_f is not None + self._running_gen_f.send(tag, Work) + self._running_gen_f.send(tag, np.copy(results)) + else: + assert self._running_gen_f is not None + self._running_gen_f.send(tag, None) + + def finalize(self) -> None: + """Stop the generator process and store the returned data.""" + if self._running_gen_f is None: + raise RuntimeError("Generator has not been started.") + self.ingest_numpy(None, PERSIS_STOP) # conversion happens in ingest + assert self._running_gen_f is not None + self.gen_result = self._running_gen_f.result() + + def export( + self, vocs_field_names: bool = False, as_dicts: bool = False + ) -> tuple[npt.NDArray | list | None, dict | None, int | None]: + """ + Return the generator's results. + + Parameters + ---------- + vocs_field_names : bool, optional + If True, return local_H with variables unmapped from arrays back to individual fields. + Default is False. + as_dicts : bool, optional + If True, return local_H as list of dictionaries instead of numpy array. + Default is False. + + Returns + ------- + local_H : npt.NDArray | list + Generator history array (unmapped if vocs_field_names=True, as dicts if as_dicts=True). + persis_info : dict + Persistent information. + tag : int + Status flag (e.g., FINISHED_PERSISTENT_GEN_TAG). + """ + if not self.gen_result: + return (None, None, None) + local_H, persis_info, tag = self.gen_result + if vocs_field_names and local_H is not None and self.variables_mapping: + local_H = unmap_numpy_array(local_H, self.variables_mapping) + if as_dicts and local_H is not None: + if vocs_field_names and self.variables_mapping: + local_H = np_to_list_dicts(local_H, self.variables_mapping) + else: + local_H = np_to_list_dicts(local_H) + return (local_H, persis_info, tag) diff --git a/libensemble/history.py b/libensemble/history.py index 3d3b5bc6fd..80f848ccaa 100644 --- a/libensemble/history.py +++ b/libensemble/history.py @@ -68,20 +68,29 @@ def __init__( H[field][: len(H0)] = H0[field] if "sim_started" not in fields: - logger.manager_warning("Marking entries in H0 as having been 'sim_started' and 'sim_ended'") + logger.manager_warning( # type: ignore[attr-defined] + "Marking entries in H0 as having been " + "'sim_started' and 'sim_ended'" + ) + H["sim_started"][: len(H0)] = 1 H["sim_ended"][: len(H0)] = 1 elif "sim_ended" not in fields: - logger.manager_warning("Marking entries in H0 as having been 'sim_ended' if 'sim_started'") + logger.manager_warning( # type: ignore[attr-defined] + "Marking entries in H0 as having been " + "'sim_ended' if 'sim_started'" + ) + H["sim_ended"][: len(H0)] = H0["sim_started"] if "sim_id" not in fields: - logger.manager_warning("Assigning sim_ids to entries in H0") + logger.manager_warning("Assigning sim_ids to entries in H0") # type: ignore[attr-defined] + H["sim_id"][: len(H0)] = np.arange(0, len(H0)) else: H = np.zeros(L + len(H0), dtype=specs_dtype_list) H["sim_id"][-L:] = -1 + if "_id" in H.dtype.names: + H["_id"][-L:] = -1 H["sim_started_time"][-L:] = np.inf H["gen_informed_time"][-L:] = np.inf @@ -107,7 +116,7 @@ def __init__( self.last_ended = -1 def _append_new_fields(self, H_f: npt.NDArray) -> None: - dtype_new = np.dtype(list(set(self.H.dtype.descr + H_f.dtype.descr))) + dtype_new = np.dtype(list(set(self.H.dtype.descr + np.lib.recfunctions.repack_fields(H_f).dtype.descr))) H_new = np.zeros(len(self.H), dtype=dtype_new) old_fields = self.H.dtype.names for field in old_fields: @@ -119,17 +128,20 @@ def update_history_f(self, D: dict, kill_canceled_sims: bool = False) -> None: Updates the history after points have been evaluated """ - new_inds = D["libE_info"]["H_rows"] # The list of rows (as a numpy array) + new_inds = D["libE_info"]["H_rows"] returned_H = D["calc_out"] - fields = returned_H.dtype.names if returned_H is not None else [] + fields = returned_H.dtype.names if returned_H is not None else [] if returned_H is not None and any([field not in self.H.dtype.names for field in returned_H.dtype.names]): self._append_new_fields(returned_H) for j, ind in enumerate(new_inds): for field in fields: - if self.safe_mode: - assert field not in protected_libE_fields, "The field '" + field + "' is protected" + if field in protected_libE_fields: + if self.safe_mode: + assert False, "The field '" + field + "' is protected" + continue + if np.isscalar(returned_H[field][j]) or returned_H.dtype[field].hasobject: self.H[field][ind] = returned_H[field][j] else: @@ -192,9 +204,10 @@ def update_history_to_gen(self, q_inds: npt.NDArray): self.H["gen_informed"][ind] = True if self.using_H0 and not self.given_back_warned: - logger.manager_warning( - "Giving entries in H0 back to gen. Marking entries in H0 as 'gen_informed' if 'sim_ended'." + logger.manager_warning( # type: ignore[attr-defined] + "Giving entries in H0 back to gen. Marking entries in " + "H0 as 'gen_informed' if 'sim_ended'." ) + self.given_back_warned = True self.H["gen_informed_time"][q_inds] = t @@ -248,8 +261,11 @@ def update_history_x_in(self, gen_worker: int, D: npt.NDArray, gen_started_time: update_inds = D["sim_id"] for field in D.dtype.names: - if self.safe_mode: - assert field not in protected_libE_fields, "The field '" + field + "' is protected" + if field in protected_libE_fields: + if self.safe_mode: + assert False, "The field '" + field + "' is protected" + continue + self.H[field][update_inds] = D[field] first_gen_inds = update_inds[self.H["gen_ended_time"][update_inds] == 0] @@ -270,6 +286,8 @@ def grow_H(self, k: int) -> None: """ H_1 = np.zeros(k, dtype=self.H.dtype) H_1["sim_id"] = -1 + if "_id" in H_1.dtype.names: + H_1["_id"] = -1 H_1["sim_started_time"] = np.inf H_1["gen_informed_time"] = np.inf if "resource_sets" in H_1.dtype.names: diff --git a/libensemble/libE.py b/libensemble/libE.py index af302d13c8..219e2cd8c4 100644 --- a/libensemble/libE.py +++ b/libensemble/libE.py @@ -30,7 +30,6 @@ from libensemble.libE import libE from generator import gen_random_sample from simulator import sim_find_sine - from libensemble.tools import add_unique_random_streams nworkers, is_manager, libE_specs, _ = parse_args() @@ -44,11 +43,9 @@ sim_specs = {"sim_f": sim_find_sine, "in": ["x"], "out": [("y", float)]} - persis_info = add_unique_random_streams({}, nworkers + 1) - exit_criteria = {"sim_max": 80} - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, libE_specs=libE_specs) This will initiate libEnsemble with a Manager and ``nworkers`` workers (parsed from the command line), and runs on laptops or supercomputers. If an exception is @@ -69,7 +66,6 @@ from libensemble.libE import libE from generator import gen_random_sample from simulator import sim_find_sine - from libensemble.tools import add_unique_random_streams if __name__ == "__main__": nworkers, is_manager, libE_specs, _ = parse_args() @@ -92,11 +88,9 @@ "out": [("y", float)], } - persis_info = add_unique_random_streams({}, nworkers + 1) - exit_criteria = {"sim_max": 80} - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, libE_specs=libE_specs) Alternatively, you may set the multiprocessing start method to ``"fork"`` via the following: @@ -121,9 +115,15 @@ import sys import traceback from pathlib import Path +from typing import TYPE_CHECKING, cast import numpy as np +if TYPE_CHECKING: + from libensemble.logger import LibensembleLogger + +from pydantic import validate_call as libE_wrapper + from libensemble.comms.comms import QCommProcess, QCommThread, Timeout from libensemble.comms.logs import manager_logging_config from libensemble.comms.tcp_mgr import ClientQCommManager, ServerQCommManager @@ -138,12 +138,11 @@ from libensemble.tools.tools import _USER_SIM_ID_WARNING from libensemble.utils import launcher from libensemble.utils.misc import specs_dump -from libensemble.utils.pydantic_bindings import libE_wrapper from libensemble.utils.timer import Timer from libensemble.version import __version__ from libensemble.worker import worker_main -logger = logging.getLogger(__name__) +logger = cast("LibensembleLogger", logging.getLogger(__name__)) # To change logging level for just this module # logger.setLevel(logging.DEBUG) @@ -155,9 +154,9 @@ def libE( exit_criteria: ExitCriteria, persis_info: dict = {}, alloc_specs: AllocSpecs = AllocSpecs(), - libE_specs: LibeSpecs = {}, + libE_specs: LibeSpecs | dict = {}, H0=None, -) -> (np.ndarray, dict, int): +) -> tuple[np.ndarray, dict, int]: """ Parameters ---------- @@ -190,7 +189,7 @@ def libE( libE_specs: :obj:`dict` or :class:`LibeSpecs`, Optional Specifications for libEnsemble - :doc:`(example)` + :doc:`(example)` H0: `NumPy structured array `_, Optional @@ -242,11 +241,23 @@ def libE( ] exit_criteria = specs_dump(ensemble.exit_criteria, by_alias=True, exclude_none=True) + # Restore objects that don't survive serialization via model_dump + if hasattr(ensemble.sim_specs, "simulator") and ensemble.sim_specs.simulator is not None: + sim_specs["simulator"] = ensemble.sim_specs.simulator + if hasattr(ensemble.sim_specs, "vocs") and ensemble.sim_specs.vocs is not None: + sim_specs["vocs"] = ensemble.sim_specs.vocs + + if ensemble.gen_specs is not None: + if hasattr(ensemble.gen_specs, "generator") and ensemble.gen_specs.generator is not None: + gen_specs["generator"] = ensemble.gen_specs.generator + if hasattr(ensemble.gen_specs, "vocs") and ensemble.gen_specs.vocs is not None: + gen_specs["vocs"] = ensemble.gen_specs.vocs + # Extract platform info from settings or environment platform_info = get_platform(libE_specs) if libE_specs["dry_run"]: - logger.manager_warning("Dry run. All libE() inputs validated. Exiting.") + logger.manager_warning("Dry run. All libE() inputs validated. Exiting.") # type: ignore[attr-defined] sys.exit() libE_funcs = {"mpi": libE_mpi, "tcp": libE_tcp, "local": libE_local, "threads": libE_local} @@ -280,8 +291,8 @@ def manager( logger.info(f"libE version v{__version__}") if "out" in gen_specs and ("sim_id", int) in gen_specs["out"]: - if "libensemble.gen_funcs" not in gen_specs["gen_f"].__module__: - logger.manager_warning(_USER_SIM_ID_WARNING) + if hasattr(gen_specs["gen_f"], "__module__") and "libensemble.gen_funcs" not in gen_specs["gen_f"].__module__: + logger.manager_warning(_USER_SIM_ID_WARNING) # type: ignore[attr-defined] try: try: @@ -458,6 +469,7 @@ def start_proc_team(nworkers, sim_specs, gen_specs, libE_specs, log_comm=True): for wcomm in wcomms: wcomm.run() + return wcomms @@ -489,6 +501,7 @@ def libE_local(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, li wcomms = start_proc_team(libE_specs["nworkers"], sim_specs, gen_specs, libE_specs) # Set manager resources after the forkpoint. + # if libE_specs["gen_on_worker"] == True, -n reflects the exact number of workers if resources is not None: resources.set_resource_manager(libE_specs["nworkers"]) diff --git a/libensemble/logger.py b/libensemble/logger.py index 378b4e02a1..2a076744a7 100644 --- a/libensemble/logger.py +++ b/libensemble/logger.py @@ -1,4 +1,5 @@ import logging +from typing import TYPE_CHECKING, Any from libensemble.comms.logs import LogConfig @@ -6,7 +7,15 @@ MANAGER_WARNING = 35 logging.addLevelName(MANAGER_WARNING, "MANAGER_WARNING") -logging.MANAGER_WARNING = MANAGER_WARNING +setattr(logging, "MANAGER_WARNING", MANAGER_WARNING) + + +if TYPE_CHECKING: + + class LibensembleLogger(logging.Logger): + def manager_warning(self, message: str, *args: Any, **kwargs: Any) -> None: ... # noqa: E704 + + def vdebug(self, message: str, *args: Any, **kwargs: Any) -> None: ... # noqa: E704 def manager_warning(self, message: str, *args, **kwargs) -> None: @@ -14,11 +23,11 @@ def manager_warning(self, message: str, *args, **kwargs) -> None: self._log(MANAGER_WARNING, message, args, **kwargs) -logging.Logger.manager_warning = manager_warning +setattr(logging.Logger, "manager_warning", manager_warning) VDEBUG = 5 logging.addLevelName(VDEBUG, "VDEBUG") -logging.VDEBUG = VDEBUG +setattr(logging, "VDEBUG", VDEBUG) def vdebug(self, message, *args, **kwargs): @@ -26,7 +35,7 @@ def vdebug(self, message, *args, **kwargs): self._log(VDEBUG, message, args, **kwargs) -logging.Logger.vdebug = vdebug +setattr(logging.Logger, "vdebug", vdebug) LogConfig(__package__) diff --git a/libensemble/manager.py b/libensemble/manager.py index b12b96a774..7995d2da97 100644 --- a/libensemble/manager.py +++ b/libensemble/manager.py @@ -12,6 +12,7 @@ import sys import time import traceback +from pathlib import Path from typing import Any import numpy as np @@ -33,7 +34,7 @@ ) from libensemble.resources.resources import Resources from libensemble.tools.fields_keys import protected_libE_fields -from libensemble.tools.tools import _PERSIS_RETURN_WARNING, _USER_CALC_DIR_WARNING +from libensemble.tools.tools import _USER_CALC_DIR_WARNING from libensemble.utils.misc import _WorkerIndexer, extract_H_ranges from libensemble.utils.output_directory import EnsembleDirectory from libensemble.utils.timer import Timer @@ -56,7 +57,7 @@ class LoggedException(Exception): """Raise exception for handling without re-logging""" -def report_worker_exc(wrk_exc: Exception = None) -> None: +def report_worker_exc(wrk_exc: Exception | None = None) -> None: """Write worker exception to log""" if wrk_exc is not None: from_line, msg, exc = wrk_exc.args @@ -74,7 +75,7 @@ def manager_main( exit_criteria: dict, persis_info: dict, wcomms: list = [], -) -> (dict, int, int): +) -> tuple[dict, int, int]: """Manager routine to coordinate the generation and simulation evaluations Parameters @@ -164,7 +165,6 @@ class Manager: ("persis_state", int), ("active_recv", bool), ("gen_started_time", float), - ("zero_resource_worker", bool), ] def _run_additional_worker(self, hist, sim_specs, gen_specs, libE_specs): @@ -215,7 +215,7 @@ def __init__( self.elapsed = lambda: timer.elapsed self.wcomms = wcomms self.WorkerExc = False - self.persis_pending = [] + self.persis_pending: list[int] = [] self.live_data = libE_specs.get("live_data") dyn_keys = ("resource_sets", "num_procs", "num_gpus") @@ -231,19 +231,20 @@ def __init__( (1, "stop_val", self.term_test_stop_val), ] - gen_on_manager = self.libE_specs.get("gen_on_manager", False) + gen_on_worker = self.libE_specs.get("gen_on_worker", False) + len_W = len(self.wcomms) + 1 - gen_on_worker # if gen_on_worker, len_W = len(self.wcomms) - self.W = np.zeros(len(self.wcomms) + gen_on_manager, dtype=Manager.worker_dtype) - if gen_on_manager: + self.W = np.zeros(len_W, dtype=Manager.worker_dtype) + if gen_on_worker: + self.W["worker_id"] = np.arange(len(self.wcomms)) + 1 # [1, 2, 3, ...] + else: self.W["worker_id"] = np.arange(len(self.wcomms) + 1) # [0, 1, 2, ...] self.W[0]["gen_worker"] = True local_worker_comm = self._run_additional_worker(hist, sim_specs, gen_specs, libE_specs) self.wcomms = [local_worker_comm] + self.wcomms - else: - self.W["worker_id"] = np.arange(len(self.wcomms)) + 1 # [1, 2, 3, ...] - self.W = _WorkerIndexer(self.W, gen_on_manager) - self.wcomms = _WorkerIndexer(self.wcomms, gen_on_manager) + self.W = _WorkerIndexer(self.W, 1 - gen_on_worker) # if gen on worker, then no additional worker + self.wcomms = _WorkerIndexer(self.wcomms, 1 - gen_on_worker) temp_EnsembleDirectory = EnsembleDirectory(libE_specs=libE_specs) self.resources = Resources.resources @@ -251,9 +252,6 @@ def __init__( if self.resources is not None: gresource = self.resources.glob_resources self.scheduler_opts = gresource.update_scheduler_opts(self.scheduler_opts) - for wrk in self.W: - if wrk["worker_id"] in gresource.zero_resource_workers: - wrk["zero_resource_worker"] = True for wrk in self.W: if wrk["worker_id"] in self.libE_specs.get("gen_workers", []): @@ -262,7 +260,7 @@ def __init__( try: temp_EnsembleDirectory.make_copyback() except AssertionError as e: # Ensemble dir exists and isn't empty. - logger.manager_warning(_USER_CALC_DIR_WARNING.format(temp_EnsembleDirectory.ensemble_dir)) + logger.manager_warning(_USER_CALC_DIR_WARNING.format(temp_EnsembleDirectory.ensemble_dir)) # type: ignore self._kill_workers() raise ManagerException( "Manager errored on initialization", @@ -338,7 +336,7 @@ def _save_every_k(self, fname: str, count: int, k: int, complete: bool) -> None: def _save_every_k_sims(self, complete: bool) -> None: """Saves history every kth sim step""" self._save_every_k( - os.path.join(self.libE_specs["workflow_dir_path"], "{}_{}after_sim_{}.npy"), + str(Path(self.libE_specs["workflow_dir_path"]) / "{}_{}after_sim_{}.npy"), self.hist.sim_ended_count, self.libE_specs["save_every_k_sims"], complete, @@ -347,7 +345,7 @@ def _save_every_k_sims(self, complete: bool) -> None: def _save_every_k_gens(self, complete: bool) -> None: """Saves history every kth gen step""" self._save_every_k( - os.path.join(self.libE_specs["workflow_dir_path"], "{}_{}after_gen_{}.npy"), + str(Path(self.libE_specs["workflow_dir_path"]) / "{}_{}after_gen_{}.npy"), self.hist.index, self.libE_specs["save_every_k_gens"], complete, @@ -393,22 +391,30 @@ def _set_resources(self, Work: dict, w: int) -> None: If rsets are not assigned, then assign using default mapping """ - resource_manager = self.resources.resource_manager + resource_manager = self.resources.resource_manager # type: ignore rset_req = Work["libE_info"].get("rset_team") if rset_req is None: rset_team = [] - default_rset = resource_manager.index_list[w - 1] + default_rset = resource_manager.index_list[w] # type: ignore if default_rset is not None: rset_team.append(default_rset) Work["libE_info"]["rset_team"] = rset_team - resource_manager.assign_rsets(Work["libE_info"]["rset_team"], w) + resource_manager.assign_rsets(Work["libE_info"]["rset_team"], w) # type: ignore def _freeup_resources(self, w: int) -> None: """Free up resources assigned to the worker""" if self.resources: - self.resources.resource_manager.free_rsets(w) + self.resources.resource_manager.free_rsets(w) # type: ignore + + def _ensure_sim_id_in_persis_in(self, D: npt.NDArray) -> None: + """Add sim_id to gen_specs persis_in if generator output contains sim_id (gest-api style generators only)""" + if self.gen_specs.get("generator") and len(D) > 0 and "sim_id" in D.dtype.names: + if "persis_in" not in self.gen_specs: + self.gen_specs["persis_in"] = [] + if "sim_id" not in self.gen_specs["persis_in"]: + self.gen_specs["persis_in"].append("sim_id") def _send_work_order(self, Work: dict, w: int) -> None: """Sends an allocation function order to a worker""" @@ -476,18 +482,17 @@ def _update_state_on_worker_msg(self, persis_info: dict, D_recv: dict, w: int) - calc_status = D_recv["calc_status"] keep_state = D_recv["libE_info"].get("keep_state", False) - if w not in self.persis_pending and not self.W[w]["active_recv"] and not keep_state: + if (w not in self.persis_pending and not self.W[w]["active_recv"] and not keep_state) or self.WorkerExc: self.W[w]["active"] = 0 if calc_status in [FINISHED_PERSISTENT_SIM_TAG, FINISHED_PERSISTENT_GEN_TAG]: final_data = D_recv.get("calc_out", None) if isinstance(final_data, np.ndarray): - if calc_status is FINISHED_PERSISTENT_GEN_TAG and self.libE_specs.get("use_persis_return_gen", False): + if calc_status is FINISHED_PERSISTENT_GEN_TAG: + self._ensure_sim_id_in_persis_in(final_data) self.hist.update_history_x_in(w, final_data, self.W[w]["gen_started_time"]) - elif calc_status is FINISHED_PERSISTENT_SIM_TAG and self.libE_specs.get("use_persis_return_sim", False): + elif calc_status is FINISHED_PERSISTENT_SIM_TAG: self.hist.update_history_f(D_recv, self.kill_canceled_sims) - else: - logger.info(_PERSIS_RETURN_WARNING) self.W[w]["persis_state"] = 0 if self.W[w]["active_recv"]: self.W[w]["active"] = 0 @@ -498,9 +503,21 @@ def _update_state_on_worker_msg(self, persis_info: dict, D_recv: dict, w: int) - self._freeup_resources(w) else: if calc_type == EVAL_SIM_TAG: - self.hist.update_history_f(D_recv, self.kill_canceled_sims) + try: + self.hist.update_history_f(D_recv, self.kill_canceled_sims) + except AttributeError as e: + if self.WorkerExc: + logger.debug(f"Manager ignoring secondary data error from worker {w} during shutdown: {e}") + else: + self.WorkerExc = True + self._kill_workers() + raise WorkerException( + f"Error in data from worker {w}", str(e), traceback.format_exc() + ) from None if calc_type == EVAL_GEN_TAG: - self.hist.update_history_x_in(w, D_recv["calc_out"], self.W[w]["gen_started_time"]) + D = D_recv["calc_out"] + self._ensure_sim_id_in_persis_in(D) + self.hist.update_history_x_in(w, D, self.W[w]["gen_started_time"]) assert ( len(D_recv["calc_out"]) or np.any(self.W["active"]) or self.W[w]["persis_state"] ), "Gen must return work when is is the only thing active and not persistent." @@ -532,7 +549,7 @@ def _handle_msg_from_worker(self, persis_info: dict, w: int) -> None: self._kill_workers() raise WorkerException(f"Received error message from worker {w}", D_recv.msg, D_recv.exc) elif isinstance(D_recv, logging.LogRecord): - logger.vdebug(f"Manager received a log message from worker {w}") + logger.vdebug(f"Manager received a log message from worker {w}") # type: ignore[attr-defined] logging.getLogger(D_recv.name).handle(D_recv) else: logger.debug(f"Manager received data message from worker {w}") @@ -563,7 +580,7 @@ def _kill_cancelled_sims(self) -> None: # --- Handle termination - def _final_receive_and_kill(self, persis_info: dict) -> (dict, int, int): + def _final_receive_and_kill(self, persis_info: dict) -> tuple[dict, int, int]: """ Tries to receive from any active workers. @@ -601,9 +618,9 @@ def _final_receive_and_kill(self, persis_info: dict) -> (dict, int, int): # Elapsed Wallclock has expired if not any(self.W["persis_state"]): if any(self.W["active"]): - logger.manager_warning(_WALLCLOCK_MSG_ACTIVE) + logger.manager_warning(_WALLCLOCK_MSG_ACTIVE) # type: ignore else: - logger.manager_warning(_WALLCLOCK_MSG_ALL_RETURNED) + logger.manager_warning(_WALLCLOCK_MSG_ALL_RETURNED) # type: ignore exit_flag = 2 if self.WorkerExc: exit_flag = 1 @@ -615,6 +632,7 @@ def _final_receive_and_kill(self, persis_info: dict) -> (dict, int, int): if self.live_data is not None: self.live_data.finalize(self.hist) + persis_info["num_gens_started"] = 0 return persis_info, exit_flag, self.elapsed() def _sim_max_given(self) -> bool: @@ -639,7 +657,7 @@ def _get_alloc_libE_info(self) -> dict: "use_resource_sets": self.use_resource_sets, "gen_num_procs": self.gen_num_procs, "gen_num_gpus": self.gen_num_gpus, - "gen_on_manager": self.libE_specs.get("gen_on_manager", False), + "gen_on_worker": self.libE_specs.get("gen_on_worker", False), } def _alloc_work(self, H: npt.NDArray, persis_info: dict) -> dict: @@ -676,7 +694,7 @@ def _alloc_work(self, H: npt.NDArray, persis_info: dict) -> dict: # --- Main loop - def run(self, persis_info: dict) -> (dict, int, int): + def run(self, persis_info: dict) -> tuple[dict, int, int]: """Runs the manager""" logger.debug(f"Manager initiated on node {socket.gethostname()}") logger.info(f"Manager exit_criteria: {self.exit_criteria}") @@ -708,7 +726,7 @@ def run(self, persis_info: dict) -> (dict, int, int): finally: # Return persis_info, exit_flag, elapsed time result = self._final_receive_and_kill(persis_info) - self.wcomms = None + self.wcomms = [] sys.stdout.flush() sys.stderr.flush() return result diff --git a/libensemble/resources/platforms.py b/libensemble/resources/platforms.py index 6bddbe6d61..69c36242fc 100644 --- a/libensemble/resources/platforms.py +++ b/libensemble/resources/platforms.py @@ -12,10 +12,14 @@ import os import subprocess -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict, field_validator, model_validator from libensemble.utils.misc import specs_dump +# These will be imported later to avoid circular imports? +# But pydantic_bindings moved them here. +# Actually I need them from validators.py. + logger = logging.getLogger(__name__) # To change logging level for just this module # logger.setLevel(logging.DEBUG) @@ -32,6 +36,28 @@ class Platform(BaseModel): All are optional, and any not defined will be determined by libEnsemble's auto-detection. """ + model_config = ConfigDict( + arbitrary_types_allowed=True, populate_by_name=True, extra="forbid", validate_assignment=True + ) + + @field_validator("gpu_setting_type") + def check_gpu_setting_type(cls, value): + from libensemble.utils.validators import check_gpu_setting_type + + return check_gpu_setting_type(cls, value) + + @field_validator("mpi_runner") + def check_mpi_runner_type(cls, value): + from libensemble.utils.validators import check_mpi_runner_type + + return check_mpi_runner_type(cls, value) + + @model_validator(mode="after") + def check_logical_cores(self): + from libensemble.utils.validators import check_logical_cores + + return check_logical_cores(self) + mpi_runner: str | None = None """MPI runner: One of ``"mpich"``, ``"openmpi"``, ``"aprun"``, ``"srun"``, ``"jsrun"``, ``"msmpi"``, ``"custom"`` """ @@ -204,16 +230,6 @@ class Polaris(Platform): scheduler_match_slots: bool = True -class Summit(Platform): - mpi_runner: str = "jsrun" - cores_per_node: int = 42 - logical_cores_per_node: int = 168 - gpus_per_node: int = 6 - gpu_setting_type: str = "option_gpus_per_task" - gpu_setting_name: str = "-g" - scheduler_match_slots: bool = False - - class Known_platforms(BaseModel): """A list of platforms with known configurations. @@ -261,7 +277,6 @@ class Known_platforms(BaseModel): perlmutter_c: PerlmutterCPU = PerlmutterCPU() perlmutter_g: PerlmutterGPU = PerlmutterGPU() polaris: Polaris = Polaris() - summit: Summit = Summit() # Dictionary of known systems (or system partitions) detectable by domain name @@ -269,7 +284,6 @@ class Known_platforms(BaseModel): "frontier.olcf.ornl.gov": "frontier", "hostmgmt.cm.aurora.alcf.anl.gov": "aurora", "hsn.cm.polaris.alcf.anl.gov": "polaris", - "summit.olcf.ornl.gov": "summit", # Need to detect gpu count } diff --git a/libensemble/resources/resources.py b/libensemble/resources/resources.py index 443424ec37..19e246fb6f 100644 --- a/libensemble/resources/resources.py +++ b/libensemble/resources/resources.py @@ -6,6 +6,7 @@ import logging import os import socket +from pathlib import Path from libensemble.resources import node_resources from libensemble.resources.env_resources import EnvResources @@ -62,12 +63,12 @@ def init_resources(cls, libE_specs: dict, platform_info: dict = {}) -> None: libE_specs=libE_specs, platform_info=platform_info, top_level_dir=top_level_dir ) - def __init__(self, libE_specs: dict, platform_info: dict = {}, top_level_dir: str = None) -> None: + def __init__(self, libE_specs: dict, platform_info: dict = {}, top_level_dir: str = "") -> None: """Initiate a new resources object""" self.top_level_dir = top_level_dir or os.getcwd() - self.glob_resources = GlobalResources(libE_specs=libE_specs, platform_info=platform_info, top_level_dir=None) - self.resource_manager = None # For Manager - self.worker_resources = None # For Workers + self.glob_resources = GlobalResources(libE_specs=libE_specs, platform_info=platform_info, top_level_dir="") + self.resource_manager: ResourceManager | None = None # For Manager + self.worker_resources: WorkerResources | None = None # For Workers def set_worker_resources(self, num_workers: int, workerid: int) -> None: """Initiate the worker resources component of resources""" @@ -95,12 +96,10 @@ class GlobalResources: :ivar list global_nodelist: list of all nodes available for running user applications :ivar int logical_cores_avail_per_node: Logical cores (including SMT threads) available on a node :ivar int physical_cores_avail_per_node: Physical cores available on a node - :ivar list zero_resource_workers: List of workerIDs to have no resources. - :ivar bool dedicated_mode: Whether to remove libE nodes from global nodelist. :ivar int num_resource_sets: Number of resource sets, if supplied by the user. """ - def __init__(self, libE_specs: dict, platform_info: dict = {}, top_level_dir: str = None) -> None: + def __init__(self, libE_specs: dict, platform_info: dict = {}, top_level_dir: str = "") -> None: """Initializes a new Resources instance Determines the compute resources available for current allocation, including @@ -120,12 +119,9 @@ def __init__(self, libE_specs: dict, platform_info: dict = {}, top_level_dir: st will not be available to worker-launched tasks (user applications). They will be removed from the nodelist (if present), before dividing into resource sets. - zero_resource_workers: List[int], Optional - List of workers that require no resources. - num_resource_sets: int, Optional The total number of resource sets. Resources will be divided into this number. - Default: None. If None, resources will be divided by workers (excluding zero_resource_workers). + Default: None. If None, resources will be divided by workers. cores_on_node: tuple (int, int), Optional If supplied gives (physical cores, logical cores) for the nodes. If not supplied, @@ -165,7 +161,6 @@ def __init__(self, libE_specs: dict, platform_info: dict = {}, top_level_dir: st """ self.top_level_dir = top_level_dir self.dedicated_mode = libE_specs.get("dedicated_mode", False) - self.zero_resource_workers = libE_specs.get("zero_resource_workers", []) self.num_resource_sets = libE_specs.get("num_resource_sets", None) self.enforce_worker_core_bounds = libE_specs.get("enforce_worker_core_bounds", False) self.gpus_per_group = libE_specs.get("gpus_per_group") @@ -295,8 +290,8 @@ def get_global_nodelist(node_file=Resources.DEFAULT_NODEFILE, rundir=None, env_r In dedicated mode, any node with a libE worker is removed from the list. """ - top_level_dir = rundir or os.getcwd() - node_filepath = os.path.join(top_level_dir, node_file) + top_level_dir = Path(rundir) if rundir else Path.cwd() + node_filepath = top_level_dir / node_file global_nodelist = [] if os.path.isfile(node_filepath): with open(node_filepath, "r") as f: diff --git a/libensemble/resources/rset_resources.py b/libensemble/resources/rset_resources.py index d35cdaee8b..e43a51b88c 100644 --- a/libensemble/resources/rset_resources.py +++ b/libensemble/resources/rset_resources.py @@ -6,7 +6,7 @@ import numpy as np if TYPE_CHECKING: - from libensemble.resources.resources import Resources + from libensemble.resources.resources import GlobalResources logger = logging.getLogger(__name__) # To change logging level for just this module @@ -36,13 +36,12 @@ class RSetResources: # ('pool', int), # Pool ID (eg. separate gen/sim resources) - not yet used. ] - def __init__(self, num_workers: int, resources: Resources): + def __init__(self, num_workers: int, resources: GlobalResources): """Initializes a new RSetResources instance Determines the compute resources available for each resource set. - Unless resource sets is set explicitly, the number of resource sets is the number of workers, - excluding any workers defined as zero resource workers. + Unless resource sets is set explicitly, the number of resource sets is the number of workers. Parameters ---------- @@ -50,8 +49,8 @@ def __init__(self, num_workers: int, resources: Resources): num_workers: int The total number of workers - resources: Resources - A Resources object containing global nodelist and intranode information + resources: GlobalResources + A GlobalResources object containing global nodelist and intranode information """ self.num_workers = num_workers @@ -129,8 +128,7 @@ def get_rsets_on_a_node(num_rsets, resources): @staticmethod def get_workers2assign2(num_workers, resources): """Returns workers to assign resources to""" - zero_resource_list = resources.zero_resource_workers - return num_workers - len(zero_resource_list) + return num_workers @staticmethod def even_assignment(nnodes, nworkers): diff --git a/libensemble/resources/worker_resources.py b/libensemble/resources/worker_resources.py index 5033b2aeee..0207b90895 100644 --- a/libensemble/resources/worker_resources.py +++ b/libensemble/resources/worker_resources.py @@ -3,7 +3,7 @@ import logging import os from collections import Counter, OrderedDict -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING import numpy as np @@ -51,7 +51,6 @@ def __init__(self, num_workers: int, resources: GlobalResources) -> None: self.index_list = ResourceManager.get_index_list( self.num_workers, self.total_num_rsets, - resources.zero_resource_workers, ) self.rsets = np.zeros(self.total_num_rsets, dtype=ResourceManager.man_rset_dtype) @@ -102,19 +101,21 @@ def free_rsets(self, worker=None): self.nongpu_rsets_free += np.count_nonzero(~self.rsets["gpus"][rsets_to_free]) @staticmethod - def get_index_list(num_workers: int, num_rsets: int, zero_resource_list: list[int | Any]) -> list[int | None]: - """Map WorkerID to index into a nodelist""" + def get_index_list(num_workers: int, num_rsets: int) -> list[int | None]: + """Map WorkerID to index into a nodelist. + + Index 0 is always None since workers are 1-indexed. Worker 0 (gen-on-manager) + resource assignment is handled separately. For gen_on_worker mode, worker 0 is + never started, so index_list[0] is never accessed. + """ index = 0 - index_list = [] + index_list: list[int | None] = [None] # index 0: worker 0 not in default rset mapping for i in range(1, num_workers + 1): - if i in zero_resource_list: + if index >= num_rsets: + # Not enough rsets index_list.append(None) else: - if index >= num_rsets: - # Not enough rsets - index_list.append(None) - else: - index_list.append(index) + index_list.append(index) index += 1 return index_list @@ -197,7 +198,6 @@ def __init__(self, num_workers, resources, workerID): self.matching_slots = True self.slot_count = None self.slots_on_node = None - self.zero_resource_workers = resources.zero_resource_workers self.local_node_count = len(self.local_nodelist) self.set_slot_count() self.gen_nprocs = None @@ -306,9 +306,6 @@ def set_rset_team(self, rset_team: list[int]) -> None: slot_count - number of slots on each node local_node_count """ - if self.workerID in self.zero_resource_workers: - return - if rset_team != self.rset_team: # Order matters self.rset_team = rset_team self.num_rsets = len(rset_team) @@ -364,7 +361,7 @@ def get_local_nodelist( local_nodelist = list(OrderedDict.fromkeys(team_list)) # Maintain order of nodes logger.debug(f"Worker's local_nodelist is {local_nodelist}") - slots = {} + slots: dict[str, list[int]] = {} for node in local_nodelist: slots[node] = [] diff --git a/libensemble/sim_funcs/augmented_branin.py b/libensemble/sim_funcs/augmented_branin.py index 5937db7305..79855b241f 100644 --- a/libensemble/sim_funcs/augmented_branin.py +++ b/libensemble/sim_funcs/augmented_branin.py @@ -4,9 +4,10 @@ Augmented Branin is a modified version of the Branin function with a fidelity parameter. """ -__all__ = ["augmented_branin", "augmented_branin_func"] +__all__ = ["augmented_branin", "augmented_branin_callable", "augmented_branin_func"] import math + import numpy as np @@ -26,12 +27,30 @@ def augmented_branin(H, persis_info, sim_specs, libE_info): return H_o, persis_info +def augmented_branin_callable(input_dict: dict) -> dict: + """ + gest-api style simulator for the augmented Branin function. + + Args: + input_dict: Dictionary with keys ``"x0"``, ``"x1"``, and ``"fidelity"``. + + Returns: + Dictionary with key ``"f"`` containing the (negated) objective value. + """ + x = np.array([[input_dict["x0"], input_dict["x1"]]]) + fidelity = input_dict["fidelity"] + f = augmented_branin_func(x, fidelity)[0] + return {"f": f} + + def augmented_branin_func(x, fidelity): """Augmented Branin function for multi-fidelity optimization.""" x0 = x[:, 0] x1 = x[:, 1] - t1 = 15 * x1 - (5.1 / (4 * math.pi**2) - 0.1 * (1 - fidelity)) * (15 * x0 - 5) ** 2 + 5 / math.pi * (15 * x0 - 5) - 6 + t1 = ( + 15 * x1 - (5.1 / (4 * math.pi**2) - 0.1 * (1 - fidelity)) * (15 * x0 - 5) ** 2 + 5 / math.pi * (15 * x0 - 5) - 6 + ) t2 = 10 * (1 - 1 / (8 * math.pi)) * np.cos(15 * x0 - 5) result = t1**2 + t2 + 10 diff --git a/libensemble/sim_funcs/borehole_kills.py b/libensemble/sim_funcs/borehole_kills.py index 54a31256b3..47a00af90a 100644 --- a/libensemble/sim_funcs/borehole_kills.py +++ b/libensemble/sim_funcs/borehole_kills.py @@ -5,7 +5,7 @@ from libensemble.sim_funcs.surmise_test_function import borehole_true -def subproc_borehole(H, delay): +def subproc_borehole(H, delay, poll_manager): """This evaluates the Borehole function using a subprocess running compiled code. @@ -15,14 +15,14 @@ def subproc_borehole(H, delay): """ with open("input", "w") as f: - H["thetas"][0].tofile(f) - H["x"][0].tofile(f) + H["thetas"].tofile(f) + H["x"].tofile(f) exctr = Executor.executor args = "input" + " " + str(delay) task = exctr.submit(app_name="borehole", app_args=args, stdout="out.txt", stderr="err.txt") - calc_status = exctr.polling_loop(task, delay=0.01, poll_manager=True) + calc_status = exctr.polling_loop(task, delay=0.01, poll_manager=poll_manager) if calc_status in MAN_KILL_SIGNALS + [TASK_FAILED]: f = np.inf @@ -45,7 +45,7 @@ def borehole(H, persis_info, sim_specs, libE_info): if sim_id > sim_specs["user"]["init_sample_size"]: delay = 2 + np.random.normal(scale=0.5) - f, calc_status = subproc_borehole(H, delay) + f, calc_status = subproc_borehole(H, delay, sim_specs["user"].get("poll_manager", True)) if calc_status in MAN_KILL_SIGNALS and "sim_killed" in H_o.dtype.names: H_o["sim_killed"] = True # For calling script to print only. diff --git a/libensemble/sim_funcs/branin/branin_obj.py b/libensemble/sim_funcs/branin/branin_obj.py index ff186732bb..1ec86f0d4c 100644 --- a/libensemble/sim_funcs/branin/branin_obj.py +++ b/libensemble/sim_funcs/branin/branin_obj.py @@ -8,11 +8,8 @@ import numpy as np from libensemble.sim_funcs.branin.branin import branin -from libensemble.specs import input_fields, output_data -@input_fields(["x"]) -@output_data([("f", float)]) def call_branin(H, _, sim_specs): """Evaluates the Branin function""" batch = len(H["x"]) diff --git a/libensemble/sim_funcs/executor_hworld.py b/libensemble/sim_funcs/executor_hworld.py index 2055b40e4e..cb7e16a355 100644 --- a/libensemble/sim_funcs/executor_hworld.py +++ b/libensemble/sim_funcs/executor_hworld.py @@ -9,7 +9,6 @@ WORKER_KILL_ON_TIMEOUT, ) from libensemble.sim_funcs.six_hump_camel import six_hump_camel_func -from libensemble.specs import input_fields, output_data __all__ = ["executor_hworld"] @@ -67,8 +66,6 @@ def custom_polling_loop(exctr, task, timeout_sec=5.0, delay=0.3): return task, calc_status -@input_fields(["x"]) -@output_data([("f", float), ("cstat", int)]) def executor_hworld(H, _, sim_specs, info): """ Tests launching and polling task and exiting on task finish diff --git a/libensemble/sim_funcs/gest_api_wrapper.py b/libensemble/sim_funcs/gest_api_wrapper.py new file mode 100644 index 0000000000..414c5a5296 --- /dev/null +++ b/libensemble/sim_funcs/gest_api_wrapper.py @@ -0,0 +1,93 @@ +""" +Wrapper for simulation functions in the gest-api format. + +Gest-api functions take an input_dict (single point as dictionary) with +VOCS variables and constants, and return a dict with VOCS objectives, +observables, and constraints. +""" + +import numpy as np + +__all__ = ["gest_api_sim"] + + +def gest_api_sim(H, persis_info, sim_specs, libE_info): + """ + LibEnsemble sim_f wrapper for gest-api format simulation functions. + + Converts between libEnsemble's numpy structured array format and + gest-api's dictionary format for individual points. + + Parameters + ---------- + H : numpy structured array + Input points from libEnsemble containing VOCS variables and constants + persis_info : dict + Persistent information dictionary + sim_specs : dict + Simulation specifications. Must contain: + - "vocs": VOCS object defining variables, constants, objectives, etc. + - "simulator": The gest-api function + libE_info : dict + LibEnsemble information dictionary + + Returns + ------- + H_o : numpy structured array + Output array with VOCS objectives, observables, and constraints + persis_info : dict + Updated persistent information + + Notes + ----- + The gest-api simulator function should have signature: + def simulator(input_dict: dict, **kwargs) -> dict + + Where input_dict contains VOCS variables and constants, + and the return dict contains VOCS objectives, observables, and constraints. + + If the simulator function accepts ``libE_info``, it will be passed. This + allows simulators to access libEnsemble information such as the executor. + """ + + simulator = sim_specs["simulator"] + vocs = sim_specs["vocs"] + user_specs = sim_specs.get("user", {}) + + batch = len(H) + H_o = np.zeros(batch, dtype=sim_specs["out"]) + + # Helper to get fields from VOCS (handles both object and dict) + def get_vocs_fields(vocs, attr_names): + fields = [] + is_object = hasattr(vocs, attr_names[0]) + for attr in attr_names: + obj = getattr(vocs, attr, None) if is_object else vocs.get(attr) + if obj: + fields.extend(list(obj.keys())) + return fields + + # Get input fields (variables + constants) and output fields (objectives + observables + constraints) + input_fields = get_vocs_fields(vocs, ["variables", "constants"]) + output_fields = get_vocs_fields(vocs, ["objectives", "observables", "constraints"]) + + # Process each point in the batch + for i in range(batch): + # Build input_dict from H for this point + input_dict = {} + for field in input_fields: + input_dict[field] = H[field][i] + + # Try to pass libE_info, fall back if function doesn't accept it + try: + output_dict = simulator(input_dict, libE_info=libE_info, **user_specs) + except TypeError: + # Function doesn't accept libE_info, call without it + output_dict = simulator(input_dict, **user_specs) + + # Extract outputs from the returned dict + for field in output_fields: + if field in output_dict: + H_o[field][i] = output_dict[field] + + return H_o, persis_info diff --git a/libensemble/sim_funcs/run_line_check.py b/libensemble/sim_funcs/run_line_check.py index 530b1e8d9b..d30d10ec38 100644 --- a/libensemble/sim_funcs/run_line_check.py +++ b/libensemble/sim_funcs/run_line_check.py @@ -22,7 +22,7 @@ def exp_nodelist_for_worker(exp_list, workerID, nodes_per_worker, persis_gens): node_list = comp.split(",") for node in node_list: node_name, node_num = node.split("-") - offset = workerID - (1 + persis_gens) + offset = workerID - 1 new_num = int(node_num) + int(nodes_per_worker * offset) new_node = "-".join([node_name, str(new_num)]) new_node_list.append(new_node) diff --git a/libensemble/sim_funcs/simple_sim.py b/libensemble/sim_funcs/simple_sim.py index 74e1932833..cfaf10a119 100644 --- a/libensemble/sim_funcs/simple_sim.py +++ b/libensemble/sim_funcs/simple_sim.py @@ -6,11 +6,7 @@ import numpy as np -from libensemble.specs import input_fields, output_data - -@input_fields(["x"]) -@output_data([("f", float)]) def norm_eval(H, persis_info, sim_specs, _): """ Evaluates the vector norm for a single point ``x``. diff --git a/libensemble/sim_funcs/six_hump_camel.py b/libensemble/sim_funcs/six_hump_camel.py index 0f309c682a..f453bf5bb2 100644 --- a/libensemble/sim_funcs/six_hump_camel.py +++ b/libensemble/sim_funcs/six_hump_camel.py @@ -18,12 +18,9 @@ import numpy as np from libensemble.message_numbers import EVAL_SIM_TAG, FINISHED_PERSISTENT_SIM_TAG, PERSIS_STOP, STOP_TAG -from libensemble.specs import input_fields, output_data from libensemble.tools.persistent_support import PersistentSupport -@input_fields(["x"]) -@output_data([("f", float)]) def six_hump_camel(H, persis_info, sim_specs, libE_info): """ Evaluates the six hump camel function for a collection of points given in ``H["x"]``. @@ -50,8 +47,6 @@ def six_hump_camel(H, persis_info, sim_specs, libE_info): return H_o, persis_info -@input_fields(["x"]) -@output_data([("f", float)]) def six_hump_camel_simple(x, _, sim_specs): """ Evaluates the six hump camel function for a single point ``x``. @@ -99,15 +94,13 @@ def persistent_six_hump_camel(H, persis_info, sim_specs, libE_info): tag, Work, calc_in = ps.send_recv(H_o) - final_return = None - # Overwrite final point - for testing only if sim_specs["user"].get("replace_final_fields", 0): calc_in = np.ones(1, dtype=[("x", float, (2,))]) H_o, persis_info = six_hump_camel(calc_in, persis_info, sim_specs, libE_info) - final_return = H_o + return H_o, persis_info, FINISHED_PERSISTENT_SIM_TAG - return final_return, persis_info, FINISHED_PERSISTENT_SIM_TAG + return None, persis_info, FINISHED_PERSISTENT_SIM_TAG def six_hump_camel_func(x): diff --git a/libensemble/sim_funcs/var_resources.py b/libensemble/sim_funcs/var_resources.py index 65f91c18bd..c9be963ef1 100644 --- a/libensemble/sim_funcs/var_resources.py +++ b/libensemble/sim_funcs/var_resources.py @@ -30,7 +30,6 @@ from libensemble.message_numbers import TASK_FAILED, UNSET_TAG, WORKER_DONE from libensemble.resources.resources import Resources from libensemble.sim_funcs.six_hump_camel import six_hump_camel_func -from libensemble.specs import input_fields, output_data from libensemble.tools.test_support import check_gpu_setting, check_mpi_runner @@ -77,8 +76,6 @@ def gpu_variable_resources(H, persis_info, sim_specs, libE_info): return H_o, persis_info, calc_status -@input_fields(["x"]) -@output_data([("f", float)]) def gpu_variable_resources_from_gen(H, persis_info, sim_specs, libE_info): """ Launches an app and assigns CPU and GPU resources as defined by the gen. diff --git a/libensemble/specs.py b/libensemble/specs.py index 308491303d..9ee04baa3e 100644 --- a/libensemble/specs.py +++ b/libensemble/specs.py @@ -3,9 +3,26 @@ from pathlib import Path import pydantic -from pydantic import BaseModel, Field - -from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator + +from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens +from libensemble.utils.validators import ( + check_any_workers_and_disable_rm_if_tcp, + check_exit_criteria, + check_H0, + check_input_dir_exists, + check_inputs_exist, + check_provided_ufuncs, + check_set_gen_specs_from_variables, + check_valid_comms_type, + check_valid_in, + check_valid_out, + enable_save_H_when_every_K, + set_calc_dirs_on_input_dir, + set_default_comms, + set_platform_specs_to_class, + set_workflow_dir, +) __all__ = ["SimSpecs", "GenSpecs", "AllocSpecs", "ExitCriteria", "LibeSpecs", "_EnsembleSpecs"] @@ -14,9 +31,44 @@ warnings.filterwarnings("ignore", message="Pydantic serializer warnings:") -""" -Pydantic-version agnostic -""" +def _get_dtype(field, name: str): + """Get dtype from a VOCS field, handling discrete variables.""" + dtype = getattr(field, "dtype", None) + # For discrete variables, infer dtype from values if not specified + if dtype is None and hasattr(field, "values"): + values = field.values + if values: + # Validate all values are the same type (required for NumPy array) + value_types = {type(v) for v in values} + if len(value_types) > 1: + raise ValueError( + f"Discrete variable '{name}' has mixed types {value_types}. " + "All values must be the same type to be stored in NumPy array." + ) + # Infer dtype from any value (all same type, scalar) + # next(iter(values)) gets an element without creating a list + sample_val = next(iter(values)) + if isinstance(sample_val, str): + max_len = max(len(v) for v in values) + dtype = f"U{max_len}" + else: + dtype = type(sample_val) + return dtype + + +def _convert_dtype_to_output_tuple(name: str, dtype): + """Convert dtype to proper output tuple format for NumPy dtype specification.""" + if dtype is None: + dtype = float + if isinstance(dtype, tuple): + # Check if first element is a type (type, (shape,)) format + if len(dtype) > 1 and (isinstance(dtype[0], type) or isinstance(dtype[0], str)): + return (name, dtype[0], dtype[1]) + else: + # Just shape (shape,) format, default to float + return (name, float, dtype) + else: + return (name, dtype) class SimSpecs(BaseModel): @@ -24,12 +76,22 @@ class SimSpecs(BaseModel): Specifications for configuring a Simulation Function. """ + model_config = ConfigDict( + arbitrary_types_allowed=True, populate_by_name=True, extra="forbid", validate_assignment=True + ) + sim_f: object = None """ Python function matching the ``sim_f`` interface. Evaluates parameters produced by a generator function. """ + simulator: object | None = None + """ + A callable (function) in gest-api format. + When provided, ``sim_f`` defaults to the ``gest_api_sim`` wrapper. + """ + inputs: list[str] | None = Field(default=[], alias="in") """ list of **field names** out of the complete history to pass @@ -70,10 +132,70 @@ class SimSpecs(BaseModel): the simulator function. """ + vocs: object | None = None + """ + A VOCS object. If provided and inputs/outputs are not explicitly set, + they will be automatically derived from VOCS. + """ + + @field_validator("outputs") + def check_valid_out(cls, v): + return check_valid_out(cls, v) + + @field_validator("inputs", "persis_in") + def check_valid_in(cls, v): + return check_valid_in(cls, v) + + @model_validator(mode="after") + def set_fields_from_vocs(self): + """Set inputs and outputs from VOCS if vocs is provided and fields are not set.""" + # If simulator is provided but sim_f is not, default to gest_api_sim + if self.simulator is not None and self.sim_f is None: + from libensemble.sim_funcs.gest_api_wrapper import gest_api_sim + + self.sim_f = gest_api_sim + + if self.vocs is None: + return self + + # Set inputs: variables + constants (what the sim receives) + if not self.inputs: + input_fields = [] + for attr in ["variables", "constants"]: + if obj := getattr(self.vocs, attr, None): + input_fields.extend(list(obj.keys())) + self.inputs = input_fields + + # Set outputs: objectives + observables + constraints (what the sim produces) + if not self.outputs: + out_fields = [] + for attr in ["objectives", "observables", "constraints"]: + if obj := getattr(self.vocs, attr, None): + for name, field in obj.items(): + dtype = getattr(field, "dtype", None) + out_fields.append(_convert_dtype_to_output_tuple(name, dtype)) + self.outputs = out_fields + + return self + class GenSpecs(BaseModel): """ - Specifications for configuring a Generator Function. + Specifications for configuring a Generator. + """ + + model_config = ConfigDict( + arbitrary_types_allowed=True, populate_by_name=True, extra="forbid", validate_assignment=True + ) + + generator: object | None = None + """ + A pre-initialized generator object. Produces parameters for evaluation by a + simulator function, and makes decisions based on simulator function output. + + These inherit from the `gest-api` + (https://github.com/campa-consortium/gest-api) base class. Recommended over + the classic ``gen_f`` interface. """ gen_f: object | None = None @@ -102,11 +224,40 @@ class GenSpecs(BaseModel): Also used to construct libEnsemble's history array. """ - globus_compute_endpoint: str | None = "" + initial_batch_size: int = 0 """ - A Globus Compute (https://www.globus.org/compute) ID corresponding to an active endpoint on a remote system. - libEnsemble's workers will submit generator function instances to this endpoint instead of - calling them locally. + Initial sample size. + For standardized generators, this is the number of initial points to request that the + generator create. If zero, falls back to ``batch_size``. + For persistent generators, this is the number of points evaluated before switching + from batch return to asynchronous return (if ``async_return`` is True). + + Note: Certain generators included with libEnsemble decide batch sizes via + ``gen_specs["user"]`` or other methods. + """ + + batch_size: int = 0 + """ + Number of points to generate in each batch. If zero, falls back to the number of + completed evaluations most recently told to the generator. + """ + + initial_sample_method: str | object | None = None + """ + Method for producing initial sample points before starting the generator. + If None (default), the generator is responsible for producing its own initial + sample via ``suggest()``. May be set to either: + + - a string naming a built-in sampler — currently ``"uniform"`` or + ``"latin_hypercube"`` — which libEnsemble instantiates with the VOCS, or + - a pre-constructed sampler instance (any object with a ``suggest()`` method, + typically a ``LibensembleGenerator`` subclass from ``gen_classes.sampling``). + Use this form when you need to pass extra constructor arguments + (``random_seed``, ``max_resource_sets``, ``components``, etc.) or want to + use a custom sampler. + + libEnsemble draws ``initial_batch_size`` points from the sampler, evaluates + them, and ingests the results into the generator before optimization begins. """ threaded: bool | None = False @@ -120,29 +271,124 @@ class GenSpecs(BaseModel): customizing the generator function """ + vocs: object | None = None + """ + A VOCS object. If provided and persis_in/outputs are not explicitly set, + they will be automatically derived from VOCS. + """ + + num_active_gens: int = 1 + """ + Maximum number of persistent generators to start. + Only used if using the ``only_persistent_gens`` allocation function (the default). + """ + + async_return: bool = False + """ + Return results to generator one-at-a-time as they come in (after sample). Default of False + implies batch return. + Only used if using the ``only_persistent_gens`` allocation function (the default). + """ + + active_recv_gen: bool = False + """ + Initialize generator in active-receive mode. The generator can receive results + even if it's not ready to produce new points. + Only used if using the ``only_persistent_gens`` allocation function (the default). + """ + + batch_evaluate_same_priority: bool = False + """ + Pass all points with the same priority value as a batch to a single simulator call. + """ + + alt_type: bool = False + """ + Enable specialized allocator behavior for ``only_persistent_gens``. + """ + + @field_validator("outputs") + def check_valid_out(cls, v): + return check_valid_out(cls, v) + + @field_validator("inputs", "persis_in") + def check_valid_in(cls, v): + return check_valid_in(cls, v) + + @model_validator(mode="after") + def set_fields_from_vocs(self): + """Set persis_in and outputs from VOCS if vocs is provided and fields are not set.""" + if self.vocs is None: + return self + + # Set persis_in: ALL VOCS fields (variables + constants + objectives + observables + constraints) + if not self.persis_in: + persis_in_fields = [] + for attr in ["variables", "constants", "objectives", "observables", "constraints"]: + if obj := getattr(self.vocs, attr, None): + persis_in_fields.extend(list(obj.keys())) + self.persis_in = persis_in_fields + + # Set inputs: same as persis_in for gest-api generators (needed for H0 ingestion) + if not self.inputs and self.generator is not None: + self.inputs = self.persis_in + + # Set outputs: variables + constants (what the generator produces) + if not self.outputs: + out_fields = [] + for attr in ["variables", "constants"]: + if obj := getattr(self.vocs, attr, None): + for name, field in obj.items(): + dtype = _get_dtype(field, name) + out_fields.append(_convert_dtype_to_output_tuple(name, dtype)) + self.outputs = out_fields + + # Add _id field if generator returns_id is True + if self.generator is not None and getattr(self.generator, "returns_id", False): + if self.outputs is None: + self.outputs = [] + if "_id" not in [f[0] for f in self.outputs]: + self.outputs.append(("_id", int)) + if self.persis_in is None: + self.persis_in = [] + if "_id" not in self.persis_in: + self.persis_in.append("_id") + + return self + + @model_validator(mode="after") + def check_set_gen_specs_from_variables(self): + return check_set_gen_specs_from_variables(self) + class AllocSpecs(BaseModel): """ Specifications for configuring an Allocation Function. """ - alloc_f: object = give_sim_work_first + model_config = ConfigDict( + arbitrary_types_allowed=True, populate_by_name=True, extra="forbid", validate_assignment=True + ) + + alloc_f: object = only_persistent_gens """ Python function matching the ``alloc_f`` interface. Decides when simulator and generator functions should be called, and with what resources and parameters. + + .. note:: + For libEnsemble v2.0, the default allocation function is now ``only_persistent_gens``, instead + of ``give_sim_work_first``. """ - user: dict | None = {"num_active_gens": 1} + user: dict | None = {} """ A user-data dictionary to place bounds, constants, settings, or other parameters for customizing the allocation function. - """ - outputs: list[tuple] = Field([], alias="out") - """ - list of 2- or 3-tuples corresponding to NumPy dtypes. e.g. ``("dim", int, (3,))``, or ``("path", str)``. - Allocation functions that modify libEnsemble's History array with additional fields should list those - fields here. Also used to construct libEnsemble's history array. + .. note:: + As of libEnsemble v2.0, options related to the default allocation function + (e.g., ``async_return``, ``num_active_gens``) have been moved to + :class:`GenSpecs`. """ # end_alloc_tag @@ -152,6 +398,10 @@ class ExitCriteria(BaseModel): Specifications for configuring when libEnsemble should stop a given run. """ + model_config = ConfigDict( + arbitrary_types_allowed=True, populate_by_name=True, extra="forbid", validate_assignment=True + ) + sim_max: int | None = None """Stop when this many new points have been evaluated by simulation functions.""" @@ -170,6 +420,10 @@ class LibeSpecs(BaseModel): Specifications for configuring libEnsemble's runtime behavior. """ + model_config = ConfigDict( + arbitrary_types_allowed=True, populate_by_name=True, extra="forbid", validate_assignment=True + ) + comms: str | None = "mpi" """ Manager/Worker communications mode. ``'mpi'``, ``'local'``, ``'threads'``, or ``'tcp'`` @@ -180,9 +434,9 @@ class LibeSpecs(BaseModel): nworkers: int | None = 0 """ Number of worker processes in ``"local"``, ``"threads"``, or ``"tcp"``.""" - gen_on_manager: bool | None = False - """ Instructs Manager process to run generator functions. - This generator function can access/modify user objects by reference. + gen_on_worker: bool = False + """ Instructs libEnsemble to run generator functions on a worker rank. + By default, the generator runs on the manager process as a thread (Worker 0). """ mpi_comm: object | None = None @@ -240,7 +494,7 @@ class LibeSpecs(BaseModel): ``False`` by default to protect results. """ - workflow_dir_path: str | Path | None = "." + workflow_dir_path: str | Path = "." """ Optional path to the workflow directory. """ @@ -312,6 +566,42 @@ class LibeSpecs(BaseModel): zeros are padded to the sim/gen ID. """ + @field_validator("comms") + def check_valid_comms_type(cls, value): + return check_valid_comms_type(cls, value) + + @field_validator("platform_specs") + def set_platform_specs_to_class(cls, value): + return set_platform_specs_to_class(cls, value) + + @field_validator("sim_input_dir", "gen_input_dir") + def check_input_dir_exists(cls, value): + return check_input_dir_exists(cls, value) + + @field_validator("sim_dir_copy_files", "sim_dir_symlink_files", "gen_dir_copy_files", "gen_dir_symlink_files") + def check_inputs_exist(cls, value): + return check_inputs_exist(cls, value) + + @model_validator(mode="before") + def set_default_comms(cls, values): + return set_default_comms(cls, values) + + @model_validator(mode="after") + def check_any_workers_and_disable_rm_if_tcp(self): + return check_any_workers_and_disable_rm_if_tcp(self) + + @model_validator(mode="after") + def enable_save_H_when_every_K(self): + return enable_save_H_when_every_K(self) + + @model_validator(mode="after") + def set_workflow_dir(self): + return set_workflow_dir(self) + + @model_validator(mode="after") + def set_calc_dirs_on_input_dir(self): + return set_calc_dirs_on_input_dir(self) + platform: str | None = "" """Name of a known platform defined in the platforms module. @@ -400,16 +690,10 @@ class LibeSpecs(BaseModel): worker_cmd: list[str] | None = [] """ TCP Only: Split string corresponding to worker/client Python process invocation. Contains - a local Python path, calling script, and manager/server format-fields for ``manager_ip``, + a local Python path, user script, and manager/server format-fields for ``manager_ip``, ``manager_port``, ``authkey``, and ``workerID``. ``nworkers`` is specified normally. """ - use_persis_return_gen: bool | None = False - """ Adds persistent generator output fields to the History array on return. """ - - use_persis_return_sim: bool | None = False - """ Adds persistent simulator output fields to the History array on return. """ - final_gen_send: bool | None = False """ Send final simulation results to persistent generators before shutdown. @@ -425,7 +709,7 @@ class LibeSpecs(BaseModel): num_resource_sets: int | None = 0 """ Total number of resource sets. Resources will be divided into this number. - If not set, resources will be divided evenly (excluding zero_resource_workers). + If not set, resources will be divided evenly by the number of workers. """ gen_num_procs: int | None = 0 @@ -471,13 +755,6 @@ class LibeSpecs(BaseModel): libEnsemble processes (manager and workers) are running. """ - zero_resource_workers: list[int] | None = [] - """ - list of workers that require no resources. For when a fixed mapping of workers - to resources is required. Otherwise, use ``num_resource_sets``. - For use with supported allocation functions. - """ - gen_workers: list[int] | None = [] """ list of workers that should only run generators. All other workers will only @@ -498,6 +775,10 @@ class LibeSpecs(BaseModel): class _EnsembleSpecs(BaseModel): """An all-encompassing model for a libEnsemble workflow.""" + model_config = ConfigDict( + arbitrary_types_allowed=True, populate_by_name=True, extra="forbid", validate_assignment=True + ) + H0: object | None = None # np.ndarray - avoids sphinx issue """ A previous or preformatted libEnsemble History array to prepend. """ @@ -519,96 +800,14 @@ class _EnsembleSpecs(BaseModel): alloc_specs: AllocSpecs | None = AllocSpecs() """ Specifications for the allocation function. """ + @model_validator(mode="after") + def check_exit_criteria(self): + return check_exit_criteria(self) -def input_fields(fields: list[str]): - """Decorates a user-function with a list of field names to pass in on initialization. - - Decorated functions don't need those fields specified in ``SimSpecs.inputs`` or ``GenSpecs.inputs``. - - .. code-block:: python - - from libensemble.specs import input_fields, output_data - - - @input_fields(["x"]) - @output_data([("f", float)]) - def norm_eval(x, persis_info, sim_specs): - H_o = np.zeros(1, dtype=sim_specs["out"]) - H_o["f"] = np.linalg.norm(x) - return H_o, persis_info - """ - - def decorator(func): - setattr(func, "inputs", fields) - if not func.__doc__: - func.__doc__ = "" - func.__doc__ = f"\n **Input Fields:** ``{func.inputs}``\n" + func.__doc__ - return func - - return decorator - - -def persistent_input_fields(fields: list[str]): - """Decorates a *persistent* user-function with a list of field names to send in throughout runtime. - - Decorated functions don't need those fields specified in ``SimSpecs.persis_in`` or ``GenSpecs.persis_in``. - - .. code-block:: python - - from libensemble.specs import persistent_input_fields, output_data - - - @persistent_input_fields(["f"]) - @output_data(["x", float]) - def persistent_uniform(_, persis_info, gen_specs, libE_info): - - b, n, lb, ub = _get_user_params(gen_specs["user"]) - ps = PersistentSupport(libE_info, EVAL_GEN_TAG) - - tag = None - while tag not in [STOP_TAG, PERSIS_STOP]: - H_o = np.zeros(b, dtype=gen_specs["out"]) - H_o["x"] = persis_info["rand_stream"].uniform(lb, ub, (b, n)) - tag, Work, calc_in = ps.send_recv(H_o) - if hasattr(calc_in, "__len__"): - b = len(calc_in) - - return H_o, persis_info, FINISHED_PERSISTENT_GEN_TAG - """ - - def decorator(func): - setattr(func, "persis_in", fields) - if not func.__doc__: - func.__doc__ = "" - func.__doc__ = f"\n **Persistent Input Fields:** ``{func.persis_in}``\n" + func.__doc__ - return func - - return decorator - - -def output_data(fields: list[tuple]): - """Decorates a user-function with a list of tuples corresponding to NumPy dtypes for the function's output data. - - Decorated functions don't need those fields specified in ``SimSpecs.outputs`` or ``GenSpecs.outputs``. - - .. code-block:: python - - from libensemble.specs import input_fields, output_data - - - @input_fields(["x"]) - @output_data([("f", float)]) - def norm_eval(x, persis_info, sim_specs): - H_o = np.zeros(1, dtype=sim_specs["out"]) - H_o["f"] = np.linalg.norm(x) - return H_o, persis_info - """ - - def decorator(func): - setattr(func, "outputs", fields) - if not func.__doc__: - func.__doc__ = "" - func.__doc__ = f"\n **Output Datatypes:** ``{func.outputs}``\n" + func.__doc__ - return func + @model_validator(mode="after") + def check_H0(self): + return check_H0(self) - return decorator + @model_validator(mode="after") + def check_provided_ufuncs(self): + return check_provided_ufuncs(self) diff --git a/libensemble/tests/functionality_tests/1d_sampling.json b/libensemble/tests/functionality_tests/1d_sampling.json deleted file mode 100644 index 6066cdd693..0000000000 --- a/libensemble/tests/functionality_tests/1d_sampling.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "libE_specs": { - "save_every_k_gens": 300, - "safe_mode": false - }, - "exit_criteria": { - "gen_max": 501 - }, - "sim_specs": { - "sim_f": "libensemble.sim_funcs.simple_sim.norm_eval", - "inputs": [ - "x" - ], - "outputs": { - "f": { - "type": "float" - } - } - }, - "gen_specs": { - "gen_f": "libensemble.gen_funcs.sampling.latin_hypercube_sample", - "outputs": { - "x": { - "type": "float", - "size": 1 - } - }, - "user": { - "gen_batch_size": 500 - } - } -} diff --git a/libensemble/tests/functionality_tests/1d_sampling.toml b/libensemble/tests/functionality_tests/1d_sampling.toml deleted file mode 100644 index 6618019a6f..0000000000 --- a/libensemble/tests/functionality_tests/1d_sampling.toml +++ /dev/null @@ -1,22 +0,0 @@ -[libE_specs] - save_every_k_gens = 300 - safe_mode = false - -[exit_criteria] - gen_max = 501 - -[sim_specs] - sim_f = "libensemble.sim_funcs.simple_sim.norm_eval" - inputs = ["x"] - [sim_specs.outputs] - [sim_specs.outputs.f] - type = "float" - -[gen_specs] - gen_f = "libensemble.gen_funcs.sampling.latin_hypercube_sample" - [gen_specs.outputs] - [gen_specs.outputs.x] - type = "float" - size = 1 - [gen_specs.user] - gen_batch_size = 500 diff --git a/libensemble/tests/functionality_tests/1d_sampling.yaml b/libensemble/tests/functionality_tests/1d_sampling.yaml deleted file mode 100644 index 3e82548ae8..0000000000 --- a/libensemble/tests/functionality_tests/1d_sampling.yaml +++ /dev/null @@ -1,24 +0,0 @@ -libE_specs: - save_every_k_gens: 300 - safe_mode: False - use_workflow_dir: True - -exit_criteria: - gen_max: 501 - -sim_specs: - sim_f: libensemble.sim_funcs.simple_sim.norm_eval - inputs: - - x - outputs: - f: - type: float - -gen_specs: - gen_f: libensemble.gen_funcs.sampling.latin_hypercube_sample - outputs: - x: - type: float - size: 1 - user: - gen_batch_size: 500 diff --git a/libensemble/tests/functionality_tests/sine_gen.py b/libensemble/tests/functionality_tests/sine_gen.py index 38f320e946..1a8dfe4ecb 100644 --- a/libensemble/tests/functionality_tests/sine_gen.py +++ b/libensemble/tests/functionality_tests/sine_gen.py @@ -11,13 +11,13 @@ def gen_random_sample(InputArray, persis_info, gen_specs): # Determine how many values to generate num = len(lower) - batch_size = user_specs["gen_batch_size"] + batch_size = gen_specs["batch_size"] # Create empty array of "batch_size" zeros. Array dtype should match "out" fields OutputArray = np.zeros(batch_size, dtype=gen_specs["out"]) - # Set the "x" output field to contain random numbers, using random stream - OutputArray["x"] = persis_info["rand_stream"].uniform(lower, upper, (batch_size, num)) + # Set the "x" output field to contain random numbers + OutputArray["x"] = np.random.uniform(lower, upper, (batch_size, num)) # Send back our output and persis_info return OutputArray, persis_info diff --git a/libensemble/tests/functionality_tests/sine_gen_std.py b/libensemble/tests/functionality_tests/sine_gen_std.py new file mode 100644 index 0000000000..43bafbf842 --- /dev/null +++ b/libensemble/tests/functionality_tests/sine_gen_std.py @@ -0,0 +1,26 @@ +import numpy as np +from gest_api import Generator + + +class RandomSample(Generator): + """ + This sampler accepts a gest-api VOCS object for configuration and returns random samples. + """ + + def __init__(self, vocs): + self.variables = vocs.variables + self.rng = np.random.default_rng(1) + self._validate_vocs(vocs) + + def _validate_vocs(self, vocs): + if not len(vocs.variables) > 0: + raise ValueError("vocs must have at least one variable") + + def suggest(self, num_points): + output = [] + for _ in range(num_points): + trial = {} + for key in self.variables.keys(): + trial[key] = self.rng.uniform(self.variables[key].domain[0], self.variables[key].domain[1]) + output.append(trial) + return output diff --git a/libensemble/tests/functionality_tests/test_1d_sampling_from_files.py b/libensemble/tests/functionality_tests/test_1d_sampling_from_files.py deleted file mode 100644 index d80025d238..0000000000 --- a/libensemble/tests/functionality_tests/test_1d_sampling_from_files.py +++ /dev/null @@ -1,44 +0,0 @@ -""" -Runs libEnsemble with Latin hypercube sampling on a simple 1D problem using -the libEnsemble yaml interface - -Execute via one of the following commands (e.g. 3 workers): - mpiexec -np 4 python test_1d_sampling_from_yaml.py - python test_1d_sampling_from_yaml.py --nworkers 3 - python test_1d_sampling_from_yaml.py --nworkers 3 --comms tcp - -The number of concurrent evaluations of the objective function will be 4-1=3. -""" - -# Do not change these lines - they are parsed by run-tests.sh -# TESTSUITE_COMMS: mpi local tcp -# TESTSUITE_NPROCS: 2 4 -# TESTSUITE_EXTRA: true - -import numpy as np - -from libensemble import Ensemble - -# Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). -if __name__ == "__main__": - sampling = Ensemble(parse_args=True) - sampling.from_json("1d_sampling.json") - sampling.from_toml("1d_sampling.toml") - sampling.from_yaml("1d_sampling.yaml") - - sampling.gen_specs.user.update( - { - "lb": np.array([-3]), - "ub": np.array([3]), - } - ) - - sampling.add_random_streams() - - # Perform the run - sampling.run() - - if sampling.is_manager: - assert len(sampling.H) >= 501 - print("\nlibEnsemble with random sampling has generated enough points") - sampling.save_output(__file__) diff --git a/libensemble/tests/functionality_tests/test_1d_sampling_no_comms_given.py b/libensemble/tests/functionality_tests/test_1d_sampling_no_comms_given.py index 563ee920e2..90dab75070 100644 --- a/libensemble/tests/functionality_tests/test_1d_sampling_no_comms_given.py +++ b/libensemble/tests/functionality_tests/test_1d_sampling_no_comms_given.py @@ -17,11 +17,12 @@ import numpy as np from libensemble import Ensemble +from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first from libensemble.gen_funcs.sampling import latin_hypercube_sample as gen_f # Import libEnsemble items for this test from libensemble.sim_funcs.simple_sim import norm_eval as sim_f -from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs +from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, LibeSpecs, SimSpecs from libensemble.tools import check_npy_file_exists # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). @@ -38,8 +39,8 @@ gen_specs = GenSpecs( gen_f=gen_f, outputs=[("x", float, (1,))], + batch_size=500, user={ - "gen_batch_size": 500, "lb": np.array([-3]), "ub": np.array([3]), }, @@ -54,7 +55,7 @@ exit_criteria=exit_criteria, ) - sampling.add_random_streams() + sampling.alloc_specs = AllocSpecs(alloc_f=give_sim_work_first) H, persis_info, flag = sampling.run() if sampling.is_manager: diff --git a/libensemble/tests/functionality_tests/test_1d_sampling_with_profile.py b/libensemble/tests/functionality_tests/test_1d_sampling_with_profile.py index 645228d137..1e59cc91cd 100644 --- a/libensemble/tests/functionality_tests/test_1d_sampling_with_profile.py +++ b/libensemble/tests/functionality_tests/test_1d_sampling_with_profile.py @@ -19,10 +19,11 @@ import numpy as np +from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first from libensemble.gen_funcs.sampling import latin_hypercube_sample as gen_f from libensemble.libE import libE from libensemble.sim_funcs.simple_sim import norm_eval as sim_f -from libensemble.tools import add_unique_random_streams, parse_args +from libensemble.tools import parse_args # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": @@ -31,6 +32,7 @@ libE_specs["profile"] = True libE_specs["safe_mode"] = False libE_specs["kill_canceled_sims"] = False + libE_specs["gen_on_worker"] = True sim_specs = { "sim_f": sim_f, @@ -41,19 +43,21 @@ gen_specs = { "gen_f": gen_f, "out": [("x", float, (1,))], + "batch_size": 500, "user": { - "gen_batch_size": 500, "lb": np.array([-3]), "ub": np.array([3]), }, } - persis_info = add_unique_random_streams({}, nworkers + 1) + alloc_specs = { + "alloc_f": give_sim_work_first, + } exit_criteria = {"sim_max": 501} # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) if is_manager: assert len(H) >= 501 diff --git a/libensemble/tests/functionality_tests/test_1d_splitcomm.py b/libensemble/tests/functionality_tests/test_1d_splitcomm.py index de73660d70..c66ed569ef 100644 --- a/libensemble/tests/functionality_tests/test_1d_splitcomm.py +++ b/libensemble/tests/functionality_tests/test_1d_splitcomm.py @@ -15,13 +15,14 @@ import numpy as np +from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first from libensemble.gen_funcs.sampling import latin_hypercube_sample as gen_f # Import libEnsemble items for this test from libensemble.libE import libE from libensemble.sim_funcs.simple_sim import norm_eval as sim_f from libensemble.tests.regression_tests.common import mpi_comm_split -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +from libensemble.tools import parse_args, save_libE_output # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": @@ -34,6 +35,7 @@ libE_specs["H_file_prefix"] = "splitcomm_" + str(sub_comm_number) libE_specs["safe_mode"] = False libE_specs["disable_log_files"] = True + libE_specs["gen_on_worker"] = True sim_specs = { "sim_f": sim_f, @@ -44,19 +46,21 @@ gen_specs = { "gen_f": gen_f, "out": [("x", float, (1,))], + "batch_size": 500, "user": { - "gen_batch_size": 500, "lb": np.array([-3]), "ub": np.array([3]), }, } - persis_info = add_unique_random_streams({}, nworkers + 1, seed=1234) + alloc_specs = { + "alloc_f": give_sim_work_first, + } exit_criteria = {"gen_max": 501} # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) if is_manager: assert len(H) >= 501 diff --git a/libensemble/tests/functionality_tests/test_1d_subcomm.py b/libensemble/tests/functionality_tests/test_1d_subcomm.py index 7f607c31c9..aa0f34d0a0 100644 --- a/libensemble/tests/functionality_tests/test_1d_subcomm.py +++ b/libensemble/tests/functionality_tests/test_1d_subcomm.py @@ -15,13 +15,14 @@ import numpy as np +from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first from libensemble.gen_funcs.sampling import latin_hypercube_sample as gen_f # Import libEnsemble items for this test from libensemble.libE import libE from libensemble.sim_funcs.simple_sim import norm_eval as sim_f from libensemble.tests.regression_tests.common import mpi_comm_excl -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +from libensemble.tools import parse_args, save_libE_output # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": @@ -48,19 +49,21 @@ gen_specs = { "gen_f": gen_f, "out": [("x", float, (1,))], + "batch_size": 500, "user": { - "gen_batch_size": 500, "lb": np.array([-3]), "ub": np.array([3]), }, } - persis_info = add_unique_random_streams({}, nworkers + 1, seed=1234) + alloc_specs = { + "alloc_f": give_sim_work_first, + } exit_criteria = {"gen_max": 501} # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) if is_manager: assert len(H) >= 501 diff --git a/libensemble/tests/functionality_tests/test_1d_super_simple.py b/libensemble/tests/functionality_tests/test_1d_super_simple.py index e84255714f..1a178c2cf8 100644 --- a/libensemble/tests/functionality_tests/test_1d_super_simple.py +++ b/libensemble/tests/functionality_tests/test_1d_super_simple.py @@ -15,11 +15,12 @@ import numpy as np +from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first from libensemble.gen_funcs.sampling import latin_hypercube_sample as gen_f # Import libEnsemble items for this test from libensemble.libE import libE -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +from libensemble.tools import parse_args, save_libE_output def sim_f(In): @@ -28,10 +29,6 @@ def sim_f(In): return Out -def sim_f_noreturn(In): - print(np.linalg.norm(In)) - - if __name__ == "__main__": nworkers, is_manager, libE_specs, _ = parse_args() @@ -44,32 +41,22 @@ def sim_f_noreturn(In): gen_specs = { "gen_f": gen_f, "out": [("x", float, (1,))], + "batch_size": 500, "user": { - "gen_batch_size": 500, "lb": np.array([-3]), "ub": np.array([3]), }, } - persis_info = add_unique_random_streams({}, nworkers + 1, seed=1234) - exit_criteria = {"gen_max": 501} - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) - - if is_manager: - assert len(H) >= 501 - print("\nlibEnsemble with random sampling has generated enough points") - save_libE_output(H, persis_info, __file__, nworkers) - - # Test running a sim_f without any returns - sim_specs = { - "sim_f": sim_f_noreturn, - "in": ["x"], + alloc_specs = { + "alloc_f": give_sim_work_first, } - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) if is_manager: assert len(H) >= 501 print("\nlibEnsemble with random sampling has generated enough points") + save_libE_output(H, persis_info, __file__, nworkers) diff --git a/libensemble/tests/functionality_tests/test_1d_uniform_sampling_with_comm_dup.py b/libensemble/tests/functionality_tests/test_1d_uniform_sampling_with_comm_dup.py index 7d2d9f588f..4bdb60e0cd 100644 --- a/libensemble/tests/functionality_tests/test_1d_uniform_sampling_with_comm_dup.py +++ b/libensemble/tests/functionality_tests/test_1d_uniform_sampling_with_comm_dup.py @@ -19,12 +19,13 @@ import numpy as np +from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first from libensemble.gen_funcs.sampling import uniform_random_sample as gen_f # Import libEnsemble items for this test from libensemble.libE import libE from libensemble.sim_funcs.simple_sim import norm_eval as sim_f -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +from libensemble.tools import parse_args, save_libE_output # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": @@ -51,19 +52,21 @@ "gen_f": gen_f, "in": ["sim_id"], "out": [("x", float, (1,))], + "batch_size": 500, "user": { "lb": np.array([-3]), "ub": np.array([3]), - "gen_batch_size": 500, }, } - persis_info = add_unique_random_streams({}, nworkers + 1) - exit_criteria = {"gen_max": 501} + alloc_specs = { + "alloc_f": give_sim_work_first, + } + # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs) if is_manager: # assert libE_specs["comms"] == "mpi", "MPI default comms should be set" diff --git a/libensemble/tests/functionality_tests/test_GPU_gen_resources.py b/libensemble/tests/functionality_tests/test_GPU_gen_resources.py index d77088d7e4..eca3db0ac8 100644 --- a/libensemble/tests/functionality_tests/test_GPU_gen_resources.py +++ b/libensemble/tests/functionality_tests/test_GPU_gen_resources.py @@ -32,7 +32,6 @@ import numpy as np -from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f from libensemble.executors.mpi_executor import MPIExecutor from libensemble.gen_funcs.persistent_sampling_var_resources import uniform_sample_with_sim_gen_resources as gen_f @@ -40,7 +39,7 @@ from libensemble.libE import libE from libensemble.sim_funcs import six_hump_camel from libensemble.sim_funcs.var_resources import gpu_variable_resources_from_gen as sim_f -from libensemble.tools import add_unique_random_streams, parse_args +from libensemble.tools import parse_args # from libensemble import logger # logger.set_level("DEBUG") # For testing the test @@ -50,7 +49,7 @@ if __name__ == "__main__": nworkers, is_manager, libE_specs, _ = parse_args() - libE_specs["num_resource_sets"] = nworkers # Persistent gen DOES need resources + libE_specs["num_resource_sets"] = nworkers + 1 # Persistent gen DOES need resources # Mock GPU system / uncomment to detect GPUs libE_specs["sim_dirs_make"] = True # Will only contain files if dry_run is False @@ -79,39 +78,36 @@ "gen_f": gen_f, "persis_in": ["f", "x", "sim_id"], "out": [("num_procs", int), ("num_gpus", int), ("x", float, n)], + "initial_batch_size": nworkers - 1, + "batch_evaluate_same_priority": False, + "async_return": False, "user": { - "initial_batch_size": nworkers - 1, - "max_procs": nworkers - 1, # Any sim created can req. 1 worker up to all. + "max_procs": nworkers, # Any sim created can req. 1 worker up to all. "lb": np.array([-3, -2]), "ub": np.array([3, 2]), "dry_run": dry_run, }, } - alloc_specs = { - "alloc_f": alloc_f, - "user": { - "give_all_with_same_priority": False, - "async_return": False, # False batch returns - }, - } - exit_criteria = {"sim_max": 20} - libE_specs["resource_info"] = {"cores_on_node": (nworkers * 2, nworkers * 4), "gpus_on_node": nworkers} + libE_specs["resource_info"] = { + "cores_on_node": ((nworkers + 1) * 2, (nworkers + 1) * 4), + "gpus_on_node": nworkers + 1, + } base_libE_specs = libE_specs.copy() - for gen_on_manager in [False, True]: + for gen_on_worker in [False, True]: for run in range(5): # reset libE_specs = base_libE_specs.copy() - libE_specs["gen_on_manager"] = gen_on_manager - persis_info = add_unique_random_streams({}, nworkers + 1) + libE_specs["gen_on_worker"] = gen_on_worker + persis_info = {} if run == 0: libE_specs["gen_num_procs"] = 2 elif run == 1: - if gen_on_manager: - print("SECOND LIBE CALL WITH GEN ON MANAGER") + if gen_on_worker: + print("SECOND LIBE CALL WITH GEN ON WORKER INSTEAD OF MANAGER") libE_specs["gen_num_gpus"] = 1 elif run == 2: persis_info["gen_num_gpus"] = 1 @@ -126,8 +122,6 @@ gen_specs["user"]["max_procs"] = max(nworkers - 2, 1) # Perform the run - H, persis_info, flag = libE( - sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs, alloc_specs=alloc_specs - ) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) # All asserts are in gen and sim funcs diff --git a/libensemble/tests/functionality_tests/test_active_persistent_worker_abort.py b/libensemble/tests/functionality_tests/test_active_persistent_worker_abort.py index 7d99968629..4a7b47c0a5 100644 --- a/libensemble/tests/functionality_tests/test_active_persistent_worker_abort.py +++ b/libensemble/tests/functionality_tests/test_active_persistent_worker_abort.py @@ -31,7 +31,7 @@ from libensemble.libE import libE from libensemble.sim_funcs.six_hump_camel import six_hump_camel as sim_f from libensemble.tests.regression_tests.support import uniform_or_localopt_gen_out as gen_out -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +from libensemble.tools import parse_args, save_libE_output # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": @@ -48,12 +48,13 @@ "gen_f": gen_f, "persis_in": ["x", "f"], "out": gen_out, + "batch_size": 2, + "num_active_gens": 1, "user": { "localopt_method": "LN_BOBYQA", "xtol_rel": 1e-4, "lb": np.array([-3, -2]), "ub": np.array([3, 2]), - "gen_batch_size": 2, "dist_to_bound_multiple": 0.5, "localopt_maxeval": 4, }, @@ -63,12 +64,9 @@ "alloc_f": alloc_f, "user": { "batch_mode": True, - "num_active_gens": 1, }, } - persis_info = add_unique_random_streams({}, nworkers + 1) - # Set sim_max small so persistent worker is quickly terminated exit_criteria = {"sim_max": 10, "wallclock_max": 300} @@ -76,7 +74,7 @@ sys.exit("Cannot run with a persistent worker if only one worker -- aborting...") # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) if is_manager: assert flag == 0 diff --git a/libensemble/tests/functionality_tests/test_asktell_sampling.py b/libensemble/tests/functionality_tests/test_asktell_sampling.py new file mode 100644 index 0000000000..49d86f317e --- /dev/null +++ b/libensemble/tests/functionality_tests/test_asktell_sampling.py @@ -0,0 +1,95 @@ +""" +Runs libEnsemble with Latin hypercube sampling on a simple 1D problem + +Execute via one of the following commands (e.g. 3 workers): + mpiexec -np 4 python test_sampling_asktell_gen.py + python test_sampling_asktell_gen.py --nworkers 3 --comms local + python test_sampling_asktell_gen.py --nworkers 3 --comms tcp + +The number of concurrent evaluations of the objective function will be 4-1=3. +""" + +# Do not change these lines - they are parsed by run-tests.sh +# TESTSUITE_COMMS: mpi local +# TESTSUITE_NPROCS: 4 + +import numpy as np +from gest_api.vocs import VOCS + +import libensemble.sim_funcs.six_hump_camel as six_hump_camel + +# Import libEnsemble items for this test +from libensemble.gen_classes.sampling import UniformSample +from libensemble.libE import libE +from libensemble.sim_funcs.executor_hworld import executor_hworld as sim_f_exec +from libensemble.tools import parse_args + + +def sim_f(In): + Out = np.zeros(1, dtype=[("f", float)]) + Out["f"] = np.linalg.norm(In) + return Out + + +if __name__ == "__main__": + nworkers, is_manager, libE_specs, _ = parse_args() + + sim_specs = { + "sim_f": sim_f, + "in": ["x"], + "out": [("f", float)], + } + + gen_specs = { + "persis_in": ["x", "f", "sim_id"], + "out": [("x", float, (2,))], + "initial_batch_size": 2, + "batch_size": 1, + "user": { + "lb": np.array([-3, -2]), + "ub": np.array([3, 2]), + }, + } + + variables = {"x0": [-3, 3], "x1": [-2, 2]} + objectives = {"energy": "EXPLORE"} + + vocs = VOCS(variables=variables, objectives=objectives) + + exit_criteria = {"gen_max": 11} + + for test in range(3): + if test == 0: + generator = UniformSample(vocs) + persis_info = {} + + elif test == 1: + persis_info["num_gens_started"] = 0 + generator = UniformSample(vocs, variables_mapping={"x": ["x0", "x1"], "f": ["energy"]}) + + elif test == 2: + from libensemble.executors.mpi_executor import MPIExecutor + + persis_info["num_gens_started"] = 0 + generator = UniformSample(vocs, variables_mapping={"x": ["x0", "x1"], "f": ["energy"]}) + sim_app2 = six_hump_camel.__file__ + + executor = MPIExecutor() + executor.register_app(full_path=sim_app2, app_name="six_hump_camel", calc_type="sim") # Named app + + sim_specs = { + "sim_f": sim_f_exec, + "in": ["x"], + "out": [("f", float), ("cstat", int)], + "user": {"cores": 1}, + } + + gen_specs["generator"] = generator + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, libE_specs=libE_specs) + + if is_manager: + # Basic sanity checks that we actually saved generated inputs/outputs. + assert len(H) >= 11, f"H has length {len(H)}" + assert np.any(np.linalg.norm(H["x"], axis=1) > 0.0), "All saved x values are zero" + assert np.any(H["f"] > 0.0), "All saved f values are zero" + print(H[["sim_id", "x", "f"]][:10]) diff --git a/libensemble/tests/functionality_tests/test_asktell_sampling_external_gen.py b/libensemble/tests/functionality_tests/test_asktell_sampling_external_gen.py new file mode 100644 index 0000000000..463fbbb0e9 --- /dev/null +++ b/libensemble/tests/functionality_tests/test_asktell_sampling_external_gen.py @@ -0,0 +1,93 @@ +""" +Runs libEnsemble with Latin hypercube sampling on a simple 1D problem + +using external gest_api compatible generators. + +Execute via one of the following commands (e.g. 3 workers): + mpiexec -np 4 python test_asktell_sampling_external_gen.py + python test_asktell_sampling_external_gen.py --nworkers 3 --comms local + python test_asktell_sampling_external_gen.py --nworkers 3 --comms tcp + +The number of concurrent evaluations of the objective function will be 3. +""" + +# Do not change these lines - they are parsed by run-tests.sh +# TESTSUITE_COMMS: mpi local +# TESTSUITE_NPROCS: 2 4 + +import numpy as np +from gest_api.vocs import VOCS + +from libensemble import Ensemble + +# from libensemble.gen_classes.external.sampling import UniformSampleArray +from libensemble.gen_classes.external.sampling import UniformSample +from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs + +# Import libEnsemble items for this test + + +# from gest_api.vocs import ContinuousVariable + + +def sim_f_array(In): + Out = np.zeros(1, dtype=[("f", float)]) + Out["f"] = np.linalg.norm(In) + return Out + + +def sim_f_scalar(In): + Out = np.zeros(1, dtype=[("f", float)]) + Out["f"] = np.linalg.norm(In["x0"], In["x1"]) + return Out + + +if __name__ == "__main__": + + libE_specs = LibeSpecs() + + for test in range(1): # 2 + + objectives = {"f": "EXPLORE"} + + if test == 0: + sim_f = sim_f_scalar + variables = {"x0": [-3, 3], "x1": [-2, 2]} + vocs = VOCS(variables=variables, objectives=objectives) + generator = UniformSample(vocs) + + # Requires gest-api variables array bounds update + # elif test == 1: + # sim_f = sim_f_array + # variables = {"x": ContinuousVariable(dtype=(float, (2,)),domain=[[-3, 3], [-2, 2]])} + # vocs = VOCS(variables=variables, objectives=objectives) + # generator = UniformSampleArray(vocs) + + sim_specs = SimSpecs( + sim_f=sim_f, + vocs=vocs, + ) + + gen_specs = GenSpecs( + generator=generator, + initial_batch_size=20, + batch_size=10, + vocs=vocs, + ) + + exit_criteria = ExitCriteria(gen_max=201) + + ensemble = Ensemble( + parse_args=True, + sim_specs=sim_specs, + gen_specs=gen_specs, + exit_criteria=exit_criteria, + libE_specs=libE_specs, + ) + + ensemble.run() + + if ensemble.is_manager: + print(ensemble.H[["sim_id", "x0", "x1", "f"]][:10]) + # print(ensemble.H[["sim_id", "x", "f"]][:10]) # For array variables + assert len(ensemble.H) >= 201, f"H has length {len(ensemble.H)}" diff --git a/libensemble/tests/functionality_tests/test_calc_exception.py b/libensemble/tests/functionality_tests/test_calc_exception.py index 361e1be3a8..55d99ca63f 100644 --- a/libensemble/tests/functionality_tests/test_calc_exception.py +++ b/libensemble/tests/functionality_tests/test_calc_exception.py @@ -13,10 +13,11 @@ import numpy as np +from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first from libensemble.gen_funcs.sampling import uniform_random_sample as gen_f from libensemble.libE import libE from libensemble.manager import LoggedException -from libensemble.tools import add_unique_random_streams, parse_args +from libensemble.tools import parse_args # Define sim_func @@ -38,14 +39,16 @@ def six_hump_camel_err(H, persis_info, sim_specs, _): "gen_f": gen_f, "in": ["sim_id"], "out": [("x", float, 2)], + "batch_size": 10, "user": { "lb": np.array([-3, -2]), "ub": np.array([3, 2]), - "gen_batch_size": 10, }, } - persis_info = add_unique_random_streams({}, nworkers + 1) + alloc_specs = { + "alloc_f": give_sim_work_first, + } exit_criteria = {"wallclock_max": 10} @@ -54,7 +57,7 @@ def six_hump_camel_err(H, persis_info, sim_specs, _): # Perform the run return_flag = 1 try: - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) except LoggedException as e: print(f"Caught deliberate exception: {e}") return_flag = 0 diff --git a/libensemble/tests/functionality_tests/test_cancel_in_alloc.py b/libensemble/tests/functionality_tests/test_cancel_in_alloc.py index d2c005a040..0098ba93fd 100644 --- a/libensemble/tests/functionality_tests/test_cancel_in_alloc.py +++ b/libensemble/tests/functionality_tests/test_cancel_in_alloc.py @@ -1,8 +1,8 @@ """ Runs libEnsemble in order to test the ability of an allocation function to cancel long-running simulations. In this case, the simulation has a run-time -in seconds that is drawn uniformly from [0,10] and any time the allocation -function is called and a sim_id has been evaluated for more than 5 seconds, +in seconds that is drawn uniformly from [0,60] and any time the allocation +function is called and a sim_id has been evaluated for more than 0.1 seconds, it is cancelled. Execute via one of the following commands (e.g. 3 workers): @@ -15,7 +15,7 @@ # Do not change these lines - they are parsed by run-tests.sh # TESTSUITE_COMMS: mpi local tcp -# TESTSUITE_NPROCS: 2 4 +# TESTSUITE_NPROCS: 4 import numpy as np @@ -25,7 +25,7 @@ # Import libEnsemble items for this test from libensemble.libE import libE from libensemble.sim_funcs.branin.branin_obj import call_branin as sim_f -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +from libensemble.tools import parse_args, save_libE_output # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": @@ -36,15 +36,16 @@ "sim_f": sim_f, "in": ["x"], "out": [("f", float)], - "user": {"uniform_random_pause_ub": 10}, + "user": {"uniform_random_pause_ub": 10}, # long sleep ensures sims are still running when cancel fires } gen_specs = { "gen_f": gen_f, "in": ["sim_id"], "out": [("x", float, (2,))], + "batch_size": nworkers, + "num_active_gens": 1, "user": { - "gen_batch_size": 5, "lb": np.array([-3, -2]), "ub": np.array([3, 2]), }, @@ -53,19 +54,20 @@ alloc_specs = { "alloc_f": give_sim_work_first, "user": { - "cancel_sims_time": 3, + "cancel_sims_time": 0.1, # fires on first alloc call after dispatch, before any sim can return "batch_mode": False, - "num_active_gens": 1, }, } - persis_info = add_unique_random_streams({}, nworkers + 1) - - exit_criteria = {"sim_max": 10, "wallclock_max": 300} + exit_criteria = {"sim_max": nworkers * 2, "wallclock_max": 30} # Perform the run H, persis_info, flag = libE( - sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs, alloc_specs=alloc_specs + sim_specs, + gen_specs, + exit_criteria, + libE_specs=libE_specs, + alloc_specs=alloc_specs, ) if is_manager: diff --git a/libensemble/tests/functionality_tests/test_comms.py b/libensemble/tests/functionality_tests/test_comms.py index 52c3e0771a..daa9e564cd 100644 --- a/libensemble/tests/functionality_tests/test_comms.py +++ b/libensemble/tests/functionality_tests/test_comms.py @@ -16,13 +16,14 @@ import numpy as np +from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first from libensemble.executors.mpi_executor import MPIExecutor # Only used to get workerID in float_x1000 from libensemble.gen_funcs.sampling import uniform_random_sample as gen_f # Import libEnsemble items for this test from libensemble.libE import libE from libensemble.sim_funcs.comms_testing import float_x1000 as sim_f -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +from libensemble.tools import parse_args, save_libE_output # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": @@ -44,19 +45,21 @@ "gen_f": gen_f, "in": ["sim_id"], "out": [("x", float, (2,))], + "batch_size": sim_max, "user": { "lb": np.array([-3, -2]), "ub": np.array([3, 2]), - "gen_batch_size": sim_max, }, } - persis_info = add_unique_random_streams({}, nworkers + 1) - exit_criteria = {"sim_max": sim_max, "wallclock_max": 300} + alloc_specs = { + "alloc_f": give_sim_work_first, + } + # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) if is_manager: assert flag == 0 diff --git a/libensemble/tests/functionality_tests/test_elapsed_time_abort.py b/libensemble/tests/functionality_tests/test_elapsed_time_abort.py index 9e7ab97049..1396da50f6 100644 --- a/libensemble/tests/functionality_tests/test_elapsed_time_abort.py +++ b/libensemble/tests/functionality_tests/test_elapsed_time_abort.py @@ -21,7 +21,7 @@ # Import libEnsemble items for this test from libensemble.libE import libE from libensemble.sim_funcs.six_hump_camel import six_hump_camel as sim_f -from libensemble.tools import add_unique_random_streams, eprint, parse_args, save_libE_output +from libensemble.tools import eprint, parse_args, save_libE_output # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": @@ -38,8 +38,9 @@ "gen_f": gen_f, "in": ["sim_id"], "out": [("x", float, (2,))], + "batch_size": 5, + "num_active_gens": 2, "user": { - "gen_batch_size": 5, "lb": np.array([-3, -2]), "ub": np.array([3, 2]), }, @@ -49,18 +50,13 @@ "alloc_f": give_sim_work_first, "user": { "batch_mode": False, - "num_active_gens": 2, }, } - persis_info = add_unique_random_streams({}, nworkers + 1) - exit_criteria = {"wallclock_max": 1} # Perform the run - H, persis_info, flag = libE( - sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs, alloc_specs=alloc_specs - ) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, libE_specs=libE_specs, alloc_specs=alloc_specs) if is_manager: eprint(flag) diff --git a/libensemble/tests/functionality_tests/test_evaluate_existing_plus_gen.py b/libensemble/tests/functionality_tests/test_evaluate_existing_plus_gen.py index fe3d8dad8e..3e37bc86dc 100644 --- a/libensemble/tests/functionality_tests/test_evaluate_existing_plus_gen.py +++ b/libensemble/tests/functionality_tests/test_evaluate_existing_plus_gen.py @@ -1,6 +1,6 @@ """ Test libEnsemble's capability to evaluate existing points and then generate -new samples via gen_on_manager. +new samples. Execute via one of the following commands (e.g. 3 workers): mpiexec -np 4 python test_evaluate_existing_sample.py @@ -18,13 +18,13 @@ # Import libEnsemble items for this test from libensemble import Ensemble +from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first from libensemble.gen_funcs.sampling import latin_hypercube_sample as gen_f from libensemble.sim_funcs.six_hump_camel import six_hump_camel as sim_f -from libensemble.specs import ExitCriteria, GenSpecs, SimSpecs -from libensemble.tools import add_unique_random_streams +from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, SimSpecs -def create_H0(persis_info, gen_specs, H0_size): +def create_H0(gen_specs, H0_size): """Create an H0 for give_pregenerated_sim_work""" # Manually creating H0 ub = gen_specs["user"]["ub"] @@ -33,7 +33,7 @@ def create_H0(persis_info, gen_specs, H0_size): b = H0_size H0 = np.zeros(b, dtype=[("x", float, 2), ("sim_id", int), ("sim_started", bool)]) - H0["x"] = persis_info[0]["rand_stream"].uniform(lb, ub, (b, n)) + H0["x"] = np.random.uniform(lb, ub, (b, n)) H0["sim_id"] = range(b) H0["sim_started"] = False return H0 @@ -43,22 +43,21 @@ def create_H0(persis_info, gen_specs, H0_size): if __name__ == "__main__": sampling = Ensemble(parse_args=True) - sampling.libE_specs.gen_on_manager = True sampling.sim_specs = SimSpecs(sim_f=sim_f, inputs=["x"], out=[("f", float)]) gen_specs = { "gen_f": gen_f, "outputs": [("x", float, (2,))], + "batch_size": 50, "user": { - "gen_batch_size": 50, "lb": np.array([-3, -3]), "ub": np.array([3, 3]), }, } sampling.gen_specs = GenSpecs(**gen_specs) sampling.exit_criteria = ExitCriteria(sim_max=100) - sampling.persis_info = add_unique_random_streams({}, sampling.nworkers + 1) - sampling.H0 = create_H0(sampling.persis_info, gen_specs, 50) + sampling.H0 = create_H0(gen_specs, 50) + sampling.alloc_specs = AllocSpecs(alloc_f=give_sim_work_first) sampling.run() if sampling.is_manager: diff --git a/libensemble/tests/functionality_tests/test_executor_forces_tutorial.py b/libensemble/tests/functionality_tests/test_executor_forces_tutorial.py index c25ee9e4dc..d6b368b93d 100644 --- a/libensemble/tests/functionality_tests/test_executor_forces_tutorial.py +++ b/libensemble/tests/functionality_tests/test_executor_forces_tutorial.py @@ -5,10 +5,9 @@ from forces_simf import run_forces # Sim func from current dir from libensemble import Ensemble -from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f from libensemble.executors import MPIExecutor from libensemble.gen_funcs.persistent_sampling import persistent_uniform as gen_f -from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, LibeSpecs, SimSpecs +from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs if __name__ == "__main__": # Initialize MPI Executor @@ -43,26 +42,18 @@ inputs=[], # No input when starting persistent generator persis_in=["sim_id"], # Return sim_ids of evaluated points to generator outputs=[("x", float, (1,))], + initial_batch_size=nsim_workers, + async_return=False, user={ - "initial_batch_size": nsim_workers, "lb": np.array([1000]), # min particles "ub": np.array([3000]), # max particles }, ) # gen_specs_end_tag # Starts one persistent generator. Simulated values are returned in batch. - ensemble.alloc_specs = AllocSpecs( - alloc_f=alloc_f, - user={ - "async_return": False, # False causes batch returns - }, - ) # Instruct libEnsemble to exit after this many simulations ensemble.exit_criteria = ExitCriteria(sim_max=8) - # Seed random streams for each worker, particularly for gen_f - ensemble.add_random_streams() - # Run ensemble ensemble.run() diff --git a/libensemble/tests/functionality_tests/test_executor_forces_tutorial_2.py b/libensemble/tests/functionality_tests/test_executor_forces_tutorial_2.py index e3c24fc4f6..2a6cda15bb 100644 --- a/libensemble/tests/functionality_tests/test_executor_forces_tutorial_2.py +++ b/libensemble/tests/functionality_tests/test_executor_forces_tutorial_2.py @@ -5,10 +5,9 @@ from forces_simf import run_forces # Sim func from current dir from libensemble import Ensemble, logger -from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f from libensemble.executors import MPIExecutor from libensemble.gen_funcs.persistent_sampling import persistent_uniform as gen_f -from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, LibeSpecs, SimSpecs +from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs logger.set_level("DEBUG") @@ -45,27 +44,19 @@ inputs=[], # No input when starting persistent generator persis_in=["sim_id"], # Return sim_ids of evaluated points to generator outputs=[("x", float, (1,))], + initial_batch_size=nsim_workers, + async_return=True, user={ - "initial_batch_size": nsim_workers, "lb": np.array([1000]), # min particles "ub": np.array([3000]), # max particles }, ) # gen_specs_end_tag # Starts one persistent generator. Simulated values are returned in batch. - ensemble.alloc_specs = AllocSpecs( - alloc_f=alloc_f, - user={ - "async_return": True, - }, - ) # Instruct libEnsemble to exit after this many simulations ensemble.exit_criteria = ExitCriteria(sim_max=8) - # Seed random streams for each worker, particularly for gen_f - ensemble.add_random_streams() - # Run ensemble ensemble.run() diff --git a/libensemble/tests/functionality_tests/test_executor_hworld_pass_fail.py b/libensemble/tests/functionality_tests/test_executor_hworld_pass_fail.py index 1b61bf8d25..a65462e0dd 100644 --- a/libensemble/tests/functionality_tests/test_executor_hworld_pass_fail.py +++ b/libensemble/tests/functionality_tests/test_executor_hworld_pass_fail.py @@ -15,6 +15,7 @@ import numpy as np import libensemble.sim_funcs.six_hump_camel as six_hump_camel +from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first from libensemble.executors.mpi_executor import MPIExecutor from libensemble.gen_funcs.sampling import uniform_random_sample as gen_f from libensemble.libE import libE @@ -23,7 +24,7 @@ from libensemble.message_numbers import TASK_FAILED, WORKER_DONE, WORKER_KILL_ON_ERR, WORKER_KILL_ON_TIMEOUT from libensemble.sim_funcs.executor_hworld import executor_hworld as sim_f from libensemble.tests.regression_tests.common import build_simfunc -from libensemble.tools import add_unique_random_streams, parse_args +from libensemble.tools import parse_args # Do not change these lines - they are parsed by run-tests.sh # TESTSUITE_COMMS: mpi local tcp @@ -76,20 +77,22 @@ "gen_f": gen_f, "in": ["sim_id"], "out": [("x", float, (2,))], + "batch_size": nworkers, "user": { "lb": np.array([-3, -2]), "ub": np.array([3, 2]), - "gen_batch_size": nworkers, }, } - persis_info = add_unique_random_streams({}, nworkers + 1) - # num sim_ended_count conditions in executor_hworld exit_criteria = {"sim_max": nworkers * 5} + alloc_specs = { + "alloc_f": give_sim_work_first, + } + # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) if is_manager: print("\nChecking expected task status against Workers ...\n") diff --git a/libensemble/tests/functionality_tests/test_executor_hworld_timeout.py b/libensemble/tests/functionality_tests/test_executor_hworld_timeout.py index 496eced316..2a604007e6 100644 --- a/libensemble/tests/functionality_tests/test_executor_hworld_timeout.py +++ b/libensemble/tests/functionality_tests/test_executor_hworld_timeout.py @@ -15,6 +15,7 @@ import numpy as np import libensemble.sim_funcs.six_hump_camel as six_hump_camel +from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first from libensemble.executors.mpi_executor import MPIExecutor from libensemble.gen_funcs.sampling import uniform_random_sample as gen_f @@ -22,7 +23,7 @@ from libensemble.libE import libE from libensemble.sim_funcs.executor_hworld import executor_hworld as sim_f from libensemble.tests.regression_tests.common import build_simfunc -from libensemble.tools import add_unique_random_streams, parse_args +from libensemble.tools import parse_args # Do not change these lines - they are parsed by run-tests.sh # TESTSUITE_COMMS: mpi local tcp @@ -78,16 +79,18 @@ "gen_f": gen_f, "in": ["sim_id"], "out": [("x", float, (2,))], + "batch_size": nworkers, "user": { "lb": np.array([-3, -2]), "ub": np.array([3, 2]), - "gen_batch_size": nworkers, }, } - persis_info = add_unique_random_streams({}, nworkers + 1) + alloc_specs = { + "alloc_f": give_sim_work_first, + } - exit_criteria = {"wallclock_max": 10} + exit_criteria = {"wallclock_max": 10, "sim_max": nworkers} # TCP does not support multiple libE calls if libE_specs["comms"] == "tcp": @@ -97,7 +100,7 @@ for i in range(iterations): # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) if is_manager: print("\nChecking expected task status against Workers ...\n") diff --git a/libensemble/tests/functionality_tests/test_executor_simple.py b/libensemble/tests/functionality_tests/test_executor_simple.py index ac4c201a7a..e6d1c7468e 100644 --- a/libensemble/tests/functionality_tests/test_executor_simple.py +++ b/libensemble/tests/functionality_tests/test_executor_simple.py @@ -10,16 +10,17 @@ """ import numpy as np +from gest_api.vocs import VOCS import libensemble.sim_funcs.six_hump_camel as six_hump_camel from libensemble.executors.mpi_executor import MPIExecutor -from libensemble.gen_funcs.sampling import uniform_random_sample as gen_f +from libensemble.gen_classes.sampling import UniformSample from libensemble.libE import libE # Import libEnsemble items for this test from libensemble.message_numbers import WORKER_DONE from libensemble.sim_funcs.executor_hworld import executor_hworld as sim_f -from libensemble.tools import add_unique_random_streams, parse_args +from libensemble.tools import parse_args # Do not change these lines - they are parsed by run-tests.sh # TESTSUITE_COMMS: mpi local @@ -46,23 +47,24 @@ } gen_specs = { - "gen_f": gen_f, "in": ["sim_id"], + "persis_in": ["x", "f", "sim_id"], "out": [("x", float, (2,))], - "user": { - "lb": np.array([-3, -2]), - "ub": np.array([3, 2]), - "gen_batch_size": nworkers, - }, + "batch_size": nworkers, } - persis_info = add_unique_random_streams({}, nworkers + 1) + variables = {"x0": [-3, 3], "x1": [-2, 2]} + objectives = {"f": "EXPLORE"} + + vocs = VOCS(variables=variables, objectives=objectives) + + gen_specs["generator"] = UniformSample(vocs) # num sim_ended_count conditions in executor_hworld exit_criteria = {"sim_max": nworkers * 5} # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, libE_specs=libE_specs) if is_manager: print("\nChecking expected task status against Workers ...\n") diff --git a/libensemble/tests/functionality_tests/test_fast_alloc.py b/libensemble/tests/functionality_tests/test_fast_alloc.py index a2b6a75666..6d49162dad 100644 --- a/libensemble/tests/functionality_tests/test_fast_alloc.py +++ b/libensemble/tests/functionality_tests/test_fast_alloc.py @@ -10,7 +10,7 @@ # Do not change these lines - they are parsed by run-tests.sh # TESTSUITE_COMMS: mpi local -# TESTSUITE_NPROCS: 2 4 +# TESTSUITE_NPROCS: 4 import gc import sys @@ -24,7 +24,7 @@ # Import libEnsemble items for this test from libensemble.libE import libE from libensemble.sim_funcs.branin.branin_obj import call_branin as sim_f -from libensemble.tools import add_unique_random_streams, parse_args +from libensemble.tools import parse_args # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": @@ -43,15 +43,13 @@ "gen_f": gen_f, "in": ["sim_id"], "out": [("x", float, (2,))], + "batch_size": num_pts, "user": { - "gen_batch_size": num_pts, "lb": np.array([-3, -2]), "ub": np.array([3, 2]), }, } - persis_info = add_unique_random_streams({}, nworkers + 1) - exit_criteria = {"sim_max": 2 * num_pts, "wallclock_max": 300} if libE_specs["comms"] == "tcp": @@ -73,8 +71,9 @@ if time == 0: sim_specs["user"].pop("uniform_random_pause_ub") - gen_specs["user"]["gen_batch_size"] = num_pts // 2 + gen_specs["batch_size"] = num_pts // 2 + persis_info = {} persis_info["next_to_give"] = 0 persis_info["total_gen_calls"] = 1 diff --git a/libensemble/tests/functionality_tests/test_local_sine_tutorial.py b/libensemble/tests/functionality_tests/test_local_sine_tutorial.py index e05a49c968..3c6fa0b323 100644 --- a/libensemble/tests/functionality_tests/test_local_sine_tutorial.py +++ b/libensemble/tests/functionality_tests/test_local_sine_tutorial.py @@ -1,5 +1,6 @@ import numpy as np -from sine_gen import gen_random_sample +from gest_api.vocs import VOCS +from sine_gen_std import RandomSample from sine_sim import sim_find_sine from libensemble import Ensemble @@ -8,14 +9,14 @@ if __name__ == "__main__": # Python-quirk required on macOS and windows libE_specs = LibeSpecs(nworkers=4, comms="local") + vocs = VOCS(variables={"x": [-3, 3]}, objectives={"y": "EXPLORE"}) # Configure our generator with this object + + generator = RandomSample(vocs) # Instantiate our generator + gen_specs = GenSpecs( - gen_f=gen_random_sample, # Our generator function - out=[("x", float, (1,))], # gen_f output (name, type, size) - user={ - "lower": np.array([-3]), # lower boundary for random sampling - "upper": np.array([3]), # upper boundary for random sampling - "gen_batch_size": 5, # number of x's gen_f generates per call - }, + generator=generator, # Pass our generator and config to libEnsemble + vocs=vocs, + batch_size=4, ) sim_specs = SimSpecs( @@ -27,7 +28,6 @@ exit_criteria = ExitCriteria(sim_max=80) # Stop libEnsemble after 80 simulations ensemble = Ensemble(sim_specs, gen_specs, exit_criteria, libE_specs) - ensemble.add_random_streams() # setup the random streams unique to each worker ensemble.run() # start the ensemble. Blocks until completion. history = ensemble.H # start visualizing our results @@ -39,9 +39,9 @@ colors = ["b", "g", "r", "y", "m", "c", "k", "w"] - for i in range(1, libE_specs.nworkers + 1): + for i in range(1, libE_specs.nworkers + 1): # type: ignore worker_xy = np.extract(history["sim_worker"] == i, history) - x = [entry.tolist()[0] for entry in worker_xy["x"]] + x = [entry.tolist() for entry in worker_xy["x"]] y = [entry for entry in worker_xy["y"]] plt.scatter(x, y, label="Worker {}".format(i), c=colors[i - 1]) diff --git a/libensemble/tests/functionality_tests/test_local_sine_tutorial_2.py b/libensemble/tests/functionality_tests/test_local_sine_tutorial_2.py index 11911f343c..286a0ecdc4 100644 --- a/libensemble/tests/functionality_tests/test_local_sine_tutorial_2.py +++ b/libensemble/tests/functionality_tests/test_local_sine_tutorial_2.py @@ -3,7 +3,8 @@ from sine_sim import sim_find_sine from libensemble import Ensemble -from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs +from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first +from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, LibeSpecs, SimSpecs if __name__ == "__main__": libE_specs = LibeSpecs(nworkers=4, comms="local") @@ -11,10 +12,10 @@ gen_specs = GenSpecs( gen_f=gen_random_sample, # Our generator function out=[("x", float, (1,))], # gen_f output (name, type, size) + batch_size=10, # number of x's gen_f generates per call user={ "lower": np.array([-6]), # lower boundary for random sampling "upper": np.array([6]), # upper boundary for random sampling - "gen_batch_size": 10, # number of x's gen_f generates per call }, ) @@ -24,10 +25,11 @@ out=[("y", float)], # sim_f output. "y" = sine("x") ) + alloc_specs = AllocSpecs(alloc_f=give_sim_work_first) + exit_criteria = ExitCriteria(gen_max=160) - ensemble = Ensemble(sim_specs, gen_specs, exit_criteria, libE_specs) - ensemble.add_random_streams() + ensemble = Ensemble(sim_specs, gen_specs, exit_criteria, libE_specs, alloc_specs) ensemble.run() if ensemble.flag != 0: diff --git a/libensemble/tests/functionality_tests/test_local_sine_tutorial_3.py b/libensemble/tests/functionality_tests/test_local_sine_tutorial_3.py index d57a0f842b..988fe0e78c 100644 --- a/libensemble/tests/functionality_tests/test_local_sine_tutorial_3.py +++ b/libensemble/tests/functionality_tests/test_local_sine_tutorial_3.py @@ -3,7 +3,8 @@ from sine_sim import sim_find_sine from libensemble import Ensemble -from libensemble.specs import ExitCriteria, GenSpecs, SimSpecs +from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first +from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, SimSpecs if __name__ == "__main__": # Python-quirk required on macOS and windows # libE_specs = LibeSpecs(nworkers=4, comms="local") @@ -11,10 +12,10 @@ gen_specs = GenSpecs( gen_f=gen_random_sample, # Our generator function out=[("x", float, (1,))], # gen_f output (name, type, size) + batch_size=5, # number of x's gen_f generates per call user={ "lower": np.array([-3]), # lower boundary for random sampling "upper": np.array([3]), # upper boundary for random sampling - "gen_batch_size": 5, # number of x's gen_f generates per call }, ) @@ -26,10 +27,11 @@ exit_criteria = ExitCriteria(sim_max=80) # Stop libEnsemble after 80 simulations + alloc_specs = AllocSpecs(alloc_f=give_sim_work_first) + # replace libE_specs with parse_args=True. Detects MPI runtime - ensemble = Ensemble(sim_specs, gen_specs, exit_criteria, parse_args=True) + ensemble = Ensemble(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, parse_args=True) - ensemble.add_random_streams() ensemble.run() # start the ensemble. Blocks until completion. if ensemble.is_manager: # only True on rank 0 diff --git a/libensemble/tests/functionality_tests/test_mpi_gpu_settings.py b/libensemble/tests/functionality_tests/test_mpi_gpu_settings.py index 31e537a31d..d83e7a13cd 100644 --- a/libensemble/tests/functionality_tests/test_mpi_gpu_settings.py +++ b/libensemble/tests/functionality_tests/test_mpi_gpu_settings.py @@ -39,7 +39,7 @@ # Do not change these lines - they are parsed by run-tests.sh # TESTSUITE_COMMS: mpi local -# TESTSUITE_NPROCS: 3 6 +# TESTSUITE_NPROCS: 4 7 import os import sys @@ -47,16 +47,15 @@ import numpy as np -from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f from libensemble.executors.mpi_executor import MPIExecutor from libensemble.gen_funcs.persistent_sampling_var_resources import uniform_sample as gen_f # Import libEnsemble items for this test from libensemble.libE import libE -from libensemble.resources.platforms import Aurora, Frontier, PerlmutterGPU, Platform, Polaris, Summit +from libensemble.resources.platforms import Aurora, Frontier, PerlmutterGPU, Platform, Polaris from libensemble.sim_funcs import six_hump_camel from libensemble.sim_funcs.var_resources import gpu_variable_resources as sim_f -from libensemble.tools import add_unique_random_streams, parse_args +from libensemble.tools import parse_args # from libensemble import logger # logger.set_level("DEBUG") # For testing the test @@ -66,7 +65,7 @@ # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": nworkers, is_manager, libE_specs, _ = parse_args() - libE_specs["num_resource_sets"] = nworkers - 1 # Persistent gen does not need resources + libE_specs["num_resource_sets"] = nworkers # Persistent gen does not need resources libE_specs["use_workflow_dir"] = True # Only a place for Open MPI machinefiles if libE_specs["comms"] == "tcp": @@ -87,23 +86,17 @@ "gen_f": gen_f, "persis_in": ["f", "x", "sim_id"], "out": [("priority", float), ("resource_sets", int), ("x", float, n)], + "batch_evaluate_same_priority": False, + "async_return": False, + "initial_batch_size": nworkers, "user": { - "initial_batch_size": nworkers - 1, - "max_resource_sets": nworkers - 1, # Any sim created can req. 1 worker up to all. + "max_resource_sets": nworkers, # Any sim created can req. 1 worker up to all. "lb": np.array([-3, -2]), "ub": np.array([3, 2]), }, } - alloc_specs = { - "alloc_f": alloc_f, - "user": { - "give_all_with_same_priority": False, - "async_return": False, # False batch returns - }, - } - - persis_info = add_unique_random_streams({}, nworkers + 1) + persis_info = {} exit_criteria = {"sim_max": 20} # Ensure LIBE_PLATFORM environment variable is not set. @@ -119,12 +112,10 @@ exctr.register_app(full_path=six_hump_camel_app, app_name="six_hump_camel") # Reset persis_info. If has num_gens_started > 0 from alloc, will not runs any sims. - persis_info = add_unique_random_streams({}, nworkers + 1) + persis_info = {} # Perform the run - H, _, flag = libE( - sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs, alloc_specs=alloc_specs - ) + H, _, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) del libE_specs["resource_info"] # this would override @@ -156,12 +147,10 @@ libE_specs["platform_specs"].logical_cores_per_node = 128 # Reset persis_info. If has num_gens_started > 0 from alloc, will not runs any sims. - persis_info = add_unique_random_streams({}, nworkers + 1) + persis_info = {} # Perform the run - H, _, flag = libE( - sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs, alloc_specs=alloc_specs - ) + H, _, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) del libE_specs["platform_specs"] @@ -193,17 +182,15 @@ libE_specs["platform_specs"]["logical_cores_per_node"] = 128 # Reset persis_info. If has num_gens_started > 0 from alloc, will not runs any sims. - persis_info = add_unique_random_streams({}, nworkers + 1) + persis_info = {} # Perform the run - H, _, flag = libE( - sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs, alloc_specs=alloc_specs - ) + H, _, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) del libE_specs["platform_specs"] # Fourth set - use platform setting ------------------------------------------------------------ - for platform in ["summit", "frontier", "perlmutter_g", "polaris", "aurora"]: + for platform in ["frontier", "perlmutter_g", "polaris", "aurora"]: print(f"\nRunning GPU setting checks (via known platform) for {platform} ------------------- ") libE_specs["platform"] = platform @@ -211,17 +198,15 @@ exctr.register_app(full_path=six_hump_camel_app, app_name="six_hump_camel") # Reset persis_info. If has num_gens_started > 0 from alloc, will not runs any sims. - persis_info = add_unique_random_streams({}, nworkers + 1) + persis_info = {} # Perform the run - H, _, flag = libE( - sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs, alloc_specs=alloc_specs - ) + H, _, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) del libE_specs["platform"] # Fifth set - use platform environment setting ----------------------------------------------- - for platform in ["summit", "frontier", "perlmutter_g", "polaris", "aurora"]: + for platform in ["frontier", "perlmutter_g", "polaris", "aurora"]: print(f"\nRunning GPU setting checks (via known platform env. variable) for {platform} ----- ") os.environ["LIBE_PLATFORM"] = platform @@ -229,17 +214,15 @@ exctr.register_app(full_path=six_hump_camel_app, app_name="six_hump_camel") # Reset persis_info. If has num_gens_started > 0 from alloc, will not runs any sims. - persis_info = add_unique_random_streams({}, nworkers + 1) + persis_info = {} # Perform the run - H, _, flag = libE( - sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs, alloc_specs=alloc_specs - ) + H, _, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) del os.environ["LIBE_PLATFORM"] # Sixth set - use platform_specs with known systems ------------------------------------------- - for platform in [Summit, Frontier, PerlmutterGPU, Polaris, Aurora]: + for platform in [Frontier, PerlmutterGPU, Polaris, Aurora]: print(f"\nRunning GPU setting checks (via known platform - platform_specs) for {platform} ------------------- ") libE_specs["platform_specs"] = platform() @@ -247,12 +230,10 @@ exctr.register_app(full_path=six_hump_camel_app, app_name="six_hump_camel") # Reset persis_info. If has num_gens_started > 0 from alloc, will not runs any sims. - persis_info = add_unique_random_streams({}, nworkers + 1) + persis_info = {} # Perform the run - H, _, flag = libE( - sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs, alloc_specs=alloc_specs - ) + H, _, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) del libE_specs["platform_specs"] diff --git a/libensemble/tests/functionality_tests/test_mpi_gpu_settings_env.py b/libensemble/tests/functionality_tests/test_mpi_gpu_settings_env.py index 814f5086cb..a32afe128e 100644 --- a/libensemble/tests/functionality_tests/test_mpi_gpu_settings_env.py +++ b/libensemble/tests/functionality_tests/test_mpi_gpu_settings_env.py @@ -26,7 +26,6 @@ import numpy as np -from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f from libensemble.executors.mpi_executor import MPIExecutor from libensemble.gen_funcs.persistent_sampling_var_resources import uniform_sample as gen_f @@ -34,7 +33,7 @@ from libensemble.libE import libE from libensemble.sim_funcs import six_hump_camel from libensemble.sim_funcs.var_resources import gpu_variable_resources_subenv as sim_f -from libensemble.tools import add_unique_random_streams, parse_args +from libensemble.tools import parse_args # from libensemble import logger # logger.set_level("DEBUG") # For testing the test @@ -43,7 +42,7 @@ # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": nworkers, is_manager, libE_specs, _ = parse_args() - libE_specs["num_resource_sets"] = nworkers - 1 # Persistent gen does not need resources + libE_specs["num_resource_sets"] = nworkers # Persistent gen does not need resources libE_specs["use_workflow_dir"] = True # Only a place for Open MPI machinefiles # Optional for organization of output scripts @@ -69,23 +68,16 @@ "gen_f": gen_f, "persis_in": ["f", "x", "sim_id"], "out": [("priority", float), ("resource_sets", int), ("x", float, n)], + "initial_batch_size": nworkers - 1, + "batch_evaluate_same_priority": False, + "async_return": False, "user": { - "initial_batch_size": nworkers - 1, - "max_resource_sets": nworkers - 1, # Any sim created can req. 1 worker up to all. + "max_resource_sets": nworkers, # Any sim created can req. 1 worker up to all. "lb": np.array([-3, -2]), "ub": np.array([3, 2]), }, } - alloc_specs = { - "alloc_f": alloc_f, - "user": { - "give_all_with_same_priority": False, - "async_return": False, # False batch returns - }, - } - - persis_info = add_unique_random_streams({}, nworkers + 1) exit_criteria = {"sim_max": 10} # Ensure LIBE_PLATFORM environment variable is not set. @@ -98,4 +90,4 @@ exctr.register_app(full_path=six_hump_camel_app, app_name="six_hump_camel") # Perform the run - H, _, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs, alloc_specs=alloc_specs) + H, _, flag = libE(sim_specs, gen_specs, exit_criteria, libE_specs=libE_specs) diff --git a/libensemble/tests/functionality_tests/test_mpi_gpu_settings_mock_nodes_multi_task.py b/libensemble/tests/functionality_tests/test_mpi_gpu_settings_mock_nodes_multi_task.py index 3c884c7dd2..b29e27fe70 100644 --- a/libensemble/tests/functionality_tests/test_mpi_gpu_settings_mock_nodes_multi_task.py +++ b/libensemble/tests/functionality_tests/test_mpi_gpu_settings_mock_nodes_multi_task.py @@ -28,7 +28,6 @@ import numpy as np -from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f from libensemble.executors.mpi_executor import MPIExecutor from libensemble.gen_funcs.persistent_sampling_var_resources import uniform_sample_diff_simulations as gen_f @@ -37,7 +36,7 @@ from libensemble.sim_funcs import six_hump_camel from libensemble.sim_funcs.var_resources import gpu_variable_resources_from_gen as sim_f from libensemble.tests.regression_tests.common import create_node_file -from libensemble.tools import add_unique_random_streams, parse_args +from libensemble.tools import parse_args # from libensemble import logger # logger.set_level("DEBUG") # For testing the test @@ -68,8 +67,10 @@ "gen_f": gen_f, "persis_in": ["f", "x", "sim_id"], "out": [("priority", float), ("num_procs", int), ("num_gpus", int), ("x", float, n)], + "initial_batch_size": nsim_workers, + "batch_evaluate_same_priority": False, + "async_return": False, "user": { - "initial_batch_size": nsim_workers, "max_procs": max(nsim_workers // 2, 1), # Any sim created can req. 1 worker up to max "lb": np.array([-3, -2]), "ub": np.array([3, 2]), @@ -77,15 +78,7 @@ }, } - alloc_specs = { - "alloc_f": alloc_f, - "user": { - "give_all_with_same_priority": False, - "async_return": False, # False batch returns - }, - } - - persis_info = add_unique_random_streams({}, nworkers + 1) + persis_info = {} exit_criteria = {"sim_max": nsim_workers * 2} # Ensure LIBE_PLATFORM environment variable is not set. @@ -106,12 +99,10 @@ exctr.register_app(full_path=six_hump_camel_app, app_name="six_hump_camel") # Reset persis_info. If has num_gens_started > 0 from alloc, will not runs any sims. - persis_info = add_unique_random_streams({}, nworkers + 1) + persis_info = {} # Perform the run - H, _, flag = libE( - sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs, alloc_specs=alloc_specs - ) + H, _, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) # Oversubscribe procs if nsim_workers >= 4: @@ -131,12 +122,10 @@ exctr.register_app(full_path=six_hump_camel_app, app_name="six_hump_camel") # Reset persis_info. If has num_gens_started > 0 from alloc, will not runs any sims. - persis_info = add_unique_random_streams({}, nworkers + 1) + persis_info = {} # Perform the run - H, _, flag = libE( - sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs, alloc_specs=alloc_specs - ) + H, _, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) del libE_specs["resource_info"] diff --git a/libensemble/tests/functionality_tests/test_mpi_runners.py b/libensemble/tests/functionality_tests/test_mpi_runners.py index af00471d59..346c224857 100644 --- a/libensemble/tests/functionality_tests/test_mpi_runners.py +++ b/libensemble/tests/functionality_tests/test_mpi_runners.py @@ -9,15 +9,15 @@ The number of concurrent evaluations of the objective function will be 4-1=3. """ -import numpy as np +from gest_api.vocs import VOCS from libensemble import logger from libensemble.executors.mpi_executor import MPIExecutor -from libensemble.gen_funcs.sampling import uniform_random_sample as gen_f +from libensemble.gen_classes import UniformSample from libensemble.libE import libE from libensemble.sim_funcs.run_line_check import runline_check as sim_f from libensemble.tests.regression_tests.common import create_node_file -from libensemble.tools import add_unique_random_streams, parse_args +from libensemble.tools import parse_args # logger.set_level("DEBUG") # For testing the test logger.set_level("INFO") @@ -60,7 +60,6 @@ } libE_specs["resource_info"] = custom_resources - persis_info = add_unique_random_streams({}, nworkers + 1) exit_criteria = {"sim_max": nworkers * rounds} sim_specs = { @@ -70,16 +69,16 @@ } gen_specs = { - "gen_f": gen_f, "in": ["sim_id"], "out": [("x", float, (2,))], - "user": { - "lb": np.array([-3, -2]), - "ub": np.array([3, 2]), - "gen_batch_size": 100, - }, + "batch_size": 100, } + variables = {"x0": [-3, 3], "x1": [-2, 2]} + objectives = {"f": "EXPLORE"} + vocs = VOCS(variables=variables, objectives=objectives) + gen_specs["generator"] = UniformSample(vocs) + # Each worker has 2 nodes. Basic test list for portable options test_list_base = [ {"testid": "base1", "nprocs": 2, "nnodes": 1, "ppn": 2, "e_args": "--xarg 1"}, # Under use @@ -234,7 +233,7 @@ def run_tests(mpi_runner, runner_name, test_list_exargs, exp_list): } # Perform the run - H, pinfo, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) + H, _, flag = libE(sim_specs, gen_specs, exit_criteria, libE_specs=libE_specs) # for run_set in ['mpich', 'openmpi', 'aprun', 'srun', 'jsrun', 'rename_mpich', 'custom']: for run_set in ["mpich", "aprun", "srun", "jsrun", "rename_mpich", "custom"]: diff --git a/libensemble/tests/functionality_tests/test_mpi_runners_subnode.py b/libensemble/tests/functionality_tests/test_mpi_runners_subnode.py index 7266678ff2..c5211f33e5 100644 --- a/libensemble/tests/functionality_tests/test_mpi_runners_subnode.py +++ b/libensemble/tests/functionality_tests/test_mpi_runners_subnode.py @@ -13,22 +13,22 @@ import sys -import numpy as np +from gest_api.vocs import VOCS from libensemble import logger from libensemble.executors.mpi_executor import MPIExecutor -from libensemble.gen_funcs.sampling import uniform_random_sample as gen_f +from libensemble.gen_classes import UniformSample from libensemble.libE import libE from libensemble.sim_funcs.run_line_check import runline_check as sim_f from libensemble.tests.regression_tests.common import create_node_file -from libensemble.tools import add_unique_random_streams, parse_args +from libensemble.tools import parse_args # logger.set_level("DEBUG") # For testing the test logger.set_level("INFO") # Do not change these lines - they are parsed by run-tests.sh # TESTSUITE_COMMS: mpi local -# TESTSUITE_NPROCS: 3 +# TESTSUITE_NPROCS: 5 # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": @@ -80,18 +80,17 @@ "out": [("f", float)], } + variables = {"x0": [-3, 3], "x1": [-2, 2]} + objectives = {"f": "EXPLORE"} + vocs = VOCS(variables=variables, objectives=objectives) + gen_specs = { - "gen_f": gen_f, - "in": [], + "generator": UniformSample(vocs), + "in": ["sim_id"], "out": [("x", float, (n,))], - "user": { - "gen_batch_size": 20, - "lb": np.array([-3, -2]), - "ub": np.array([3, 2]), - }, + "batch_size": 20, } - persis_info = add_unique_random_streams({}, nworkers + 1) exit_criteria = {"sim_max": (nsim_workers) * rounds} # Each worker has 2 nodes. Basic test list for portable options @@ -118,6 +117,6 @@ } # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) + H, _, flag = libE(sim_specs, gen_specs, exit_criteria, libE_specs=libE_specs) # All asserts are in sim func diff --git a/libensemble/tests/functionality_tests/test_mpi_runners_subnode_uneven.py b/libensemble/tests/functionality_tests/test_mpi_runners_subnode_uneven.py index a5145965b9..c179028abd 100644 --- a/libensemble/tests/functionality_tests/test_mpi_runners_subnode_uneven.py +++ b/libensemble/tests/functionality_tests/test_mpi_runners_subnode_uneven.py @@ -14,19 +14,20 @@ import numpy as np from libensemble import logger +from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first from libensemble.executors.mpi_executor import MPIExecutor from libensemble.gen_funcs.sampling import uniform_random_sample as gen_f from libensemble.libE import libE from libensemble.sim_funcs.run_line_check import runline_check_by_worker as sim_f from libensemble.tests.regression_tests.common import create_node_file -from libensemble.tools import add_unique_random_streams, parse_args +from libensemble.tools import parse_args # logger.set_level("DEBUG") # For testing the test logger.set_level("INFO") # Do not change these lines - they are parsed by run-tests.sh # TESTSUITE_COMMS: mpi local -# TESTSUITE_NPROCS: 4 6 +# TESTSUITE_NPROCS: 6 8 # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": @@ -83,16 +84,17 @@ gen_specs = { "gen_f": gen_f, - "in": [], + "in": ["sim_id"], "out": [("x", float, (n,))], + "batch_size": 20, "user": { - "gen_batch_size": 20, "lb": np.array([-3, -2]), "ub": np.array([3, 2]), }, } - persis_info = add_unique_random_streams({}, nworkers + 1) + alloc_specs = {"alloc_f": give_sim_work_first} + exit_criteria = {"sim_max": (nsim_workers) * rounds} test_list_base = [ @@ -137,6 +139,6 @@ } # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) + H, _, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) # All asserts are in sim func diff --git a/libensemble/tests/functionality_tests/test_mpi_runners_supernode_uneven.py b/libensemble/tests/functionality_tests/test_mpi_runners_supernode_uneven.py index 77975e200d..428297d5d4 100644 --- a/libensemble/tests/functionality_tests/test_mpi_runners_supernode_uneven.py +++ b/libensemble/tests/functionality_tests/test_mpi_runners_supernode_uneven.py @@ -11,12 +11,13 @@ import numpy as np from libensemble import logger +from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first from libensemble.executors.mpi_executor import MPIExecutor from libensemble.gen_funcs.sampling import uniform_random_sample as gen_f from libensemble.libE import libE from libensemble.sim_funcs.run_line_check import runline_check_by_worker as sim_f from libensemble.tests.regression_tests.common import create_node_file -from libensemble.tools import add_unique_random_streams, parse_args +from libensemble.tools import parse_args # logger.set_level("DEBUG") # For testing the test logger.set_level("INFO") @@ -75,14 +76,13 @@ "gen_f": gen_f, "in": [], "out": [("x", float, (n,))], + "batch_size": 20, "user": { - "gen_batch_size": 20, "lb": np.array([-3, -2]), "ub": np.array([3, 2]), }, } - persis_info = add_unique_random_streams({}, nworkers + 1) exit_criteria = {"sim_max": (nsim_workers) * rounds} # Each worker has either 3 or 2 nodes. Basic test list for portable options @@ -131,7 +131,9 @@ "expect": exp_list, } + alloc_specs = {"alloc_f": give_sim_work_first} + # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) + H, _, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) # All asserts are in sim func diff --git a/libensemble/tests/functionality_tests/test_mpi_runners_zrw_subnode_uneven.py b/libensemble/tests/functionality_tests/test_mpi_runners_zrw_subnode_uneven.py deleted file mode 100644 index cc73d0e427..0000000000 --- a/libensemble/tests/functionality_tests/test_mpi_runners_zrw_subnode_uneven.py +++ /dev/null @@ -1,165 +0,0 @@ -""" -Runs libEnsemble testing the MPI Runners command creation with uneven workers per node. - -This test must be run on an even number of workers >= 4 and <= 32 (e.g. odd no. of procs when using mpi4py). - -Execute via one of the following commands (e.g. 6 workers - one is zero resource): - mpiexec -np 7 python test_mpi_runners_zrw_subnode_uneven.py - python test_mpi_runners_zrw_subnode_uneven.py --nworkers 6 - python test_mpi_runners_zrw_subnode_uneven.py --nworkers 6 --comms tcp - -The resource sets are split unevenly between the two nodes (e.g. 3 and 2). - -Two tests are run. In the first, num_resource_sets is used, and thus the dynamic scheduler. -This will fill node two slots first as there are fewer resource sets on node two, and the -scheduler will preference a smaller space for assigning the task. On the second test, -zero_resource_workers are used, and the static scheduler will fill node one first. -""" - -import sys - -import numpy as np - -from libensemble import logger -from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f -from libensemble.executors.mpi_executor import MPIExecutor -from libensemble.gen_funcs.persistent_sampling import persistent_uniform as gen_f -from libensemble.libE import libE -from libensemble.sim_funcs.run_line_check import runline_check_by_worker as sim_f -from libensemble.tests.regression_tests.common import create_node_file -from libensemble.tools import add_unique_random_streams, parse_args - -# logger.set_level("DEBUG") # For testing the test -logger.set_level("INFO") - -# Do not change these lines - they are parsed by run-tests.sh -# TESTSUITE_COMMS: mpi local -# TESTSUITE_NPROCS: 5 7 - -# Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). -if __name__ == "__main__": - nworkers, is_manager, libE_specs, _ = parse_args() - rounds = 1 - sim_app = "/path/to/fakeapp.x" - comms = libE_specs["comms"] - - libE_specs["dedicated_mode"] = True - libE_specs["enforce_worker_core_bounds"] = True - - # To allow visual checking - log file not used in test - log_file = "ensemble_mpi_runners_zrw_subnode_uneven_comms_" + str(comms) + "_wrks_" + str(nworkers) + ".log" - logger.set_filename(log_file) - - # For varying size test - relate node count to nworkers - n_gens = 1 - nsim_workers = nworkers - n_gens - - if nsim_workers % 2 == 0: - sys.exit( - "This test must be run with an odd number of sim workers >= 3 and <= 31. There are {} sim workers.".format( - nsim_workers - ) - ) - - comms = libE_specs["comms"] - node_file = "nodelist_mpi_runners_zrw_subnode_uneven_comms_" + str(comms) + "_wrks_" + str(nworkers) - nnodes = 2 - - # Mock up system - custom_resources = { - "cores_on_node": (16, 64), # Tuple (physical cores, logical cores) - "node_file": node_file, - } # Name of file containing a node-list - libE_specs["resource_info"] = custom_resources - - if is_manager: - create_node_file(num_nodes=nnodes, name=node_file) - - if comms == "mpi": - libE_specs["mpi_comm"].Barrier() - - # Create executor and register sim to it. - exctr = MPIExecutor(custom_info={"mpi_runner": "srun"}) - exctr.register_app(full_path=sim_app, calc_type="sim") - - n = 2 - sim_specs = { - "sim_f": sim_f, - "in": ["x"], - "out": [("f", float)], - } - - gen_specs = { - "gen_f": gen_f, - "in": [], - "out": [("x", float, (n,))], - "user": { - "initial_batch_size": 20, - "lb": np.array([-3, -2]), - "ub": np.array([3, 2]), - }, - } - - alloc_specs = {"alloc_f": alloc_f} - exit_criteria = {"sim_max": (nsim_workers) * rounds} - - test_list_base = [ - {"testid": "base1"}, # Give no config and no extra_args - ] - - # Example: On 5 workers, runlines should be ... - # [w1]: Gen only - # [w2]: srun -w node-1 --ntasks 5 --nodes 1 --ntasks-per-node 5 /path/to/fakeapp.x --testid base1 - # [w3]: srun -w node-1 --ntasks 5 --nodes 1 --ntasks-per-node 5 /path/to/fakeapp.x --testid base1 - # [w4]: srun -w node-1 --ntasks 5 --nodes 1 --ntasks-per-node 5 /path/to/fakeapp.x --testid base1 - # [w5]: srun -w node-2 --ntasks 8 --nodes 1 --ntasks-per-node 8 /path/to/fakeapp.x --testid base1 - # [w6]: srun -w node-2 --ntasks 8 --nodes 1 --ntasks-per-node 8 /path/to/fakeapp.x --testid base1 - - srun_p1 = "srun -w " - srun_p2 = " --ntasks " - srun_p3 = " --nodes 1 --ntasks-per-node " - srun_p4 = " --exact /path/to/fakeapp.x --testid base1" - - exp_tasks = [] - exp_srun = [] - - # Hard coding an example for 2 nodes to avoid replicating general logic in libEnsemble. - low_wpn = nsim_workers // nnodes - high_wpn = nsim_workers // nnodes + 1 - - for i in range(nsim_workers): - if i < (nsim_workers // nnodes + 1): - nodename = "node-1" - ntasks = 16 // high_wpn - else: - nodename = "node-2" - ntasks = 16 // low_wpn - exp_tasks.append(ntasks) - exp_srun.append(srun_p1 + str(nodename) + srun_p2 + str(ntasks) + srun_p3 + str(ntasks) + srun_p4) - - test_list = test_list_base - exp_list = exp_srun - sim_specs["user"] = { - "tests": test_list, - "expect": exp_list, - "persis_gens": n_gens, - } - - iterations = 2 - for prob_id in range(iterations): - if prob_id == 0: - # Uses dynamic scheduler - will find node 2 slots first (as fewer) - libE_specs["num_resource_sets"] = nworkers - 1 # Any worker can be the gen - sim_specs["user"]["offset_for_scheduler"] = True # Changes expected values - persis_info = add_unique_random_streams({}, nworkers + 1) - - else: - # Uses static scheduler - will find node 1 slots first - del libE_specs["num_resource_sets"] - libE_specs["zero_resource_workers"] = [1] # Gen must be worker 1 - sim_specs["user"]["offset_for_scheduler"] = False - persis_info = add_unique_random_streams({}, nworkers + 1) - - # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) - # Run-line asserts are in sim func diff --git a/libensemble/tests/functionality_tests/test_mpi_runners_zrw_supernode_uneven.py b/libensemble/tests/functionality_tests/test_mpi_runners_zrw_supernode_uneven.py deleted file mode 100644 index 640d613bff..0000000000 --- a/libensemble/tests/functionality_tests/test_mpi_runners_zrw_supernode_uneven.py +++ /dev/null @@ -1,144 +0,0 @@ -""" -Runs libEnsemble testing the MPI Runners command creation with multiple and uneven nodes per worker. - -This test must be run on a number of workers >= 3. - -Execute via one of the following commands (e.g. 6 workers - one is zero resource): - mpiexec -np 7 python test_mpi_runners_zrw_supernode_uneven.py - python test_mpi_runners_zrw_supernode_uneven.py --nworkers 6 -""" - -import numpy as np - -from libensemble import logger -from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f -from libensemble.executors.mpi_executor import MPIExecutor -from libensemble.gen_funcs.persistent_sampling import persistent_uniform as gen_f -from libensemble.libE import libE -from libensemble.sim_funcs.run_line_check import runline_check_by_worker as sim_f -from libensemble.tests.regression_tests.common import create_node_file -from libensemble.tools import add_unique_random_streams, parse_args - -# logger.set_level("DEBUG") # For testing the test -logger.set_level("INFO") - -# Do not change these lines - they are parsed by run-tests.sh -# TESTSUITE_COMMS: mpi local -# TESTSUITE_NPROCS: 4 5 6 - -# Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). -if __name__ == "__main__": - nworkers, is_manager, libE_specs, _ = parse_args() - rounds = 1 - sim_app = "/path/to/fakeapp.x" - comms = libE_specs["comms"] - - libE_specs["zero_resource_workers"] = [1] - libE_specs["dedicated_mode"] = True - libE_specs["enforce_worker_core_bounds"] = True - - # To allow visual checking - log file not used in test - log_file = "ensemble_mpi_runners_zrw_supernode_uneven_comms_" + str(comms) + "_wrks_" + str(nworkers) + ".log" - logger.set_filename(log_file) - - nodes_per_worker = 2.5 - - # For varying size test - relate node count to nworkers - in_place = libE_specs["zero_resource_workers"] - n_gens = len(in_place) - nsim_workers = nworkers - n_gens - comms = libE_specs["comms"] - node_file = "nodelist_mpi_runners_zrw_supernode_uneven_comms_" + str(comms) + "_wrks_" + str(nworkers) - nnodes = int(nsim_workers * nodes_per_worker) - - # Mock up system - custom_resources = { - "cores_on_node": (16, 64), # Tuple (physical cores, logical cores) - "node_file": node_file, # Name of file containing a node-list - } - libE_specs["resource_info"] = custom_resources - - if is_manager: - create_node_file(num_nodes=nnodes, name=node_file) - - if comms == "mpi": - libE_specs["mpi_comm"].Barrier() - - # Create executor and register sim to it. - exctr = MPIExecutor(custom_info={"mpi_runner": "srun"}) - exctr.register_app(full_path=sim_app, calc_type="sim") - - n = 2 - sim_specs = { - "sim_f": sim_f, - "in": ["x"], - "out": [("f", float)], - } - - gen_specs = { - "gen_f": gen_f, - "in": [], - "out": [("x", float, (n,))], - "user": { - "initial_batch_size": 20, - "lb": np.array([-3, -2]), - "ub": np.array([3, 2]), - }, - } - - alloc_specs = {"alloc_f": alloc_f} - persis_info = add_unique_random_streams({}, nworkers + 1) - exit_criteria = {"sim_max": (nsim_workers) * rounds} - - # Each worker has either 3 or 2 nodes. Basic test list for portable options - test_list_base = [ - {"testid": "base1"}, # Give no config and no extra_args - ] - - # Example: On 3 workers, runlines should be ... - # (one workers has 3 nodes, the other 2 - does not split 2.5 nodes each). - # [w1]: Gen only - # [w2]: srun -w node-1,node-2,node-3 --ntasks 48 --nodes 3 --ntasks-per-node 16 --exact /path/to/fakeapp.x --testid base1 - # [w3]: srun -w node-4,node-5 --ntasks 32 --nodes 2 --ntasks-per-node 16 --exact /path/to/fakeapp.x --testid base1 - - srun_p1 = "srun -w " - srun_p2 = " --ntasks " - srun_p3 = " --nodes " - srun_p4 = " --ntasks-per-node 16 --exact /path/to/fakeapp.x --testid base1" - - exp_tasks = [] - exp_srun = [] - - # Hard coding an example for 2 nodes to avoid replicating general logic in libEnsemble. - low_npw = nnodes // nsim_workers - high_npw = nnodes // nsim_workers + 1 - - nodelist = [] - for i in range(1, nnodes + 1): - nodelist.append("node-" + str(i)) - - inode = 0 - for i in range(nsim_workers): - if i < (nsim_workers // 2): - npw = high_npw - else: - npw = low_npw - nodename = ",".join(nodelist[inode : inode + npw]) - inode += npw - ntasks = 16 * npw - loc_nodes = npw - exp_tasks.append(ntasks) - exp_srun.append(srun_p1 + str(nodename) + srun_p2 + str(ntasks) + srun_p3 + str(loc_nodes) + srun_p4) - - test_list = test_list_base - exp_list = exp_srun - sim_specs["user"] = { - "tests": test_list, - "expect": exp_list, - "persis_gens": n_gens, - } - - # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) - - # All asserts are in sim func diff --git a/libensemble/tests/functionality_tests/test_mpi_warning.py b/libensemble/tests/functionality_tests/test_mpi_warning.py index daf6125b62..f58620b90f 100644 --- a/libensemble/tests/functionality_tests/test_mpi_warning.py +++ b/libensemble/tests/functionality_tests/test_mpi_warning.py @@ -17,11 +17,12 @@ import numpy as np from libensemble import Ensemble, logger +from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first from libensemble.gen_funcs.sampling import latin_hypercube_sample as gen_f # Import libEnsemble items for this test from libensemble.sim_funcs.simple_sim import norm_eval as sim_f -from libensemble.specs import ExitCriteria, GenSpecs, SimSpecs +from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, SimSpecs # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": @@ -31,19 +32,19 @@ sampling = Ensemble() sampling.libE_specs.save_every_k_sims = 100 - sampling.sim_specs = SimSpecs(sim_f=sim_f) + sampling.sim_specs = SimSpecs(sim_f=sim_f, inputs=["x"], outputs=[("f", float)]) sampling.gen_specs = GenSpecs( gen_f=gen_f, outputs=[("x", float, 2)], + batch_size=100, user={ - "gen_batch_size": 100, "lb": np.array([-3, -2]), "ub": np.array([3, 2]), }, ) + sampling.alloc_specs = AllocSpecs(alloc_f=give_sim_work_first) sampling.exit_criteria = ExitCriteria(sim_max=100) - sampling.add_random_streams() if sampling.is_manager: if os.path.exists(log_file): diff --git a/libensemble/tests/functionality_tests/test_new_field.py b/libensemble/tests/functionality_tests/test_new_field.py index c130b1d6e3..158edd60bf 100644 --- a/libensemble/tests/functionality_tests/test_new_field.py +++ b/libensemble/tests/functionality_tests/test_new_field.py @@ -11,15 +11,16 @@ # Do not change these lines - they are parsed by run-tests.sh # TESTSUITE_COMMS: mpi local -# TESTSUITE_NPROCS: 2 4 +# TESTSUITE_NPROCS: 4 import numpy as np +from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first from libensemble.gen_funcs.sampling import latin_hypercube_sample as gen_f # Import libEnsemble items for this test from libensemble.libE import libE -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +from libensemble.tools import parse_args, save_libE_output def sim_f(In): @@ -41,18 +42,26 @@ def sim_f(In): gen_specs = { "gen_f": gen_f, "out": [("x", float, (1,))], + "batch_size": 500, "user": { - "gen_batch_size": 500, "lb": np.array([-3]), "ub": np.array([3]), }, } - persis_info = add_unique_random_streams({}, nworkers + 1, seed=1234) + alloc_specs = { + "alloc_f": give_sim_work_first, + } exit_criteria = {"gen_max": 501} - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) + H, persis_info, flag = libE( + sim_specs, + gen_specs, + exit_criteria, + alloc_specs=alloc_specs, + libE_specs=libE_specs, + ) if is_manager: assert len(H) >= 501 diff --git a/libensemble/tests/functionality_tests/test_persistent_sampling_CUDA_variable_resources.py b/libensemble/tests/functionality_tests/test_persistent_sampling_CUDA_variable_resources.py index 327cff0d7d..584049de61 100644 --- a/libensemble/tests/functionality_tests/test_persistent_sampling_CUDA_variable_resources.py +++ b/libensemble/tests/functionality_tests/test_persistent_sampling_CUDA_variable_resources.py @@ -17,7 +17,6 @@ import numpy as np -from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f from libensemble.executors.mpi_executor import MPIExecutor from libensemble.gen_funcs.persistent_sampling_var_resources import uniform_sample as gen_f @@ -25,7 +24,7 @@ from libensemble.libE import libE from libensemble.sim_funcs import six_hump_camel from libensemble.sim_funcs.var_resources import CUDA_variable_resources as sim_f -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +from libensemble.tools import parse_args, save_libE_output # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": @@ -35,8 +34,6 @@ libE_specs["num_resource_sets"] = nworkers - 1 # Any worker can be the gen - # libE_specs["zero_resource_workers"] = [1] # If first worker must be gen, use this instead - libE_specs["sim_dirs_make"] = True libE_specs["workflow_dir_path"] = "./ensemble_CUDA/workflow_" + libE_specs["comms"] + "_w" + str(nworkers) + "_N" libE_specs["sim_dir_copy_files"] = [".gitignore"] @@ -62,34 +59,25 @@ "gen_f": gen_f, "persis_in": ["f", "x", "sim_id"], "out": [("resource_sets", int), ("x", float, n)], + "initial_batch_size": nworkers - 1, + "batch_evaluate_same_priority": False, + "async_return": True, "user": { - "initial_batch_size": nworkers - 1, "max_resource_sets": nworkers - 1, # Any sim created can req. 1 worker up to all. "lb": np.array([-3, -2]), "ub": np.array([3, 2]), }, } - alloc_specs = { - "alloc_f": alloc_f, - "user": { - "give_all_with_same_priority": False, - "async_return": True, - }, - } - libE_specs["scheduler_opts"] = {"match_slots": True} - persis_info = add_unique_random_streams({}, nworkers + 1) - exit_criteria = {"sim_max": 40, "wallclock_max": 300} + exit_criteria = {"sim_max": 10, "wallclock_max": 300} # Perform the run for i in range(2): - persis_info = add_unique_random_streams({}, nworkers + 1) + persis_info = {} libE_specs["workflow_dir_path"] = libE_specs["workflow_dir_path"][:-1] + str(i) - H, persis_info, flag = libE( - sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs, alloc_specs=alloc_specs - ) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) if is_manager: assert flag == 0 diff --git a/libensemble/tests/functionality_tests/test_persistent_sim_uniform_sampling.py b/libensemble/tests/functionality_tests/test_persistent_sim_uniform_sampling.py index f1f0616663..7314f42fc5 100644 --- a/libensemble/tests/functionality_tests/test_persistent_sim_uniform_sampling.py +++ b/libensemble/tests/functionality_tests/test_persistent_sim_uniform_sampling.py @@ -14,20 +14,19 @@ # Do not change these lines - they are parsed by run-tests.sh # TESTSUITE_COMMS: mpi local tcp -# TESTSUITE_NPROCS: 3 4 +# TESTSUITE_NPROCS: 4 # TESTSUITE_OS_SKIP: WIN import sys import numpy as np -from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f from libensemble.gen_funcs.persistent_sampling import persistent_uniform as gen_f # Import libEnsemble items for this test from libensemble.libE import libE from libensemble.sim_funcs.six_hump_camel import persistent_six_hump_camel as sim_f -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +from libensemble.tools import parse_args, save_libE_output # from libensemble import logger # logger.set_level("DEBUG") @@ -37,9 +36,6 @@ nworkers, is_manager, libE_specs, _ = parse_args() libE_specs["num_resource_sets"] = nworkers - 1 # Only matters if sims use resources. - # Only used to test returning/overwriting a point at the end of the persistent sim. - libE_specs["use_persis_return_sim"] = True - if nworkers < 2: sys.exit("Cannot run with a persistent worker if only one worker -- aborting...") @@ -57,21 +53,18 @@ "in": [], "persis_in": ["sim_id", "f", "grad"], "out": [("x", float, (n,))], + "initial_batch_size": 5, + "alt_type": True, "user": { - "initial_batch_size": 5, "lb": np.array([-3, -2]), "ub": np.array([3, 2]), }, } - alloc_specs = {"alloc_f": alloc_f, "user": {"alt_type": True}} - - persis_info = add_unique_random_streams({}, nworkers + 1) - exit_criteria = {"sim_max": 40, "wallclock_max": 300} # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, libE_specs=libE_specs) if is_manager: assert len(np.unique(H["gen_ended_time"])) == 8 diff --git a/libensemble/tests/functionality_tests/test_persistent_uniform_gen_decides_stop.py b/libensemble/tests/functionality_tests/test_persistent_uniform_gen_decides_stop.py index 68c8aaaa05..4d0e7a5eb7 100644 --- a/libensemble/tests/functionality_tests/test_persistent_uniform_gen_decides_stop.py +++ b/libensemble/tests/functionality_tests/test_persistent_uniform_gen_decides_stop.py @@ -13,20 +13,19 @@ # Do not change these lines - they are parsed by run-tests.sh # TESTSUITE_COMMS: mpi local -# TESTSUITE_NPROCS: 5 7 +# TESTSUITE_NPROCS: 4 6 # TESTSUITE_OS_SKIP: WIN import sys import numpy as np -from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f from libensemble.gen_funcs.persistent_sampling import persistent_request_shutdown as gen_f # Import libEnsemble items for this test from libensemble.libE import libE from libensemble.sim_funcs.branin.branin_obj import call_branin as sim_f -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +from libensemble.tools import parse_args, save_libE_output # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": @@ -36,6 +35,8 @@ n = 2 init_batch_size = nworkers - ngens + libE_specs["gen_on_worker"] = True + if ngens >= nworkers: sys.exit("The number of generators must be less than the number of workers -- aborting...") @@ -50,28 +51,20 @@ "gen_f": gen_f, "persis_in": ["f", "x", "sim_id"], "out": [("x", float, (n,))], + "initial_batch_size": init_batch_size, + "async_return": True, + "num_active_gens": ngens, "user": { - "initial_batch_size": init_batch_size, "shutdown_limit": 10, # Iterations on a gen before it triggers a shutdown. "lb": np.array([-3, -2]), "ub": np.array([3, 2]), }, } - alloc_specs = { - "alloc_f": alloc_f, - "user": { - "async_return": True, - "num_active_gens": ngens, - }, - } - - persis_info = add_unique_random_streams({}, nworkers + 1) - exit_criteria = {"gen_max": 50, "wallclock_max": 300} # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, libE_specs=libE_specs) if is_manager: [ended_times, counts] = np.unique(H["gen_ended_time"], return_counts=True) @@ -82,9 +75,7 @@ assert ( sum(counts == init_batch_size) >= ngens ), "The initial batch of each gen should be common among initial_batch_size number of points" - assert ( - len(counts) > 1 - ), "All gen_ended_times are the same; they should be different for the async case" + assert len(counts) > 1, "All gen_ended_times are the same; they should be different for the async case" gen_workers = np.unique(H["gen_worker"]) print("Generators that issued points", gen_workers) diff --git a/libensemble/tests/functionality_tests/test_persistent_uniform_sampling.py b/libensemble/tests/functionality_tests/test_persistent_uniform_sampling.py index 81a18a5285..e9a4c75b2b 100644 --- a/libensemble/tests/functionality_tests/test_persistent_uniform_sampling.py +++ b/libensemble/tests/functionality_tests/test_persistent_uniform_sampling.py @@ -14,20 +14,19 @@ # Do not change these lines - they are parsed by run-tests.sh # TESTSUITE_COMMS: mpi local -# TESTSUITE_NPROCS: 3 4 +# TESTSUITE_NPROCS: 4 import sys import numpy as np -from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f from libensemble.gen_funcs.persistent_sampling import batched_history_matching as gen_f2 from libensemble.gen_funcs.persistent_sampling import persistent_uniform as gen_f1 # Import libEnsemble items for this test from libensemble.libE import libE from libensemble.sim_funcs.rosenbrock import rosenbrock_eval as sim_f -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +from libensemble.tools import parse_args, save_libE_output # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": @@ -56,14 +55,12 @@ }, } - alloc_specs = {"alloc_f": alloc_f} - exit_criteria = {"gen_max": num_batches * batch, "wallclock_max": 300} libE_specs["kill_canceled_sims"] = False for run in range(5): - persis_info = add_unique_random_streams({}, nworkers + 1) + persis_info = {} for i in persis_info: persis_info[i]["get_grad"] = True @@ -87,13 +84,13 @@ sim_specs["in"] = ["x", "obj_component"] # sim_specs["out"] = [("f", float), ("grad", float, n)] elif run == 3: - libE_specs["gen_on_manager"] = True + libE_specs["gen_on_worker"] = True elif run == 4: - libE_specs["gen_on_manager"] = False + libE_specs["gen_on_worker"] = False libE_specs["gen_workers"] = [2] # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) if is_manager: assert len(np.unique(H["gen_ended_time"])) == num_batches diff --git a/libensemble/tests/functionality_tests/test_persistent_uniform_sampling_async.py b/libensemble/tests/functionality_tests/test_persistent_uniform_sampling_async.py index 77d9a6a7b8..fc9632a581 100644 --- a/libensemble/tests/functionality_tests/test_persistent_uniform_sampling_async.py +++ b/libensemble/tests/functionality_tests/test_persistent_uniform_sampling_async.py @@ -20,18 +20,19 @@ import numpy as np -from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f from libensemble.gen_funcs.persistent_sampling import persistent_uniform as gen_f # Import libEnsemble items for this test from libensemble.libE import libE from libensemble.sim_funcs.branin.branin_obj import call_branin as sim_f -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +from libensemble.tools import parse_args, save_libE_output # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": nworkers, is_manager, libE_specs, _ = parse_args() + libE_specs["gen_on_worker"] = True + if nworkers < 2: sys.exit("Cannot run with a persistent worker if only one worker -- aborting...") @@ -47,24 +48,18 @@ "gen_f": gen_f, "persis_in": ["f", "x", "sim_id"], "out": [("x", float, (n,))], + "initial_batch_size": nworkers, + "async_return": True, "user": { - "initial_batch_size": nworkers, # Ensure > 1 alloc to send all sims "lb": np.array([-3, -2]), "ub": np.array([3, 2]), }, } - alloc_specs = { - "alloc_f": alloc_f, - "user": {"async_return": True}, - } - - persis_info = add_unique_random_streams({}, nworkers + 1) - - exit_criteria = {"gen_max": 100, "wallclock_max": 300} + exit_criteria = {"gen_max": 10, "wallclock_max": 300} # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, libE_specs=libE_specs) if is_manager: [_, counts] = np.unique(H["gen_ended_time"], return_counts=True) diff --git a/libensemble/tests/functionality_tests/test_persistent_uniform_sampling_cancel.py b/libensemble/tests/functionality_tests/test_persistent_uniform_sampling_cancel.py index 15c2d5b097..9068b8313e 100644 --- a/libensemble/tests/functionality_tests/test_persistent_uniform_sampling_cancel.py +++ b/libensemble/tests/functionality_tests/test_persistent_uniform_sampling_cancel.py @@ -20,13 +20,12 @@ import numpy as np -from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f from libensemble.gen_funcs.persistent_sampling import persistent_uniform_with_cancellations as gen_f # Import libEnsemble items for this test from libensemble.libE import libE from libensemble.sim_funcs.rosenbrock import rosenbrock_eval as sim_f -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +from libensemble.tools import parse_args, save_libE_output # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": @@ -47,24 +46,18 @@ "gen_f": gen_f, "persis_in": ["x", "f", "grad", "sim_id"], "out": [("x", float, (n,))], + "initial_batch_size": 100, + "async_return": True, "user": { - "initial_batch_size": 100, "lb": np.array([-3, -2]), "ub": np.array([3, 2]), }, } - alloc_specs = { - "alloc_f": alloc_f, - "user": {"async_return": True}, - } - exit_criteria = {"gen_max": 150, "wallclock_max": 300} - persis_info = add_unique_random_streams({}, nworkers + 1) - # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, libE_specs=libE_specs) if is_manager: # For reproducible test, only tests if cancel requested on points - not whether got evaluated diff --git a/libensemble/tests/functionality_tests/test_persistent_uniform_sampling_nonblocking.py b/libensemble/tests/functionality_tests/test_persistent_uniform_sampling_nonblocking.py index 5425578849..2a4877cf03 100644 --- a/libensemble/tests/functionality_tests/test_persistent_uniform_sampling_nonblocking.py +++ b/libensemble/tests/functionality_tests/test_persistent_uniform_sampling_nonblocking.py @@ -20,13 +20,12 @@ import numpy as np -from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f from libensemble.gen_funcs.persistent_sampling import uniform_nonblocking as gen_f # Import libEnsemble items for this test from libensemble.libE import libE from libensemble.sim_funcs.rosenbrock import rosenbrock_eval as sim_f -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +from libensemble.tools import parse_args, save_libE_output # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": @@ -53,19 +52,17 @@ }, } - alloc_specs = {"alloc_f": alloc_f} - - persis_info = add_unique_random_streams({}, nworkers + 1) + persis_info = {i: {} for i in range(nworkers)} for i in persis_info: persis_info[i]["get_grad"] = True exit_criteria = {"gen_max": 40, "wallclock_max": 300} # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) if is_manager: assert len(np.unique(H["gen_ended_time"])) == 2 save_libE_output(H, persis_info, __file__, nworkers) - assert persis_info[1]["spin_count"] > 0, "This should have been a nonblocking receive" + assert persis_info[0]["spin_count"] > 0, "This should have been a nonblocking receive" diff --git a/libensemble/tests/functionality_tests/test_persistent_uniform_sampling_running_mean.py b/libensemble/tests/functionality_tests/test_persistent_uniform_sampling_running_mean.py index 9cc92dadd3..acd15d3706 100644 --- a/libensemble/tests/functionality_tests/test_persistent_uniform_sampling_running_mean.py +++ b/libensemble/tests/functionality_tests/test_persistent_uniform_sampling_running_mean.py @@ -20,18 +20,15 @@ import numpy as np -from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f from libensemble.gen_funcs.persistent_sampling import persistent_uniform_final_update as gen_f from libensemble.libE import libE from libensemble.sim_funcs.six_hump_camel import six_hump_camel_simple as sim_f -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +from libensemble.tools import parse_args, save_libE_output # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": nworkers, is_manager, libE_specs, _ = parse_args() - libE_specs["use_persis_return_gen"] = True - if nworkers < 2: sys.exit("Cannot run with a persistent worker if only one worker -- aborting...") @@ -57,8 +54,6 @@ }, } - alloc_specs = {"alloc_f": alloc_f} - sim_max = 120 exit_criteria = {"sim_max": sim_max} libE_specs["final_gen_send"] = True @@ -71,8 +66,8 @@ "pause_time": 1e-4, } - persis_info = add_unique_random_streams({}, nworkers + 1) - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) + persis_info = {} + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) if is_manager: # Check that last saved history agrees with returned history. diff --git a/libensemble/tests/functionality_tests/test_runlines_adaptive_workers.py b/libensemble/tests/functionality_tests/test_runlines_adaptive_workers.py index 34dbb02224..17ac37ab83 100644 --- a/libensemble/tests/functionality_tests/test_runlines_adaptive_workers.py +++ b/libensemble/tests/functionality_tests/test_runlines_adaptive_workers.py @@ -25,7 +25,7 @@ from libensemble.sim_funcs import helloworld from libensemble.sim_funcs.var_resources import multi_points_with_variable_resources as sim_f from libensemble.tests.regression_tests.common import create_node_file -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +from libensemble.tools import parse_args, save_libE_output nworkers, is_manager, libE_specs, _ = parse_args() @@ -51,8 +51,10 @@ "gen_f": gen_f, "in": ["sim_id"], "out": [("priority", float), ("resource_sets", int), ("x", float, n), ("x_on_cube", float, n)], + "batch_evaluate_same_priority": True, + "num_active_gens": 1, + "initial_batch_size": 5, "user": { - "initial_batch_size": 5, "max_resource_sets": 4, "lb": np.array([-3, -2]), "ub": np.array([3, 2]), @@ -61,11 +63,6 @@ alloc_specs = { "alloc_f": give_sim_work_first, - "user": { - "batch_mode": False, - "give_all_with_same_priority": True, - "num_active_gens": 1, - }, } comms = libE_specs["comms"] @@ -82,13 +79,10 @@ "node_file": node_file, } # Name of file containing a node-list - persis_info = add_unique_random_streams({}, nworkers + 1) exit_criteria = {"sim_max": 40, "wallclock_max": 300} # Perform the run - H, persis_info, flag = libE( - sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs, alloc_specs=alloc_specs - ) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) if is_manager: assert flag == 0 diff --git a/libensemble/tests/functionality_tests/test_runlines_adaptive_workers_persistent.py b/libensemble/tests/functionality_tests/test_runlines_adaptive_workers_persistent.py index ab5aa144de..2e1cf0a070 100644 --- a/libensemble/tests/functionality_tests/test_runlines_adaptive_workers_persistent.py +++ b/libensemble/tests/functionality_tests/test_runlines_adaptive_workers_persistent.py @@ -2,7 +2,7 @@ Runs libEnsemble run-lines for adaptive workers with persistent gen. Default setup is designed to run on 4*N + 1 workers - to modify, change total_nodes. -where one worker is a zero-resource persistent gen. +where one worker is a persistent gen without assigned resources. Execute via one of the following commands (e.g. 9 workers): mpiexec -np 10 python test_runlines_adaptive_workers_persistent.py @@ -17,7 +17,6 @@ import numpy as np -from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f from libensemble.executors.mpi_executor import MPIExecutor from libensemble.gen_funcs.persistent_sampling_var_resources import uniform_sample_with_var_priorities as gen_f @@ -26,7 +25,7 @@ from libensemble.sim_funcs import helloworld from libensemble.sim_funcs.var_resources import multi_points_with_variable_resources as sim_f from libensemble.tests.regression_tests.common import create_node_file -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +from libensemble.tools import parse_args, save_libE_output # from libensemble.gen_funcs.persistent_sampling import persistent_uniform as gen_f @@ -61,19 +60,15 @@ "gen_f": gen_f, "persis_in": ["x", "f", "sim_id"], "out": [("priority", float), ("resource_sets", int), ("x", float, n), ("x_on_cube", float, n)], + "initial_batch_size": nworkers - 1, + "batch_evaluate_same_priority": False, "user": { - "initial_batch_size": nworkers - 1, "max_resource_sets": max_rsets, "lb": np.array([-3, -2]), "ub": np.array([3, 2]), }, } - alloc_specs = { - "alloc_f": alloc_f, - "user": {"give_all_with_same_priority": False}, - } - comms = libE_specs["comms"] node_file = "nodelist_adaptive_workers_persistent_comms_" + str(comms) + "_wrks_" + str(nworkers) if is_manager: @@ -87,13 +82,10 @@ "node_file": node_file, } # Name of file containing a node-list - persis_info = add_unique_random_streams({}, nworkers + 1) exit_criteria = {"sim_max": 40, "wallclock_max": 300} # Perform the run - H, persis_info, flag = libE( - sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs, alloc_specs=alloc_specs - ) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, libE_specs=libE_specs) if is_manager: assert flag == 0 diff --git a/libensemble/tests/functionality_tests/test_runlines_adaptive_workers_persistent_oversubscribe_rsets.py b/libensemble/tests/functionality_tests/test_runlines_adaptive_workers_persistent_oversubscribe_rsets.py index fb730b966b..5033339e71 100644 --- a/libensemble/tests/functionality_tests/test_runlines_adaptive_workers_persistent_oversubscribe_rsets.py +++ b/libensemble/tests/functionality_tests/test_runlines_adaptive_workers_persistent_oversubscribe_rsets.py @@ -2,7 +2,7 @@ Runs libEnsemble run-lines for adaptive workers with persistent gen. Default setup is designed to run on 2*N + 1 workers - to modify, change total_nodes. -where one worker is a zero-resource persistent gen. +where one worker is a persistent gen without assigned resources. Execute via one of the following commands (e.g. 5 workers): mpiexec -np 6 python test_runlines_adaptive_workers_persistent_oversubscribe_rsets.py @@ -17,7 +17,6 @@ import numpy as np -from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f from libensemble.executors.mpi_executor import MPIExecutor from libensemble.gen_funcs.persistent_sampling_var_resources import uniform_sample_with_var_priorities as gen_f @@ -26,18 +25,17 @@ from libensemble.sim_funcs import helloworld from libensemble.sim_funcs.var_resources import multi_points_with_variable_resources as sim_f from libensemble.tests.regression_tests.common import create_node_file -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +from libensemble.tools import parse_args, save_libE_output # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": nworkers, is_manager, libE_specs, _ = parse_args() - nsim_workers = nworkers - 1 + nsim_workers = nworkers - libE_specs["zero_resource_workers"] = [1] rsets = nsim_workers * 2 libE_specs["num_resource_sets"] = rsets - num_gens = len(libE_specs["zero_resource_workers"]) + num_gens = 1 total_nodes = (nworkers - num_gens) // 2 # 2 resourced workers per node. print(f"sim_workers: {nsim_workers}. rsets: {rsets}. Nodes: {total_nodes}", flush=True) @@ -63,19 +61,15 @@ "gen_f": gen_f, "persis_in": ["f", "x", "sim_id"], "out": [("priority", float), ("resource_sets", int), ("x", float, n), ("x_on_cube", float, n)], + "initial_batch_size": nworkers - 1, + "batch_evaluate_same_priority": False, "user": { - "initial_batch_size": nworkers - 1, "max_resource_sets": max_rsets, "lb": np.array([-3, -2]), "ub": np.array([3, 2]), }, } - alloc_specs = { - "alloc_f": alloc_f, - "user": {"give_all_with_same_priority": False}, - } - # comms = libE_specs["disable_resource_manager"] = True # SH TCP testing comms = libE_specs["comms"] @@ -91,13 +85,10 @@ "node_file": node_file, } # Name of file containing a node-list - persis_info = add_unique_random_streams({}, nworkers + 1) exit_criteria = {"sim_max": 40, "wallclock_max": 300} # Perform the run - H, persis_info, flag = libE( - sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs, alloc_specs=alloc_specs - ) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, libE_specs=libE_specs) if is_manager: assert flag == 0 diff --git a/libensemble/tests/functionality_tests/test_sim_dirs_per_calc.py b/libensemble/tests/functionality_tests/test_sim_dirs_per_calc.py index b4c30f9d2d..baa34b8381 100644 --- a/libensemble/tests/functionality_tests/test_sim_dirs_per_calc.py +++ b/libensemble/tests/functionality_tests/test_sim_dirs_per_calc.py @@ -18,10 +18,11 @@ import numpy as np +from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first from libensemble.gen_funcs.sampling import uniform_random_sample as gen_f from libensemble.libE import libE from libensemble.tests.regression_tests.support import write_sim_func as sim_f -from libensemble.tools import add_unique_random_streams, parse_args +from libensemble.tools import parse_args nworkers, is_manager, libE_specs, _ = parse_args() @@ -54,18 +55,20 @@ gen_specs = { "gen_f": gen_f, "out": [("x", float, (1,))], + "batch_size": 20, "user": { - "gen_batch_size": 20, "lb": np.array([-3]), "ub": np.array([3]), }, } - persis_info = add_unique_random_streams({}, nworkers + 1) + alloc_specs = { + "alloc_f": give_sim_work_first, + } exit_criteria = {"sim_max": 21} - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) + H, _, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) if is_manager: assert os.path.isdir(c_ensemble), f"Ensemble directory {c_ensemble} not created." diff --git a/libensemble/tests/functionality_tests/test_sim_dirs_per_worker.py b/libensemble/tests/functionality_tests/test_sim_dirs_per_worker.py index 69bb34ab84..74c77252cf 100644 --- a/libensemble/tests/functionality_tests/test_sim_dirs_per_worker.py +++ b/libensemble/tests/functionality_tests/test_sim_dirs_per_worker.py @@ -18,19 +18,20 @@ import numpy as np +from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first from libensemble.gen_funcs.sampling import uniform_random_sample as gen_f from libensemble.libE import libE from libensemble.tests.regression_tests.support import write_sim_func as sim_f -from libensemble.tools import add_unique_random_streams, parse_args +from libensemble.tools import parse_args -nworkers, is_manager, libE_specs, _ = parse_args() +n_simworkers, is_manager, libE_specs, _ = parse_args() # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": sim_input_dir = "./sim_input_dir" dir_to_copy = sim_input_dir + "/copy_this" dir_to_symlink = sim_input_dir + "/symlink_this" - w_ensemble = "./ensemble_workdirs_w" + str(nworkers) + "_" + libE_specs.get("comms") + w_ensemble = "./ensemble_workdirs_w" + str(n_simworkers) + "_" + libE_specs.get("comms") print("creating ensemble dir: ", w_ensemble, flush=True) for dir in [sim_input_dir, dir_to_copy, dir_to_symlink]: @@ -53,25 +54,25 @@ gen_specs = { "gen_f": gen_f, "out": [("x", float, (1,))], + "batch_size": 20, "user": { - "gen_batch_size": 20, "lb": np.array([-3]), "ub": np.array([3]), }, } - persis_info = add_unique_random_streams({}, nworkers + 1) + alloc_specs = {"alloc_f": give_sim_work_first} exit_criteria = {"sim_max": 21} - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) + H, _, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) if is_manager: assert os.path.isdir(w_ensemble), f"Ensemble directory {w_ensemble} not created." worker_dir_sum = sum(["worker" in i for i in os.listdir(w_ensemble)]) - assert worker_dir_sum == nworkers, "Number of worker dirs ({}) does not match nworkers ({}).".format( - worker_dir_sum, nworkers - ) + assert ( + worker_dir_sum == n_simworkers + 1 + ), "Number of worker dirs ({}) does not match n_simworkers ({}).".format(worker_dir_sum, n_simworkers) input_copied = [] sim_dir_sum = 0 diff --git a/libensemble/tests/functionality_tests/test_sim_dirs_with_exception.py b/libensemble/tests/functionality_tests/test_sim_dirs_with_exception.py index 229f6d5f54..b3189dc1ab 100644 --- a/libensemble/tests/functionality_tests/test_sim_dirs_with_exception.py +++ b/libensemble/tests/functionality_tests/test_sim_dirs_with_exception.py @@ -18,11 +18,12 @@ import numpy as np +from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first from libensemble.gen_funcs.sampling import uniform_random_sample as gen_f from libensemble.libE import libE from libensemble.manager import LoggedException from libensemble.tests.regression_tests.support import write_sim_func as sim_f -from libensemble.tools import add_unique_random_streams, parse_args +from libensemble.tools import parse_args nworkers, is_manager, libE_specs, _ = parse_args() @@ -46,20 +47,22 @@ gen_specs = { "gen_f": gen_f, "out": [("x", float, (1,))], + "batch_size": 20, "user": { - "gen_batch_size": 20, "lb": np.array([-3]), "ub": np.array([3]), }, } - persis_info = add_unique_random_streams({}, nworkers + 1) + alloc_specs = { + "alloc_f": give_sim_work_first, + } exit_criteria = {"sim_max": 21} return_flag = 1 try: - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) + H, _, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) except LoggedException as e: print(f"Caught deliberate exception: {e}") return_flag = 0 diff --git a/libensemble/tests/functionality_tests/test_sim_dirs_with_gen_dirs.py b/libensemble/tests/functionality_tests/test_sim_dirs_with_gen_dirs.py index c73eeabadf..a636cc2f0c 100644 --- a/libensemble/tests/functionality_tests/test_sim_dirs_with_gen_dirs.py +++ b/libensemble/tests/functionality_tests/test_sim_dirs_with_gen_dirs.py @@ -18,10 +18,11 @@ import numpy as np +from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first from libensemble.libE import libE from libensemble.tests.regression_tests.support import write_sim_func as sim_f from libensemble.tests.regression_tests.support import write_uniform_gen_func as gen_f -from libensemble.tools import add_unique_random_streams, parse_args +from libensemble.tools import parse_args nworkers, is_manager, libE_specs, _ = parse_args() @@ -65,18 +66,20 @@ gen_specs = { "gen_f": gen_f, "out": [("x", float, (1,))], + "batch_size": 20, "user": { - "gen_batch_size": 20, "lb": np.array([-3]), "ub": np.array([3]), }, } - persis_info = add_unique_random_streams({}, nworkers + 1) - exit_criteria = {"sim_max": 20} - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) + alloc_specs = { + "alloc_f": give_sim_work_first, + } + + H, _, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) def check_copied(type): input_copied = [] diff --git a/libensemble/tests/functionality_tests/test_sim_input_dir_option.py b/libensemble/tests/functionality_tests/test_sim_input_dir_option.py index 7dc4da7057..4e58d27d60 100644 --- a/libensemble/tests/functionality_tests/test_sim_input_dir_option.py +++ b/libensemble/tests/functionality_tests/test_sim_input_dir_option.py @@ -18,10 +18,11 @@ import numpy as np +from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first from libensemble.gen_funcs.sampling import uniform_random_sample as gen_f from libensemble.libE import libE from libensemble.tests.regression_tests.support import write_sim_func as sim_f -from libensemble.tools import add_unique_random_streams, parse_args +from libensemble.tools import parse_args nworkers, is_manager, libE_specs, _ = parse_args() @@ -49,18 +50,20 @@ gen_specs = { "gen_f": gen_f, "out": [("x", float, (1,))], + "batch_size": 20, "user": { - "gen_batch_size": 20, "lb": np.array([-3]), "ub": np.array([3]), }, } - persis_info = add_unique_random_streams({}, nworkers + 1) - exit_criteria = {"sim_max": 21} - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) + alloc_specs = { + "alloc_f": give_sim_work_first, + } + + H, _, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) if is_manager: assert os.path.isdir(o_ensemble), f"Ensemble directory {o_ensemble} not created." diff --git a/libensemble/tests/functionality_tests/test_stats_output.py b/libensemble/tests/functionality_tests/test_stats_output.py index 58d012cdeb..0f9bf7e1ae 100644 --- a/libensemble/tests/functionality_tests/test_stats_output.py +++ b/libensemble/tests/functionality_tests/test_stats_output.py @@ -13,7 +13,7 @@ # Do not change these lines - they are parsed by run-tests.sh # TESTSUITE_COMMS: mpi local -# TESTSUITE_NPROCS: 2 4 +# TESTSUITE_NPROCS: 4 import sys import warnings @@ -28,7 +28,7 @@ from libensemble.libE import libE from libensemble.sim_funcs import helloworld, six_hump_camel from libensemble.sim_funcs.var_resources import multi_points_with_variable_resources as sim_f -from libensemble.tools import add_unique_random_streams, parse_args +from libensemble.tools import parse_args warnings.filterwarnings("ignore", category=DeprecationWarning) from check_libE_stats import check_libE_stats @@ -58,7 +58,10 @@ "sim_f": sim_f, "in": ["x"], "out": [("f", float)], - "user": {"app": "helloworld"}, # helloworld or six_hump_camel + "user": { + "app": "helloworld", + "dry_run": True, + }, # dry_run avoids real MPI launches; stats format still exercised } gen_specs = { @@ -70,34 +73,29 @@ ("x", float, n), ("x_on_cube", float, n), ], + "batch_evaluate_same_priority": True, + "num_active_gens": 1, + "async_return": True, + "batch_size": 5, "user": { - "gen_batch_size": 5, "max_resource_sets": nworkers, "lb": np.array([-3, -2]), "ub": np.array([3, 2]), }, } - alloc_specs = { - "alloc_f": give_sim_work_first, - "user": { - "batch_mode": False, - "give_all_with_same_priority": True, - "num_active_gens": 1, - "async_return": True, - }, - } + alloc_specs = {"alloc_f": give_sim_work_first, "user": {"batch_mode": False}} # This can improve scheduling when tasks may run across multiple nodes libE_specs["scheduler_opts"] = {"match_slots": False} - exit_criteria = {"sim_max": 40, "wallclock_max": 300} + exit_criteria = {"sim_max": 12, "wallclock_max": 60} iterations = 2 # Note that libE_stats.txt output will be appended across libE calls. for prob_id in range(iterations): - sim_specs["user"]["app"] = "six_hump_camel" + sim_specs["user"]["app"] = "helloworld" libE_specs["ensemble_dir_path"] = ( "./ensemble_test_stats" + str(nworkers) + "_" + libE_specs.get("comms") + "_" + str(prob_id) @@ -112,7 +110,7 @@ libE_specs["stats_fmt"] = {"task_datetime": True, "show_resource_sets": True} check_task_datetime = True - persis_info = add_unique_random_streams({}, nworkers + 1) + persis_info = {} # Perform the run H, persis_info, flag = libE( diff --git a/libensemble/tests/functionality_tests/test_uniform_sampling.py b/libensemble/tests/functionality_tests/test_uniform_sampling.py index 2867f94df9..4d8a6baa75 100644 --- a/libensemble/tests/functionality_tests/test_uniform_sampling.py +++ b/libensemble/tests/functionality_tests/test_uniform_sampling.py @@ -19,6 +19,7 @@ import numpy as np +from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first from libensemble.gen_funcs.sampling import uniform_random_sample # Import libEnsemble items for this test @@ -27,7 +28,7 @@ from libensemble.sim_funcs.six_hump_camel import six_hump_camel from libensemble.tests.regression_tests.common import read_generated_file from libensemble.tests.regression_tests.support import six_hump_camel_minima as minima -from libensemble.tools import add_unique_random_streams, parse_args +from libensemble.tools import parse_args # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": @@ -47,18 +48,20 @@ gen_specs = { "gen_f": uniform_random_sample, # Function generating sim_f input "out": [("x", float, (2,))], # Tell libE gen_f output, type, size + "batch_size": 500, "user": { - "gen_batch_size": 500, # Used by this specific gen_f "lb": np.array([-3, -2]), # Used by this specific gen_f "ub": np.array([3, 2]), # Used by this specific gen_f }, } # end_gen_specs_rst_tag - persis_info = add_unique_random_streams({}, nworkers + 1) - exit_criteria = {"gen_max": 501, "wallclock_max": 300} + alloc_specs = { + "alloc_f": give_sim_work_first, + } + for run in range(2): if run == 1: # Test running a mock sim using previous history file @@ -67,7 +70,7 @@ sim_specs["user"] = {"history_file": hfile} # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) + H, _, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) if is_manager: assert flag == 0 diff --git a/libensemble/tests/functionality_tests/test_uniform_sampling_cancel.py b/libensemble/tests/functionality_tests/test_uniform_sampling_cancel.py index 521b6e169d..98e2b7814d 100644 --- a/libensemble/tests/functionality_tests/test_uniform_sampling_cancel.py +++ b/libensemble/tests/functionality_tests/test_uniform_sampling_cancel.py @@ -14,7 +14,7 @@ # Do not change these lines - they are parsed by run-tests.sh # TESTSUITE_COMMS: mpi local -# TESTSUITE_NPROCS: 2 4 +# TESTSUITE_NPROCS: 4 import gc @@ -30,7 +30,7 @@ from libensemble.libE import libE from libensemble.sim_funcs.six_hump_camel import six_hump_camel from libensemble.tests.regression_tests.support import six_hump_camel_minima as minima -from libensemble.tools import add_unique_random_streams, parse_args +from libensemble.tools import get_rng, parse_args def create_H0(persis_info, gen_specs, sim_max): @@ -41,8 +41,17 @@ def create_H0(persis_info, gen_specs, sim_max): n = len(lb) b = sim_max - H0 = np.zeros(b, dtype=[("x", float, 2), ("sim_id", int), ("sim_started", bool), ("cancel_requested", bool)]) - H0["x"] = persis_info[0]["rand_stream"].uniform(lb, ub, (b, n)) + H0 = np.zeros( + b, + dtype=[ + ("x", float, 2), + ("sim_id", int), + ("sim_started", bool), + ("cancel_requested", bool), + ], + ) + rng = get_rng(gen_specs, {}) + H0["x"] = rng.uniform(lb, ub, (b, n)) H0["sim_id"] = range(b) H0["sim_started"] = False for i in range(b): @@ -76,15 +85,16 @@ def create_H0(persis_info, gen_specs, sim_max): gen_specs = { "gen_f": uniform_random_sample_cancel, # Function generating sim_f input "out": [("x", float, (2,)), ("cancel_requested", bool)], + "batch_size": 50, + "num_active_gens": 1, "user": { - "gen_batch_size": 50, # Used by this specific gen_f "lb": np.array([-3, -2]), # Used by this specific gen_f "ub": np.array([3, 2]), # Used by this specific gen_f }, } # end_gen_specs_rst_tag - persis_info = add_unique_random_streams({}, nworkers + 1) + persis_info = {} sim_max = 500 exit_criteria = {"sim_max": sim_max, "wallclock_max": 300} @@ -92,21 +102,22 @@ def create_H0(persis_info, gen_specs, sim_max): "alloc_f": gswf, "user": { "batch_mode": True, - "num_active_gens": 1, }, } a_spec_2 = { "alloc_f": gswf, "user": { - "batch_mode": True, "num_active_gens": 2, + "batch_mode": True, }, } a_spec_3 = { "alloc_f": fast_gswf, - "user": {}, + "user": { + "batch_mode": True, + }, } a_spec_4 = { @@ -124,6 +135,7 @@ def create_H0(persis_info, gen_specs, sim_max): if is_manager: print("Testing cancellations with non-persistent gen functions") + persis_info = {0: {}} for testnum in range(1, 6): alloc_specs = allocs[testnum] if is_manager: @@ -140,14 +152,20 @@ def create_H0(persis_info, gen_specs, sim_max): # Perform the run - do not overwrite persis_info H, persis_out, flag = libE( - sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs=libE_specs, H0=H0 + sim_specs, + gen_specs, + exit_criteria, + persis_info, + alloc_specs=alloc_specs, + libE_specs=libE_specs, + H0=H0, ) if is_manager: assert flag == 0 assert np.all(H["cancel_requested"][::10]), "Some values should be cancelled but are not" assert np.all(~H["sim_started"][::10]), "Some values are given that should not have been" - tol = 0.1 + tol = 0.5 for m in minima: assert np.min(np.sum((H["x"] - m) ** 2, 1)) < tol diff --git a/libensemble/tests/functionality_tests/test_uniform_sampling_one_residual_at_a_time.py b/libensemble/tests/functionality_tests/test_uniform_sampling_one_residual_at_a_time.py index 6e96210db3..f44b117c2a 100644 --- a/libensemble/tests/functionality_tests/test_uniform_sampling_one_residual_at_a_time.py +++ b/libensemble/tests/functionality_tests/test_uniform_sampling_one_residual_at_a_time.py @@ -29,7 +29,7 @@ from libensemble.libE import libE from libensemble.sim_funcs.chwirut1 import chwirut_eval as sim_f from libensemble.tests.regression_tests.support import persis_info_3 as persis_info -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +from libensemble.tools import parse_args, save_libE_output # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": @@ -57,8 +57,9 @@ "gen_f": gen_f, "in": ["pt_id"], "out": [("x", float, n), ("priority", float), ("paused", bool), ("obj_component", int), ("pt_id", int)], + "batch_size": 2, + "num_active_gens": 1, # Only allow one active generator "user": { - "gen_batch_size": 2, "single_component_at_a_time": True, "combine_component_func": lambda x: np.sum(np.power(x, 2)), "lb": (-2 - np.pi / 10) * np.ones(n), @@ -71,14 +72,12 @@ "alloc_f": give_sim_work_first, # Allocation function "user": { "stop_on_NaNs": True, # Should alloc preempt evals - "batch_mode": True, # Wait until all sim evals are done - "num_active_gens": 1, # Only allow one active generator "stop_partial_fvec_eval": True, # Should alloc preempt evals + "batch_mode": True, # Wait until all sim evals are done }, } # end_alloc_specs_rst_tag - persis_info = add_unique_random_streams(persis_info, nworkers + 1) persis_info_safe = deepcopy(persis_info) exit_criteria = {"sim_max": budget, "wallclock_max": 300} diff --git a/libensemble/tests/functionality_tests/test_uniform_sampling_then_persistent_localopt_runs.py b/libensemble/tests/functionality_tests/test_uniform_sampling_then_persistent_localopt_runs.py index ec126607ef..7367d13b2a 100644 --- a/libensemble/tests/functionality_tests/test_uniform_sampling_then_persistent_localopt_runs.py +++ b/libensemble/tests/functionality_tests/test_uniform_sampling_then_persistent_localopt_runs.py @@ -33,7 +33,7 @@ from libensemble.sim_funcs.six_hump_camel import six_hump_camel as sim_f from libensemble.tests.regression_tests.support import six_hump_camel_minima as minima from libensemble.tests.regression_tests.support import uniform_or_localopt_gen_out as gen_out -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +from libensemble.tools import parse_args, save_libE_output # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": @@ -54,24 +54,23 @@ "gen_f": gen_f, "persis_in": ["x", "f", "grad", "sim_id"], "out": gen_out, + "batch_size": 2, + "num_active_gens": 1, "user": { "xtol_rel": 1e-4, "lb": np.array([-3, -2]), "ub": np.array([3, 2]), - "gen_batch_size": 2, "localopt_method": "LD_MMA", "xtol_rel": 1e-4, }, } - alloc_specs = {"alloc_f": alloc_f, "user": {"batch_mode": True, "num_active_gens": 1}} - - persis_info = add_unique_random_streams({}, nworkers + 1) + alloc_specs = {"alloc_f": alloc_f, "user": {"batch_mode": True}} exit_criteria = {"sim_max": 1000, "wallclock_max": 300} # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) if is_manager: assert flag == 0 diff --git a/libensemble/tests/functionality_tests/test_uniform_sampling_with_variable_resources.py b/libensemble/tests/functionality_tests/test_uniform_sampling_with_variable_resources.py index 9123f7db17..cf6eda18aa 100644 --- a/libensemble/tests/functionality_tests/test_uniform_sampling_with_variable_resources.py +++ b/libensemble/tests/functionality_tests/test_uniform_sampling_with_variable_resources.py @@ -13,7 +13,7 @@ # Do not change these lines - they are parsed by run-tests.sh # TESTSUITE_COMMS: mpi local -# TESTSUITE_NPROCS: 2 4 +# TESTSUITE_NPROCS: 4 # TESTSUITE_EXTRA: true import sys @@ -29,7 +29,7 @@ from libensemble.libE import libE from libensemble.sim_funcs import helloworld, six_hump_camel from libensemble.sim_funcs.var_resources import multi_points_with_variable_resources as sim_f -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +from libensemble.tools import parse_args, save_libE_output if __name__ == "__main__": nworkers, is_manager, libE_specs, _ = parse_args() @@ -67,8 +67,11 @@ ("x", float, n), ("x_on_cube", float, n), ], + "batch_size": 5, + "batch_evaluate_same_priority": True, + "num_active_gens": 1, + "async_return": True, "user": { - "gen_batch_size": 5, "max_resource_sets": nworkers, "lb": np.array([-3, -2]), "ub": np.array([3, 2]), @@ -79,9 +82,6 @@ "alloc_f": give_sim_work_first, "user": { "batch_mode": False, - "give_all_with_same_priority": True, - "num_active_gens": 1, - "async_return": True, }, } @@ -110,11 +110,16 @@ libE_specs["ensemble_dir_path"] = "ensemble_hw_forkserver" + en_suffix set_start_method("forkserver", force=True) - persis_info = add_unique_random_streams({}, nworkers + 1) + persis_info = {} # Perform the run H, persis_info, flag = libE( - sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs, alloc_specs=alloc_specs + sim_specs, + gen_specs, + exit_criteria, + persis_info, + libE_specs=libE_specs, + alloc_specs=alloc_specs, ) if is_manager: diff --git a/libensemble/tests/functionality_tests/test_worker_exceptions.py b/libensemble/tests/functionality_tests/test_worker_exceptions.py index 435e621e16..efdba0ec48 100644 --- a/libensemble/tests/functionality_tests/test_worker_exceptions.py +++ b/libensemble/tests/functionality_tests/test_worker_exceptions.py @@ -16,11 +16,12 @@ import numpy as np +from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first from libensemble.gen_funcs.sampling import uniform_random_sample as gen_f from libensemble.libE import libE from libensemble.manager import LoggedException from libensemble.tests.regression_tests.support import nan_func as sim_f -from libensemble.tools import add_unique_random_streams, parse_args +from libensemble.tools import parse_args # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": @@ -44,18 +45,20 @@ }, } - persis_info = add_unique_random_streams({}, nworkers + 1) - libE_specs["abort_on_exception"] = False libE_specs["save_H_and_persis_on_abort"] = False # Tell libEnsemble when to stop exit_criteria = {"wallclock_max": 10} + alloc_specs = { + "alloc_f": give_sim_work_first, + } + # Perform the run return_flag = 1 try: - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) + H, _, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) except LoggedException as e: print(f"Caught deliberate exception: {e}") return_flag = 0 diff --git a/libensemble/tests/functionality_tests/test_workflow_dir.py b/libensemble/tests/functionality_tests/test_workflow_dir.py index 1abbc10bcf..6502b78ed2 100644 --- a/libensemble/tests/functionality_tests/test_workflow_dir.py +++ b/libensemble/tests/functionality_tests/test_workflow_dir.py @@ -18,10 +18,11 @@ import numpy as np +from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first from libensemble.gen_funcs.sampling import uniform_random_sample as gen_f from libensemble.libE import libE from libensemble.tests.regression_tests.support import write_sim_func as sim_f -from libensemble.tools import add_unique_random_streams, parse_args +from libensemble.tools import parse_args nworkers, is_manager, libE_specs, _ = parse_args() @@ -51,14 +52,16 @@ gen_specs = { "gen_f": gen_f, "out": [("x", float, (1,))], + "batch_size": 20, "user": { - "gen_batch_size": 20, "lb": np.array([-3]), "ub": np.array([3]), }, } - persis_info = add_unique_random_streams({}, nworkers + 1) + alloc_specs = { + "alloc_f": give_sim_work_first, + } exit_criteria = {"sim_max": 21} @@ -70,7 +73,7 @@ "./test_workflow" + str(i) + "_nworkers" + str(nworkers) + "_comms-" + libE_specs["comms"] ) - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) + H, _, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) assert os.path.isdir(libE_specs["workflow_dir_path"]), "workflow_dir not created" assert all( diff --git a/libensemble/tests/functionality_tests/test_zero_resource_workers.py b/libensemble/tests/functionality_tests/test_zero_resource_workers.py deleted file mode 100644 index c8f0786d06..0000000000 --- a/libensemble/tests/functionality_tests/test_zero_resource_workers.py +++ /dev/null @@ -1,130 +0,0 @@ -""" -Runs libEnsemble testing the zero_resource_workers argument. - -Execute via one of the following commands (e.g. 3 workers): - mpiexec -np 4 python test_zero_resource_workers.py - python test_zero_resource_workers.py --nworkers 3 - python test_zero_resource_workers.py --nworkers 3 --comms tcp - -The number of concurrent evaluations of the objective function will be 4-1=3. -""" - -import sys - -import numpy as np - -from libensemble import logger -from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f -from libensemble.executors.mpi_executor import MPIExecutor -from libensemble.gen_funcs.persistent_sampling import persistent_uniform as gen_f -from libensemble.libE import libE -from libensemble.sim_funcs.run_line_check import runline_check as sim_f -from libensemble.tests.regression_tests.common import create_node_file -from libensemble.tools import add_unique_random_streams, parse_args - -# logger.set_level("DEBUG") # For testing the test -logger.set_level("INFO") - -# Do not change these lines - they are parsed by run-tests.sh -# TESTSUITE_COMMS: mpi local -# TESTSUITE_NPROCS: 3 4 - -# Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). -if __name__ == "__main__": - nworkers, is_manager, libE_specs, _ = parse_args() - rounds = 1 - sim_app = "/path/to/fakeapp.x" - comms = libE_specs["comms"] - - libE_specs["zero_resource_workers"] = [1] - libE_specs["dedicated_mode"] = True - libE_specs["enforce_worker_core_bounds"] = True - - # To allow visual checking - log file not used in test - log_file = "ensemble_zrw_comms_" + str(comms) + "_wrks_" + str(nworkers) + ".log" - logger.set_filename(log_file) - - nodes_per_worker = 2 - - # For varying size test - relate node count to nworkers - in_place = libE_specs["zero_resource_workers"] - n_gens = len(in_place) - nsim_workers = nworkers - n_gens - - comms = libE_specs["comms"] - nodes_per_worker = 2 - node_file = "nodelist_zero_resource_workers_comms_" + str(comms) + "_wrks_" + str(nworkers) - nnodes = nsim_workers * nodes_per_worker - - # Mock up system - custom_resources = { - "cores_on_node": (16, 64), # Tuple (physical cores, logical cores) - "node_file": node_file, - } # Name of file containing a node-list - libE_specs["resource_info"] = custom_resources - - if is_manager: - create_node_file(num_nodes=nnodes, name=node_file) - - if comms == "mpi": - libE_specs["mpi_comm"].Barrier() - - # Mock up system - mpi_customizer = { - "mpi_runner": "mpich", # Select runner: mpich, openmpi, aprun, srun, jsrun - "runner_name": "mpirun", - } # Runner name: Replaces run command if not None - - # Create executor and register sim to it. - exctr = MPIExecutor(custom_info=mpi_customizer) - exctr.register_app(full_path=sim_app, calc_type="sim") - - if nworkers < 2: - sys.exit("Cannot run with a persistent worker if only one worker -- aborting...") - - n = 2 - sim_specs = { - "sim_f": sim_f, - "in": ["x"], - "out": [("f", float)], - } - - gen_specs = { - "gen_f": gen_f, - "in": [], - "out": [("x", float, (n,))], - "user": { - "initial_batch_size": 20, - "lb": np.array([-3, -2]), - "ub": np.array([3, 2]), - }, - } - - alloc_specs = {"alloc_f": alloc_f} - persis_info = add_unique_random_streams({}, nworkers + 1) - exit_criteria = {"sim_max": (nsim_workers) * rounds} - - # Each worker has 2 nodes. Basic test list for portable options - test_list_base = [ - {"testid": "base1", "nprocs": 2, "nnodes": 1, "ppn": 2, "e_args": "--xarg 1"}, # Under use - {"testid": "base2"}, # Give no config and no extra_args - ] - - exp_mpich = [ - "mpirun -hosts node-1 -np 2 --ppn 2 --xarg 1 /path/to/fakeapp.x --testid base1", - "mpirun -hosts node-1,node-2 -np 32 --ppn 16 /path/to/fakeapp.x --testid base2", - ] - - test_list = test_list_base - exp_list = exp_mpich - sim_specs["user"] = { - "tests": test_list, - "expect": exp_list, - "nodes_per_worker": nodes_per_worker, - "persis_gens": n_gens, - } - - # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) - - # All asserts are in sim func diff --git a/libensemble/tests/functionality_tests/test_zero_resource_workers_subnode.py b/libensemble/tests/functionality_tests/test_zero_resource_workers_subnode.py deleted file mode 100644 index 69ea2b559c..0000000000 --- a/libensemble/tests/functionality_tests/test_zero_resource_workers_subnode.py +++ /dev/null @@ -1,133 +0,0 @@ -""" -Runs libEnsemble testing the zero_resource_workers argument with 2 workers per -node. - -This test must be run on an odd number of workers >= 3 (e.g. even number of -procs when using mpi4py). - -Execute via one of the following commands (e.g. 3 workers): - mpiexec -np 4 python test_zero_resource_workers_subnode.py - python test_zero_resource_workers_subnode.py --nworkers 3 - python test_zero_resource_workers_subnode.py --nworkers 3 --comms tcp -""" - -import sys - -import numpy as np - -from libensemble import logger -from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f -from libensemble.executors.mpi_executor import MPIExecutor -from libensemble.gen_funcs.persistent_sampling import persistent_uniform as gen_f -from libensemble.libE import libE -from libensemble.sim_funcs.run_line_check import runline_check as sim_f -from libensemble.tests.regression_tests.common import create_node_file -from libensemble.tools import add_unique_random_streams, parse_args - -# logger.set_level("DEBUG") # For testing the test -logger.set_level("INFO") - -# Do not change these lines - they are parsed by run-tests.sh -# TESTSUITE_COMMS: mpi local -# TESTSUITE_NPROCS: 4 - -# Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). -if __name__ == "__main__": - nworkers, is_manager, libE_specs, _ = parse_args() - - rounds = 1 - sim_app = "/path/to/fakeapp.x" - comms = libE_specs["comms"] - - libE_specs["zero_resource_workers"] = [1] - libE_specs["dedicated_mode"] = True - libE_specs["enforce_worker_core_bounds"] = True - - # To allow visual checking - log file not used in test - log_file = "ensemble_zrw_subnode_comms_" + str(comms) + "_wrks_" + str(nworkers) + ".log" - logger.set_filename(log_file) - - nodes_per_worker = 0.5 - - # For varying size test - relate node count to nworkers - in_place = libE_specs["zero_resource_workers"] - n_gens = len(in_place) - nsim_workers = nworkers - n_gens - - if not (nsim_workers * nodes_per_worker).is_integer(): - sys.exit(f"Sim workers ({nsim_workers}) must divide evenly into nodes") - - comms = libE_specs["comms"] - node_file = "nodelist_zero_resource_workers_subnode_comms_" + str(comms) + "_wrks_" + str(nworkers) - nnodes = int(nsim_workers * nodes_per_worker) - - # Mock up system - custom_resources = { - "cores_on_node": (16, 64), # Tuple (physical cores, logical cores) - "node_file": node_file, - } # Name of file containing a node-list - libE_specs["resource_info"] = custom_resources - - if is_manager: - create_node_file(num_nodes=nnodes, name=node_file) - - if comms == "mpi": - libE_specs["mpi_comm"].Barrier() - - # Create executor and register sim to it. - exctr = MPIExecutor(custom_info={"mpi_runner": "srun"}) - exctr.register_app(full_path=sim_app, calc_type="sim") - - if nworkers < 2: - sys.exit("Cannot run with a persistent worker if only one worker -- aborting...") - - n = 2 - sim_specs = { - "sim_f": sim_f, - "in": ["x"], - "out": [("f", float)], - } - - gen_specs = { - "gen_f": gen_f, - "in": [], - "out": [("x", float, (n,))], - "user": { - "initial_batch_size": 20, - "lb": np.array([-3, -2]), - "ub": np.array([3, 2]), - }, - } - - alloc_specs = {"alloc_f": alloc_f} - persis_info = add_unique_random_streams({}, nworkers + 1) - exit_criteria = {"sim_max": (nsim_workers) * rounds} - - # Each worker has 2 nodes. Basic test list for portable options - test_list_base = [ - {"testid": "base1"}, # Give no config and no extra_args - {"testid": "base2", "nprocs": 5}, - {"testid": "base3", "nnodes": 1}, - {"testid": "base4", "ppn": 6}, - ] - - exp_srun = [ - "srun -w node-1 --ntasks 8 --nodes 1 --ntasks-per-node 8 --exact /path/to/fakeapp.x --testid base1", - "srun -w node-1 --ntasks 5 --nodes 1 --ntasks-per-node 5 --exact /path/to/fakeapp.x --testid base2", - "srun -w node-1 --ntasks 8 --nodes 1 --ntasks-per-node 8 --exact /path/to/fakeapp.x --testid base3", - "srun -w node-1 --ntasks 6 --nodes 1 --ntasks-per-node 6 --exact /path/to/fakeapp.x --testid base4", - ] - - test_list = test_list_base - exp_list = exp_srun - sim_specs["user"] = { - "tests": test_list, - "expect": exp_list, - "nodes_per_worker": nodes_per_worker, - "persis_gens": n_gens, - } - - # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) - - # All asserts are in sim func diff --git a/libensemble/tests/regression_tests/common.py b/libensemble/tests/regression_tests/common.py index cb174d09c9..ebf45cdaac 100644 --- a/libensemble/tests/regression_tests/common.py +++ b/libensemble/tests/regression_tests/common.py @@ -5,6 +5,7 @@ import glob import os import os.path +import sys import time @@ -72,6 +73,8 @@ def build_simfunc(): # Build simfunc # buildstring='mpif90 -o my_simtask.x my_simtask.f90' # On cray need to use ftn buildstring = "mpicc -o my_simtask.x ../unit_tests/simdir/my_simtask.c" + if sys.platform == "darwin": + buildstring = "mpicc -cc=clang -o my_simtask.x ../unit_tests/simdir/my_simtask.c" # subprocess.run(buildstring.split(),check=True) #Python3.5+ subprocess.check_call(buildstring.split()) diff --git a/libensemble/tests/regression_tests/run_botorch_mfkg_branin.py b/libensemble/tests/regression_tests/run_botorch_mfkg_branin.py deleted file mode 100644 index be599ae705..0000000000 --- a/libensemble/tests/regression_tests/run_botorch_mfkg_branin.py +++ /dev/null @@ -1,79 +0,0 @@ -""" -Example of multi-fidelity optimization using a persistent BoTorch MFKG gen_func. - -This test uses the gen_on_manager option (persistent generator runs on -a thread). Therefore nworkers is the number of simulation workers. - -Execute via one of the following commands: - mpiexec -np 5 python run_botorch_mfkg_branin.py - python run_botorch_mfkg_branin.py --nworkers 4 - python run_botorch_mfkg_branin.py --nworkers 4 --comms tcp - -When running with the above commands, the number of concurrent evaluations of -the objective function will be 3. - -""" - -# Do not change these lines - they are parsed by run-tests.sh -# TESTSUITE_COMMS: local mpi -# TESTSUITE_NPROCS: 4 -# TESTSUITE_EXTRA: true -# TESTSUITE_OS_SKIP: OSX - -import numpy as np - -from libensemble import logger -from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens -from libensemble.gen_funcs.persistent_botorch_mfkg_branin import persistent_botorch_mfkg -from libensemble.libE import libE -from libensemble.sim_funcs.augmented_branin import augmented_branin -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output - -# Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). -if __name__ == "__main__": - nworkers, is_manager, libE_specs, _ = parse_args() - libE_specs["gen_on_manager"] = True - - sim_specs = { - "sim_f": augmented_branin, - "in": ["x", "fidelity"], - "out": [("f", float)], - } - - gen_specs = { - "gen_f": persistent_botorch_mfkg, - # "in": ["sim_id", "x", "f", "fidelity"], - "persis_in": ["sim_id", "x", "f", "fidelity"], - "out": [ - ("x", float, (2,)), - ("fidelity", float), - ], - "user": { - "lb": np.array([0.0, 0.0]), - "ub": np.array([1.0, 1.0]), - "n_init_samples": 4, # Each of these points will have a high-fidelity and low-fidelity evaluation - "q": 2, - }, - } - - alloc_specs = { - "alloc_f": only_persistent_gens, - "user": {"async_return": False}, - } - - # libE logger - logger.set_level("INFO") - - # Exit criteria - exit_criteria = {"sim_max": 12} # Exit after running sim_max simulations - - # Create a different random number stream for each worker and the manager - persis_info = add_unique_random_streams({}, nworkers + 1) - - # Run LibEnsemble, and store results in history array H - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) - - # Save results to numpy file - if is_manager: - save_libE_output(H, persis_info, __file__, nworkers) - diff --git a/libensemble/tests/regression_tests/support.py b/libensemble/tests/regression_tests/support.py index 4189bcfe48..9d0819e464 100644 --- a/libensemble/tests/regression_tests/support.py +++ b/libensemble/tests/regression_tests/support.py @@ -2,8 +2,6 @@ import numpy as np -from libensemble.specs import input_fields, output_data - branin_vals_and_minima = np.array( [ [-3.14159, 12.275, 0.397887], @@ -31,8 +29,6 @@ def nan_func(calc_in, persis_info, sim_specs, libE_info): return (H, persis_info) -@input_fields(["x"]) -@output_data([("f", float, (2,))]) def write_sim_func(calc_in, persis_info, sim_specs, libE_info): out = np.zeros(1, dtype=sim_specs["out"]) out["f"] = calc_in["x"] @@ -65,13 +61,16 @@ def remote_write_gen_func(calc_in, persis_info, gen_specs, libE_info): return H_o, persis_info -def write_uniform_gen_func(H, persis_info, gen_specs, _): +def write_uniform_gen_func(H, persis_info, gen_specs, libE_info): + from libensemble.tools import get_rng + ub = gen_specs["user"]["ub"] lb = gen_specs["user"]["lb"] n = len(lb) - b = gen_specs["user"]["gen_batch_size"] + b = gen_specs["batch_size"] H_o = np.zeros(b, dtype=gen_specs["out"]) - H_o["x"] = persis_info["rand_stream"].uniform(lb, ub, (b, n)) + rng = get_rng(gen_specs, libE_info) + H_o["x"] = rng.uniform(lb, ub, (b, n)) with open("test_gen_out.txt", "a") as f: f.write(f"gen_f produced: {H_o['x']}\n") return H_o, persis_info @@ -105,7 +104,7 @@ def write_uniform_gen_func(H, persis_info, gen_specs, _): "next_to_give": 0, # Remembers next H row to give in alloc_f } -persis_info_1[0] = { +persis_info_1[0] = { # noqa "run_order": {}, # Used by manager to remember run order "total_runs": 0, # Used by manager to count total runs "rand_stream": np.random.default_rng(1), diff --git a/libensemble/tests/regression_tests/test_1d_sampling.py b/libensemble/tests/regression_tests/test_1d_sampling.py index edecabb668..456b3ca601 100644 --- a/libensemble/tests/regression_tests/test_1d_sampling.py +++ b/libensemble/tests/regression_tests/test_1d_sampling.py @@ -11,34 +11,32 @@ # Do not change these lines - they are parsed by run-tests.sh # TESTSUITE_COMMS: mpi local threads tcp -# TESTSUITE_NPROCS: 2 4 +# TESTSUITE_NPROCS: 3 4 import numpy as np from libensemble import Ensemble -from libensemble.gen_funcs.sampling import latin_hypercube_sample as gen_f +from libensemble.gen_funcs.persistent_sampling import persistent_uniform # Import libEnsemble items for this test from libensemble.sim_funcs.simple_sim import norm_eval as sim_f from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs -from libensemble.tools import add_unique_random_streams -# Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": sampling = Ensemble(parse_args=True) sampling.libE_specs = LibeSpecs(save_every_k_gens=300, safe_mode=False, disable_log_files=True) - sampling.sim_specs = SimSpecs(sim_f=sim_f) + sampling.sim_specs = SimSpecs(sim_f=sim_f, inputs=["x"], outputs=[("f", float)]) sampling.gen_specs = GenSpecs( - gen_f=gen_f, + gen_f=persistent_uniform, + persis_in=["f"], outputs=[("x", float, (1,))], + initial_batch_size=100, user={ - "gen_batch_size": 100, "lb": np.array([-3]), "ub": np.array([3]), }, ) - sampling.persis_info = add_unique_random_streams({}, sampling.nworkers + 1) sampling.exit_criteria = ExitCriteria(sim_max=500) sampling.run() diff --git a/libensemble/tests/regression_tests/test_2d_sampling.py b/libensemble/tests/regression_tests/test_2d_sampling.py index 8164c2844a..c26a662010 100644 --- a/libensemble/tests/regression_tests/test_2d_sampling.py +++ b/libensemble/tests/regression_tests/test_2d_sampling.py @@ -16,29 +16,31 @@ import numpy as np from libensemble import Ensemble +from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first from libensemble.gen_funcs.sampling import latin_hypercube_sample as gen_f # Import libEnsemble items for this test from libensemble.sim_funcs.simple_sim import norm_eval as sim_f -from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs +from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, LibeSpecs, SimSpecs # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": sampling = Ensemble(parse_args=True) sampling.libE_specs = LibeSpecs(save_every_k_sims=100) - sampling.sim_specs = SimSpecs(sim_f=sim_f) + sampling.sim_specs = SimSpecs(sim_f=sim_f, inputs=["x"], outputs=[("f", float)]) sampling.gen_specs = GenSpecs( gen_f=gen_f, outputs=[("x", float, 2)], + batch_size=100, user={ - "gen_batch_size": 100, "lb": np.array([-3, -2]), "ub": np.array([3, 2]), }, ) + sampling.alloc_specs = AllocSpecs(alloc_f=give_sim_work_first) + sampling.exit_criteria = ExitCriteria(sim_max=200) - sampling.add_random_streams() sampling.run() if sampling.is_manager: diff --git a/libensemble/tests/regression_tests/test_2d_sampling_vocs.py b/libensemble/tests/regression_tests/test_2d_sampling_vocs.py new file mode 100644 index 0000000000..f535e17096 --- /dev/null +++ b/libensemble/tests/regression_tests/test_2d_sampling_vocs.py @@ -0,0 +1,58 @@ +""" +VOCS-based version of test_2d_sampling.py. using the + ``LatinHypercubeSample`` class. + +Execute via one of the following commands (e.g. 3 workers): + mpiexec -np 4 python test_2d_sampling_vocs.py + python test_2d_sampling_vocs.py --nworkers 3 + python test_2d_sampling_vocs.py --nworkers 3 --comms tcp +""" + +# Do not change these lines - they are parsed by run-tests.sh +# TESTSUITE_COMMS: mpi local threads tcp +# TESTSUITE_NPROCS: 2 4 + +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 ExitCriteria, GenSpecs, LibeSpecs, SimSpecs + + +def sim_f(In, persis_info, sim_specs, _): + Out = np.zeros(1, dtype=sim_specs["out"]) + Out["f"] = np.sqrt(In["x0"] ** 2 + In["x1"] ** 2) + return Out, persis_info + + +if __name__ == "__main__": + sampling = Ensemble(parse_args=True) + sampling.libE_specs = LibeSpecs(save_every_k_sims=100) + sampling.sim_specs = SimSpecs(sim_f=sim_f, inputs=["x0", "x1"], outputs=[("f", float)]) + + vocs = VOCS( + variables={"x0": [-3.0, 3.0], "x1": [-2.0, 2.0]}, + objectives={"f": "MINIMIZE"}, + ) + generator = LatinHypercubeSample(vocs, random_seed=1) + + sampling.gen_specs = GenSpecs( + generator=generator, + persis_in=["x0", "x1", "f", "sim_id"], + outputs=[("x0", float), ("x1", float)], + initial_batch_size=100, + batch_size=100, + ) + + sampling.exit_criteria = ExitCriteria(sim_max=200) + + sampling.run() + if sampling.is_manager: + assert len(sampling.H) >= 200 + x0 = sampling.H["x0"] + x1 = sampling.H["x1"] + f = sampling.H["f"] + assert np.all(np.isclose(f, np.sqrt(x0 ** 2 + x1 ** 2))) + print("\nlibEnsemble has calculated the 2D vector norm of all points") + sampling.save_output(__file__) diff --git a/libensemble/tests/regression_tests/test_GPU_variable_resources.py b/libensemble/tests/regression_tests/test_GPU_variable_resources.py index b6b3197f90..c8455b459c 100644 --- a/libensemble/tests/regression_tests/test_GPU_variable_resources.py +++ b/libensemble/tests/regression_tests/test_GPU_variable_resources.py @@ -28,7 +28,6 @@ import numpy as np from libensemble import Ensemble -from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f from libensemble.executors.mpi_executor import MPIExecutor from libensemble.gen_funcs.persistent_sampling_var_resources import uniform_sample_with_procs_gpus as gen_f1 from libensemble.gen_funcs.persistent_sampling_var_resources import uniform_sample_with_var_gpus as gen_f2 @@ -36,10 +35,8 @@ # Import libEnsemble items for this test from libensemble.sim_funcs import six_hump_camel from libensemble.sim_funcs.var_resources import gpu_variable_resources_from_gen as sim_f -from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, LibeSpecs, SimSpecs -from libensemble.tools import add_unique_random_streams +from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs -# from libensemble import logger # logger.set_level("DEBUG") # For testing the test @@ -69,25 +66,19 @@ gen_f=gen_f1, persis_in=["f", "x", "sim_id"], out=[("num_procs", int), ("num_gpus", int), ("x", float, 2)], + initial_batch_size=gpu_test.nworkers - 1, + batch_evaluate_same_priority=False, + async_return=False, user={ - "initial_batch_size": gpu_test.nworkers - 1, "max_procs": gpu_test.nworkers - 1, # Any sim created can req. 1 worker up to max "lb": np.array([-3, -2]), "ub": np.array([3, 2]), }, ) - gpu_test.alloc_specs = AllocSpecs( - alloc_f=alloc_f, - user={ - "give_all_with_same_priority": False, - "async_return": False, # False causes batch returns - }, - ) - # Run with random num_procs/num_gpus for each simulation - gpu_test.persis_info = add_unique_random_streams({}, gpu_test.nworkers + 1) - gpu_test.exit_criteria = ExitCriteria(sim_max=20) + gpu_test.persis_info = {} + gpu_test.exit_criteria = ExitCriteria(sim_max=10) gpu_test.run() if gpu_test.is_manager: @@ -96,7 +87,7 @@ # Run with num_gpus based on x[0] for each simulation gpu_test.gen_specs.gen_f = gen_f2 gpu_test.gen_specs.user["max_gpus"] = gpu_test.nworkers - 1 - gpu_test.persis_info = add_unique_random_streams({}, gpu_test.nworkers + 1) + gpu_test.persis_info = {} gpu_test.exit_criteria = ExitCriteria(sim_max=20) gpu_test.run() diff --git a/libensemble/tests/regression_tests/test_GPU_variable_resources_multi_task.py b/libensemble/tests/regression_tests/test_GPU_variable_resources_multi_task.py index 2b583d4f06..5564e7f968 100644 --- a/libensemble/tests/regression_tests/test_GPU_variable_resources_multi_task.py +++ b/libensemble/tests/regression_tests/test_GPU_variable_resources_multi_task.py @@ -37,7 +37,6 @@ import numpy as np from libensemble import Ensemble -from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f from libensemble.executors.mpi_executor import MPIExecutor # Using num_procs / num_gpus in gen @@ -46,10 +45,8 @@ # Import libEnsemble items for this test from libensemble.sim_funcs import six_hump_camel from libensemble.sim_funcs.var_resources import gpu_variable_resources_from_gen as sim_f -from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, LibeSpecs, SimSpecs -from libensemble.tools import add_unique_random_streams +from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs -# from libensemble import logger # logger.set_level("DEBUG") # For testing the test @@ -79,8 +76,10 @@ gen_f=gen_f, persis_in=["f", "x", "sim_id"], out=[("num_procs", int), ("num_gpus", int), ("x", float, 2)], + initial_batch_size=nworkers - 1, + batch_evaluate_same_priority=False, + async_return=False, user={ - "initial_batch_size": nworkers - 1, "max_procs": (nworkers - 1) // 2, # Any sim created can req. 1 worker up to max "lb": np.array([-3, -2]), "ub": np.array([3, 2]), @@ -88,16 +87,7 @@ }, ) - gpu_test.alloc_specs = AllocSpecs( - alloc_f=alloc_f, - user={ - "give_all_with_same_priority": False, - "async_return": False, # False causes batch returns - }, - ) - - gpu_test.persis_info = add_unique_random_streams({}, gpu_test.nworkers + 1) - gpu_test.exit_criteria = ExitCriteria(sim_max=40, wallclock_max=300) + gpu_test.exit_criteria = ExitCriteria(sim_max=10, wallclock_max=300) if gpu_test.ready(): gpu_test.run() diff --git a/libensemble/tests/regression_tests/test_asktell_aposmm_nlopt.py b/libensemble/tests/regression_tests/test_asktell_aposmm_nlopt.py new file mode 100644 index 0000000000..2a68d6e722 --- /dev/null +++ b/libensemble/tests/regression_tests/test_asktell_aposmm_nlopt.py @@ -0,0 +1,107 @@ +""" +Runs libEnsemble with APOSMM with the NLopt local optimizer. + +Execute via one of the following commands (e.g. 3 workers): + mpiexec -np 4 python test_persistent_aposmm_nlopt.py + python test_persistent_aposmm_nlopt.py --nworkers 3 --comms local + python test_persistent_aposmm_nlopt.py --nworkers 3 --comms tcp + +When running with the above commands, the number of concurrent evaluations of +the objective function will be 2, as one of the three workers will be the +persistent generator. +""" + +# Do not change these lines - they are parsed by run-tests.sh +# TESTSUITE_COMMS: local mpi tcp +# TESTSUITE_NPROCS: 4 + +from math import gamma, pi, sqrt + +import numpy as np + +import libensemble.gen_funcs + +# Import libEnsemble items for this test + +libensemble.gen_funcs.rc.aposmm_optimizers = "nlopt" +from time import time + +from gest_api.vocs import VOCS + +from libensemble import Ensemble +from libensemble.gen_classes import APOSMM +from libensemble.specs import ExitCriteria, GenSpecs, SimSpecs +from libensemble.tests.regression_tests.support import six_hump_camel_minima as minima + + +def six_hump_camel_func(x): + """ + Definition of the six-hump camel + """ + x1 = x["core"] + x2 = x["edge"] + term1 = (4 - 2.1 * x1**2 + (x1**4) / 3) * x1**2 + term2 = x1 * x2 + term3 = (-4 + 4 * x2**2) * x2**2 + + return {"energy": term1 + term2 + term3} + + +# Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). +if __name__ == "__main__": + workflow = Ensemble(parse_args=True) + + if workflow.is_manager: + start_time = time() + + n = 2 + + vocs = VOCS( + variables={ + "core": [-3, 3], + "edge": [-2, 2], + "core_on_cube": [0, 1], + "edge_on_cube": [0, 1], + }, + objectives={"energy": "MINIMIZE"}, + ) + + aposmm = APOSMM( + vocs, + max_active_runs=6, + variables_mapping={ + "x": ["core", "edge"], + "x_on_cube": ["core_on_cube", "edge_on_cube"], + "f": ["energy"], + }, + initial_sample_size=200, + sample_points=np.round(minima, 1), + localopt_method="LN_BOBYQA", + rk_const=0.5 * ((gamma(1 + (n / 2)) * 5) ** (1 / n)) / sqrt(pi), + xtol_abs=1e-6, + ftol_abs=1e-6, + ) + + workflow.gen_specs = GenSpecs( + generator=aposmm, + vocs=vocs, + batch_size=5, + initial_batch_size=10, + ) + + workflow.sim_specs = SimSpecs(simulator=six_hump_camel_func, vocs=vocs) + workflow.exit_criteria = ExitCriteria(sim_max=3000, wallclock_max=600) + + # Perform the run + H, _, _ = workflow.run() + + if workflow.is_manager: + print("[Manager]:", H[np.where(H["local_min"])]["x"]) + print("[Manager]: Time taken =", time() - start_time, flush=True) + + tol = 1e-5 + for m in minima: + # The minima are known on this test problem. + # We use their values to test APOSMM has identified all minima + print(np.min(np.sum((H[H["local_min"]]["x"] - m) ** 2, 1)), flush=True) + assert np.min(np.sum((H[H["local_min"]]["x"] - m) ** 2, 1)) < tol diff --git a/libensemble/tests/regression_tests/test_asktell_gpCAM.py b/libensemble/tests/regression_tests/test_asktell_gpCAM.py new file mode 100644 index 0000000000..f59fc135d7 --- /dev/null +++ b/libensemble/tests/regression_tests/test_asktell_gpCAM.py @@ -0,0 +1,93 @@ +""" +Tests libEnsemble with gpCAM + +Execute via one of the following commands (e.g. 3 workers): + mpiexec -np 4 python test_gpCAM_class.py + python test_gpCAM_class.py --nworkers 3 --comms local + +When running with the above commands, the number of concurrent evaluations of +the objective function will be 2, as one of the three workers will be the +persistent generator. + +See libensemble.gen_funcs.persistent_gpCAM for more details about the generator +setup. +""" + +# Do not change these lines - they are parsed by run-tests.sh +# TESTSUITE_COMMS: mpi local +# TESTSUITE_NPROCS: 4 +# TESTSUITE_EXTRA: true + +import sys +import warnings + +import numpy as np +from gest_api.vocs import VOCS + +from libensemble.gen_classes.gpCAM import GP_CAM, GP_CAM_Covar + +# Import libEnsemble items for this test +from libensemble.libE import libE +from libensemble.sim_funcs.rosenbrock import rosenbrock_eval as sim_f +from libensemble.tools import parse_args, save_libE_output + +warnings.filterwarnings("ignore", message="Default hyperparameter_bounds") + + +# Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). +if __name__ == "__main__": + nworkers, is_manager, libE_specs, _ = parse_args() + + if nworkers < 2: + sys.exit("Cannot run with a persistent worker if only one worker -- aborting...") + + n = 4 + batch_size = 15 + + sim_specs = { + "sim_f": sim_f, + "in": ["x"], + "out": [ + ("f", float), + ], + } + + gen_specs = { + "persis_in": ["x", "f", "sim_id"], + "out": [("x", float, (n,))], + "batch_size": batch_size, + "user": { + "lb": np.array([-3, -2, -1, -1]), + "ub": np.array([3, 2, 1, 1]), + }, + } + + vocs = VOCS(variables={"x0": [-3, 3], "x1": [-2, 2], "x2": [-1, 1], "x3": [-1, 1]}, objectives={"f": "MINIMIZE"}) + + gen = GP_CAM_Covar(vocs) + + for inst in range(3): + if inst == 0: + gen_specs["generator"] = gen + num_batches = 10 + exit_criteria = {"sim_max": num_batches * batch_size, "wallclock_max": 300} + libE_specs["save_every_k_gens"] = 150 + libE_specs["H_file_prefix"] = "gpCAM_nongrid" + if inst == 1: + gen = GP_CAM_Covar(vocs, use_grid=True, test_points_file="gpCAM_nongrid_after_gen_150.npy") + gen_specs["generator"] = gen + libE_specs["final_gen_send"] = True + del libE_specs["H_file_prefix"] + del libE_specs["save_every_k_gens"] + elif inst == 2: + gen = GP_CAM(vocs, ask_max_iter=1) + gen_specs["generator"] = gen + num_batches = 3 # Few because the ask_tell gen can be slow + exit_criteria = {"sim_max": num_batches * batch_size, "wallclock_max": 300} + + # Perform the run + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, {}, libE_specs=libE_specs) + if is_manager: + assert len(np.unique(H["gen_ended_time"])) == num_batches + + save_libE_output(H, persis_info, __file__, nworkers) diff --git a/libensemble/tests/regression_tests/test_botorch_mfkg_branin.py b/libensemble/tests/regression_tests/test_botorch_mfkg_branin.py new file mode 100644 index 0000000000..f93daf4af6 --- /dev/null +++ b/libensemble/tests/regression_tests/test_botorch_mfkg_branin.py @@ -0,0 +1,85 @@ +""" +Tests libEnsemble with BoTorchMFKG generator (gest-api style) and the +augmented Branin multi-fidelity simulator. + +The generator runs on the manager thread (default). All allocated workers +are available for simulation tasks. + +Execute via one of the following commands (e.g. 4 workers): + mpiexec -np 5 python test_botorch_mfkg_branin.py + python test_botorch_mfkg_branin.py -n 4 + +When running with the above commands, the number of concurrent evaluations +of the objective function will be 4. + +""" + +# Do not change these lines - they are parsed by run-tests.sh +# TESTSUITE_COMMS: local mpi +# TESTSUITE_NPROCS: 4 +# TESTSUITE_EXTRA: true + +from gest_api.vocs import VOCS + +from libensemble import Ensemble +from libensemble.gen_classes.botorch_mfkg import BoTorchMFKG +from libensemble.sim_funcs.augmented_branin import augmented_branin_callable +from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs + +# Main block is necessary only when using local comms with spawn start method +# (default on macOS and Windows). +if __name__ == "__main__": + # q candidates per MFKG iteration; batch_size must match so libEnsemble + # always requests exactly the number the generator will produce. + q = 2 + n_workers = 4 + + libE_specs = LibeSpecs(nworkers=n_workers) + + # Variables: two design dimensions + fidelity (all in [0, 1]). + # Objective: maximise the (negated) augmented Branin value. + vocs = VOCS( + variables={"x0": [0.0, 1.0], "x1": [0.0, 1.0], "fidelity": [0.0, 1.0]}, + objectives={"f": "MAXIMIZE"}, + ) + + gen = BoTorchMFKG( + vocs=vocs, + n_init_samples=4, # produces 2 * 4 = 8 initial simulations + q=q, + num_fantasies=128, + num_restarts=1, # reduced for faster testing + raw_samples=10, # reduced for faster testing + seed=42, + ) + + gen_specs = GenSpecs( + generator=gen, + # initial_batch_size matches the 2 * n_init_samples points suggest() + # returns on the first call. + initial_batch_size=2 * gen.n_init_samples, + batch_size=q, + vocs=vocs, + ) + + sim_specs = SimSpecs( + simulator=augmented_branin_callable, + vocs=vocs, + ) + + # Exit after running sim_max simulations (8 initial + at least 2 iterations) + exit_criteria = ExitCriteria(sim_max=12) + + workflow = Ensemble( + libE_specs=libE_specs, + sim_specs=sim_specs, + gen_specs=gen_specs, + exit_criteria=exit_criteria, + ) + + H, _, flag = workflow.run() + + if workflow.is_manager: + print(f"Completed {len(H)} simulations") + assert len(H) >= exit_criteria.sim_max, f"Expected at least {exit_criteria.sim_max} simulations, got {len(H)}" + print("Test passed") diff --git a/libensemble/tests/regression_tests/test_ensemble_platform_workdir.py b/libensemble/tests/regression_tests/test_ensemble_platform_workdir.py index 121b0cd37a..3f863b2320 100644 --- a/libensemble/tests/regression_tests/test_ensemble_platform_workdir.py +++ b/libensemble/tests/regression_tests/test_ensemble_platform_workdir.py @@ -6,7 +6,6 @@ # Import libEnsemble items for this test from libensemble import Ensemble -from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f from libensemble.executors.mpi_executor import MPIExecutor from libensemble.gen_funcs.persistent_sampling_var_resources import uniform_sample as gen_f from libensemble.resources.platforms import PerlmutterGPU @@ -23,14 +22,6 @@ six_hump_camel_app = six_hump_camel.__file__ n = 2 - alloc_specs = { - "alloc_f": alloc_f, - "user": { - "give_all_with_same_priority": False, - "async_return": False, # False batch returns - }, - } - exit_criteria = {"sim_max": 20} # Ensure LIBE_PLATFORM environment variable is not set. @@ -43,9 +34,7 @@ ensemble = Ensemble( parse_args=True, executor=exctr, - alloc_specs=alloc_specs, exit_criteria=exit_criteria, - # libE_specs = LibeSpecs(use_workflow_dir=True, platform_specs=platform_specs), # works ) platform_specs = PerlmutterGPU() @@ -60,8 +49,10 @@ "gen_f": gen_f, "persis_in": ["f", "x", "sim_id"], "out": [("priority", float), ("resource_sets", int), ("x", float, n)], + "initial_batch_size": ensemble.nworkers - 1, + "batch_evaluate_same_priority": False, + "async_return": False, "user": { - "initial_batch_size": ensemble.nworkers - 1, "max_resource_sets": ensemble.nworkers - 1, # Any sim created can req. 1 worker up to all. "lb": np.array([-3, -2]), "ub": np.array([3, 2]), @@ -75,7 +66,6 @@ "user": {"dry_run": True}, } - ensemble.add_random_streams() ensemble.run() if ensemble.is_manager: diff --git a/libensemble/tests/regression_tests/test_evaluate_mixed_sample.py b/libensemble/tests/regression_tests/test_evaluate_mixed_sample.py index 481db84191..60e43fa57e 100644 --- a/libensemble/tests/regression_tests/test_evaluate_mixed_sample.py +++ b/libensemble/tests/regression_tests/test_evaluate_mixed_sample.py @@ -44,7 +44,6 @@ H0["sim_ended"][:500] = True sampling = Ensemble(parse_args=True) - sampling.libE_specs.gen_on_manager = True sampling.H0 = H0 sampling.sim_specs = SimSpecs(sim_f=sim_f, inputs=["x"], out=[("f", float)]) sampling.alloc_specs = AllocSpecs(alloc_f=alloc_f) diff --git a/libensemble/tests/regression_tests/test_gpCAM.py b/libensemble/tests/regression_tests/test_gpCAM.py index 218ecfc918..cc6b84e3c1 100644 --- a/libensemble/tests/regression_tests/test_gpCAM.py +++ b/libensemble/tests/regression_tests/test_gpCAM.py @@ -28,13 +28,12 @@ import numpy as np -from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f from libensemble.gen_funcs.persistent_gpCAM import persistent_gpCAM, persistent_gpCAM_covar # Import libEnsemble items for this test from libensemble.libE import libE from libensemble.sim_funcs.rosenbrock import rosenbrock_eval as sim_f -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +from libensemble.tools import parse_args, save_libE_output warnings.filterwarnings("ignore", message="Default hyperparameter_bounds") warnings.filterwarnings("ignore", message="Hyperparameters initialized") @@ -67,8 +66,6 @@ }, } - alloc_specs = {"alloc_f": alloc_f} - for inst in range(3): if inst == 0: gen_specs["gen_f"] = persistent_gpCAM_covar @@ -88,10 +85,8 @@ gen_specs["user"]["ask_max_iter"] = 1 # For quicker test exit_criteria = {"sim_max": num_batches * batch_size, "wallclock_max": 300} - persis_info = add_unique_random_streams({}, nworkers + 1) - # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, libE_specs=libE_specs) if is_manager: assert len(np.unique(H["gen_ended_time"])) == num_batches diff --git a/libensemble/tests/regression_tests/test_inverse_bayes_example.py b/libensemble/tests/regression_tests/test_inverse_bayes_example.py index f1e5d1cc3a..72fa6eed08 100644 --- a/libensemble/tests/regression_tests/test_inverse_bayes_example.py +++ b/libensemble/tests/regression_tests/test_inverse_bayes_example.py @@ -25,9 +25,7 @@ from libensemble.gen_funcs.persistent_inverse_bayes import persistent_updater_after_likelihood as gen_f from libensemble.sim_funcs.inverse_bayes import likelihood_calculator as sim_f from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, SimSpecs -from libensemble.tools import add_unique_random_streams -# Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": # Parse args for test code bayes_test = Ensemble( @@ -58,7 +56,7 @@ alloc_specs=AllocSpecs(alloc_f=alloc_f), ) - bayes_test.persis_info = add_unique_random_streams({}, bayes_test.nworkers + 1) + bayes_test.persis_info = {} gen_user = bayes_test.gen_specs.user val = gen_user["subbatch_size"] * gen_user["num_subbatches"] * gen_user["num_batches"] bayes_test.exit_criteria = ExitCriteria(sim_max=val, wallclock_max=300) diff --git a/libensemble/tests/regression_tests/test_optimas_ax_mf.py b/libensemble/tests/regression_tests/test_optimas_ax_mf.py new file mode 100644 index 0000000000..fb5b75c321 --- /dev/null +++ b/libensemble/tests/regression_tests/test_optimas_ax_mf.py @@ -0,0 +1,78 @@ +""" +Tests libEnsemble with Optimas Multi-Fidelity Ax Generator + +*****currently fixing nworkers to batch_size***** + +Execute via one of the following commands (e.g. 4 workers): + mpiexec -np 5 python test_optimas_ax_mf.py + python test_optimas_ax_mf.py -n 4 + +When running with the above commands, the number of concurrent evaluations of +the objective function will be 4 as the generator is on the manager. + +""" + +# Do not change these lines - they are parsed by run-tests.sh +# TESTSUITE_COMMS: mpi local +# TESTSUITE_NPROCS: 4 +# TESTSUITE_EXTRA: true +# TESTSUITE_OS_SKIP: OSX + +import numpy as np +from gest_api.vocs import VOCS +from optimas.generators import AxMultiFidelityGenerator + +from libensemble import Ensemble +from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs + + +def eval_func_mf(input_params): + """Evaluation function for multifidelity test.""" + x0 = input_params["x0"] + x1 = input_params["x1"] + resolution = input_params["res"] + result = -((x0 + 10 * np.cos(x0 + 0.1 * resolution)) * (x1 + 5 * np.cos(x1 - 0.2 * resolution))) + return {"f": result} + + +# Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). +if __name__ == "__main__": + + n = 2 + batch_size = 2 + + libE_specs = LibeSpecs(nworkers=batch_size) + + vocs = VOCS( + variables={"x0": [-50.0, 5.0], "x1": [-5.0, 15.0], "res": [1.0, 8.0]}, + objectives={"f": "MAXIMIZE"}, + ) + + gen = AxMultiFidelityGenerator(vocs=vocs) + + gen_specs = GenSpecs( + generator=gen, + batch_size=batch_size, + vocs=vocs, + ) + + sim_specs = SimSpecs( + simulator=eval_func_mf, + vocs=vocs, + ) + + exit_criteria = ExitCriteria(sim_max=6) + + workflow = Ensemble( + libE_specs=libE_specs, + sim_specs=sim_specs, + gen_specs=gen_specs, + exit_criteria=exit_criteria, + ) + + H, _, _ = workflow.run() + + # Perform the run + if workflow.is_manager: + workflow.save_output(__file__) + print(f"Completed {len(H)} simulations") diff --git a/libensemble/tests/regression_tests/test_optimas_ax_multitask.py b/libensemble/tests/regression_tests/test_optimas_ax_multitask.py new file mode 100644 index 0000000000..08d97ed6b6 --- /dev/null +++ b/libensemble/tests/regression_tests/test_optimas_ax_multitask.py @@ -0,0 +1,107 @@ +""" +Tests libEnsemble with Optimas Multitask Ax Generator + +Runs an initial ensemble, followed by another using the first as an H0. + +*****currently fixing nworkers to batch_size***** + +Execute via one of the following commands (e.g. 4 workers): + mpiexec -np 5 python test_optimas_ax_multitask.py + python test_optimas_ax_multitask.py -n 4 + +When running with the above commands, the number of concurrent evaluations of +the objective function will be 4 as the generator is on the manager. + +Issues: In some cases, the generator fails to produce points. This is +intermittent and can be seen by the message "alloc_f did not return any work". +This needs to be resolved in the generator by generating extra points +as needed (excluding from until then). +""" + +# Do not change these lines - they are parsed by run-tests.sh +# TESTSUITE_COMMS: local +# TESTSUITE_NPROCS: 4 +# TESTSUITE_EXTRA: true +# TESTSUITE_EXCLUDE: true +# TESTSUITE_OS_SKIP: OSX + +import numpy as np +from gest_api.vocs import VOCS +from optimas.core import Task +from optimas.generators import AxMultitaskGenerator + +from libensemble import Ensemble +from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs + + +def eval_func_multitask(input_params): + """Evaluation function for task1 or task2 in multitask test""" + print(f"input_params: {input_params}") + x0 = input_params["x0"] + x1 = input_params["x1"] + trial_type = input_params["trial_type"] + + if trial_type == "task_1": + result = -(x0 + 10 * np.cos(x0)) * (x1 + 5 * np.cos(x1)) + else: + result = -0.5 * (x0 + 10 * np.cos(x0)) * (x1 + 5 * np.cos(x1)) + + output_params = {"f": result} + return output_params + + +# Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). +if __name__ == "__main__": + + n = 2 + batch_size = 2 + + libE_specs = LibeSpecs(nworkers=batch_size) + + vocs = VOCS( + variables={ + "x0": [-50.0, 5.0], + "x1": [-5.0, 15.0], + "trial_type": {"task_1", "task_2"}, + }, + objectives={"f": "MAXIMIZE"}, + ) + + sim_specs = SimSpecs( + simulator=eval_func_multitask, + vocs=vocs, + ) + + exit_criteria = ExitCriteria(sim_max=15) + + H0 = None # or np.load("multitask_first_pass.npy") + for run_num in range(2): + print(f"\nRun number: {run_num}") + task1 = Task("task_1", n_init=2, n_opt=1) + task2 = Task("task_2", n_init=5, n_opt=3) + gen = AxMultitaskGenerator(vocs=vocs, hifi_task=task1, lofi_task=task2) + + gen_specs = GenSpecs( + generator=gen, + batch_size=batch_size, + vocs=vocs, + ) + + workflow = Ensemble( + libE_specs=libE_specs, + sim_specs=sim_specs, + gen_specs=gen_specs, + exit_criteria=exit_criteria, + H0=H0, + ) + + H, _, _ = workflow.run() + + if run_num == 0: + H0 = H + workflow.save_output("multitask_first_pass", append_attrs=False) # Allows restart only run + + if workflow.is_manager: + if run_num == 1: + workflow.save_output("multitask_with_H0") + print(f"Second run completed: {len(H)} simulations") diff --git a/libensemble/tests/regression_tests/test_optimas_ax_sf.py b/libensemble/tests/regression_tests/test_optimas_ax_sf.py new file mode 100644 index 0000000000..b9fbbb34b6 --- /dev/null +++ b/libensemble/tests/regression_tests/test_optimas_ax_sf.py @@ -0,0 +1,80 @@ +""" +Tests libEnsemble with Optimas Single-Fidelity Ax Generator + +*****currently fixing nworkers to batch_size***** + +Execute via one of the following commands (e.g. 4 workers): + mpiexec -np 5 python test_optimas_ax_sf.py + python test_optimas_ax_sf.py -n 4 + +When running with the above commands, the number of concurrent evaluations of +the objective function will be 4 as the generator is on the manager. + +""" + +# Do not change these lines - they are parsed by run-tests.sh +# TESTSUITE_COMMS: mpi local +# TESTSUITE_NPROCS: 4 +# TESTSUITE_EXTRA: true +# TESTSUITE_OS_SKIP: OSX + +import numpy as np +from gest_api.vocs import VOCS +from optimas.generators import AxSingleFidelityGenerator + +from libensemble import Ensemble +from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs + + +def eval_func_sf(input_params): + """Evaluation function for single-fidelity test.""" + x0 = input_params["x0"] + x1 = input_params["x1"] + result = -(x0 + 10 * np.cos(x0)) * (x1 + 5 * np.cos(x1)) + return {"f": result} + + +# Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). +if __name__ == "__main__": + + n = 2 + batch_size = 2 + + libE_specs = LibeSpecs(nworkers=batch_size) + + vocs = VOCS( + variables={ + "x0": [-50.0, 5.0], + "x1": [-5.0, 15.0], + }, + objectives={"f": "MAXIMIZE"}, + ) + + gen = AxSingleFidelityGenerator(vocs=vocs) + + gen_specs = GenSpecs( + generator=gen, + batch_size=batch_size, + vocs=vocs, + ) + + sim_specs = SimSpecs( + simulator=eval_func_sf, + vocs=vocs, + ) + + exit_criteria = ExitCriteria(sim_max=10) + + workflow = Ensemble( + libE_specs=libE_specs, + sim_specs=sim_specs, + gen_specs=gen_specs, + exit_criteria=exit_criteria, + ) + + H, _, _ = workflow.run() + + # Perform the run + if workflow.is_manager: + workflow.save_output(__file__) + print(f"Completed {len(H)} simulations") diff --git a/libensemble/tests/regression_tests/test_optimas_grid_sample.py b/libensemble/tests/regression_tests/test_optimas_grid_sample.py new file mode 100644 index 0000000000..5ec670aa95 --- /dev/null +++ b/libensemble/tests/regression_tests/test_optimas_grid_sample.py @@ -0,0 +1,106 @@ +""" +Tests libEnsemble with Optimas GridSamplingGenerator + +*****currently fixing nworkers to batch_size***** + +From Optimas test test_grid_sampling.py + +Execute via one of the following commands (e.g. 4 workers): + mpiexec -np 5 python test_optimas_grid_sample.py + python test_optimas_grid_sample.py -n 4 + +When running with the above commands, the number of concurrent evaluations of +the objective function will be 4 as the generator is on the manager. + +""" + +# Do not change these lines - they are parsed by run-tests.sh +# TESTSUITE_COMMS: mpi local +# TESTSUITE_NPROCS: 4 +# TESTSUITE_EXTRA: true + +import numpy as np +from gest_api.vocs import VOCS +from optimas.generators import GridSamplingGenerator + +from libensemble import Ensemble +from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs + + +def eval_func(input_params: dict): + """Evaluation function for single-fidelity test""" + x0 = input_params["x0"] + x1 = input_params["x1"] + result = -(x0 + 10 * np.cos(x0)) * (x1 + 5 * np.cos(x1)) + return {"f": result} + + +# Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). +if __name__ == "__main__": + + n = 2 + batch_size = 4 + + libE_specs = LibeSpecs(nworkers=batch_size) + + # Create varying parameters. + lower_bounds = [-3.0, 2.0] + upper_bounds = [1.0, 5.0] + n_steps = [7, 15] + + # Set number of evaluations. + n_evals = np.prod(n_steps) + + vocs = VOCS( + variables={ + "x0": [lower_bounds[0], upper_bounds[0]], + "x1": [lower_bounds[1], upper_bounds[1]], + }, + objectives={"f": "MAXIMIZE"}, + ) + + gen = GridSamplingGenerator(vocs=vocs, n_steps=n_steps) + + gen_specs = GenSpecs( + generator=gen, + batch_size=batch_size, + vocs=vocs, + ) + + sim_specs = SimSpecs( + simulator=eval_func, + vocs=vocs, + ) + + exit_criteria = ExitCriteria(sim_max=n_evals) + + workflow = Ensemble( + libE_specs=libE_specs, + sim_specs=sim_specs, + gen_specs=gen_specs, + exit_criteria=exit_criteria, + ) + + H, _, _ = workflow.run() + + # Perform the run + if workflow.is_manager: + print(f"Completed {len(H)} simulations") + + # Get generated points. + h = H[H["sim_ended"]] + x0_gen = h["x0"] + x1_gen = h["x1"] + + # Get expected 1D steps along each variable. + x0_steps = np.linspace(lower_bounds[0], upper_bounds[0], n_steps[0]) + x1_steps = np.linspace(lower_bounds[1], upper_bounds[1], n_steps[1]) + + # Check that the scan along each variable is as expected. + np.testing.assert_array_equal(np.unique(x0_gen), x0_steps) + np.testing.assert_array_equal(np.unique(x1_gen), x1_steps) + + # Check that for every x0 step, the expected x1 steps are performed. + for x0_step in x0_steps: + x1_in_x0_step = x1_gen[x0_gen == x0_step] + np.testing.assert_array_equal(x1_in_x0_step, x1_steps) diff --git a/libensemble/tests/regression_tests/test_persistent_aposmm_dfols.py b/libensemble/tests/regression_tests/test_persistent_aposmm_dfols.py index 6e19930691..f73f7e6939 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_dfols.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_dfols.py @@ -33,7 +33,7 @@ from libensemble.alloc_funcs.persistent_aposmm_alloc import persistent_aposmm_alloc as alloc_f from libensemble.gen_funcs.persistent_aposmm import aposmm as gen_f -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +from libensemble.tools import parse_args, save_libE_output def combine_component(x): @@ -69,6 +69,7 @@ def combine_component(x): "gen_f": gen_f, "persis_in": ["f", "fvec"] + [n[0] for n in gen_out], "out": gen_out, + "initial_batch_size": 100, "user": { "initial_sample_size": 100, "localopt_method": "dfols", @@ -88,8 +89,6 @@ def combine_component(x): alloc_specs = {"alloc_f": alloc_f} - persis_info = add_unique_random_streams({}, nworkers + 1) - # Tell libEnsemble when to stop (stop_val key must be in H) exit_criteria = { "sim_max": 1000, @@ -99,10 +98,10 @@ def combine_component(x): # end_exit_criteria_rst_tag # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) if is_manager: - assert persis_info[1].get("run_order"), "Run_order should have been given back" + assert persis_info[0].get("run_order"), "Run_order should have been given back" assert flag == 0 assert np.min(H["f"][H["sim_ended"]]) <= 3000, "Didn't find a value below 3000" @@ -127,10 +126,8 @@ def combine_component(x): # assert np.linalg.norm(d) <= 1e-5 if libE_specs["comms"] == "mpi": - # Quickly try a different DFO-LS exit condition - persis_info = add_unique_random_streams({}, nworkers + 1) - gen_specs["user"]["dfols_kwargs"]["rhoend"] = 1e-16 - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) + # Quickly try a different DFO-LS exit condition gen_specs["user"]["dfols_kwargs"]["rhoend"] = 1e-16 + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) if is_manager: assert flag == 0 diff --git a/libensemble/tests/regression_tests/test_persistent_aposmm_exception.py b/libensemble/tests/regression_tests/test_persistent_aposmm_exception.py index b197dc3f07..d018c307e9 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_exception.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_exception.py @@ -29,7 +29,7 @@ libensemble.gen_funcs.rc.aposmm_optimizers = "nlopt" from libensemble.alloc_funcs.persistent_aposmm_alloc import persistent_aposmm_alloc as alloc_f from libensemble.gen_funcs.persistent_aposmm import aposmm as gen_f -from libensemble.tools import add_unique_random_streams, parse_args +from libensemble.tools import parse_args def assertion(passed): @@ -69,6 +69,7 @@ def assertion(passed): "gen_f": gen_f, "persis_in": ["f"] + [n[0] for n in gen_out], "out": gen_out, + "initial_batch_size": 100, "user": { "initial_sample_size": 100, "localopt_method": "LN_BOBYQA", @@ -81,12 +82,10 @@ def assertion(passed): exit_criteria = {"sim_max": 1000} - persis_info = add_unique_random_streams({}, nworkers + 1) - libE_specs["abort_on_exception"] = False try: # Perform the run, which will fail because we want to test exception handling - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) except Exception as e: if is_manager: if e.args[1].endswith("NLopt roundoff-limited"): diff --git a/libensemble/tests/regression_tests/test_persistent_aposmm_external_localopt.py b/libensemble/tests/regression_tests/test_persistent_aposmm_external_localopt.py index dd01d1069e..1d8eefb854 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_external_localopt.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_external_localopt.py @@ -40,7 +40,7 @@ from libensemble.alloc_funcs.persistent_aposmm_alloc import persistent_aposmm_alloc as alloc_f from libensemble.gen_funcs.persistent_aposmm import aposmm as gen_f from libensemble.tests.regression_tests.support import six_hump_camel_minima as minima -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +from libensemble.tools import parse_args, save_libE_output np.set_printoptions(precision=16) @@ -75,6 +75,7 @@ "gen_f": gen_f, "persis_in": ["f"] + [n[0] for n in gen_out], "out": gen_out, + "initial_batch_size": 100, "user": { "initial_sample_size": 100, "sample_points": np.round(minima, 1), @@ -89,12 +90,10 @@ alloc_specs = {"alloc_f": alloc_f} - persis_info = add_unique_random_streams({}, nworkers + 1) - exit_criteria = {"sim_max": 500} # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) if is_manager: print("[Manager]:", H[np.where(H["local_min"])]["x"]) diff --git a/libensemble/tests/regression_tests/test_persistent_aposmm_ibcdfo_manifold_sampling.py b/libensemble/tests/regression_tests/test_persistent_aposmm_ibcdfo_manifold_sampling.py index 9f987eadc9..8d90b523e6 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_ibcdfo_manifold_sampling.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_ibcdfo_manifold_sampling.py @@ -36,11 +36,10 @@ from libensemble.alloc_funcs.persistent_aposmm_alloc import persistent_aposmm_alloc as alloc_f from libensemble.gen_funcs.persistent_aposmm import aposmm as gen_f -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +from libensemble.tools import parse_args, save_libE_output try: - from ibcdfo.manifold_sampling import manifold_sampling_primal # noqa: F401 - from ibcdfo.manifold_sampling.h_examples import pw_maximum as hfun + import ibcdfo # noqa: F401 except ModuleNotFoundError: sys.exit("Please 'pip install ibcdfo'") @@ -110,20 +109,18 @@ def synthetic_beamline_mapping(H, _, sim_specs): }, } - gen_specs["user"]["hfun"] = hfun + gen_specs["user"]["hfun"] = ibcdfo.manifold_sampling.h_pw_maximum alloc_specs = {"alloc_f": alloc_f} - persis_info = add_unique_random_streams({}, nworkers + 1) - exit_criteria = {"sim_max": 500} # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) if is_manager: - assert np.min(H["f"]) == 2.0, "The best is 2" - assert persis_info[1].get("run_order"), "Run_order should have been given back" + assert np.min(H["f"][H["f"] > 0]) == 2.0, "The best is 2" # nonzero + assert persis_info[0].get("run_order"), "Run_order should have been given back" assert flag == 0 save_libE_output(H, persis_info, __file__, nworkers) diff --git a/libensemble/tests/regression_tests/test_persistent_aposmm_ibcdfo_pounders.py b/libensemble/tests/regression_tests/test_persistent_aposmm_ibcdfo_pounders.py index 753337c9b9..619c3633bd 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_ibcdfo_pounders.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_ibcdfo_pounders.py @@ -37,11 +37,10 @@ from libensemble.alloc_funcs.persistent_aposmm_alloc import persistent_aposmm_alloc as alloc_f from libensemble.gen_funcs.persistent_aposmm import aposmm as gen_f -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +from libensemble.tools import parse_args, save_libE_output try: - from ibcdfo.pounders import pounders # noqa: F401 - from ibcdfo.pounders.general_h_funs import emittance_combine, emittance_h + import ibcdfo # noqa: F401 except ModuleNotFoundError: sys.exit("Please 'pip install ibcdfo'") @@ -52,10 +51,6 @@ sys.exit("Ensure https://github.com/POptUS/minq has been cloned and that minq/py/minq5/ is on the PYTHONPATH") -def sum_squared(x): - return np.sum(np.power(x, 2)) - - def synthetic_beamline_mapping(H, _, sim_specs): x = H["x"][0] assert len(x) == 4, "Assuming 4 inputs to this function" @@ -76,7 +71,7 @@ def synthetic_beamline_mapping(H, _, sim_specs): nworkers, is_manager, libE_specs, _ = parse_args() - assert nworkers == 2, "This test is just for two workers" + assert nworkers == 2, "This test is just for two workers, as only one localopt run is being performed" for inst in range(2): if inst == 0: @@ -109,9 +104,9 @@ def synthetic_beamline_mapping(H, _, sim_specs): "persis_in": ["f", "fvec"] + [n[0] for n in gen_out], "out": gen_out, "user": { - "initial_sample_size": 1, - "stop_after_k_runs": 1, - "max_active_runs": 1, + "initial_sample_size": 1, # The initial sampled point will be the starting point + "stop_after_k_runs": 1, # Only one local optimization run will be performed + "max_active_runs": 1, # Only one local optimization run will be performed, "sample_points": np.atleast_2d(0.1 * (np.arange(n) + 1)), "localopt_method": "ibcdfo_pounders", "run_max_eval": 100 * (n + 1), @@ -122,20 +117,18 @@ def synthetic_beamline_mapping(H, _, sim_specs): } if inst == 1: - gen_specs["user"]["hfun"] = emittance_h - gen_specs["user"]["combinemodels"] = emittance_combine + gen_specs["user"]["hfun"] = ibcdfo.pounders.h_emittance + gen_specs["user"]["combinemodels"] = ibcdfo.pounders.combine_emittance alloc_specs = {"alloc_f": alloc_f} - persis_info = add_unique_random_streams({}, nworkers + 1) - exit_criteria = {"sim_max": 500} # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) if is_manager: - assert persis_info[1].get("run_order"), "Run_order should have been given back" + assert persis_info[0].get("run_order"), "Run_order should have been given back" assert flag == 0 save_libE_output(H, persis_info, __file__, nworkers) diff --git a/libensemble/tests/regression_tests/test_persistent_aposmm_ibcdfo_pounders_jax.py b/libensemble/tests/regression_tests/test_persistent_aposmm_ibcdfo_pounders_jax.py index 9d635ae60f..2108182a6e 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_ibcdfo_pounders_jax.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_ibcdfo_pounders_jax.py @@ -36,11 +36,11 @@ from libensemble.alloc_funcs.persistent_aposmm_alloc import persistent_aposmm_alloc as alloc_f from libensemble.gen_funcs.persistent_aposmm import aposmm as gen_f -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +from libensemble.tools import parse_args, save_libE_output try: - from ibcdfo.pounders import pounders # noqa: F401 - from declare_hfun_and_combine_model_with_jax import hfun, combinemodels_jax + import ibcdfo # noqa: F401 + from declare_hfun_and_combine_model_with_jax import combinemodels_jax, hfun except ModuleNotFoundError: sys.exit("Please 'pip install ibcdfo'") @@ -119,16 +119,14 @@ def synthetic_beamline_mapping(H, _, sim_specs): alloc_specs = {"alloc_f": alloc_f} - persis_info = add_unique_random_streams({}, nworkers + 1) - exit_criteria = {"sim_max": 500} # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) if is_manager: print(H[["x", "f", "local_min"]]) - assert persis_info[1].get("run_order"), "Run_order should have been given back" + assert persis_info[0].get("run_order"), "Run_order should have been given back" assert flag == 0 save_libE_output(H, persis_info, __file__, nworkers) diff --git a/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt.py b/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt.py index 3cf69bf5dd..28da42d53a 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt.py @@ -14,6 +14,7 @@ # Do not change these lines - they are parsed by run-tests.sh # TESTSUITE_COMMS: local mpi tcp # TESTSUITE_NPROCS: 3 +# TESTSUITE_EXTRA: true import sys from math import gamma, pi, sqrt @@ -32,7 +33,7 @@ from libensemble.alloc_funcs.persistent_aposmm_alloc import persistent_aposmm_alloc as alloc_f from libensemble.gen_funcs.persistent_aposmm import aposmm as gen_f from libensemble.tests.regression_tests.support import six_hump_camel_minima as minima -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +from libensemble.tools import parse_args, save_libE_output # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": @@ -63,6 +64,7 @@ "gen_f": gen_f, "persis_in": ["f"] + [n[0] for n in gen_out], "out": gen_out, + "initial_batch_size": 100, "user": { "initial_sample_size": 100, "sample_points": np.round(minima, 1), @@ -79,12 +81,16 @@ alloc_specs = {"alloc_f": alloc_f} - persis_info = add_unique_random_streams({}, nworkers + 1) - - exit_criteria = {"sim_max": 2000} + exit_criteria = {"sim_max": 3000} # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) + H, persis_info, flag = libE( + sim_specs, + gen_specs, + exit_criteria, + alloc_specs=alloc_specs, + libE_specs=libE_specs, + ) if is_manager: print("[Manager]:", H[np.where(H["local_min"])]["x"]) diff --git a/libensemble/tests/regression_tests/test_persistent_aposmm_periodic.py b/libensemble/tests/regression_tests/test_persistent_aposmm_periodic.py index d99e8802a0..509e851efa 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_periodic.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_periodic.py @@ -30,7 +30,7 @@ # Import libEnsemble items for this test from libensemble.libE import libE from libensemble.sim_funcs.periodic_func import func_wrapper as sim_f -from libensemble.tools import add_unique_random_streams, parse_args +from libensemble.tools import parse_args # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": @@ -84,12 +84,12 @@ gen_specs["user"].pop("ftol_abs") gen_specs["user"]["scipy_kwargs"] = {"tol": 1e-8} - persis_info = add_unique_random_streams({}, nworkers + 1) + persis_info = {} # Perform the run H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) if is_manager: - assert persis_info[1].get("run_order"), "Run_order should have been given back" + assert persis_info[0].get("run_order"), "Run_order should have been given back" min_ids = np.where(H["local_min"]) # The minima are known on this test problem. If the above [lb, ub] domain is diff --git a/libensemble/tests/regression_tests/test_persistent_aposmm_pounders.py b/libensemble/tests/regression_tests/test_persistent_aposmm_pounders.py index 5b038a0ce8..9a74249952 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_pounders.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_pounders.py @@ -34,7 +34,7 @@ from libensemble.alloc_funcs.persistent_aposmm_alloc import persistent_aposmm_alloc as alloc_f from libensemble.gen_funcs.persistent_aposmm import aposmm as gen_f from libensemble.gen_funcs.sampling import lhs_sample -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +from libensemble.tools import parse_args, save_libE_output def combine_component(x): @@ -92,11 +92,10 @@ def combine_component(x): "lb": lb, "ub": ub, }, + "num_active_gens": 1, } - alloc_specs = {"alloc_f": alloc_f, "user": {"batch_mode": True, "num_active_gens": 1}} - - persis_info = add_unique_random_streams({}, nworkers + 1) + alloc_specs = {"alloc_f": alloc_f} exit_criteria = {"sim_max": 500} @@ -108,7 +107,7 @@ def combine_component(x): gen_specs["user"]["sample_points"] = sample_points * (ub - lb) + lb # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) if is_manager: assert flag == 0 diff --git a/libensemble/tests/regression_tests/test_persistent_aposmm_scipy.py b/libensemble/tests/regression_tests/test_persistent_aposmm_scipy.py index ee4ec225b3..6556a6c739 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_scipy.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_scipy.py @@ -14,7 +14,6 @@ # Do not change these lines - they are parsed by run-tests.sh # TESTSUITE_COMMS: mpi local # TESTSUITE_NPROCS: 4 -# TESTSUITE_EXTRA: true import multiprocessing import sys @@ -33,7 +32,7 @@ from libensemble.alloc_funcs.persistent_aposmm_alloc import persistent_aposmm_alloc as alloc_f from libensemble.gen_funcs.persistent_aposmm import aposmm as gen_f from libensemble.tests.regression_tests.support import six_hump_camel_minima as minima -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +from libensemble.tools import parse_args, save_libE_output # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": @@ -66,6 +65,7 @@ "gen_f": gen_f, "persis_in": ["f"] + [n[0] for n in gen_out], "out": gen_out, + "initial_batch_size": 100, "user": { "initial_sample_size": 100, "sample_points": np.round(minima, 1), @@ -85,8 +85,6 @@ exit_criteria = {"sim_max": 1000} for run in range(2): - persis_info = add_unique_random_streams({}, nworkers + 1) - if run == 1: gen_specs["user"]["localopt_method"] = "scipy_BFGS" gen_specs["user"]["opt_return_codes"] = [0] @@ -94,7 +92,7 @@ sim_specs["out"] = [("f", float), ("grad", float, n)] # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) if is_manager: print("[Manager]:", H[np.where(H["local_min"])]["x"]) @@ -115,7 +113,6 @@ # Now let's run on the same problem with a really large n (but we won't test # convergence to all local min). Note that sim_f uses only entries x[0:2] n = 400 - persis_info = add_unique_random_streams({}, nworkers + 1) gen_specs["out"][0:2] = [("x", float, n), ("x_on_cube", float, n)] gen_specs["user"]["lb"] = np.zeros(n) gen_specs["user"]["ub"] = np.ones(n) @@ -127,7 +124,7 @@ sim_specs["out"] = [("f", float)] gen_specs["persis_in"].remove("grad") - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) if is_manager: - assert np.sum(H["sim_ended"]) >= exit_criteria["sim_max"], "Run didn't finish" + assert np.sum(H["sim_ended"]) >= exit_criteria["sim_max"] - 10, "Not enough runs finished" diff --git a/libensemble/tests/regression_tests/test_persistent_aposmm_tao_blmvm.py b/libensemble/tests/regression_tests/test_persistent_aposmm_tao_blmvm.py index 39ff3b79fd..8e057f80ad 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_tao_blmvm.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_tao_blmvm.py @@ -34,7 +34,7 @@ from libensemble.alloc_funcs.persistent_aposmm_alloc import persistent_aposmm_alloc as alloc_f from libensemble.gen_funcs.persistent_aposmm import aposmm as gen_f from libensemble.tests.regression_tests.support import six_hump_camel_minima as minima -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +from libensemble.tools import parse_args, save_libE_output # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": @@ -67,6 +67,7 @@ "gen_f": gen_f, "persis_in": ["f", "grad"] + [n[0] for n in gen_out], "out": gen_out, + "initial_batch_size": 100, "user": { "initial_sample_size": 100, "sample_points": np.round(minima, 1), @@ -83,12 +84,10 @@ alloc_specs = {"alloc_f": alloc_f} - persis_info = add_unique_random_streams({}, nworkers + 1) - exit_criteria = {"sim_max": 1000} # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) if is_manager: print("[Manager]:", H[np.where(H["local_min"])]["x"]) diff --git a/libensemble/tests/regression_tests/test_persistent_aposmm_tao_nm.py b/libensemble/tests/regression_tests/test_persistent_aposmm_tao_nm.py index d6db6b63a1..3dfe488fa5 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_tao_nm.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_tao_nm.py @@ -30,7 +30,7 @@ libensemble.gen_funcs.rc.aposmm_optimizers = "petsc" from libensemble.alloc_funcs.persistent_aposmm_alloc import persistent_aposmm_alloc as alloc_f from libensemble.gen_funcs.persistent_aposmm import aposmm as gen_f -from libensemble.tools import add_unique_random_streams, parse_args +from libensemble.tools import parse_args # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": @@ -60,6 +60,7 @@ "gen_f": gen_f, "persis_in": ["f", "grad"] + [n[0] for n in gen_out], "out": gen_out, + "initial_batch_size": 100, "user": { "initial_sample_size": 100, "localopt_method": "nm", @@ -70,12 +71,10 @@ alloc_specs = {"alloc_f": alloc_f} - persis_info = add_unique_random_streams({}, nworkers + 1) - exit_criteria = {"sim_max": 1000} # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) if is_manager: print("[Manager]:", H[np.where(H["local_min"])]["x"]) @@ -83,8 +82,7 @@ assert np.sum(H["local_pt"]) > 100, "Why didn't at least 100 local points occur?" if libE_specs["comms"] == "mpi": - persis_info = add_unique_random_streams({}, nworkers + 1) gen_specs["user"]["run_max_eval"] = 10 * (n + 1) - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) if is_manager: assert flag == 0 diff --git a/libensemble/tests/regression_tests/test_persistent_aposmm_timeout.py b/libensemble/tests/regression_tests/test_persistent_aposmm_timeout.py index e61843fd71..e1c115bb95 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_timeout.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_timeout.py @@ -32,7 +32,7 @@ # Import libEnsemble items for this test from libensemble.libE import libE from libensemble.sim_funcs.periodic_func import func_wrapper as sim_f -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +from libensemble.tools import parse_args, save_libE_output # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": @@ -62,6 +62,7 @@ "gen_f": gen_f, "persis_in": ["f"] + [n[0] for n in gen_out], "out": gen_out, + "initial_batch_size": 100, "user": { "initial_sample_size": 100, "localopt_method": "LN_BOBYQA", @@ -80,13 +81,11 @@ # Setting a very high sim_max value and a short wallclock_max so timeout will occur exit_criteria = {"sim_max": 50000, "wallclock_max": 5} - persis_info = add_unique_random_streams({}, nworkers + 1) - # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) if is_manager: assert flag == 2, "Test should have timed out" - assert persis_info[1].get("run_order"), "Run_order should have been given back" + assert persis_info[0].get("run_order"), "Run_order should have been given back" min_ids = np.where(H["local_min"]) save_libE_output(H, persis_info, __file__, nworkers) diff --git a/libensemble/tests/regression_tests/test_persistent_aposmm_with_grad.py b/libensemble/tests/regression_tests/test_persistent_aposmm_with_grad.py index f2d2f09cc0..79cc04afc7 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_with_grad.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_with_grad.py @@ -36,7 +36,7 @@ from libensemble.alloc_funcs.persistent_aposmm_alloc import persistent_aposmm_alloc as alloc_f from libensemble.gen_funcs.persistent_aposmm import aposmm as gen_f from libensemble.tests.regression_tests.support import six_hump_camel_minima as minima -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +from libensemble.tools import parse_args, save_libE_output # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": @@ -72,6 +72,7 @@ "in": gen_in, "persis_in": gen_in, "out": gen_out, + "initial_batch_size": 0, "user": { "initial_sample_size": 0, # Don't need to do evaluations because the sampling already done below "localopt_method": "LD_MMA", @@ -87,7 +88,7 @@ alloc_specs = {"alloc_f": alloc_f} - persis_info = add_unique_random_streams({}, nworkers + 1) + persis_info = {} exit_criteria = {"sim_max": 1000} @@ -121,9 +122,9 @@ H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs, H0=H0) if is_manager: - assert persis_info[1].get("run_order"), "Run_order should have been given back" + assert persis_info[0].get("run_order"), "Run_order should have been given back" assert ( - len(persis_info[1]["run_order"]) >= gen_specs["user"]["stop_after_k_minima"] + len(persis_info[0]["run_order"]) >= gen_specs["user"]["stop_after_k_minima"] ), "This test should have many runs started." assert len(H) < exit_criteria["sim_max"], "Test should have stopped early due to 'stop_after_k_minima'" diff --git a/libensemble/tests/regression_tests/test_persistent_fd_param_finder.py b/libensemble/tests/regression_tests/test_persistent_fd_param_finder.py index ac01d5683b..c0f2cff170 100644 --- a/libensemble/tests/regression_tests/test_persistent_fd_param_finder.py +++ b/libensemble/tests/regression_tests/test_persistent_fd_param_finder.py @@ -29,9 +29,7 @@ from libensemble.sim_funcs.noisy_vector_mapping import func_wrapper as sim_f from libensemble.sim_funcs.noisy_vector_mapping import noisy_function from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, SimSpecs -from libensemble.tools import add_unique_random_streams -# Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": x0 = np.array([1.23, 4.56]) # point about which we are calculating finite difference parameters f0 = noisy_function(x0) @@ -62,7 +60,7 @@ alloc_specs=AllocSpecs(alloc_f=alloc_f), exit_criteria=ExitCriteria(gen_max=1000), ) - fd_test.persis_info = add_unique_random_streams({}, fd_test.nworkers + 1) + fd_test.persis_info = {} shutil.copy("./scripts_used_by_reg_tests/ECnoise.m", "./") @@ -70,6 +68,6 @@ if fd_test.is_manager: assert len(H) < fd_test.exit_criteria.gen_max, "Problem didn't stop early, which should have been the case." - assert np.all(persis_info[1]["Fnoise"] > 0), "gen_f didn't find noise for all F_i components." + assert np.all(persis_info[0]["Fnoise"] > 0), "gen_f didn't find noise for all F_i components." fd_test.save_output(__file__) diff --git a/libensemble/tests/regression_tests/test_persistent_gp_multitask_ax.py b/libensemble/tests/regression_tests/test_persistent_gp_multitask_ax.py index 990493a176..79236b2db8 100644 --- a/libensemble/tests/regression_tests/test_persistent_gp_multitask_ax.py +++ b/libensemble/tests/regression_tests/test_persistent_gp_multitask_ax.py @@ -2,8 +2,6 @@ Example of multi-fidelity optimization using a persistent GP gen_func (calling Ax). -This test uses the gen_on_manager option (persistent generator runs on -a thread). Therefore nworkers is the number of simulation workers. Execute via one of the following commands: mpiexec -np 4 python test_persistent_gp_multitask_ax.py @@ -26,10 +24,9 @@ import numpy as np from libensemble import logger -from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens from libensemble.libE import libE from libensemble.message_numbers import WORKER_DONE -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +from libensemble.tools import parse_args, save_libE_output # Ax uses a deprecated warn command. warnings.filterwarnings("ignore", category=UserWarning) @@ -64,7 +61,6 @@ def run_simulation(H, persis_info, sim_specs, libE_info): # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": nworkers, is_manager, libE_specs, _ = parse_args() - libE_specs["gen_on_manager"] = True mt_params = { "name_hifi": "expensive_model", @@ -90,13 +86,13 @@ def run_simulation(H, persis_info, sim_specs, libE_info): "out": [ # parameters to input into the simulation. ("x", float, (2,)), - ("task", str, max([len(mt_params["name_hifi"]), len(mt_params["name_lofi"])])), + ("task", str, max(len(str(mt_params["name_hifi"])), len(str(mt_params["name_lofi"])))), ("resource_sets", int), ], + "async_return": False, + "batch_size": nworkers - 1, "user": { "range": [1, 8], - # Total max number of sims running concurrently. - "gen_batch_size": nworkers - 1, # Lower bound for the n parameters. "lb": np.array([0, 0]), # Upper bound for the n parameters. @@ -105,22 +101,14 @@ def run_simulation(H, persis_info, sim_specs, libE_info): } gen_specs["user"] = {**gen_specs["user"], **mt_params} - alloc_specs = { - "alloc_f": only_persistent_gens, - "user": {"async_return": False}, - } - # libE logger logger.set_level("INFO") # Exit criteria exit_criteria = {"sim_max": 20} # Exit after running sim_max simulations - # Create a different random number stream for each worker and the manager - persis_info = add_unique_random_streams({}, nworkers + 1) - # Run LibEnsemble, and store results in history array H - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, libE_specs=libE_specs) # Save results to numpy file if is_manager: diff --git a/libensemble/tests/regression_tests/test_persistent_surmise_calib.py b/libensemble/tests/regression_tests/test_persistent_surmise_calib.py index 39cf11b5de..3820bfdec8 100644 --- a/libensemble/tests/regression_tests/test_persistent_surmise_calib.py +++ b/libensemble/tests/regression_tests/test_persistent_surmise_calib.py @@ -23,26 +23,33 @@ # Do not change these lines - they are parsed by run-tests.sh # TESTSUITE_COMMS: mpi local tcp -# TESTSUITE_NPROCS: 3 4 +# TESTSUITE_NPROCS: 4 # TESTSUITE_EXTRA: true # TESTSUITE_OS_SKIP: OSX # Requires: # Install Surmise package +import sys + import numpy as np from libensemble import Ensemble -from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f from libensemble.gen_funcs.persistent_surmise_calib import surmise_calib as gen_f # Import libEnsemble items for this test from libensemble.sim_funcs.surmise_test_function import borehole as sim_f from libensemble.sim_funcs.surmise_test_function import tstd2theta -from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, SimSpecs -from libensemble.tools import add_unique_random_streams +from libensemble.specs import ExitCriteria, GenSpecs, SimSpecs +from libensemble.tools import parse_args + + +def run_surmise_calib(): + nworkers, is_manager, libE_specs, _ = parse_args() + + if nworkers < 2: + sys.exit("Cannot run with a persistent worker if only one worker -- aborting...") -if __name__ == "__main__": n_init_thetas = 15 # Initial batch of thetas n_x = 25 # No. of x values nparams = 4 # No. of theta params @@ -84,24 +91,16 @@ "step_add_theta": step_add_theta, # No. of thetas to generate per step "n_explore_theta": n_explore_theta, # No. of thetas to explore each step "obsvar": obsvar, # Variance for generating noise in obs - "init_sample_size": init_sample_size, # Initial batch size inc. observations "priorloc": 1, # Prior location in the unit cube "priorscale": 0.5, # Standard deviation of prior }, - ), - alloc_specs=AllocSpecs( - alloc_f=alloc_f, - user={ - "init_sample_size": init_sample_size, - "async_return": True, # True = Return results to gen as they come in (after sample) - "active_recv_gen": True, # Persistent gen can handle irregular communications - }, + initial_batch_size=init_sample_size, + async_return=True, + active_recv_gen=True, ), exit_criteria=ExitCriteria(sim_max=max_evals), ) - test.persis_info = add_unique_random_streams({}, test.nworkers + 1) - # Perform the run H, _, _ = test.run() @@ -113,3 +112,7 @@ # The following line is only to cover parts of tstd2theta tstd2theta(H[0]["thetas"].squeeze(), hard=False) + + +if __name__ == "__main__": + run_surmise_calib() diff --git a/libensemble/tests/regression_tests/test_persistent_surmise_killsims.py b/libensemble/tests/regression_tests/test_persistent_surmise_killsims.py index 11095f61f2..aeb10f4f52 100644 --- a/libensemble/tests/regression_tests/test_persistent_surmise_killsims.py +++ b/libensemble/tests/regression_tests/test_persistent_surmise_killsims.py @@ -34,7 +34,6 @@ import numpy as np -from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f from libensemble.executors.executor import Executor from libensemble.gen_funcs.persistent_surmise_calib import surmise_calib as gen_f @@ -42,7 +41,7 @@ from libensemble.libE import libE from libensemble.sim_funcs.borehole_kills import borehole as sim_f from libensemble.tests.regression_tests.common import build_borehole # current location -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +from libensemble.tools import parse_args, save_libE_output # from libensemble import logger # logger.set_level("DEBUG") # To get debug logging in ensemble.log @@ -105,34 +104,25 @@ "gen_f": gen_f, "persis_in": [o[0] for o in gen_out] + ["f", "sim_ended", "sim_id"], "out": gen_out, + "initial_batch_size": init_sample_size, + "async_return": True, + "active_recv_gen": True, "user": { "n_init_thetas": n_init_thetas, # Num thetas in initial batch "num_x_vals": n_x, # Num x points to create "step_add_theta": step_add_theta, # No. of thetas to generate per step "n_explore_theta": n_explore_theta, # No. of thetas to explore each step "obsvar": obsvar, # Variance for generating noise in obs - "init_sample_size": init_sample_size, # Initial batch size inc. observations "priorloc": 1, # Prior location in the unit cube. "priorscale": 0.2, # Standard deviation of prior }, } - alloc_specs = { - "alloc_f": alloc_f, - "user": { - "init_sample_size": init_sample_size, - "async_return": True, # True = Return results to gen as they come in (after sample) - "active_recv_gen": True, # Persistent gen can handle irregular communications - }, - } - - persis_info = add_unique_random_streams({}, nworkers + 1) + persis_info = {} exit_criteria = {"sim_max": max_evals} # Perform the run - H, persis_info, flag = libE( - sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs=alloc_specs, libE_specs=libE_specs - ) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) if is_manager: print("Cancelled sims", H["sim_id"][H["cancel_requested"]]) diff --git a/libensemble/tests/regression_tests/test_persistent_tasmanian.py b/libensemble/tests/regression_tests/test_persistent_tasmanian.py deleted file mode 100644 index 269c4ba595..0000000000 --- a/libensemble/tests/regression_tests/test_persistent_tasmanian.py +++ /dev/null @@ -1,151 +0,0 @@ -""" -Tests the batch-mode of the Tasmanian generator function. - -Execute via one of the following commands (e.g. 3 workers): - mpiexec -np 4 python test_persistent_tasmanian.py - python test_persistent_tasmanian.py --nworkers 3 - python test_persistent_tasmanian.py --nworkers 3 --comms tcp - -When running with the above commands, the number of concurrent evaluations of -the objective function will be 2, as one of the three workers will be the -persistent generator. -""" - -# Do not change these lines - they are parsed by run-tests.sh -# TESTSUITE_COMMS: local -# TESTSUITE_NPROCS: 4 -# TESTSUITE_OS_SKIP: OSX -# TESTSUITE_EXTRA: true - -import sys -from time import time - -import numpy as np - -from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f -from libensemble.gen_funcs.persistent_tasmanian import sparse_grid_batched as gen_f_batched - -# Import libEnsemble items for this test -from libensemble.libE import libE -from libensemble.sim_funcs.six_hump_camel import six_hump_camel as sim_f -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output - - -def tasmanian_init_global(): - # Note: if Tasmanian has been compiled with OpenMP support (i.e., the usual way) - # libEnsemble calls cannot be made after the `import Tasmanian` clause - # there is a conflict between the OpenMP environment and Python threading - # thus Tasmanian has to be imported inside the `tasmanian_init` method - import Tasmanian - - grid = Tasmanian.makeGlobalGrid(num_dimensions, 1, 6, "iptotal", "clenshaw-curtis") - grid.setDomainTransform(np.array([[-5.0, 5.0], [-2.0, 2.0]])) - return grid - - -def tasmanian_init_localp(): - import Tasmanian - - grid = Tasmanian.makeLocalPolynomialGrid(num_dimensions, 1, 3) - grid.setDomainTransform(np.array([[-5.0, 5.0], [-2.0, 2.0]])) - return grid - - -# Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). -if __name__ == "__main__": - nworkers, is_manager, libE_specs, _ = parse_args() - - if is_manager: - start_time = time() - - if nworkers < 2: - sys.exit("Cannot run with a persistent worker if only one worker -- aborting...") - - num_dimensions = 2 - sim_specs = { - "sim_f": sim_f, - "in": ["x"], - "out": [("f", float)], - } - - gen_specs = { - "gen_f": gen_f_batched, - "persis_in": ["x", "f", "sim_id"], - "out": [("x", float, num_dimensions)], - } - - alloc_specs = {"alloc_f": alloc_f} - - grid_files = [] - - for run in range(3): - # testing two cases, static construction without refinement - # and refinement until 100 points have been computed - persis_info = add_unique_random_streams({}, nworkers + 1) - - # set the stopping criteria - if run != 1: - # note that using 'setAnisotropicRefinement' without 'gen_max' will create an infinite loop - # other stopping criteria could be used with 'setSurplusRefinement' or no refinement - exit_criteria = {"wallclock_max": 10} - elif run == 1: - exit_criteria = {"gen_max": 100} # This will test persistent_tasmanian stopping early. - - # tasmanian_init has to be a method that returns an initialized TasmanianSparseGrid object - # tasmanian_checkpoint_file will be overwritten between each step of the iterative refinement - # the final grid will also be stored in the file - gen_specs["user"] = { - "tasmanian_init": tasmanian_init_global if run < 2 else tasmanian_init_localp, - "tasmanian_checkpoint_file": f"tasmanian{run}.grid", - } - - # setup the refinement criteria - if run == 0: - gen_specs["user"]["refinement"] = "none" - - if run == 1: - # See Tasmanian manual: https://ornl.github.io/TASMANIAN/stable/classTasGrid_1_1TasmanianSparseGrid.html - gen_specs["user"]["refinement"] = "setAnisotropicRefinement" - gen_specs["user"]["sType"] = "iptotal" - gen_specs["user"]["iMinGrowth"] = 10 - gen_specs["user"]["iOutput"] = 0 - - if run == 2: - gen_specs["user"]["refinement"] = "setSurplusRefinement" - gen_specs["user"]["fTolerance"] = 1.0e-2 - gen_specs["user"]["sCriteria"] = "classic" - gen_specs["user"]["iOutput"] = 0 - - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) - - if is_manager: - grid_files.append(gen_specs["user"]["tasmanian_checkpoint_file"]) - - if run == 0: - print("[Manager]: Time taken =", time() - start_time, flush=True) - - save_libE_output(H, persis_info, __file__, nworkers) - - if is_manager: - # run sanity check on the computed results - # using the checkpoint file to read the grids from the filesystem - # Note: cannot make any more libEnsemble calls after importing Tasmanian, - # see the earlier note in tasmanian_init_global() - import Tasmanian - - assert len(grid_files) == 3, "Failed to generate three Tasmanian grid files" - - for run in range(len(grid_files)): - grid = Tasmanian.SparseGrid() - grid.read(grid_files[run]) - if run == 0: - assert grid.getNumNeeded() == 0, "Failed to leave no points needing data" - assert grid.getNumLoaded() == 49, "Failed to load all points" - - if run == 1: - assert grid.getNumNeeded() == 0, "Failed to stop after completing the refinement iteration" - assert grid.getNumLoaded() == 89, "Failed to load all points" - - if run == 2: - assert grid.getNumNeeded() == 0, "Failed to stop after completing the refinement iteration" - assert grid.getNumLoaded() == 93, "Failed to load all points" diff --git a/libensemble/tests/regression_tests/test_persistent_tasmanian_async.py b/libensemble/tests/regression_tests/test_persistent_tasmanian_async.py deleted file mode 100644 index f21544d185..0000000000 --- a/libensemble/tests/regression_tests/test_persistent_tasmanian_async.py +++ /dev/null @@ -1,108 +0,0 @@ -""" -Tests the async-mode of the Tasmanian generator function. - -Execute via one of the following commands (e.g. 3 workers): - mpiexec -np 4 python test_persistent_tasmanian_async.py - python test_persistent_tasmanian_async.py --nworkers 3 - python test_persistent_tasmanian_async.py --nworkers 3 --comms tcp - -When running with the above commands, the number of concurrent evaluations of -the objective function will be 2, as one of the three workers will be the -persistent generator. -""" - -# Do not change these lines - they are parsed by run-tests.sh -# TESTSUITE_COMMS: local -# TESTSUITE_NPROCS: 4 -# TESTSUITE_OS_SKIP: OSX -# TESTSUITE_EXTRA: true - -import itertools -import sys -from time import time - -import numpy as np - -from libensemble.gen_funcs.persistent_tasmanian import get_sparse_grid_specs - -# Import libEnsemble items for this test -from libensemble.libE import libE -from libensemble.sim_funcs.six_hump_camel import six_hump_camel_func -from libensemble.tools import parse_args - - -# Define some grid initializers. -def tasmanian_init_global(): - # Note: if Tasmanian has been compiled with OpenMP support (i.e., the usual way) - # libEnsemble calls cannot be made after the `import Tasmanian` clause - # there is a conflict between the OpenMP environment and Python threading - # thus Tasmanian has to be imported inside the `tasmanian_init` method - import Tasmanian - - grid = Tasmanian.makeGlobalGrid(2, 1, 6, "iptotal", "clenshaw-curtis") - grid.setDomainTransform(np.array([[-5.0, 5.0], [-2.0, 2.0]])) - return grid - - -def tasmanian_init_localp(): - import Tasmanian - - grid = Tasmanian.makeLocalPolynomialGrid(2, 1, 3) - grid.setDomainTransform(np.array([[-5.0, 5.0], [-2.0, 2.0]])) - return grid - - -# Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). -if __name__ == "__main__": - # Get node info. - nworkers, is_manager, libE_specs, _ = parse_args() - if nworkers < 2: - sys.exit("Cannot run with a persistent worker if only one worker -- aborting...") - - # Create an async simulator function (must return 'x' and 'f'). - def sim_f(H, persis_info, sim_specs, _): - batch = len(H["x"]) - H0 = np.zeros(batch, dtype=sim_specs["out"]) - H0["x"] = H["x"] - for i, x in enumerate(H["x"]): - H0["f"][i] = six_hump_camel_func(x) - return H0, persis_info - - # Set up test parameters. - user_specs_arr = [] - user_specs_arr.append( - { - "refinement": "getCandidateConstructionPoints", - "tasmanian_init": lambda: tasmanian_init_global(), - "sType": "iptotal", - "liAnisotropicWeightsOrOutput": -1, - } - ) - user_specs_arr.append( - { - "refinement": "getCandidateConstructionPointsSurplus", - "tasmanian_init": lambda: tasmanian_init_localp(), - "fTolerance": 1.0e-2, - "sRefinementType": "classic", - } - ) - exit_criteria_arr = [] - exit_criteria_arr.append({"wallclock_max": 3}) - exit_criteria_arr.append({"gen_max": 100}) - - run_num = 0 - # Test over all possible parameter combinations. - for user_specs, exit_criteria in itertools.product(user_specs_arr, exit_criteria_arr): - sim_specs, gen_specs, alloc_specs, persis_info = get_sparse_grid_specs(user_specs, sim_f, 2, mode="async") - - if run_num == 0: - gen_specs["user"]["tasmanian_checkpoint_file"] = "tasmanian.grid" - run_num += 1 - - if is_manager: - print(f"[Manager]: user_specs = {user_specs}") - print(f"[Manager]: exit_criteria = {exit_criteria}") - start_time = time() - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) - if is_manager: - print("[Manager]: Time taken = ", time() - start_time, "\n", flush=True) diff --git a/libensemble/tests/regression_tests/test_proxystore_integration.py b/libensemble/tests/regression_tests/test_proxystore_integration.py index 885e6fe6bf..22e4472868 100644 --- a/libensemble/tests/regression_tests/test_proxystore_integration.py +++ b/libensemble/tests/regression_tests/test_proxystore_integration.py @@ -24,7 +24,7 @@ from libensemble import Ensemble from libensemble.alloc_funcs.give_pregenerated_work import give_pregenerated_sim_work as alloc_f from libensemble.sim_funcs.borehole import gen_borehole_input -from libensemble.specs import AllocSpecs, ExitCriteria, SimSpecs, input_fields, output_data +from libensemble.specs import AllocSpecs, ExitCriteria, SimSpecs def insert_proxy(H0): @@ -50,8 +50,6 @@ def check_H(H): assert all([isinstance(H[i]["proxy"], Proxy) for i in range(len(H))]) -@input_fields(["x", "proxy"]) -@output_data([("f", float)]) def one_d_example(x, persis_info, sim_specs, info): H_o = np.zeros(1, dtype=sim_specs["out"]) @@ -79,7 +77,7 @@ def one_d_example(x, persis_info, sim_specs, info): sampling = Ensemble(parse_args=True) sampling.H0 = H0 - sampling.sim_specs = SimSpecs(sim_f=one_d_example) + sampling.sim_specs = SimSpecs(sim_f=one_d_example, inputs=["x", "proxy"], outputs=[("f", float)]) sampling.alloc_specs = AllocSpecs(alloc_f=alloc_f) sampling.exit_criteria = ExitCriteria(sim_max=len(H0)) sampling.run() diff --git a/libensemble/tests/regression_tests/test_with_app_persistent_aposmm_tao_nm.py b/libensemble/tests/regression_tests/test_with_app_persistent_aposmm_tao_nm.py index 9592e479d1..77f200aaf4 100644 --- a/libensemble/tests/regression_tests/test_with_app_persistent_aposmm_tao_nm.py +++ b/libensemble/tests/regression_tests/test_with_app_persistent_aposmm_tao_nm.py @@ -33,7 +33,7 @@ from libensemble.libE import libE from libensemble.sim_funcs import six_hump_camel from libensemble.sim_funcs.var_resources import multi_points_with_variable_resources as sim_f -from libensemble.tools import add_unique_random_streams, parse_args +from libensemble.tools import parse_args # For Open-MPI the following lines cannot be used, thus allowing PETSc to import. # import libensemble.gen_funcs @@ -83,9 +83,7 @@ alloc_specs = {"alloc_f": alloc_f} - persis_info = add_unique_random_streams({}, nworkers + 1) - exit_criteria = {"sim_max": 20} # must be bigger than sample size to enter into optimization code. # Perform the run - H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) + H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, libE_specs=libE_specs) diff --git a/libensemble/tests/regression_tests/test_xopt_EI.py b/libensemble/tests/regression_tests/test_xopt_EI.py new file mode 100644 index 0000000000..7fb158b58b --- /dev/null +++ b/libensemble/tests/regression_tests/test_xopt_EI.py @@ -0,0 +1,102 @@ +""" +Tests libEnsemble with Xopt ExpectedImprovementGenerator + +*****currently fixing nworkers to batch_size***** + +Execute via one of the following commands (e.g. 4 workers): + mpiexec -np 5 python test_xopt_EI.py + python test_xopt_EI.py -n 4 + +When running with the above commands, the number of concurrent evaluations of +the objective function will be 4 as the generator is on the manager. + +""" + +# Do not change these lines - they are parsed by run-tests.sh +# TESTSUITE_COMMS: mpi local +# TESTSUITE_NPROCS: 4 +# TESTSUITE_EXTRA: true +# TESTSUITE_EXCLUDE: true + +import numpy as np +from gest_api.vocs import VOCS +from xopt.generators.bayesian.expected_improvement import ExpectedImprovementGenerator + +from libensemble import Ensemble +from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs + + +# Adapted from Xopt/xopt/resources/testing.py +def xtest_sim(H, persis_info, sim_specs, _): + """ + Simple sim function that takes x1, x2, constant1 from H and returns y1, c1. + Logic: y1 = x2, c1 = x1 + """ + batch = len(H) + H_o = np.zeros(batch, dtype=sim_specs["out"]) + + for i in range(batch): + x1 = H["x1"][i] + x2 = H["x2"][i] + # constant1 is available but not used in the calculation + + H_o["y1"][i] = x2 + H_o["c1"][i] = x1 + + return H_o, persis_info + + +# Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). +if __name__ == "__main__": + + n = 2 + batch_size = 4 + + libE_specs = LibeSpecs(nworkers=batch_size) + libE_specs.reuse_output_dir = True + + vocs = VOCS( + variables={"x1": [0, 1.0], "x2": [0, 10.0]}, + objectives={"y1": "MINIMIZE"}, + constraints={"c1": ["GREATER_THAN", 0.5]}, + constants={"constant1": 1.0}, + ) + + gen = ExpectedImprovementGenerator(vocs=vocs) + + # Create 4 initial points and ingest them + initial_points = [ + {"x1": 0.2, "x2": 2.0, "constant1": 1.0, "y1": 2.0, "c1": 0.2}, + {"x1": 0.5, "x2": 5.0, "constant1": 1.0, "y1": 5.0, "c1": 0.5}, + {"x1": 0.7, "x2": 7.0, "constant1": 1.0, "y1": 7.0, "c1": 0.7}, + {"x1": 0.9, "x2": 9.0, "constant1": 1.0, "y1": 9.0, "c1": 0.9}, + ] + gen.ingest(initial_points) + + gen_specs = GenSpecs( + generator=gen, + batch_size=batch_size, + vocs=vocs, + ) + + sim_specs = SimSpecs( + sim_f=xtest_sim, + vocs=vocs, + ) + + exit_criteria = ExitCriteria(sim_max=20) + + workflow = Ensemble( + libE_specs=libE_specs, + sim_specs=sim_specs, + gen_specs=gen_specs, + exit_criteria=exit_criteria, + ) + + H, _, _ = workflow.run() + + # Perform the run + if workflow.is_manager: + print(f"Completed {len(H)} simulations") + assert np.array_equal(H["y1"], H["x2"]) + assert np.array_equal(H["c1"], H["x1"]) diff --git a/libensemble/tests/regression_tests/test_xopt_EI_initial_sample.py b/libensemble/tests/regression_tests/test_xopt_EI_initial_sample.py new file mode 100644 index 0000000000..d89de94493 --- /dev/null +++ b/libensemble/tests/regression_tests/test_xopt_EI_initial_sample.py @@ -0,0 +1,87 @@ +""" +Tests libEnsemble with Xopt ExpectedImprovementGenerator using +initial_sample_method="uniform" to produce initial sample points. + +EI requires pre-evaluated data before it can suggest points. This test +verifies that setting initial_sample_method="uniform" in GenSpecs causes +libEnsemble to generate uniform random samples, evaluate them through +the sim, and ingest results into the generator before optimization begins. + +Execute via one of the following commands (e.g. 4 workers): + mpiexec -np 5 python test_xopt_EI_initial_sample.py + python test_xopt_EI_initial_sample.py -n 4 + +""" + +# Do not change these lines - they are parsed by run-tests.sh +# TESTSUITE_COMMS: local +# TESTSUITE_NPROCS: 4 +# TESTSUITE_EXTRA: true +# TESTSUITE_EXCLUDE: true + +import numpy as np +from gest_api.vocs import VOCS +from xopt.generators.bayesian.expected_improvement import ExpectedImprovementGenerator + +from libensemble import Ensemble +from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f +from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, LibeSpecs, SimSpecs + + +def xtest_sim(H, persis_info, sim_specs, _): + """y1 = x2, c1 = x1""" + batch = len(H) + H_o = np.zeros(batch, dtype=sim_specs["out"]) + for i in range(batch): + H_o["y1"][i] = H["x2"][i] + H_o["c1"][i] = H["x1"][i] + return H_o, persis_info + + +if __name__ == "__main__": + + batch_size = 4 + + libE_specs = LibeSpecs(gen_on_manager=True, nworkers=batch_size) + libE_specs.reuse_output_dir = True + + vocs = VOCS( + variables={"x1": [0, 1.0], "x2": [0, 10.0]}, + objectives={"y1": "MINIMIZE"}, + constraints={"c1": ["GREATER_THAN", 0.5]}, + constants={"constant1": 1.0}, + ) + + gen = ExpectedImprovementGenerator(vocs=vocs) + + # NO pre-ingested data — libEnsemble handles initial sampling. + gen_specs = GenSpecs( + generator=gen, + initial_batch_size=batch_size, + initial_sample_method="uniform", + batch_size=batch_size, + vocs=vocs, + ) + + sim_specs = SimSpecs( + sim_f=xtest_sim, + vocs=vocs, + ) + + alloc_specs = AllocSpecs(alloc_f=alloc_f) + exit_criteria = ExitCriteria(sim_max=20) + + workflow = Ensemble( + libE_specs=libE_specs, + sim_specs=sim_specs, + alloc_specs=alloc_specs, + gen_specs=gen_specs, + exit_criteria=exit_criteria, + ) + + H, _, _ = workflow.run() + + if workflow.is_manager: + print(f"Completed {len(H)} simulations") + assert len(H) >= 8, f"Expected at least 8 sims, got {len(H)}" + print("Test passed") diff --git a/libensemble/tests/regression_tests/test_xopt_EI_initial_sample_instance.py b/libensemble/tests/regression_tests/test_xopt_EI_initial_sample_instance.py new file mode 100644 index 0000000000..c7db6d363a --- /dev/null +++ b/libensemble/tests/regression_tests/test_xopt_EI_initial_sample_instance.py @@ -0,0 +1,92 @@ +""" +Tests libEnsemble with Xopt ExpectedImprovementGenerator using a +pre-constructed sampler instance for ``initial_sample_method``. + +Companion to ``test_xopt_EI_initial_sample.py``, which uses the string form +(``initial_sample_method="uniform"``). This test instead passes a pre-configured +``LatinHypercubeSample`` instance — exercising the path that lets the user +supply constructor kwargs (here, ``random_seed``) and choose any sampler from +``gen_classes.sampling`` (or a custom one) without going through the string +registry in ``runners.py``. + +Execute via one of the following commands (e.g. 4 workers): + mpiexec -np 5 python test_xopt_EI_initial_sample_instance.py + python test_xopt_EI_initial_sample_instance.py -n 4 +""" + +# Do not change these lines - they are parsed by run-tests.sh +# TESTSUITE_COMMS: local +# TESTSUITE_NPROCS: 4 +# TESTSUITE_EXTRA: true +# TESTSUITE_EXCLUDE: true + +import numpy as np +from gest_api.vocs import VOCS +from xopt.generators.bayesian.expected_improvement import ExpectedImprovementGenerator + +from libensemble import Ensemble +from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f +from libensemble.gen_classes.sampling import LatinHypercubeSample +from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, LibeSpecs, SimSpecs + + +def xtest_sim(H, persis_info, sim_specs, _): + """y1 = x2, c1 = x1""" + batch = len(H) + H_o = np.zeros(batch, dtype=sim_specs["out"]) + for i in range(batch): + H_o["y1"][i] = H["x2"][i] + H_o["c1"][i] = H["x1"][i] + return H_o, persis_info + + +if __name__ == "__main__": + + batch_size = 4 + + libE_specs = LibeSpecs(gen_on_manager=True, nworkers=batch_size) + libE_specs.reuse_output_dir = True + + vocs = VOCS( + variables={"x1": [0, 1.0], "x2": [0, 10.0]}, + objectives={"y1": "MINIMIZE"}, + constraints={"c1": ["GREATER_THAN", 0.5]}, + constants={"constant1": 1.0}, + ) + + gen = ExpectedImprovementGenerator(vocs=vocs) + + # Pre-constructed sampler with a custom random_seed — not reachable via the + # string form, which always instantiates with sampler defaults. + initial_sampler = LatinHypercubeSample(vocs=vocs, random_seed=42) + + gen_specs = GenSpecs( + generator=gen, + initial_batch_size=batch_size, + initial_sample_method=initial_sampler, + batch_size=batch_size, + vocs=vocs, + ) + + sim_specs = SimSpecs( + sim_f=xtest_sim, + vocs=vocs, + ) + + alloc_specs = AllocSpecs(alloc_f=alloc_f) + exit_criteria = ExitCriteria(sim_max=20) + + workflow = Ensemble( + libE_specs=libE_specs, + sim_specs=sim_specs, + alloc_specs=alloc_specs, + gen_specs=gen_specs, + exit_criteria=exit_criteria, + ) + + H, _, _ = workflow.run() + + if workflow.is_manager: + print(f"Completed {len(H)} simulations") + assert len(H) >= 8, f"Expected at least 8 sims, got {len(H)}" + print("Test passed") diff --git a/libensemble/tests/regression_tests/test_xopt_EI_xopt_sim.py b/libensemble/tests/regression_tests/test_xopt_EI_xopt_sim.py new file mode 100644 index 0000000000..13939169f7 --- /dev/null +++ b/libensemble/tests/regression_tests/test_xopt_EI_xopt_sim.py @@ -0,0 +1,96 @@ +""" +Tests libEnsemble with Xopt ExpectedImprovementGenerator and a gest-api form simulator. + +*****currently fixing nworkers to batch_size***** + +Execute via one of the following commands (e.g. 4 workers): + mpiexec -np 5 python test_xopt_EI_xopt_sim.py + python test_xopt_EI_xopt_sim.py -n 4 + +When running with the above commands, the number of concurrent evaluations of +the objective function will be 4 as the generator is on the manager. + +""" + +# Do not change these lines - they are parsed by run-tests.sh +# TESTSUITE_COMMS: mpi local +# TESTSUITE_NPROCS: 4 +# TESTSUITE_EXTRA: true +# TESTSUITE_EXCLUDE: true + +import numpy as np +from gest_api.vocs import VOCS +from xopt.generators.bayesian.expected_improvement import ExpectedImprovementGenerator + +from libensemble import Ensemble +from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs + + +# From Xopt/xopt/resources/testing.py +def xtest_callable(input_dict: dict, a=0) -> dict: + """Single-objective callable test function""" + assert isinstance(input_dict, dict) + x1 = input_dict["x1"] + x2 = input_dict["x2"] + + assert "constant1" in input_dict + + y1 = x2 + c1 = x1 + return {"y1": y1, "c1": c1} + + +# Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). +if __name__ == "__main__": + + n = 2 + batch_size = 4 + + libE_specs = LibeSpecs(nworkers=batch_size) + libE_specs.reuse_output_dir = True + + vocs = VOCS( + variables={"x1": [0, 1.0], "x2": [0, 10.0]}, + objectives={"y1": "MINIMIZE"}, + constraints={"c1": ["GREATER_THAN", 0.5]}, + constants={"constant1": 1.0}, + ) + + gen = ExpectedImprovementGenerator(vocs=vocs) + + # Create 4 initial points and ingest them + initial_points = [ + {"x1": 0.2, "x2": 2.0, "constant1": 1.0, "y1": 2.0, "c1": 0.2}, + {"x1": 0.5, "x2": 5.0, "constant1": 1.0, "y1": 5.0, "c1": 0.5}, + {"x1": 0.7, "x2": 7.0, "constant1": 1.0, "y1": 7.0, "c1": 0.7}, + {"x1": 0.9, "x2": 9.0, "constant1": 1.0, "y1": 9.0, "c1": 0.9}, + ] + gen.ingest(initial_points) + + gen_specs = GenSpecs( + generator=gen, + batch_size=batch_size, + vocs=vocs, + ) + + sim_specs = SimSpecs( + simulator=xtest_callable, + vocs=vocs, + ) + + exit_criteria = ExitCriteria(sim_max=20) + + workflow = Ensemble( + libE_specs=libE_specs, + sim_specs=sim_specs, + gen_specs=gen_specs, + exit_criteria=exit_criteria, + ) + + H, _, _ = workflow.run() + + # Perform the run + if workflow.is_manager: + print(f"Completed {len(H)} simulations") + assert np.array_equal(H["y1"], H["x2"]) + assert np.array_equal(H["c1"], H["x1"]) diff --git a/libensemble/tests/regression_tests/test_xopt_nelder_mead.py b/libensemble/tests/regression_tests/test_xopt_nelder_mead.py new file mode 100644 index 0000000000..30d0952077 --- /dev/null +++ b/libensemble/tests/regression_tests/test_xopt_nelder_mead.py @@ -0,0 +1,85 @@ +""" +Tests libEnsemble with Xopt NelderMeadGenerator using Rosenbrock function + +Execute via one of the following commands (e.g. 4 workers): + mpiexec -np 5 python test_xopt_nelder_mead.py + python test_xopt_nelder_mead.py -n 4 + +When running with the above commands, the number of concurrent evaluations of +the objective function will be 4 as the generator is on the manager. + +""" + +# Do not change these lines - they are parsed by run-tests.sh +# TESTSUITE_COMMS: mpi local +# TESTSUITE_NPROCS: 2 +# TESTSUITE_EXTRA: true +# TESTSUITE_EXCLUDE: true + +import numpy as np +from gest_api.vocs import VOCS +from xopt.generators.sequential.neldermead import NelderMeadGenerator + +from libensemble import Ensemble +from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs + + +def rosenbrock_callable(input_dict: dict) -> dict: + """2D Rosenbrock function for gest-api style simulator""" + x1 = input_dict["x1"] + x2 = input_dict["x2"] + y1 = 100 * (x2 - x1**2) ** 2 + (1 - x1) ** 2 + return {"y1": y1} + + +# Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). +if __name__ == "__main__": + + batch_size = 1 + + libE_specs = LibeSpecs(nworkers=batch_size) + libE_specs.reuse_output_dir = True + + vocs = VOCS( + variables={"x1": [-2.0, 2.0], "x2": [-2.0, 2.0]}, + objectives={"y1": "MINIMIZE"}, + ) + + gen = NelderMeadGenerator(vocs=vocs) + + # Create initial points with evaluated rosenbrock values + initial_points = [ + {"x1": -1.2, "x2": 1.0, "y1": rosenbrock_callable({"x1": -1.2, "x2": 1.0})["y1"]}, + {"x1": -1.0, "x2": 1.0, "y1": rosenbrock_callable({"x1": -1.0, "x2": 1.0})["y1"]}, + {"x1": -0.8, "x2": 0.8, "y1": rosenbrock_callable({"x1": -0.8, "x2": 0.8})["y1"]}, + ] + gen.ingest(initial_points) + + gen_specs = GenSpecs( + generator=gen, + batch_size=batch_size, + vocs=vocs, + ) + + sim_specs = SimSpecs( + simulator=rosenbrock_callable, + vocs=vocs, + ) + + exit_criteria = ExitCriteria(sim_max=30) + + workflow = Ensemble( + libE_specs=libE_specs, + sim_specs=sim_specs, + gen_specs=gen_specs, + exit_criteria=exit_criteria, + ) + + H, _, _ = workflow.run() + + # Perform the run + if workflow.is_manager: + print(f"Completed {len(H)} simulations") + initial_value = H["y1"][0] + best_value = H["y1"][np.argmin(H["y1"])] + assert best_value <= initial_value diff --git a/libensemble/tests/run_tests.py b/libensemble/tests/run_tests.py index 073ab1a541..e566d451f5 100755 --- a/libensemble/tests/run_tests.py +++ b/libensemble/tests/run_tests.py @@ -35,16 +35,16 @@ # Test Directories - all relative to project root dir CODE_DIR = "libensemble" LIBE_SRC_DIR = CODE_DIR -TESTING_DIR = os.path.join(CODE_DIR, "tests") +TESTING_DIR = Path(CODE_DIR) / "tests" UNIT_TEST_SUBDIRS = [ "unit_tests", "unit_tests_mpi_import", "unit_tests_nompi", "unit_tests_logger", ] -UNIT_TEST_DIRS = [os.path.join(TESTING_DIR, subdir) for subdir in UNIT_TEST_SUBDIRS] -REG_TEST_SUBDIR = os.path.join(TESTING_DIR, "regression_tests") -FUNC_TEST_SUBDIR = os.path.join(TESTING_DIR, "functionality_tests") +UNIT_TEST_DIRS = [TESTING_DIR / subdir for subdir in UNIT_TEST_SUBDIRS] +REG_TEST_SUBDIR = TESTING_DIR / "regression_tests" +FUNC_TEST_SUBDIR = TESTING_DIR / "functionality_tests" # Coverage merge and report dir COV_MERGE_DIR = TESTING_DIR @@ -129,15 +129,25 @@ def cleanup(root_dir): "opt_*.txt_flag", "test_executor_forces_tutorial", "test_executor_forces_tutorial_2", + # Coverage output generated by merge_coverage_reports + "coverage.xml", + # Cache files created by Ensemble/calling scripts + ".libe_cache_*.meta.json", + # Artifacts from forces build step + "forces_app", + "scaling_tests/forces/forces_app/forces.x", + # Task output scripts in unit tests + "libe_task_*.sh", ] dirs_to_clean = UNIT_TEST_DIRS + [REG_TEST_SUBDIR, FUNC_TEST_SUBDIR] for dir_path in dirs_to_clean: - full_path = os.path.join(root_dir, dir_path) - if "libensemble/tests/" not in full_path.replace("\\", "/"): - cprint(f"Safety check failed for {full_path}. Check directory", style="red") + full_path = Path(root_dir) / dir_path + full_path_str = str(full_path) + if "libensemble/tests/" not in full_path_str.replace("\\", "/"): + cprint(f"Safety check failed for {full_path_str}. Check directory", style="red") sys.exit(2) for pattern in patterns: - for file_path in glob.glob(os.path.join(full_path, pattern)): + for file_path in glob.glob(str(full_path / pattern)): try: if os.path.isfile(file_path) or os.path.islink(file_path): os.remove(file_path) @@ -199,8 +209,8 @@ def print_test_failed(test_num, test_script_name, comm, nprocs, duration): def merge_coverage_reports(root_dir): """Merge coverage data from multiple tests and generate a report.""" print_heading("Generating coverage reports") - tests_dir = os.path.join(root_dir, "libensemble", "tests") - cov_files = glob.glob(os.path.join(tests_dir, "**", ".cov_*"), recursive=True) + tests_dir = Path(root_dir) / "libensemble" / "tests" + cov_files = glob.glob(str(tests_dir / "**" / ".cov_*"), recursive=True) if cov_files: try: @@ -262,11 +272,14 @@ def is_open_mpi(): def build_forces(root_dir): """Build forces.x using mpicc.""" cprint("Building forces.x before running regression tests...", style="yellow", newline=True) - forces_app_dir = os.path.join(root_dir, "libensemble/tests/scaling_tests/forces/forces_app") - subprocess.run(["mpicc", "-O3", "-o", "forces.x", "forces.c", "-lm"], cwd=forces_app_dir, check=True) - destination_dir = os.path.join(root_dir, "libensemble/tests/forces_app") + forces_app_dir = Path(root_dir) / "libensemble/tests/scaling_tests/forces/forces_app" + build_cmd = ["mpicc", "-O3", "-o", "forces.x", "forces.c", "-lm"] + if platform.system() == "Darwin": + build_cmd = ["mpicc", "-cc=clang", "-O3", "-o", "forces.x", "forces.c", "-lm"] + subprocess.run(build_cmd, cwd=forces_app_dir, check=True) + destination_dir = Path(root_dir) / "libensemble/tests/forces_app" os.makedirs(destination_dir, exist_ok=True) - shutil.copy(os.path.join(forces_app_dir, "forces.x"), destination_dir) + shutil.copy(forces_app_dir / "forces.x", destination_dir) def skip_test(directives, args, current_os): @@ -335,7 +348,7 @@ def run_unit_tests(root_dir, python_exec, args): print_heading(f"Running unit tests (with pytest)") for dir_path in UNIT_TEST_DIRS: cprint(f"Entering unit test dir: {dir_path}", style="yellow", newline=True) - full_path = os.path.join(root_dir, dir_path) + full_path = Path(root_dir) / dir_path cov_rep = cov_report_type + ":cov_unit" cmd = python_exec + ["-m", "pytest", "--color=yes", "--timeout=120", "--cov", "--cov-report", cov_rep] if args.e: @@ -366,8 +379,8 @@ def run_regression_tests(root_dir, python_exec, args, current_os): reg_test_list = REG_TEST_LIST reg_test_files = [] for dir_path in test_dirs: - full_path = os.path.join(root_dir, dir_path) - reg_test_files.extend(glob.glob(os.path.join(full_path, reg_test_list))) + full_path = Path(root_dir) / dir_path + reg_test_files.extend(glob.glob(str(full_path / reg_test_list))) reg_test_files = sorted(reg_test_files) reg_pass = 0 diff --git a/libensemble/tests/scaling_tests/forces/forces_adv/forces.yaml b/libensemble/tests/scaling_tests/forces/forces_adv/forces.yaml deleted file mode 100644 index e3f38da8ac..0000000000 --- a/libensemble/tests/scaling_tests/forces/forces_adv/forces.yaml +++ /dev/null @@ -1,45 +0,0 @@ -libE_specs: - save_every_k_gens: 1000 - sim_dirs_make: True - profile: False - -exit_criteria: - sim_max: 8 - -sim_specs: - sim_f: forces_simf.run_forces - inputs: - - x - outputs: - energy: - type: float - - user: - keys: - - seed - cores: 1 - sim_particles: 1.e+3 - sim_timesteps: 5 - sim_kill_minutes: 10.0 - particle_variance: 0.2 - kill_rate: 0.5 - fail_on_sim: False - fail_on_submit: False - -gen_specs: - gen_f: libensemble.gen_funcs.sampling.uniform_random_sample - outputs: - x: - type: float - size: 1 - user: - gen_batch_size: 1000 - -alloc_specs: - alloc_f: libensemble.alloc_funcs.give_sim_work_first.give_sim_work_first - outputs: - allocated: - type: bool - user: - batch_mode: True - num_active_gens: 1 diff --git a/libensemble/tests/scaling_tests/forces/forces_adv/forces_simf.py b/libensemble/tests/scaling_tests/forces/forces_adv/forces_simf.py index 25097205d5..51e65fcc5d 100644 --- a/libensemble/tests/scaling_tests/forces/forces_adv/forces_simf.py +++ b/libensemble/tests/scaling_tests/forces/forces_adv/forces_simf.py @@ -1,5 +1,6 @@ import os import time +from pathlib import Path import numpy as np @@ -101,7 +102,7 @@ def run_forces(H, persis_info, sim_specs, libE_info): # Stat file to check for bad runs statfile = "forces.stat" - filepath = os.path.join(task.workdir, statfile) + filepath = Path(task.workdir) / statfile line = None poll_interval = 0.1 # secs diff --git a/libensemble/tests/scaling_tests/forces/forces_adv/forces_support.py b/libensemble/tests/scaling_tests/forces/forces_adv/forces_support.py index 9a10aa5a86..8dcb4937e7 100644 --- a/libensemble/tests/scaling_tests/forces/forces_adv/forces_support.py +++ b/libensemble/tests/scaling_tests/forces/forces_adv/forces_support.py @@ -1,4 +1,5 @@ import os +from pathlib import Path outfiles = ["err.txt", "forces.stat", "out.txt"] @@ -18,6 +19,7 @@ def test_libe_stats(status): def test_ensemble_dir(libE_specs, dir, nworkers, sim_max): + dir = Path(dir) if not os.path.isdir(dir): print(f"Specified ensemble directory {dir} not found.") return @@ -36,11 +38,11 @@ def test_ensemble_dir(libE_specs, dir, nworkers, sim_max): worker_dirs = [i for i in os.listdir(dir) if i.startswith("worker")] for worker_dir in worker_dirs: - sim_dirs = [i for i in os.listdir(os.path.join(dir, worker_dir)) if i.startswith("sim")] + sim_dirs = [i for i in os.listdir(dir / worker_dir) if i.startswith("sim")] num_sim_dirs += len(sim_dirs) for sim_dir in sim_dirs: - files_found.append(all([i in os.listdir(os.path.join(dir, worker_dir, sim_dir)) for i in outfiles])) + files_found.append(all([i in os.listdir(dir / worker_dir / sim_dir) for i in outfiles])) assert ( num_sim_dirs == sim_max @@ -62,7 +64,7 @@ def test_ensemble_dir(libE_specs, dir, nworkers, sim_max): files_found = [] for sim_dir in sim_dirs: - files_found.append(all([i in os.listdir(os.path.join(dir, sim_dir)) for i in outfiles])) + files_found.append(all([i in os.listdir(dir / sim_dir) for i in outfiles])) assert all( files_found diff --git a/libensemble/tests/scaling_tests/forces/forces_adv/readme.md b/libensemble/tests/scaling_tests/forces/forces_adv/readme.md index 2d0daf9f79..e34cdfe8ca 100644 --- a/libensemble/tests/scaling_tests/forces/forces_adv/readme.md +++ b/libensemble/tests/scaling_tests/forces/forces_adv/readme.md @@ -45,12 +45,6 @@ To remove output before the next run: ./cleanup.sh -### Using YAML in calling script (optional) - -An alternative calling script `run_libe_forces_from_yaml.py` can be run in the same -way as `run_libe_forces.py` above. This uses an alternative libEnsemble interface, where -an ensemble object is created and parameters can be read from the `forces.yaml` file. - ### Using batch scripts See `examples/libE_submission_scripts` diff --git a/libensemble/tests/scaling_tests/forces/forces_adv/run_libe_forces.py b/libensemble/tests/scaling_tests/forces/forces_adv/run_libe_forces.py index 3b5a489d62..a9820a7f06 100644 --- a/libensemble/tests/scaling_tests/forces/forces_adv/run_libe_forces.py +++ b/libensemble/tests/scaling_tests/forces/forces_adv/run_libe_forces.py @@ -1,6 +1,7 @@ #!/usr/bin/env python import os import sys +from pathlib import Path import numpy as np from forces_simf import run_forces # Sim func from current dir @@ -12,7 +13,7 @@ # Import libEnsemble modules from libensemble.libE import libE from libensemble.manager import ManagerException -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +from libensemble.tools import parse_args, save_libE_output PERSIS_GEN = False @@ -20,15 +21,15 @@ from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f from libensemble.gen_funcs.persistent_sampling import persistent_uniform as gen_f else: - from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first as alloc_f - from libensemble.gen_funcs.sampling import uniform_random_sample as gen_f + from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first as alloc_f # type: ignore[no-redef] + from libensemble.gen_funcs.sampling import uniform_random_sample as gen_f # type: ignore[no-redef] logger.set_level("INFO") # INFO is now default nworkers, is_manager, libE_specs, _ = parse_args() -sim_app = os.path.join(os.getcwd(), "../forces_app/forces.x") +sim_app = Path.cwd() / "../forces_app/forces.x" if not os.path.isfile(sim_app): sys.exit("forces.x not found - please build first in ../forces_app dir") @@ -65,23 +66,19 @@ "gen_f": gen_f, # Generator function "in": [], # Generator input "out": [("x", float, (1,))], # Name, type and size of data produced (must match sim_specs 'in') + "batch_size": 1000, # How many random samples to generate in one call "user": { "lb": np.array([0]), # Lower bound for random sample array (1D) "ub": np.array([32767]), # Upper bound for random sample array (1D) - "gen_batch_size": 1000, # How many random samples to generate in one call }, } if PERSIS_GEN: - alloc_specs = {"alloc_f": alloc_f} + alloc_specs = {} + gen_specs["async_return"] = False else: - alloc_specs = { - "alloc_f": alloc_f, - "user": { - "batch_mode": True, # If true wait for all sims to process before generate more - "num_active_gens": 1, # Only one active generator at a time - }, - } + alloc_specs = {"alloc_f": alloc_f, "user": {"batch_mode": True}} + gen_specs["num_active_gens"] = 1 libE_specs["save_every_k_gens"] = 1000 # Save every K steps libE_specs["sim_dirs_make"] = True # Separate each sim into a separate directory @@ -91,16 +88,11 @@ sim_max = 8 exit_criteria = {"sim_max": sim_max} -# Create a different random number stream for each worker and the manager -persis_info = {} -persis_info = add_unique_random_streams(persis_info, nworkers + 1) - try: H, persis_info, flag = libE( sim_specs, gen_specs, exit_criteria, - persis_info=persis_info, alloc_specs=alloc_specs, libE_specs=libE_specs, ) diff --git a/libensemble/tests/scaling_tests/forces/forces_adv/run_libe_forces_from_yaml.py b/libensemble/tests/scaling_tests/forces/forces_adv/run_libe_forces_from_yaml.py deleted file mode 100644 index 0bca5e93d8..0000000000 --- a/libensemble/tests/scaling_tests/forces/forces_adv/run_libe_forces_from_yaml.py +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env python -import os -import sys - -import numpy as np - -from libensemble.ensemble import Ensemble -from libensemble.executors.mpi_executor import MPIExecutor -from libensemble.tools import add_unique_random_streams - -#################### - -sim_app = os.path.join(os.getcwd(), "../forces_app/forces.x") - -if not os.path.isfile(sim_app): - sys.exit("forces.x not found - please build first in ../forces_app dir") - - -#################### - -forces = Ensemble(parse_args=True) -forces.from_yaml("forces.yaml") - -forces.logger.set_level("INFO") - -if forces.is_manager: - print(f"\nRunning with {forces.nworkers} workers\n") - -exctr = MPIExecutor() -exctr.register_app(full_path=sim_app, app_name="forces") - -forces.libE_specs["ensemble_dir_path"] = "./ensemble" -forces.gen_specs.user.update( - { - "lb": np.array([0]), - "ub": np.array([32767]), - } -) - -forces.persis_info = add_unique_random_streams({}, forces.nworkers + 1) - -forces.run() -forces.save_output(__file__) diff --git a/libensemble/tests/scaling_tests/forces/forces_app/build_forces.sh b/libensemble/tests/scaling_tests/forces/forces_app/build_forces.sh index b8b379e0ee..972695850f 100755 --- a/libensemble/tests/scaling_tests/forces/forces_app/build_forces.sh +++ b/libensemble/tests/scaling_tests/forces/forces_app/build_forces.sh @@ -4,8 +4,12 @@ # Building flat MPI # ------------------------------------------------- -# GCC -mpicc -O3 -o forces.x forces.c -lm +# macOS (Apple Silicon with pixi) / GCC +if [[ "$OSTYPE" == "darwin"* ]]; then + mpicc -cc=clang -O3 -o forces.x forces.c -lm +else + mpicc -O3 -o forces.x forces.c -lm +fi # Intel # mpiicc -O3 -o forces.x forces.c @@ -45,10 +49,3 @@ mpicc -O3 -o forces.x forces.c -lm # Nvidia (nvc) compiler with mpicc and on Cray system with target (Perlmutter) # mpicc -DGPU -O3 -fopenmp -mp=gpu -o forces.x forces.c # cc -DGPU -Wl,-znoexecstack -O3 -fopenmp -mp=gpu -target-accel=nvidia80 -o forces.x forces.c - -# xl (plain and using mpicc on Summit) -# xlc_r -DGPU -O3 -qsmp=omp -qoffload -o forces.x forces.c -# mpicc -DGPU -O3 -qsmp=omp -qoffload -o forces.x forces.c - -# Summit with gcc (Need up to offload capable gcc: module load gcc/12.1.0) - slower than xlc -# mpicc -DGPU -Ofast -fopenmp -Wl,-rpath=/sw/summit/gcc/12.1.0-0/lib64 -lm -foffload=nvptx-none forces.c -o forces.x diff --git a/libensemble/tests/scaling_tests/forces/forces_gpu/run_libe_forces.py b/libensemble/tests/scaling_tests/forces/forces_gpu/run_libe_forces.py index 932709d57a..7b45ebd574 100644 --- a/libensemble/tests/scaling_tests/forces/forces_gpu/run_libe_forces.py +++ b/libensemble/tests/scaling_tests/forces/forces_gpu/run_libe_forces.py @@ -17,6 +17,7 @@ import os import sys +from pathlib import Path import numpy as np from forces_simf import run_forces # Sim func from current dir @@ -30,7 +31,7 @@ if __name__ == "__main__": # Initialize MPI Executor exctr = MPIExecutor() - sim_app = os.path.join(os.getcwd(), "../forces_app/forces.x") + sim_app = Path.cwd() / "../forces_app/forces.x" if not os.path.isfile(sim_app): sys.exit("forces.x not found - please build first in ../forces_app dir") @@ -77,9 +78,6 @@ # Instruct libEnsemble to exit after this many simulations ensemble.exit_criteria = ExitCriteria(sim_max=8) - # Seed random streams for each worker, particularly for gen_f - ensemble.add_random_streams() - # Run ensemble ensemble.run() diff --git a/libensemble/tests/scaling_tests/forces/forces_gpu_var_resources/run_libe_forces.py b/libensemble/tests/scaling_tests/forces/forces_gpu_var_resources/run_libe_forces.py index 8eec289a32..09a43e175f 100644 --- a/libensemble/tests/scaling_tests/forces/forces_gpu_var_resources/run_libe_forces.py +++ b/libensemble/tests/scaling_tests/forces/forces_gpu_var_resources/run_libe_forces.py @@ -20,6 +20,7 @@ import os import sys +from pathlib import Path import numpy as np from forces_simf import run_forces # Sim func from current dir @@ -33,7 +34,7 @@ if __name__ == "__main__": # Initialize MPI Executor exctr = MPIExecutor() - sim_app = os.path.join(os.getcwd(), "../forces_app/forces.x") + sim_app = Path.cwd() / "../forces_app/forces.x" if not os.path.isfile(sim_app): sys.exit("forces.x not found - please build first in ../forces_app dir") @@ -85,9 +86,6 @@ # Instruct libEnsemble to exit after this many simulations. ensemble.exit_criteria = ExitCriteria(sim_max=8) - # Seed random streams for each worker, particularly for gen_f - ensemble.add_random_streams() - # Run ensemble ensemble.run() diff --git a/libensemble/tests/scaling_tests/forces/forces_multi_app/run_libe_forces.py b/libensemble/tests/scaling_tests/forces/forces_multi_app/run_libe_forces.py index a55d502ead..e5faeb9b49 100644 --- a/libensemble/tests/scaling_tests/forces/forces_multi_app/run_libe_forces.py +++ b/libensemble/tests/scaling_tests/forces/forces_multi_app/run_libe_forces.py @@ -24,6 +24,7 @@ import os import sys +from pathlib import Path import numpy as np from forces_simf import run_forces # Sim func from current dir @@ -39,8 +40,8 @@ exctr = MPIExecutor() # Register simulation executable with executor - cpu_app = os.path.join(os.getcwd(), "../forces_app/forces_cpu.x") - gpu_app = os.path.join(os.getcwd(), "../forces_app/forces_gpu.x") + cpu_app = Path.cwd() / "../forces_app/forces_cpu.x" + gpu_app = Path.cwd() / "../forces_app/forces_gpu.x" if not os.path.isfile(cpu_app): sys.exit(f"{cpu_app} not found - please build first in ../forces_app dir") @@ -97,9 +98,6 @@ # Instruct libEnsemble to exit after this many simulations. ensemble.exit_criteria = ExitCriteria(sim_max=nsim_workers * 2) - # Seed random streams for each worker, particularly for gen_f. - ensemble.add_random_streams() - # Run ensemble ensemble.run() diff --git a/libensemble/tests/scaling_tests/forces/forces_simple/readme.md b/libensemble/tests/scaling_tests/forces/forces_simple/readme.md index da4dcebd57..6bbb763c61 100644 --- a/libensemble/tests/scaling_tests/forces/forces_simple/readme.md +++ b/libensemble/tests/scaling_tests/forces/forces_simple/readme.md @@ -15,9 +15,6 @@ Then return here and run: python run_libe_forces.py --comms local --nworkers 5 -This will run with four workers. One worker will run the persistent generator. -The other four will run the forces simulations. - ## Detailed instructions Naive Electrostatics Code Test diff --git a/libensemble/tests/scaling_tests/forces/forces_simple/run_libe_forces.py b/libensemble/tests/scaling_tests/forces/forces_simple/run_libe_forces.py index a70477748b..a337a09e43 100644 --- a/libensemble/tests/scaling_tests/forces/forces_simple/run_libe_forces.py +++ b/libensemble/tests/scaling_tests/forces/forces_simple/run_libe_forces.py @@ -1,22 +1,22 @@ #!/usr/bin/env python import os import sys +from pathlib import Path import numpy as np from forces_simf import run_forces # Sim func from current dir from libensemble import Ensemble -from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f from libensemble.executors import MPIExecutor from libensemble.gen_funcs.persistent_sampling import persistent_uniform as gen_f -from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, LibeSpecs, SimSpecs +from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs if __name__ == "__main__": # Initialize MPI Executor exctr = MPIExecutor() # Register simulation executable with executor - sim_app = os.path.join(os.getcwd(), "../forces_app/forces.x") + sim_app = Path.cwd() / "../forces_app/forces.x" if not os.path.isfile(sim_app): sys.exit("forces.x not found - please build first in ../forces_app dir") @@ -44,27 +44,19 @@ inputs=[], # No input when start persistent generator persis_in=["sim_id"], # Return sim_ids of evaluated points to generator outputs=[("x", float, (1,))], + initial_batch_size=nsim_workers, + async_return=False, user={ - "initial_batch_size": nsim_workers, "lb": np.array([1000]), # min particles "ub": np.array([3000]), # max particles }, ) # Starts one persistent generator. Simulated values are returned in batch. - ensemble.alloc_specs = AllocSpecs( - alloc_f=alloc_f, - user={ - "async_return": False, # False causes batch returns - }, - ) # Instruct libEnsemble to exit after this many simulations ensemble.exit_criteria = ExitCriteria(sim_max=8) - # Seed random streams for each worker, particularly for gen_f - ensemble.add_random_streams() - # Run ensemble ensemble.run() diff --git a/libensemble/tests/scaling_tests/forces/forces_simple_with_input_file/readme.md b/libensemble/tests/scaling_tests/forces/forces_simple_with_input_file/readme.md index 558e73ac4f..5bfed8c88d 100644 --- a/libensemble/tests/scaling_tests/forces/forces_simple_with_input_file/readme.md +++ b/libensemble/tests/scaling_tests/forces/forces_simple_with_input_file/readme.md @@ -16,9 +16,6 @@ Then return here and run: python run_libe_forces.py --comms local --nworkers 5 -This will run with four workers. One worker will run the persistent generator. -The other four will run the forces simulations. - ## Detailed instructions Naive Electrostatics Code Test diff --git a/libensemble/tests/scaling_tests/forces/forces_simple_with_input_file/run_libe_forces.py b/libensemble/tests/scaling_tests/forces/forces_simple_with_input_file/run_libe_forces.py index 8f3e8d442a..9c066ec932 100644 --- a/libensemble/tests/scaling_tests/forces/forces_simple_with_input_file/run_libe_forces.py +++ b/libensemble/tests/scaling_tests/forces/forces_simple_with_input_file/run_libe_forces.py @@ -1,22 +1,22 @@ #!/usr/bin/env python import os import sys +from pathlib import Path import numpy as np from forces_simf import run_forces # Sim func from current dir from libensemble import Ensemble -from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f from libensemble.executors import MPIExecutor from libensemble.gen_funcs.persistent_sampling import persistent_uniform as gen_f -from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, LibeSpecs, SimSpecs +from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs if __name__ == "__main__": # Initialize MPI Executor exctr = MPIExecutor() # Register simulation executable with executor - sim_app = os.path.join(os.getcwd(), "../forces_app/forces.x") + sim_app = Path.cwd() / "../forces_app/forces.x" if not os.path.isfile(sim_app): sys.exit("forces.x not found - please build first in ../forces_app dir") @@ -48,27 +48,19 @@ inputs=[], # No input when start persistent generator persis_in=["sim_id"], # Return sim_ids of evaluated points to generator outputs=[("x", float, (1,))], + initial_batch_size=nsim_workers, + async_return=False, user={ - "initial_batch_size": nsim_workers, "lb": np.array([1000]), # min particles "ub": np.array([3000]), # max particles }, ) # Starts one persistent generator. Simulated values are returned in batch. - ensemble.alloc_specs = AllocSpecs( - alloc_f=alloc_f, - user={ - "async_return": False, # False causes batch returns - }, - ) # Instruct libEnsemble to exit after this many simulations ensemble.exit_criteria = ExitCriteria(sim_max=8) - # Seed random streams for each worker, particularly for gen_f - ensemble.add_random_streams() - # Run ensemble ensemble.run() diff --git a/libensemble/tests/scaling_tests/forces/forces_simple_xopt/cleanup.sh b/libensemble/tests/scaling_tests/forces/forces_simple_xopt/cleanup.sh new file mode 100755 index 0000000000..eaaa23635a --- /dev/null +++ b/libensemble/tests/scaling_tests/forces/forces_simple_xopt/cleanup.sh @@ -0,0 +1 @@ +rm -r ensemble *.npy *.pickle ensemble.log lib*.txt diff --git a/libensemble/tests/scaling_tests/forces/forces_simple_xopt/forces_simf.py b/libensemble/tests/scaling_tests/forces/forces_simple_xopt/forces_simf.py new file mode 100644 index 0000000000..3f8c2a3684 --- /dev/null +++ b/libensemble/tests/scaling_tests/forces/forces_simple_xopt/forces_simf.py @@ -0,0 +1,106 @@ +""" +Module containing alternative functions for running the forces MPI application + +run_forces: Uses classic libEnsemble sim_f. +run_forces_dict: Uses gest-api/xopt style simulator. +""" + +import numpy as np + +# Optional status codes to display in libE_stats.txt for each gen or sim +from libensemble.message_numbers import TASK_FAILED, WORKER_DONE + +__all__ = [ + "run_forces", + "run_forces_dict", +] + + +def run_forces(H, persis_info, sim_specs, libE_info): + """Runs the forces MPI application. + + By default assigns the number of MPI ranks to the number + of cores available to this worker. + + To assign a different number give e.g., `num_procs=4` to + ``exctr.submit``. + """ + + calc_status = 0 + + # Parse out num particles, from generator function + particles = str(int(H["x"][0])) # x is a scalar for each point + + # app arguments: num particles, timesteps, also using num particles as seed + args = particles + " " + str(10) + " " + particles + + # Retrieve our MPI Executor + exctr = libE_info["executor"] + + # Submit our forces app for execution. + task = exctr.submit(app_name="forces", app_args=args) + + # Block until the task finishes + task.wait() + + # Try loading final energy reading, set the sim's status + statfile = "forces.stat" + try: + data = np.loadtxt(statfile) + final_energy = data[-1] + calc_status = WORKER_DONE + except Exception: + final_energy = np.nan + calc_status = TASK_FAILED + + # Define our output array, populate with energy reading + output = np.zeros(1, dtype=sim_specs["out"]) + output["energy"] = final_energy + + # Return final information to worker, for reporting to manager + return output, persis_info, calc_status + + +def run_forces_dict(input_dict: dict, libE_info: dict) -> dict: + """Runs the forces MPI application (gest-api/xopt style simulator). + + Parameters + ---------- + input_dict : dict + Input dictionary containing VOCS variables. Must contain "x" key + with the number of particles. + libE_info : dict, optional + LibEnsemble information dictionary containing executor and other info. + + Returns + ------- + dict + Output dictionary containing "energy" key with the final energy value. + """ + assert "executor" in libE_info, "executor must be available in libE_info" + + # Extract executor from libE_info + executor = libE_info["executor"] + + # Parse out num particles from input dictionary + x = input_dict["x"] + particles = str(int(x)) + + # app arguments: num particles, timesteps, also using num particles as seed + args = particles + " " + str(10) + " " + particles + + # Submit our forces app for execution. + task = executor.submit(app_name="forces", app_args=args) + + # Block until the task finishes + task.wait() + + # Try loading final energy reading + statfile = "forces.stat" + try: + data = np.loadtxt(statfile) + final_energy = float(data[-1]) + except Exception: + final_energy = np.nan + + return {"energy": final_energy} diff --git a/libensemble/tests/scaling_tests/forces/forces_simple_xopt/readme.md b/libensemble/tests/scaling_tests/forces/forces_simple_xopt/readme.md new file mode 100644 index 0000000000..96fefa4f5f --- /dev/null +++ b/libensemble/tests/scaling_tests/forces/forces_simple_xopt/readme.md @@ -0,0 +1,57 @@ +## Tutorial + +This example is a variation of that in the tutorial **Executor with Electrostatic Forces**. + +https://libensemble.readthedocs.io/en/develop/tutorials/executor_forces_tutorial.html + +This version uses an Xopt random number generator. + +Simulation input `x` is a scalar. + +## QuickStart + +Build forces application and run the ensemble. Go to `forces_app` directory and build `forces.x`: + + cd ../forces_app + ./build_forces.sh + +Then return here and run: + + python run_libe_forces.py -n 4 + +## Detailed instructions + +Naive Electrostatics Code Test + +This is a synthetic, highly configurable simulation function. Its primary use +is to test libEnsemble's capability to launch application instances via the `MPIExecutor`. + +### Forces Mini-App + +A system of charged particles is initialized and simulated over a number of time-steps. + +See `forces_app` directory for details. + +### Running with libEnsemble. + +A random sample of seeds is taken and used as input to the simulation function +(forces miniapp). + +In the `forces_app` directory, modify `build_forces.sh` for the target platform +and run to build `forces.x`: + + ./build_forces.sh + +Then to run with local comms (multiprocessing) with one manager and `N` workers: + + python run_libe_forces.py --comms local --nworkers N + +To run with MPI comms using one manager and `N-1` workers: + + mpirun -np N python run_libe_forces.py + +Application parameters can be adjusted in the file `run_libe_forces.py`. + +To remove output before the next run: + + ./cleanup.sh diff --git a/libensemble/tests/scaling_tests/forces/forces_simple_xopt/run_libe_forces.py b/libensemble/tests/scaling_tests/forces/forces_simple_xopt/run_libe_forces.py new file mode 100644 index 0000000000..2d23c83a88 --- /dev/null +++ b/libensemble/tests/scaling_tests/forces/forces_simple_xopt/run_libe_forces.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python +import os +import sys + +import numpy as np +from forces_simf import run_forces # Classic libEnsemble sim_f. +from gest_api.vocs import VOCS +from xopt.generators.random import RandomGenerator + +from libensemble import Ensemble +from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f +from libensemble.executors import MPIExecutor +from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, LibeSpecs, SimSpecs + +# from forces_simf import run_forces_dict # gest-api/xopt style simulator. + + +if __name__ == "__main__": + # Initialize MPI Executor + exctr = MPIExecutor() + + # Register simulation executable with executor + sim_app = os.path.join(os.getcwd(), "../forces_app/forces.x") + + if not os.path.isfile(sim_app): + sys.exit("forces.x not found - please build first in ../forces_app dir") + + exctr.register_app(full_path=sim_app, app_name="forces") + + # Parse number of workers, comms type, etc. from arguments + ensemble = Ensemble(parse_args=True, executor=exctr) + + # Persistent gen does not need resources + ensemble.libE_specs = LibeSpecs( + sim_dirs_make=True, + ) + + # Define VOCS specification + vocs = VOCS( + variables={"x": [1000, 3000]}, # min and max particles + objectives={"energy": "MINIMIZE"}, + ) + + # Create xopt random sampling generator + gen = RandomGenerator(vocs=vocs) + + ensemble.gen_specs = GenSpecs( + initial_batch_size=ensemble.nworkers, + generator=gen, + vocs=vocs, + ) + + ensemble.sim_specs = SimSpecs( + sim_f=run_forces, + # simulator=run_forces_dict, + vocs=vocs, + ) + + # Starts one persistent generator. Simulated values are returned in batch. + ensemble.alloc_specs = AllocSpecs( + alloc_f=alloc_f, + user={ + "async_return": False, # False causes batch returns + }, + ) + + # Instruct libEnsemble to exit after this many simulations + ensemble.exit_criteria = ExitCriteria(sim_max=8) + + # Run ensemble + ensemble.run() + + if ensemble.is_manager: + # Note, this will change if changing sim_max, nworkers, lb, ub, etc. + print(f'Final energy checksum: {np.sum(ensemble.H["energy"])}') diff --git a/libensemble/tests/scaling_tests/forces/globus_compute_forces/cleanup.sh b/libensemble/tests/scaling_tests/forces/globus_compute_forces/cleanup.sh deleted file mode 100755 index 54c41aa6ec..0000000000 --- a/libensemble/tests/scaling_tests/forces/globus_compute_forces/cleanup.sh +++ /dev/null @@ -1 +0,0 @@ -rm -r ensemble_* *.npy *.pickle ensemble.log lib*.txt diff --git a/libensemble/tests/scaling_tests/forces/globus_compute_forces/forces_simf.py b/libensemble/tests/scaling_tests/forces/globus_compute_forces/forces_simf.py deleted file mode 100644 index c3a31ff5dc..0000000000 --- a/libensemble/tests/scaling_tests/forces/globus_compute_forces/forces_simf.py +++ /dev/null @@ -1,137 +0,0 @@ -def run_forces_globus_compute(H, persis_info, sim_specs, libE_info): - import os - import secrets - import time - - import numpy as np - - from libensemble.executors.mpi_executor import MPIExecutor - from libensemble.message_numbers import TASK_FAILED, WORKER_DONE, WORKER_KILL - - class ForcesException(Exception): - """Raised on some issue with Forces""" - - def perturb(particles, seed, max_fraction): - MAX_SEED = 32767 - """Modify particle count""" - seed_fraction = seed / MAX_SEED - max_delta = particles * max_fraction - delta = seed_fraction * max_delta - delta = delta - max_delta / 2 # translate so -/+ - new_particles = particles + delta - return int(new_particles) - - def read_last_line(filepath): - """Read last line of statfile""" - try: - with open(filepath, "rb") as fh: - line = fh.readlines()[-1].decode().rstrip() - except Exception: - line = "" # In case file is empty or not yet created - return line - - if sim_specs["user"]["fail_on_sim"]: - raise ForcesException(Exception) - - calc_status = 0 # Returns to worker - - x = H["x"] - sim_particles = sim_specs["user"]["sim_particles"] - sim_timesteps = sim_specs["user"]["sim_timesteps"] - time_limit = sim_specs["user"]["sim_kill_minutes"] * 60.0 - sim_app = sim_specs["user"]["sim_app"] - - exctr = MPIExecutor() - exctr.register_app(full_path=sim_app, app_name="forces") - - calc_dir = os.path.join(sim_specs["user"]["remote_ensemble_dir"], secrets.token_hex(nbytes=4)) - os.makedirs(calc_dir, exist_ok=True) - os.chdir(calc_dir) - - # Get from dictionary if key exists, else return default (e.g. 0) - cores = sim_specs["user"].get("cores", None) - kill_rate = sim_specs["user"].get("kill_rate", 0) - particle_variance = sim_specs["user"].get("particle_variance", 0) - - # Composing variable names and x values to set up simulation - seed = int(np.rint(x[0][0])) - - # This is to give a random variance of work-load - sim_particles = perturb(sim_particles, seed, particle_variance) - print(f"seed: {seed} particles: {sim_particles}") - - args = str(int(sim_particles)) + " " + str(sim_timesteps) + " " + str(seed) + " " + str(kill_rate) - - machinefile = None - if sim_specs["user"]["fail_on_submit"]: - machinefile = "fail" - - # Machinefile only used here for exception testing - if cores: - task = exctr.submit( - app_name="forces", - num_procs=cores, - app_args=args, - stdout="out.txt", - stderr="err.txt", - wait_on_start=True, - machinefile=machinefile, - ) - else: - task = exctr.submit( - app_name="forces", - app_args=args, - stdout="out.txt", - stderr="err.txt", - wait_on_start=True, - hyperthreads=True, - machinefile=machinefile, - ) # Auto-partition - - # Stat file to check for bad runs - statfile = "forces.stat" - filepath = os.path.join(task.workdir, statfile) - line = None - - poll_interval = 1 # secs - while not task.finished: - # Read last line of statfile - line = read_last_line(filepath) - if line == "kill": - task.kill() # Bad run - elif task.runtime > time_limit: - task.kill() # Timeout - else: - time.sleep(poll_interval) - task.poll() - - if task.finished: - if task.state == "FINISHED": - print(f"Task {task.name} completed") - calc_status = WORKER_DONE - if read_last_line(filepath) == "kill": - # Generally mark as complete if want results (completed after poll - before readline) - print("Warning: Task completed although marked as a bad run (kill flag set in forces.stat)") - elif task.state == "FAILED": - print(f"Warning: Task {task.name} failed: Error code {task.errcode}") - calc_status = TASK_FAILED - elif task.state == "USER_KILLED": - print(f"Warning: Task {task.name} has been killed") - calc_status = WORKER_KILL - else: - print(f"Warning: Task {task.name} in unknown state {task.state}. Error code {task.errcode}") - - time.sleep(0.2) - try: - data = np.loadtxt(filepath) - # task.read_file_in_workdir(statfile) - final_energy = data[-1] - except Exception: - final_energy = np.nan - # print('Warning - Energy Nan') - - outspecs = sim_specs["out"] - output = np.zeros(1, dtype=outspecs) - output["energy"][0] = final_energy - - return output, persis_info, calc_status diff --git a/libensemble/tests/scaling_tests/forces/globus_compute_forces/globus_compute_forces.yaml b/libensemble/tests/scaling_tests/forces/globus_compute_forces/globus_compute_forces.yaml deleted file mode 100644 index c7c0463bc2..0000000000 --- a/libensemble/tests/scaling_tests/forces/globus_compute_forces/globus_compute_forces.yaml +++ /dev/null @@ -1,37 +0,0 @@ -libE_specs: - save_every_k_gens: 1000 - profile: False - -exit_criteria: - sim_max: 8 - -sim_specs: - sim_f: libensemble.tests.scaling_tests.forces.globus_compute_forces.forces_simf.run_forces_globus_compute - inputs: - - x - outputs: - energy: - type: float - globus_compute_endpoint: ca766d22-49df-466a-8b51-cd0190c58bb0 - user: - keys: - - seed - sim_app: /home/jnavarro/libensemble/libensemble/tests/scaling_tests/forces/forces_app/forces.x - remote_ensemble_dir: /home/jnavarro/bebop_output/ensemble_ - cores: 1 - sim_particles: 1.e+3 - sim_timesteps: 5 - sim_kill_minutes: 10.0 - particle_variance: 0.2 - kill_rate: 0.5 - fail_on_sim: False - fail_on_submit: False - -gen_specs: - gen_f: libensemble.gen_funcs.sampling.uniform_random_sample - outputs: - x: - type: float - size: 1 - user: - gen_batch_size: 1000 diff --git a/libensemble/tests/scaling_tests/forces/globus_compute_forces/readme.md b/libensemble/tests/scaling_tests/forces/globus_compute_forces/readme.md deleted file mode 100644 index 793d869f48..0000000000 --- a/libensemble/tests/scaling_tests/forces/globus_compute_forces/readme.md +++ /dev/null @@ -1,39 +0,0 @@ -## Running test run_libe_forces_funcx.py - -Naive Electrostatics Code Test - -This is designed only as an artificial, highly configurable test -code for a libEnsemble sim func. This variant is primarily to test libEnsemble's -capability to submit simulation functions to a separate machine from where libEnsemble's -manager and workers are running. - -### Forces Mini-App - -A system of charged particles is initialized and simulated over a number of time-steps. - -See `forces_app` directory for details. - -This application will need to be compiled on the remote machine where the sim_f will run. -See below. - -### Running with libEnsemble. - -On the remote machine, Configure the endpoint's `config.py` to include your project information and -match the machine's specifications. - -Then to run with local comms (multiprocessing) with one manager and `N` workers: - - python run_libe_forces_globus_compute.py --comms local --nworkers N - -To run with MPI comms using one manager and `N-1` workers: - - mpirun -np N python run_libe_forces_globus_compute.py - -Application parameters can be adjusted in `globus_compute_forces.yaml`. - -Note that each function and path must be accessible and/or importable on the -remote machine. Absolute paths are recommended. - -To remove output before the next run: - - ./cleanup.sh diff --git a/libensemble/tests/scaling_tests/forces/globus_compute_forces/run_libe_forces_globus_compute.py b/libensemble/tests/scaling_tests/forces/globus_compute_forces/run_libe_forces_globus_compute.py deleted file mode 100644 index d9ac4e04b1..0000000000 --- a/libensemble/tests/scaling_tests/forces/globus_compute_forces/run_libe_forces_globus_compute.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python -import secrets - -import numpy as np - -from libensemble.ensemble import Ensemble - -if __name__ == "__main__": - forces = Ensemble(parse_args=True) - forces.from_yaml("globus_compute_forces.yaml") - - forces.sim_specs.user["remote_ensemble_dir"] += secrets.token_hex(nbytes=3) - - forces.gen_specs.user.update( - { - "lb": np.array([0]), - "ub": np.array([32767]), - } - ) - - forces.add_random_streams() - - forces.run() diff --git a/libensemble/tests/scaling_tests/forces/submission_scripts/summit_submit_mproc.sh b/libensemble/tests/scaling_tests/forces/submission_scripts/summit_submit_mproc.sh deleted file mode 100755 index 268ba64a36..0000000000 --- a/libensemble/tests/scaling_tests/forces/submission_scripts/summit_submit_mproc.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/bin/bash -x -#BSUB -P -#BSUB -J libe_mproc -#BSUB -W 20 -#BSUB -nnodes 4 -#BSUB -alloc_flags "smt1" - -# Script to run libEnsemble using multiprocessing on launch nodes. -# Assumes Conda environment is set up. - -# To be run with central job management -# - Manager and workers run on launch node. -# - Workers submit tasks to the nodes in the job available. - -# Name of calling script- -export EXE=run_libe_forces.py - -# Communication Method -export COMMS="--comms local" - -# Number of workers. -export NWORKERS="--nworkers 5" - -# Wallclock for libE. Slightly smaller than job wallclock -#export LIBE_WALLCLOCK=15 # Optional if pass to script - -# Name of Conda environment -export CONDA_ENV_NAME= - -export LIBE_PLOTS=true # Require plot scripts in $PLOT_DIR (see at end) -export PLOT_DIR=.. - -# Need these if not already loaded -# module load python -# module load gcc/4.8.5 - -# Activate conda environment -export PYTHONNOUSERSITE=1 -. activate $CONDA_ENV_NAME - -# hash -d python # Check pick up python in conda env -hash -r # Check no commands hashed (pip/python...) - -# Launch libE. -#python $EXE $NUM_WORKERS $LIBE_WALLCLOCK > out.txt 2>&1 -python $EXE $COMMS $NWORKERS > out.txt 2>&1 - -if [[ $LIBE_PLOTS = "true" ]]; then - python $PLOT_DIR/plot_libe_calcs_util_v_time.py - python $PLOT_DIR/plot_libe_tasks_util_v_time.py - python $PLOT_DIR/plot_libe_histogram.py -fi diff --git a/libensemble/tests/scaling_tests/persistent_gp/run_example.py b/libensemble/tests/scaling_tests/persistent_gp/run_example.py index 035fa9c1e0..b8a6f81502 100644 --- a/libensemble/tests/scaling_tests/persistent_gp/run_example.py +++ b/libensemble/tests/scaling_tests/persistent_gp/run_example.py @@ -9,11 +9,10 @@ import numpy as np from libensemble import logger -from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens from libensemble.gen_funcs.persistent_ax_multitask import persistent_gp_mt_ax_gen_f from libensemble.libE import libE from libensemble.message_numbers import WORKER_DONE -from libensemble.tools import add_unique_random_streams, parse_args, save_libE_output +from libensemble.tools import parse_args, save_libE_output nworkers, is_manager, libE_specs, _ = parse_args() @@ -63,13 +62,13 @@ def run_simulation(H, persis_info, sim_specs, libE_info): "out": [ # parameters to input into the simulation. ("x", float, (2,)), - ("task", str, max([len(mt_params["name_hifi"]), len(mt_params["name_lofi"])])), + ("task", str, max(len(str(mt_params["name_hifi"])), len(str(mt_params["name_lofi"])))), ("resource_sets", int), ], + "async_return": False, + "batch_size": nworkers - 1, "user": { "range": [1, 8], - # Total max number of sims running concurrently. - "gen_batch_size": nworkers - 1, # Lower bound for the n parameters. "lb": np.array([0, 0]), # Upper bound for the n parameters. @@ -78,11 +77,6 @@ def run_simulation(H, persis_info, sim_specs, libE_info): } gen_specs["user"] = {**gen_specs["user"], **mt_params} -alloc_specs = { - "alloc_f": only_persistent_gens, - "out": [("gen_informed", bool)], - "user": {"async_return": False}, -} # libE logger logger.set_level("INFO") @@ -91,10 +85,10 @@ def run_simulation(H, persis_info, sim_specs, libE_info): exit_criteria = {"sim_max": 20} # Exit after running sim_max simulations # Create a different random number stream for each worker and the manager -persis_info = add_unique_random_streams({}, nworkers + 1) +persis_info = {} # Run LibEnsemble, and store results in history array H -H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, alloc_specs, libE_specs) +H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info, libE_specs=libE_specs) # Save results to numpy file if is_manager: diff --git a/libensemble/tests/standalone_tests/kill_test/build.sh b/libensemble/tests/standalone_tests/kill_test/build.sh index 8e47571e8c..0094221036 100755 --- a/libensemble/tests/standalone_tests/kill_test/build.sh +++ b/libensemble/tests/standalone_tests/kill_test/build.sh @@ -1,2 +1,7 @@ -mpicc -g -o burn_time.x burn_time.c -mpicc -g -o sleep_and_print.x sleep_and_print.c +if [[ "$OSTYPE" == "darwin"* ]]; then + mpicc -cc=clang -g -o burn_time.x burn_time.c + mpicc -cc=clang -g -o sleep_and_print.x sleep_and_print.c +else + mpicc -g -o burn_time.x burn_time.c + mpicc -g -o sleep_and_print.x sleep_and_print.c +fi diff --git a/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py b/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py deleted file mode 100644 index b08bc85fa3..0000000000 --- a/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py +++ /dev/null @@ -1,175 +0,0 @@ -import multiprocessing -import platform - -import pytest - -import libensemble.gen_funcs - -libensemble.gen_funcs.rc.aposmm_optimizers = "nlopt" - -if platform.system() in ["Linux", "Darwin"]: - multiprocessing.set_start_method("fork", force=True) - -import numpy as np - -import libensemble.tests.unit_tests.setup as setup -from libensemble.sim_funcs.six_hump_camel import six_hump_camel_func, six_hump_camel_grad - -libE_info = {"comm": {}} - - -@pytest.mark.extra -def test_persis_aposmm_localopt_test(): - from libensemble.gen_funcs.persistent_aposmm import aposmm - - _, _, gen_specs_0, _, _ = setup.hist_setup1() - - H = np.zeros(4, dtype=[("f", float), ("sim_id", bool), ("dist_to_unit_bounds", float), ("sim_ended", bool)]) - H["sim_ended"] = True - H["sim_id"] = range(len(H)) - gen_specs_0["user"]["localopt_method"] = "BADNAME" - gen_specs_0["user"]["ub"] = np.ones(2) - gen_specs_0["user"]["lb"] = np.zeros(2) - - try: - aposmm(H, {}, gen_specs_0, libE_info) - except NotImplementedError: - assert 1, "Failed because method is unknown." - else: - assert 0 - - -@pytest.mark.extra -def test_update_history_optimal(): - from libensemble.gen_funcs.persistent_aposmm import update_history_optimal - - hist, _, _, _, _ = setup.hist_setup1(n=2) - - H = hist.H - - H["sim_ended"] = True - H["sim_id"] = range(len(H)) - H["f"][0] = -1e-8 - H["x_on_cube"][-1] = 1e-10 - - # Perturb x_opt point to test the case where the reported minimum isn't - # exactly in H. Also, a point in the neighborhood of x_opt has a better - # function value. - opt_ind = update_history_optimal(H["x_on_cube"][-1] + 1e-12, 1, H, np.arange(len(H))) - - assert opt_ind == 9, "Wrong point declared minimum" - - -def combined_func(x): - return six_hump_camel_func(x), six_hump_camel_grad(x) - - -@pytest.mark.extra -def test_standalone_persistent_aposmm(): - from math import gamma, pi, sqrt - - import libensemble.gen_funcs - from libensemble.message_numbers import FINISHED_PERSISTENT_GEN_TAG - from libensemble.sim_funcs.six_hump_camel import six_hump_camel_func, six_hump_camel_grad - from libensemble.tests.regression_tests.support import six_hump_camel_minima as minima - - libensemble.gen_funcs.rc.aposmm_optimizers = "nlopt" - from libensemble.gen_funcs.persistent_aposmm import aposmm - - persis_info = {"rand_stream": np.random.default_rng(1), "nworkers": 4} - - n = 2 - eval_max = 2000 - - gen_out = [("x", float, n), ("x_on_cube", float, n), ("sim_id", int), ("local_min", bool), ("local_pt", bool)] - - gen_specs = { - "in": ["x", "f", "grad", "local_pt", "sim_id", "sim_ended", "x_on_cube", "local_min"], - "out": gen_out, - "user": { - "initial_sample_size": 100, - # 'localopt_method': 'LD_MMA', # Needs gradients - "sample_points": np.round(minima, 1), - "localopt_method": "LN_BOBYQA", - "standalone": { - "eval_max": eval_max, - "obj_func": six_hump_camel_func, - "grad_func": six_hump_camel_grad, - }, - "rk_const": 0.5 * ((gamma(1 + (n / 2)) * 5) ** (1 / n)) / sqrt(pi), - "xtol_abs": 1e-6, - "ftol_abs": 1e-6, - "dist_to_bound_multiple": 0.5, - "max_active_runs": 6, - "lb": np.array([-3, -2]), - "ub": np.array([3, 2]), - }, - } - H = [] - H, persis_info, exit_code = aposmm(H, persis_info, gen_specs, libE_info) - assert exit_code == FINISHED_PERSISTENT_GEN_TAG, "Standalone persistent_aposmm didn't exit correctly" - assert np.sum(H["sim_ended"]) >= eval_max, "Standalone persistent_aposmm, didn't evaluate enough points" - assert persis_info.get("run_order"), "Standalone persistent_aposmm didn't do any localopt runs" - - tol = 1e-3 - min_found = 0 - for m in minima: - # The minima are known on this test problem. - # We use their values to test APOSMM has identified all minima - print(np.min(np.sum((H[H["local_min"]]["x"] - m) ** 2, 1)), flush=True) - if np.min(np.sum((H[H["local_min"]]["x"] - m) ** 2, 1)) < tol: - min_found += 1 - assert min_found >= 6, f"Found {min_found} minima" - - -@pytest.mark.extra -def test_standalone_persistent_aposmm_combined_func(): - from math import gamma, pi, sqrt - - import libensemble.gen_funcs - from libensemble.message_numbers import FINISHED_PERSISTENT_GEN_TAG - from libensemble.tests.regression_tests.support import six_hump_camel_minima as minima - - libensemble.gen_funcs.rc.aposmm_optimizers = "nlopt" - from libensemble.gen_funcs.persistent_aposmm import aposmm - - persis_info = {"rand_stream": np.random.default_rng(1), "nworkers": 4} - - n = 2 - eval_max = 100 - - gen_out = [("x", float, n), ("x_on_cube", float, n), ("sim_id", int), ("local_min", bool), ("local_pt", bool)] - - gen_specs = { - "in": ["x", "f", "grad", "local_pt", "sim_id", "sim_ended", "x_on_cube", "local_min"], - "out": gen_out, - "user": { - "initial_sample_size": 100, - # 'localopt_method': 'LD_MMA', # Needs gradients - "sample_points": np.round(minima, 1), - "localopt_method": "LN_BOBYQA", - "standalone": {"eval_max": eval_max, "obj_and_grad_func": combined_func}, - "rk_const": 0.5 * ((gamma(1 + (n / 2)) * 5) ** (1 / n)) / sqrt(pi), - "xtol_abs": 1e-6, - "ftol_abs": 1e-6, - "dist_to_bound_multiple": 0.5, - "max_active_runs": 6, - "lb": np.array([-3, -2]), - "ub": np.array([3, 2]), - }, - } - - H = [] - persis_info = {"rand_stream": np.random.default_rng(1), "nworkers": 3} - H, persis_info, exit_code = aposmm(H, persis_info, gen_specs, libE_info) - - assert exit_code == FINISHED_PERSISTENT_GEN_TAG, "Standalone persistent_aposmm didn't exit correctly" - assert np.sum(H["sim_ended"]) >= eval_max, "Standalone persistent_aposmm, didn't evaluate enough points" - assert persis_info.get("run_order"), "Standalone persistent_aposmm didn't do any localopt runs" - - -if __name__ == "__main__": - test_persis_aposmm_localopt_test() - test_update_history_optimal() - test_standalone_persistent_aposmm() - test_standalone_persistent_aposmm_combined_func() diff --git a/libensemble/tests/unit_tests/simdir/test_example.json b/libensemble/tests/unit_tests/simdir/test_example.json deleted file mode 100644 index a6f5e2cb8a..0000000000 --- a/libensemble/tests/unit_tests/simdir/test_example.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "libE_specs": { - "use_persis_return_sim": true - }, - "exit_criteria": { - "sim_max": 10 - }, - "sim_specs": { - "sim_f": "numpy.linalg.norm", - "inputs": [ - "x_on_cube" - ], - "outputs": { - "f": { - "type": "float" - }, - "fvec": { - "type": "float", - "size": 3 - } - } - }, - "gen_specs": { - "gen_f": "numpy.random.uniform", - "outputs": { - "priority": { - "type": "float" - }, - "local_pt": { - "type": "bool" - }, - "local_min": { - "type": "bool" - }, - "num_active_runs": { - "type": "int" - }, - "x_on_cube": { - "type": "float" - } - }, - "user": { - "nu": 0 - } - } -} diff --git a/libensemble/tests/unit_tests/simdir/test_example.toml b/libensemble/tests/unit_tests/simdir/test_example.toml deleted file mode 100644 index 1ac156cbb0..0000000000 --- a/libensemble/tests/unit_tests/simdir/test_example.toml +++ /dev/null @@ -1,31 +0,0 @@ -[libE_specs] - use_persis_return_sim = true - -[exit_criteria] - sim_max = 10 - -[sim_specs] - sim_f = "numpy.linalg.norm" - inputs = ["x_on_cube"] - [sim_specs.outputs] - [sim_specs.outputs.f] - type = "float" - [sim_specs.outputs.fvec] - type = "float" - size = 3 - -[gen_specs] - gen_f = "numpy.random.uniform" - [gen_specs.outputs] - [gen_specs.outputs.priority] - type = "float" - [gen_specs.outputs.local_pt] - type = "bool" - [gen_specs.outputs.local_min] - type = "bool" - [gen_specs.outputs.num_active_runs] - type = "int" - [gen_specs.outputs.x_on_cube] - type = "float" - [gen_specs.user] - nu = 0 diff --git a/libensemble/tests/unit_tests/simdir/test_example.yaml b/libensemble/tests/unit_tests/simdir/test_example.yaml deleted file mode 100644 index af2cb34b32..0000000000 --- a/libensemble/tests/unit_tests/simdir/test_example.yaml +++ /dev/null @@ -1,32 +0,0 @@ -libE_specs: - use_persis_return_sim: True - -exit_criteria: - sim_max: 10 - -sim_specs: - sim_f: numpy.linalg.norm - inputs: - - x_on_cube - outputs: - f: - type: float - fvec: - type: float - size: 3 - -gen_specs: - gen_f: numpy.random.uniform - outputs: - priority: - type: float - local_pt: - type: bool - local_min: - type: bool - num_active_runs: - type: int - x_on_cube: - type: float - user: - nu: 0 diff --git a/libensemble/tests/unit_tests/simdir/test_example_badfuncs_attribute.yaml b/libensemble/tests/unit_tests/simdir/test_example_badfuncs_attribute.yaml deleted file mode 100644 index 85b3c90f48..0000000000 --- a/libensemble/tests/unit_tests/simdir/test_example_badfuncs_attribute.yaml +++ /dev/null @@ -1,32 +0,0 @@ -libE_specs: - use_persis_return_gen: True - -exit_criteria: - sim_max: 10 - -sim_specs: - sim_f: numpy.linalg.asdf - inputs: - - x_on_cube - outputs: - f: - type: float - fvec: - type: float - size: 3 - -gen_specs: - gen_f: numpy.random.uniform - outputs: - priority: - type: float - local_pt: - type: bool - local_min: - type: bool - num_active_runs: - type: int - x_on_cube: - type: float - user: - nu: 0 diff --git a/libensemble/tests/unit_tests/simdir/test_example_badfuncs_notfound.yaml b/libensemble/tests/unit_tests/simdir/test_example_badfuncs_notfound.yaml deleted file mode 100644 index da95afd1d9..0000000000 --- a/libensemble/tests/unit_tests/simdir/test_example_badfuncs_notfound.yaml +++ /dev/null @@ -1,32 +0,0 @@ -libE_specs: - use_persis_return_sim: True - -exit_criteria: - sim_max: 10 - -sim_specs: - sim_f: numpy.linalg.norm - inputs: - - x_on_cube - outputs: - f: - type: float - fvec: - type: float - size: 3 - -gen_specs: - gen_f: asdf - outputs: - priority: - type: float - local_pt: - type: bool - local_min: - type: bool - num_active_runs: - type: int - x_on_cube: - type: float - user: - nu: 0 diff --git a/libensemble/tests/unit_tests/test_allocation_funcs_and_support.py b/libensemble/tests/unit_tests/test_allocation_funcs_and_support.py index 6d056b1e01..8eed1f24e5 100644 --- a/libensemble/tests/unit_tests/test_allocation_funcs_and_support.py +++ b/libensemble/tests/unit_tests/test_allocation_funcs_and_support.py @@ -8,7 +8,6 @@ from libensemble.message_numbers import EVAL_GEN_TAG, EVAL_SIM_TAG from libensemble.resources.resources import Resources from libensemble.resources.scheduler import InsufficientResourcesError, ResourceScheduler -from libensemble.tools import add_unique_random_streams from libensemble.tools.alloc_support import AllocException, AllocSupport from libensemble.tools.fields_keys import libE_fields from libensemble.utils.misc import _WorkerIndexer @@ -19,10 +18,10 @@ W = np.array( [ - (1, False, 0, 0, False, False), - (2, False, 0, 0, False, False), - (3, False, 0, 0, False, False), - (4, False, 0, 0, False, False), + (1, False, 0, 0, False), + (2, False, 0, 0, False), + (3, False, 0, 0, False), + (4, False, 0, 0, False), ], dtype=[ ("worker_id", "= 101 @@ -143,8 +96,8 @@ def test_flakey_workflow(): sim_specs=SimSpecs(sim_f=norm_eval), gen_specs=GenSpecs( gen_f=latin_hypercube_sample, + batch_size=100, user={ - "gen_batch_size": 100, "lb": np.array([-3]), "ub": np.array([3]), }, @@ -152,7 +105,6 @@ def test_flakey_workflow(): exit_criteria=ExitCriteria(gen_max=101), ) ens.sim_specs.inputs = (["x"],) # note trailing comma - ens.add_random_streams() ens.run() except ValidationError: flag = 0 @@ -230,13 +182,104 @@ def test_local_comms_without_nworkers(): assert not flag, "'local' ensemble without nworkers should not be created" +def test_ready_missing_sim_callable(): + """ready() should flag a missing sim callable.""" + from libensemble.ensemble import Ensemble + from libensemble.specs import ExitCriteria, LibeSpecs, SimSpecs + + e = Ensemble( + libE_specs=LibeSpecs(comms="local", nworkers=4), + sim_specs=SimSpecs(), # no sim_f or simulator + exit_criteria=ExitCriteria(sim_max=10), + ) + ok, issues = e.ready() + assert not ok, "Should not be ready without a sim callable" + assert any("sim_f" in msg for msg in issues), f"Expected sim_f mention in issues: {issues}" + + +def test_ready_missing_exit_criteria(): + """ready() should flag an exit_criteria with no stop condition.""" + from libensemble.ensemble import Ensemble + from libensemble.sim_funcs.simple_sim import norm_eval + from libensemble.specs import ExitCriteria, LibeSpecs, SimSpecs + + e = Ensemble( + libE_specs=LibeSpecs(comms="local", nworkers=4), + sim_specs=SimSpecs(sim_f=norm_eval), + exit_criteria=ExitCriteria(), # nothing set + ) + ok, issues = e.ready() + assert not ok, "Should not be ready with no exit condition" + assert any("exit_criteria" in msg for msg in issues), f"Expected exit_criteria mention in issues: {issues}" + + +def test_ready_missing_nworkers_local(): + """ready() should flag local comms without nworkers.""" + from libensemble.ensemble import Ensemble + from libensemble.sim_funcs.simple_sim import norm_eval + from libensemble.specs import ExitCriteria, LibeSpecs, SimSpecs + + # Bypass the constructor ValueError by using mpi comms first, + # then patch to local after construction. + e = Ensemble( + libE_specs=LibeSpecs(comms="mpi"), + sim_specs=SimSpecs(sim_f=norm_eval), + exit_criteria=ExitCriteria(sim_max=10), + ) + # Manually force comms=local and nworkers=0 on the internal specs object + e._libE_specs.comms = "local" + e._nworkers = 0 + e._libE_specs.nworkers = 0 + + ok, issues = e.ready() + assert not ok, "Should not be ready with local comms and no nworkers" + assert any("nworkers" in msg for msg in issues), f"Expected nworkers mention in issues: {issues}" + + +def test_ready_field_mismatch(): + """ready() should flag when sim_specs.inputs requests fields not in gen_specs.outputs.""" + from libensemble.ensemble import Ensemble + from libensemble.sim_funcs.simple_sim import norm_eval + from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs + + e = Ensemble( + libE_specs=LibeSpecs(comms="local", nworkers=4), + sim_specs=SimSpecs(sim_f=norm_eval, inputs=["x", "z"]), + gen_specs=GenSpecs(outputs=[("x", float, (1,))]), # missing "z" + exit_criteria=ExitCriteria(sim_max=10), + ) + ok, issues = e.ready() + assert not ok, "Should not be ready with mismatched gen/sim fields" + assert any("z" in msg for msg in issues), f"Expected missing field 'z' in issues: {issues}" + + +def test_ready_happy_path(): + """ready() should return (True, []) for a fully configured ensemble.""" + from libensemble.ensemble import Ensemble + from libensemble.sim_funcs.simple_sim import norm_eval + from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs + + e = Ensemble( + libE_specs=LibeSpecs(comms="local", nworkers=4), + sim_specs=SimSpecs(sim_f=norm_eval, inputs=["x"], outputs=[("f", float)]), + gen_specs=GenSpecs(outputs=[("x", float, (1,))]), + exit_criteria=ExitCriteria(sim_max=10), + ) + ok, issues = e.ready() + assert ok, f"Should be ready but got issues: {issues}" + assert issues == [], f"Issues should be empty but got: {issues}" + + if __name__ == "__main__": test_ensemble_init() test_ensemble_parse_args_false() - test_from_files() - test_bad_func_loads() test_full_workflow() test_flakey_workflow() test_ensemble_specs_update_libE_specs() test_ensemble_prevent_comms_overwrite() test_local_comms_without_nworkers() + test_ready_missing_sim_callable() + test_ready_missing_exit_criteria() + test_ready_missing_nworkers_local() + test_ready_field_mismatch() + test_ready_happy_path() diff --git a/libensemble/tests/unit_tests/test_executor.py b/libensemble/tests/unit_tests/test_executor.py index df5c8cc32d..bf90464e84 100644 --- a/libensemble/tests/unit_tests/test_executor.py +++ b/libensemble/tests/unit_tests/test_executor.py @@ -84,6 +84,10 @@ def build_simfuncs(): app_name = ".".join([sim.split(".")[0], "x"]) if not os.path.isfile(app_name): buildstring = "mpicc -o " + os.path.join("simdir", app_name) + " " + os.path.join("simdir", sim) + if sys.platform == "darwin": + buildstring = ( + "mpicc -cc=clang -o " + os.path.join("simdir", app_name) + " " + os.path.join("simdir", sim) + ) subprocess.check_call(buildstring.split()) @@ -162,7 +166,7 @@ def is_ompi(): # ----------------------------------------------------------------------------- # The following would typically be in the user sim_func. -def polling_loop(exctr, task, timeout_sec=2, delay=0.05): +def polling_loop(exctr, task, timeout_sec=2, delay=0.1): """Iterate over a loop, polling for an exit condition""" start = time.time() @@ -194,7 +198,7 @@ def polling_loop(exctr, task, timeout_sec=2, delay=0.05): return task -def polling_loop_multitask(exctr, task_list, timeout_sec=4.0, delay=0.05): +def polling_loop_multitask(exctr, task_list, timeout_sec=4.0, delay=0.1): """Iterate over a loop, polling for exit conditions on multiple tasks""" start = time.time() @@ -421,7 +425,7 @@ def test_procs_and_machinefile_logic(): f.write(socket.gethostname() + "\n") task = exctr.submit(calc_type="sim", machinefile=machinefilename, app_args=args_for_sim) - task = polling_loop(exctr, task, timeout_sec=4, delay=0.05) + task = polling_loop(exctr, task, timeout_sec=4, delay=0.1) assert task.finished, "task.finished should be True. Returned " + str(task.finished) assert task.state == "FINISHED", "task.state should be FINISHED. Returned " + str(task.state) @@ -437,7 +441,7 @@ def test_procs_and_machinefile_logic(): ) else: task = exctr.submit(calc_type="sim", num_procs=6, num_nodes=2, procs_per_node=3, app_args=args_for_sim) - task = polling_loop(exctr, task, timeout_sec=4, delay=0.05) + task = polling_loop(exctr, task, timeout_sec=4, delay=0.1) time.sleep(0.25) assert task.finished, "task.finished should be True. Returned " + str(task.finished) assert task.state == "FINISHED", "task.state should be FINISHED. Returned " + str(task.state) @@ -462,7 +466,7 @@ def test_procs_and_machinefile_logic(): else: task = exctr.submit(calc_type="sim", num_nodes=2, procs_per_node=3, app_args=args_for_sim) assert 1 - task = polling_loop(exctr, task, timeout_sec=4, delay=0.05) + task = polling_loop(exctr, task, timeout_sec=4, delay=0.1) time.sleep(0.25) assert task.finished, "task.finished should be True. Returned " + str(task.finished) assert task.state == "FINISHED", "task.state should be FINISHED. Returned " + str(task.state) @@ -478,14 +482,14 @@ def test_procs_and_machinefile_logic(): # Testing no num_nodes (should not fail). task = exctr.submit(calc_type="sim", num_procs=2, procs_per_node=2, app_args=args_for_sim) assert 1 - task = polling_loop(exctr, task, timeout_sec=4, delay=0.05) + task = polling_loop(exctr, task, timeout_sec=4, delay=0.1) assert task.finished, "task.finished should be True. Returned " + str(task.finished) assert task.state == "FINISHED", "task.state should be FINISHED. Returned " + str(task.state) # Testing no procs_per_node (shouldn't fail) task = exctr.submit(calc_type="sim", num_nodes=1, num_procs=2, app_args=args_for_sim) assert 1 - task = polling_loop(exctr, task, timeout_sec=4, delay=0.05) + task = polling_loop(exctr, task, timeout_sec=4, delay=0.1) assert task.finished, "task.finished should be True. Returned " + str(task.finished) assert task.state == "FINISHED", "task.state should be FINISHED. Returned " + str(task.state) @@ -936,6 +940,25 @@ def test_non_existent_app_mpi(): assert 0 +def test_non_existent_app_precedent(): + """Tests exception on non-existent app is not thrown if precedent is set. + This is common when running apps in containers, where the executable is not + check-able from the host system.""" + from libensemble.executors.executor import Executor + + print(f"\nTest: {sys._getframe().f_code.co_name}\n") + + exctr = Executor() + + # Can register a non-existent app in case created as part of workflow. + exctr.register_app(full_path=non_existent_app, app_name="nonexist") + + w_exctr = Executor.executor # simulate on worker + + # all should be ok + w_exctr.submit(app_name="nonexist", dry_run=True) + + def test_man_signal_unrec_tag(): print(f"\nTest: {sys._getframe().f_code.co_name}\n") @@ -990,5 +1013,6 @@ def test_man_signal_unrec_tag(): test_dry_run() test_non_existent_app() test_non_existent_app_mpi() + test_non_existent_app_precedent() test_man_signal_unrec_tag() teardown_module(__file__) diff --git a/libensemble/tests/unit_tests/test_executor_gpus.py b/libensemble/tests/unit_tests/test_executor_gpus.py index 1eba92110f..8a1e02700e 100644 --- a/libensemble/tests/unit_tests/test_executor_gpus.py +++ b/libensemble/tests/unit_tests/test_executor_gpus.py @@ -48,6 +48,10 @@ def build_simfuncs(): app_name = ".".join([sim.split(".")[0], "x"]) if not os.path.isfile(app_name): buildstring = "mpicc -o " + os.path.join("simdir", app_name) + " " + os.path.join("simdir", sim) + if sys.platform == "darwin": + buildstring = ( + "mpicc -cc=clang -o " + os.path.join("simdir", app_name) + " " + os.path.join("simdir", sim) + ) subprocess.check_call(buildstring.split()) @@ -118,7 +122,9 @@ def run_check(exp_env, exp_cmd, **kwargs): args_for_sim = "sleep 0" exp_runline = exp_cmd + " simdir/my_simtask.x sleep 0" task = exctr.submit(calc_type="sim", app_args=args_for_sim, dry_run=True, **kwargs) - assert task.env == exp_env, f"Task env does not match expected:\n Received: {task.env}\n Expected: {exp_env}" + for key, value in exp_env.items(): + assert key in task.env, f"Expected env key '{key}' not found in task.env: {task.env}" + assert task.env[key] == value, f"Env key '{key}' has value '{task.env[key]}', expected '{value}'" assert ( task.runline == exp_runline ), f"Run line does not match expected.\n Received: {task.runline}\n Expected: {exp_runline}" diff --git a/libensemble/tests/unit_tests/test_manager_main.py b/libensemble/tests/unit_tests/test_manager_main.py index 4e246eb570..e34bc76301 100644 --- a/libensemble/tests/unit_tests/test_manager_main.py +++ b/libensemble/tests/unit_tests/test_manager_main.py @@ -6,7 +6,7 @@ import libensemble.manager as man import libensemble.tests.unit_tests.setup as setup -libE_specs = {"comms": "local"} +libE_specs = {"comms": "local", "gen_on_worker": True} def test_term_test_1(): diff --git a/libensemble/tests/unit_tests/test_models.py b/libensemble/tests/unit_tests/test_models.py index 8477ef6f62..fa6e2c1f91 100644 --- a/libensemble/tests/unit_tests/test_models.py +++ b/libensemble/tests/unit_tests/test_models.py @@ -1,7 +1,10 @@ import numpy as np +from gest_api.vocs import VOCS from pydantic import ValidationError import libensemble.tests.unit_tests.setup as setup +from libensemble.gen_funcs.sampling import latin_hypercube_sample +from libensemble.sim_funcs.simple_sim import norm_eval from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs, _EnsembleSpecs from libensemble.utils.misc import specs_dump @@ -77,8 +80,8 @@ def test_libe_specs(): libE_specs = {"mpi_comm": Fake_MPI(), "comms": "mpi"} ls = LibeSpecs.model_validate(libE_specs) - libE_specs["sim_input_dir"] = "./simdir" - libE_specs["sim_dir_copy_files"] = ["./simdir"] + libE_specs["sim_input_dir"] = "." + libE_specs["sim_dir_copy_files"] = ["."] ls = LibeSpecs.model_validate(libE_specs) libE_specs = {"comms": "tcp", "nworkers": 4} @@ -91,7 +94,7 @@ def test_libe_specs(): def test_libe_specs_invalid(): - bad_specs = {"comms": "local", "zero_resource_workers": 2, "sim_input_dirs": ["obj"]} + bad_specs = {"comms": "local", "sim_input_dirs": ["obj"]} try: LibeSpecs.model_validate(bad_specs) @@ -116,9 +119,66 @@ def test_ensemble_specs(): _EnsembleSpecs(H0=H0, libE_specs=ls, sim_specs=ss, gen_specs=gs, exit_criteria=ec) +def test_vocs_to_sim_specs(): + """Test that SimSpecs correctly derives inputs and outputs from VOCS""" + + vocs = VOCS( + variables={"x1": [0, 1], "x2": [0, 10]}, + constants={"c1": 1.0}, + objectives={"y1": "MINIMIZE"}, + observables={"o1": float, "o2": int, "o3": (float, (3,))}, + constraints={"con1": ["GREATER_THAN", 0]}, + ) + + ss = SimSpecs(sim_f=norm_eval, vocs=vocs) + + assert ss.inputs == ["x1", "x2", "c1"] + assert len(ss.outputs) == 5 + output_dict = {} + for item in ss.outputs: + if len(item) == 2: + name, dtype = item + output_dict[name] = dtype + else: + name, dtype, shape = item + output_dict[name] = (dtype, shape) + assert output_dict["o1"] == float and output_dict["o2"] == int and output_dict["o3"] == (float, (3,)) + + # Explicit values take precedence + ss2 = SimSpecs(sim_f=norm_eval, vocs=vocs, inputs=["custom"], outputs=[("custom_out", int)]) + + assert ss2.inputs == ["custom"] and ss2.outputs == [("custom_out", int)] + + +def test_vocs_to_gen_specs(): + """Test that GenSpecs correctly derives persis_in and outputs from VOCS""" + + vocs = VOCS( + variables={"x1": [0, 1], "x2": [0, 10]}, + constants={"c1": 1.0}, + objectives={"y1": "MINIMIZE"}, + observables=["obs1"], + constraints={"con1": ["GREATER_THAN", 0]}, + ) + + gs = GenSpecs(gen_f=latin_hypercube_sample, vocs=vocs) + + assert gs.persis_in == ["x1", "x2", "c1", "y1", "obs1", "con1"] + assert len(gs.outputs) == 3 + # All default to float if dtype not specified + for name, dtype in gs.outputs: + assert dtype == float + + # Explicit values take precedence + gs2 = GenSpecs(gen_f=latin_hypercube_sample, vocs=vocs, persis_in=["custom"], out=[("custom_out", int)]) + assert gs2.persis_in == ["custom"] and gs2.outputs == [("custom_out", int)] + + if __name__ == "__main__": test_sim_gen_alloc_exit_specs() test_sim_gen_alloc_exit_specs_invalid() test_libe_specs() test_libe_specs_invalid() test_ensemble_specs() + test_vocs_to_sim_specs() + test_vocs_to_gen_specs() diff --git a/libensemble/tests/unit_tests/test_persistent_aposmm.py b/libensemble/tests/unit_tests/test_persistent_aposmm.py new file mode 100644 index 0000000000..900bfe0704 --- /dev/null +++ b/libensemble/tests/unit_tests/test_persistent_aposmm.py @@ -0,0 +1,644 @@ +import multiprocessing +import platform + +import pytest + +import libensemble.gen_funcs + +libensemble.gen_funcs.rc.aposmm_optimizers = "scipy" + +if platform.system() in ["Linux", "Darwin"]: + multiprocessing.set_start_method("fork", force=True) + +import numpy as np + +import libensemble.tests.unit_tests.setup as setup +from libensemble.sim_funcs.six_hump_camel import six_hump_camel_func, six_hump_camel_grad + +libE_info = {"comm": {}} + + +@pytest.mark.extra +def test_persis_aposmm_localopt_test(): + from libensemble.gen_funcs.persistent_aposmm import aposmm + + _, _, gen_specs_0, _, _ = setup.hist_setup1() + + H = np.zeros(4, dtype=[("f", float), ("sim_id", bool), ("dist_to_unit_bounds", float), ("sim_ended", bool)]) + H["sim_ended"] = True + H["sim_id"] = range(len(H)) + gen_specs_0["user"]["localopt_method"] = "BADNAME" + gen_specs_0["user"]["ub"] = np.ones(2) + gen_specs_0["user"]["lb"] = np.zeros(2) + + try: + aposmm(H, {}, gen_specs_0, libE_info) + except NotImplementedError: + assert 1, "Failed because method is unknown." + else: + assert 0 + + +@pytest.mark.extra +def test_update_history_optimal(): + from libensemble.gen_funcs.persistent_aposmm import update_history_optimal + + hist, _, _, _, _ = setup.hist_setup1(n=2) + + H = hist.H + + H["sim_ended"] = True + H["sim_id"] = range(len(H)) + H["f"][0] = -1e-8 + H["x_on_cube"][-1] = 1e-10 + + # Perturb x_opt point to test the case where the reported minimum isn't + # exactly in H. Also, a point in the neighborhood of x_opt has a better + # function value. + opt_ind = update_history_optimal(H["x_on_cube"][-1] + 1e-12, 1, H, np.arange(len(H))) + + assert opt_ind == 9, "Wrong point declared minimum" + + +def combined_func(x): + return six_hump_camel_func(x), six_hump_camel_grad(x) + + +@pytest.mark.extra +def test_standalone_persistent_aposmm(): + + import libensemble.gen_funcs + from libensemble.message_numbers import FINISHED_PERSISTENT_GEN_TAG + from libensemble.sim_funcs.six_hump_camel import six_hump_camel_func, six_hump_camel_grad + from libensemble.tests.regression_tests.support import six_hump_camel_minima as minima + + libensemble.gen_funcs.rc.aposmm_optimizers = "scipy" + from libensemble.gen_funcs.persistent_aposmm import aposmm + + persis_info = {"rand_stream": np.random.default_rng(1), "nworkers": 4} + + n = 2 + eval_max = 2000 + + gen_out = [("x", float, n), ("x_on_cube", float, n), ("sim_id", int), ("local_min", bool), ("local_pt", bool)] + + gen_specs = { + "in": ["x", "f", "grad", "local_pt", "sim_id", "sim_ended", "x_on_cube", "local_min"], + "out": gen_out, + "user": { + "initial_sample_size": 100, + # 'localopt_method': 'LD_MMA', # Needs gradients + "sample_points": np.round(minima, 1), + "localopt_method": "scipy_Nelder-Mead", + "standalone": { + "eval_max": eval_max, + "obj_func": six_hump_camel_func, + "grad_func": six_hump_camel_grad, + }, + "opt_return_codes": [0], + "nu": 1e-8, + "mu": 1e-8, + "dist_to_bound_multiple": 0.01, + "max_active_runs": 6, + "lb": np.array([-3, -2]), + "ub": np.array([3, 2]), + }, + } + H = [] + H, persis_info, exit_code = aposmm(H, persis_info, gen_specs, libE_info) + assert exit_code == FINISHED_PERSISTENT_GEN_TAG, "Standalone persistent_aposmm didn't exit correctly" + assert np.sum(H["sim_ended"]) >= eval_max, "Standalone persistent_aposmm, didn't evaluate enough points" + assert persis_info.get("run_order"), "Standalone persistent_aposmm didn't do any localopt runs" + + tol = 1e-3 + min_found = 0 + for m in minima: + # The minima are known on this test problem. + # We use their values to test APOSMM has identified all minima + print(np.min(np.sum((H[H["local_min"]]["x"] - m) ** 2, 1)), flush=True) + if np.min(np.sum((H[H["local_min"]]["x"] - m) ** 2, 1)) < tol: + min_found += 1 + assert min_found >= 6, f"Found {min_found} minima" + + +def _evaluate_aposmm_instance(my_APOSMM, minimum_minima=6): + from libensemble.message_numbers import FINISHED_PERSISTENT_GEN_TAG + from libensemble.sim_funcs.six_hump_camel import six_hump_camel_func + from libensemble.tests.regression_tests.support import six_hump_camel_minima as minima + + initial_sample = my_APOSMM.suggest(100) + + total_evals = 0 + eval_max = 2000 + + for point in initial_sample: + point["energy"] = six_hump_camel_func(np.array([point["core"], point["edge"]])) + total_evals += 1 + + my_APOSMM.ingest(initial_sample) + + potential_minima = [] + + while total_evals < eval_max: + + sample, detected_minima = my_APOSMM.suggest(6), my_APOSMM.suggest_updates() + if len(detected_minima): + for m in detected_minima: + potential_minima.append(m) + for point in sample: + point["energy"] = six_hump_camel_func(np.array([point["core"], point["edge"]])) + total_evals += 1 + my_APOSMM.ingest(sample) + my_APOSMM.finalize() + H, persis_info, exit_code = my_APOSMM.export() + + assert exit_code == FINISHED_PERSISTENT_GEN_TAG, "Standalone persistent_aposmm didn't exit correctly" + assert persis_info.get("run_order"), "Standalone persistent_aposmm didn't do any localopt runs" + + assert len(potential_minima) >= 6, f"Found {len(potential_minima)} minima" + + tol = 1e-3 + min_found = 0 + for m in minima: + # The minima are known on this test problem. + # We use their values to test APOSMM has identified all minima + print(np.min(np.sum((H[H["local_min"]]["x"] - m) ** 2, 1)), flush=True) + if np.min(np.sum((H[H["local_min"]]["x"] - m) ** 2, 1)) < tol: + min_found += 1 + assert min_found >= minimum_minima, f"Found {min_found} minima" + + +@pytest.mark.extra +def test_standalone_persistent_aposmm_combined_func(): + + import libensemble.gen_funcs + from libensemble.message_numbers import FINISHED_PERSISTENT_GEN_TAG + from libensemble.tests.regression_tests.support import six_hump_camel_minima as minima + + libensemble.gen_funcs.rc.aposmm_optimizers = "scipy" + from libensemble.gen_funcs.persistent_aposmm import aposmm + + persis_info = {"rand_stream": np.random.default_rng(1), "nworkers": 4} + + n = 2 + eval_max = 100 + + gen_out = [("x", float, n), ("x_on_cube", float, n), ("sim_id", int), ("local_min", bool), ("local_pt", bool)] + + gen_specs = { + "in": ["x", "f", "grad", "local_pt", "sim_id", "sim_ended", "x_on_cube", "local_min"], + "out": gen_out, + "user": { + "initial_sample_size": 100, + # 'localopt_method': 'LD_MMA', # Needs gradients + "sample_points": np.round(minima, 1), + "localopt_method": "scipy_Nelder-Mead", + "standalone": {"eval_max": eval_max, "obj_and_grad_func": combined_func}, + "opt_return_codes": [0], + "nu": 1e-8, + "mu": 1e-8, + "dist_to_bound_multiple": 0.01, + "max_active_runs": 6, + "lb": np.array([-3, -2]), + "ub": np.array([3, 2]), + }, + } + + H = [] + persis_info = {"rand_stream": np.random.default_rng(1), "nworkers": 3} + H, persis_info, exit_code = aposmm(H, persis_info, gen_specs, libE_info) + + assert exit_code == FINISHED_PERSISTENT_GEN_TAG, "Standalone persistent_aposmm didn't exit correctly" + assert np.sum(H["sim_ended"]) >= eval_max, "Standalone persistent_aposmm, didn't evaluate enough points" + assert persis_info.get("run_order"), "Standalone persistent_aposmm didn't do any localopt runs" + + +@pytest.mark.extra +def test_asktell_with_persistent_aposmm(): + + from gest_api.vocs import VOCS + + import libensemble.gen_funcs + from libensemble.gen_classes import APOSMM + from libensemble.tests.regression_tests.support import six_hump_camel_minima as minima + + libensemble.gen_funcs.rc.aposmm_optimizers = "scipy" + + variables = {"core": [-3, 3], "edge": [-2, 2], "core_on_cube": [0, 1], "edge_on_cube": [0, 1]} + objectives = {"energy": "MINIMIZE"} + + variables_mapping = { + "x": ["core", "edge"], + "x_on_cube": ["core_on_cube", "edge_on_cube"], + "f": ["energy"], + } + + vocs = VOCS(variables=variables, objectives=objectives) + + my_APOSMM = APOSMM( + vocs, + max_active_runs=6, + initial_sample_size=100, + variables_mapping=variables_mapping, + sample_points=np.round(minima, 1), + localopt_method="scipy_Nelder-Mead", + opt_return_codes=[0], + nu=1e-8, + mu=1e-8, + dist_to_bound_multiple=0.01, + ) + + _evaluate_aposmm_instance(my_APOSMM) + + +@pytest.mark.extra +def test_asktell_errors(): + + from gest_api.vocs import VOCS + + import libensemble.gen_funcs + from libensemble.gen_classes import APOSMM + from libensemble.tests.regression_tests.support import six_hump_camel_minima as minima + + libensemble.gen_funcs.rc.aposmm_optimizers = "scipy" + + variables = {"core": [-3, 3], "edge": [-2, 2], "core_on_cube": [0, 1], "edge_on_cube": [0, 1]} + objectives = {"energy": "MINIMIZE"} + + bad_mapping = { + "x": ["core", "edge"], + "f": ["energy"], + } + + vocs = VOCS( + variables=variables, + objectives=objectives, + constraints={"c1": ["LESS_THAN", 0]}, + constants={"alpha": 0.55}, + observables={"o1"}, + ) + + with pytest.raises(ValueError): + APOSMM( + vocs, + max_active_runs=6, + variables_mapping=bad_mapping, + initial_sample_size=100, + sample_points=np.round(minima, 1), + localopt_method="scipy_Nelder-Mead", + opt_return_codes=[0], + nu=1e-8, + mu=1e-8, + dist_to_bound_multiple=0.01, + ) + pytest.fail("Should have raised error for bad mapping") + + bad_mapping = { + "x": ["core", "edge"], + "x_on_cube": ["core_on_cube", "edge_on_cube", "blah"], + "f": ["energy"], + } + + with pytest.raises(ValueError): + APOSMM( + vocs, + max_active_runs=6, + variables_mapping=bad_mapping, + initial_sample_size=100, + sample_points=np.round(minima, 1), + localopt_method="scipy_Nelder-Mead", + opt_return_codes=[0], + nu=1e-8, + mu=1e-8, + dist_to_bound_multiple=0.01, + ) + pytest.fail("Should have raised error for bad mapping") + + variables_mapping = { + "x": ["core", "edge"], + "x_on_cube": ["core_on_cube", "edge_on_cube"], + "f": ["energy"], + } + + vocs = VOCS(variables=variables, objectives=objectives) + + my_APOSMM = APOSMM( + vocs, + max_active_runs=6, + initial_sample_size=6, + variables_mapping=variables_mapping, + localopt_method="scipy_Nelder-Mead", + opt_return_codes=[0], + nu=1e-8, + mu=1e-8, + dist_to_bound_multiple=0.01, + ) + + my_APOSMM.suggest() + with pytest.raises(RuntimeError): + my_APOSMM.suggest() + pytest.fail("Should've failed on consecutive empty suggests") + + my_APOSMM = APOSMM( + vocs, + max_active_runs=6, + initial_sample_size=6, + variables_mapping=variables_mapping, + localopt_method="scipy_Nelder-Mead", + opt_return_codes=[0], + nu=1e-8, + mu=1e-8, + dist_to_bound_multiple=0.5, + ) + + with pytest.raises(RuntimeError): + my_APOSMM.finalize() + pytest.fail("Should've failed on finalize before start") + + my_APOSMM = APOSMM( + vocs, + max_active_runs=6, + initial_sample_size=6, + variables_mapping=variables_mapping, + localopt_method="scipy_Nelder-Mead", + opt_return_codes=[0], + nu=1e-8, + mu=1e-8, + dist_to_bound_multiple=0.01, + ) + + my_APOSMM.suggest() + with pytest.raises(RuntimeError): + my_APOSMM.setup() + pytest.fail("Should've failed on consecutive setup") + my_APOSMM.finalize() + + from libensemble.utils.runners import Runner + + def gest_style_sim(_): + return {"energy": 0.0} + + runner = Runner({"sim_f": gest_style_sim}) + with pytest.raises(AttributeError, match="SimSpecs.simulator"): + runner.run(np.zeros(1), {"persis_info": {}, "libE_info": {}}) + + +@pytest.mark.extra +def test_asktell_ingest_first(): + from math import gamma, pi, sqrt + + from gest_api.vocs import VOCS + + import libensemble.gen_funcs + from libensemble.gen_classes import APOSMM + from libensemble.sim_funcs.six_hump_camel import six_hump_camel_func + from libensemble.tests.regression_tests.support import six_hump_camel_minima as minima + + libensemble.gen_funcs.rc.aposmm_optimizers = "nlopt" + + n = 2 + + variables = {"core": [-3, 3], "edge": [-2, 2], "core_on_cube": [0, 1], "edge_on_cube": [0, 1]} + objectives = {"energy": "MINIMIZE"} + + variables_mapping = { + "x": ["core", "edge"], + "x_on_cube": ["core_on_cube", "edge_on_cube"], + "f": ["energy"], + } + + vocs = VOCS(variables=variables, objectives=objectives) + + my_APOSMM = APOSMM( + vocs, + max_active_runs=6, + initial_sample_size=6, + variables_mapping=variables_mapping, + localopt_method="LN_BOBYQA", + opt_return_codes=[0], + rk_const=0.5 * ((gamma(1 + (n / 2)) * 5) ** (1 / n)) / sqrt(pi), + xtol_abs=1e-6, + ftol_abs=1e-6, + dist_to_bound_multiple=0.01, + ) + + # local_H["x_on_cube"][-num_pts:] = (pts - lb) / (ub - lb) + initial_sample = [ + { + "core": minima[i][0], + "edge": minima[i][1], + "core_on_cube": (minima[i][0] - variables["core"][0]) / (variables["core"][1] - variables["core"][0]), + "edge_on_cube": (minima[i][1] - variables["edge"][0]) / (variables["edge"][1] - variables["edge"][0]), + "energy": six_hump_camel_func(np.array([minima[i][0], minima[i][1]])), + } + for i in range(6) + ] + my_APOSMM.ingest(initial_sample) + + total_evals = 0 + eval_max = 2000 + + potential_minima = [] + + while total_evals < eval_max: + + sample, detected_minima = my_APOSMM.suggest(6), my_APOSMM.suggest_updates() + if len(detected_minima): + for m in detected_minima: + potential_minima.append(m) + for point in sample: + point["energy"] = six_hump_camel_func(np.array([point["core"], point["edge"]])) + total_evals += 1 + my_APOSMM.ingest(sample) + my_APOSMM.finalize() + H, persis_info, exit_code = my_APOSMM.export() + + assert persis_info.get("run_order"), "Standalone persistent_aposmm didn't do any localopt runs" + + assert len(potential_minima) >= 6, f"Found {len(potential_minima)} minima" + + tol = 1e-4 + min_found = 0 + for m in minima: + # The minima are known on this test problem. + # We use their values to test APOSMM has identified all minima + print(np.min(np.sum((H[H["local_min"]]["x"] - m) ** 2, 1)), flush=True) + if np.min(np.sum((H[H["local_min"]]["x"] - m) ** 2, 1)) < tol: + min_found += 1 + assert min_found >= 4, f"Found {min_found} minima" + + +@pytest.mark.extra +def test_asktell_consecutive_during_sample(): + """Test consecutive suggest and ingest during sample""" + + from gest_api.vocs import VOCS + + import libensemble.gen_funcs + from libensemble.gen_classes import APOSMM + from libensemble.tests.regression_tests.support import six_hump_camel_minima as minima + + libensemble.gen_funcs.rc.aposmm_optimizers = "scipy" + + variables = {"core": [-3, 3], "edge": [-2, 2], "core_on_cube": [0, 1], "edge_on_cube": [0, 1]} + objectives = {"energy": "MINIMIZE"} + + variables_mapping = { + "x": ["core", "edge"], + "x_on_cube": ["core_on_cube", "edge_on_cube"], + "f": ["energy"], + } + + vocs = VOCS(variables=variables, objectives=objectives) + + my_APOSMM = APOSMM( + vocs, + max_active_runs=6, + initial_sample_size=6, + variables_mapping=variables_mapping, + localopt_method="scipy_Nelder-Mead", + opt_return_codes=[0], + nu=1e-8, + mu=1e-8, + dist_to_bound_multiple=0.01, + ) + + # Test consecutive suggest + first = my_APOSMM.suggest(1) + first[0]["energy"] = six_hump_camel_func(np.array([first[0]["core"], first[0]["edge"]])) + my_APOSMM.ingest(first) + second = my_APOSMM.suggest(1) + second += my_APOSMM.suggest(4) + for point in second: + point["energy"] = six_hump_camel_func(np.array([point["core"], point["edge"]])) + # Test consecutive ingest + my_APOSMM.ingest(second[:3]) + my_APOSMM.ingest(second[3:]) + + total_evals = 0 + eval_max = 2000 + + potential_minima = [] + + while total_evals < eval_max: + + sample, detected_minima = my_APOSMM.suggest(3), my_APOSMM.suggest_updates() + sample += my_APOSMM.suggest(3) + if len(detected_minima): + for m in detected_minima: + potential_minima.append(m) + for point in sample: + point["energy"] = six_hump_camel_func(np.array([point["core"], point["edge"]])) + total_evals += 1 + my_APOSMM.ingest(sample) + + my_APOSMM.finalize() + H, persis_info, exit_code = my_APOSMM.export() + + assert persis_info.get("run_order"), "Standalone persistent_aposmm didn't do any localopt runs" + + assert len(potential_minima) >= 6, f"Found {len(potential_minima)} minima" + + tol = 1e-3 + min_found = 0 + for m in minima: + # The minima are known on this test problem. + # We use their values to test APOSMM has identified all minima + print(np.min(np.sum((H[H["local_min"]]["x"] - m) ** 2, 1)), flush=True) + if np.min(np.sum((H[H["local_min"]]["x"] - m) ** 2, 1)) < tol: + min_found += 1 + assert min_found >= 4, f"Found {min_found} minima" + + +def _run_aposmm_export_test(variables_mapping): + """Helper function to run APOSMM export tests with given variables_mapping""" + from gest_api.vocs import VOCS + + from libensemble.gen_classes import APOSMM + + variables = { + "core": [-3, 3], + "edge": [-2, 2], + "core_on_cube": [0, 1], + "edge_on_cube": [0, 1], + } + objectives = {"energy": "MINIMIZE"} + + vocs = VOCS(variables=variables, objectives=objectives) + aposmm = APOSMM( + vocs, + max_active_runs=6, + initial_sample_size=10, + variables_mapping=variables_mapping, + localopt_method="scipy_Nelder-Mead", + opt_return_codes=[0], + nu=1e-8, + mu=1e-8, + dist_to_bound_multiple=0.01, + ) + # Test basic export before finalize + H, _, _ = aposmm.export() + print(f"Export before finalize: {H}") # Debug + assert H is None # Should be None before finalize + # Test export after suggest/ingest cycle + sample = aposmm.suggest(5) + for point in sample: + point["energy"] = 1.0 # Mock evaluation + aposmm.ingest(sample) + aposmm.finalize() + + # Test export with unmapped fields + H, _, _ = aposmm.export() + if H is not None: + assert "x" in H.dtype.names and H["x"].ndim == 2 + assert "f" in H.dtype.names and H["f"].ndim == 1 + + # Test export with vocs_field_names + H_unmapped, _, _ = aposmm.export(vocs_field_names=True) + print(f"H_unmapped: {H_unmapped}") # Debug + if H_unmapped is not None: + assert "core" in H_unmapped.dtype.names + assert "edge" in H_unmapped.dtype.names + assert "energy" in H_unmapped.dtype.names + # Test export with as_dicts + H_dicts, _, _ = aposmm.export(as_dicts=True) + assert isinstance(H_dicts, list) + assert isinstance(H_dicts[0], dict) + assert "x" in H_dicts[0] # x remains as array + assert "f" in H_dicts[0] + # Test export with both options + H_both, _, _ = aposmm.export(vocs_field_names=True, as_dicts=True) + assert isinstance(H_both, list) + assert "core" in H_both[0] + assert "edge" in H_both[0] + assert "energy" in H_both[0] + + +@pytest.mark.extra +def test_aposmm_export(): + """Test APOSMM export function with different options""" + + # Test with full variables_mapping + full_mapping = { + "x": ["core", "edge"], + "x_on_cube": ["core_on_cube", "edge_on_cube"], + "f": ["energy"], + } + _run_aposmm_export_test(full_mapping) + # Test with just x_on_cube mapping (should auto-map x and f) + minimal_mapping = { + "x_on_cube": ["core_on_cube", "edge_on_cube"], + } + _run_aposmm_export_test(minimal_mapping) + + +if __name__ == "__main__": + test_persis_aposmm_localopt_test() + test_update_history_optimal() + test_standalone_persistent_aposmm() + test_standalone_persistent_aposmm_combined_func() + test_asktell_with_persistent_aposmm() + test_asktell_ingest_first() + test_asktell_consecutive_during_sample() + test_asktell_errors() + test_aposmm_export() diff --git a/libensemble/tests/unit_tests/test_resources.py b/libensemble/tests/unit_tests/test_resources.py index b87583f377..b79a5eeffe 100644 --- a/libensemble/tests/unit_tests/test_resources.py +++ b/libensemble/tests/unit_tests/test_resources.py @@ -688,40 +688,6 @@ def test_get_local_nodelist_distrib_mode_uneven_split(tmp_path): os.remove(tmp_path / "node_list") -def test_map_workerid_to_index(): - num_workers = 4 - num_rsets = 4 - - zero_resource_list = [] - index_list = ResourceManager.get_index_list(num_workers, num_rsets, zero_resource_list) - for workerID in range(1, num_workers + 1): - index = index_list[workerID - 1] - assert index == workerID - 1, "index incorrect. Received: " + str(index) - - zero_resource_list = [1] - index_list = ResourceManager.get_index_list(num_workers, num_rsets, zero_resource_list) - for workerID in range(2, num_workers + 1): - index = index_list[workerID - 1] - assert index == workerID - 2, "index incorrect. Received: " + str(index) - - zero_resource_list = [1, 2] - index_list = ResourceManager.get_index_list(num_workers, num_rsets, zero_resource_list) - for workerID in range(3, num_workers + 1): - index = index_list[workerID - 1] - assert index == workerID - 3, "index incorrect. Received: " + str(index) - - zero_resource_list = [1, 3] - index_list = ResourceManager.get_index_list(num_workers, num_rsets, zero_resource_list) - - workerID = 2 - index = index_list[workerID - 1] - assert index == 0, "index incorrect. Received: " + str(index) - - workerID = 4 - index = index_list[workerID - 1] - assert index == 1, "index incorrect. Received: " + str(index) - - def test_get_group_list(): # 8 resource sets on different nodes -------------------------------------- split_list = [ @@ -1033,7 +999,6 @@ def test_wresources_set_no_gpus(): test_get_local_nodelist_distrib_mode() test_get_local_nodelist_distrib_mode_uneven_split() - test_map_workerid_to_index() test_get_group_list() test_machinefile_from_resources() test_wresources_set_gpus() diff --git a/libensemble/tests/unit_tests/test_ufunc_runners.py b/libensemble/tests/unit_tests/test_ufunc_runners.py index 09f17b07ec..79fda7c28a 100644 --- a/libensemble/tests/unit_tests/test_ufunc_runners.py +++ b/libensemble/tests/unit_tests/test_ufunc_runners.py @@ -30,8 +30,8 @@ def get_ufunc_args(): def test_normal_runners(): calc_in, sim_specs, gen_specs = get_ufunc_args() - simrunner = Runner(sim_specs) - genrunner = Runner(gen_specs) + simrunner = Runner.from_specs(sim_specs) + genrunner = Runner.from_specs(gen_specs) assert not hasattr(simrunner, "globus_compute_executor") and not hasattr( genrunner, "globus_compute_executor" ), "Globus Compute use should not be detected without setting endpoint fields" @@ -47,7 +47,7 @@ def tupilize(arg1, arg2): sim_specs["sim_f"] = tupilize persis_info = {"hello": "threads"} - simrunner = Runner(sim_specs) + simrunner = Runner.from_specs(sim_specs) result = simrunner._result(calc_in, persis_info, {}) assert result == (calc_in, persis_info) assert hasattr(simrunner, "thread_handle") @@ -75,7 +75,7 @@ def test_globus_compute_runner_init(): sim_specs["globus_compute_endpoint"] = "1234" with mock.patch("globus_compute_sdk.Executor"): - runner = Runner(sim_specs) + runner = Runner.from_specs(sim_specs) assert hasattr( runner, "globus_compute_executor" @@ -89,7 +89,7 @@ def test_globus_compute_runner_pass(): sim_specs["globus_compute_endpoint"] = "1234" with mock.patch("globus_compute_sdk.Executor"): - runner = Runner(sim_specs) + runner = Runner.from_specs(sim_specs) # Creating Mock Globus ComputeExecutor and Globus Compute future object - no exception globus_compute_mock = mock.Mock() @@ -112,10 +112,10 @@ def test_globus_compute_runner_pass(): def test_globus_compute_runner_fail(): calc_in, sim_specs, gen_specs = get_ufunc_args() - gen_specs["globus_compute_endpoint"] = "4321" + sim_specs["globus_compute_endpoint"] = "4321" with mock.patch("globus_compute_sdk.Executor"): - runner = Runner(gen_specs) + runner = Runner.from_specs(sim_specs) # Creating Mock Globus ComputeExecutor and Globus Compute future object - yes exception globus_compute_mock = mock.Mock() @@ -124,12 +124,12 @@ def test_globus_compute_runner_fail(): globus_compute_future.exception.return_value = Exception runner.globus_compute_executor = globus_compute_mock - runners = {2: runner.run} + runners = {1: runner.run} libE_info = {"H_rows": np.array([2, 3, 4]), "workerID": 1, "comm": "fakecomm"} with pytest.raises(Exception): - out, persis_info = runners[2](calc_in, {"libE_info": libE_info, "persis_info": {}, "tag": 2}) + out, persis_info = runners[1](calc_in, {"libE_info": libE_info, "persis_info": {}, "tag": 1}) pytest.fail("Expected exception") diff --git a/libensemble/tools/__init__.py b/libensemble/tools/__init__.py index cb7612f483..71254c51bf 100644 --- a/libensemble/tools/__init__.py +++ b/libensemble/tools/__init__.py @@ -1,12 +1,12 @@ from .forkable_pdb import ForkablePdb from .parse_args import parse_args -from .tools import add_unique_random_streams, check_npy_file_exists, eprint, save_libE_output +from .tools import check_npy_file_exists, eprint, get_rng, save_libE_output __all__ = [ - "add_unique_random_streams", "check_npy_file_exists", "eprint", "ForkablePdb", + "get_rng", "parse_args", "save_libE_output", ] diff --git a/libensemble/tools/alloc_support.py b/libensemble/tools/alloc_support.py index bbb5e275e4..6fda1e6b27 100644 --- a/libensemble/tools/alloc_support.py +++ b/libensemble/tools/alloc_support.py @@ -4,7 +4,11 @@ from libensemble.message_numbers import EVAL_GEN_TAG, EVAL_SIM_TAG from libensemble.resources.resources import Resources -from libensemble.resources.scheduler import InsufficientFreeResources, InsufficientResourcesError, ResourceScheduler +from libensemble.resources.scheduler import ( # noqa + InsufficientFreeResources, + InsufficientResourcesError, + ResourceScheduler, +) from libensemble.utils.misc import extract_H_ranges logger = logging.getLogger(__name__) @@ -76,28 +80,24 @@ def assign_resources(self, rsets_req, use_gpus=None, user_params=[]): """ rset_team = None if self.resources is not None: - # Try schedule to non-gpu rsets first - if use_gpus is None: + # When GPUs exist and use_gpus not explicitly set, try GPU rsets first + if use_gpus is None and self.sched.resources.total_num_gpu_rsets > 0: try: - rset_team = self.sched.assign_resources(rsets_req, use_gpus=False, user_params=user_params) + rset_team = self.sched.assign_resources(rsets_req, use_gpus=True, user_params=user_params) return rset_team - except (InsufficientFreeResources, InsufficientResourcesError): - pass + except InsufficientResourcesError: + pass # More rsets requested than GPU rsets exist - fall back to any rset_team = self.sched.assign_resources(rsets_req, use_gpus, user_params) return rset_team - def avail_worker_ids(self, persistent=None, active_recv=False, zero_resource_workers=None, gen_workers=None): + def avail_worker_ids(self, persistent=None, active_recv=False, gen_workers=None): """Returns available workers as a list of IDs, filtered by the given options. :param persistent: (Optional) Int. Only return workers with given ``persis_state`` (1=sim, 2=gen). :param active_recv: (Optional) Boolean. Only return workers with given active_recv state. - :param zero_resource_workers: (Optional) Boolean. Only return workers that require no resources. :param gen_workers: (Optional) Boolean. If True, return gen-only workers. If False, return all other workers. :returns: List of worker IDs. - - If there are no zero resource workers defined, then the ``zero_resource_workers`` argument will - be ignored. """ # For abbrev. @@ -106,12 +106,6 @@ def fltr_persis(): return True return wrk["persis_state"] == persistent - def fltr_zrw(): - # If none exist or you did not ask for zrw then return True - if no_zrw or zero_resource_workers is None: - return True - return wrk["zero_resource_worker"] == zero_resource_workers - def fltr_recving(): if active_recv: return wrk["active_recv"] @@ -126,13 +120,12 @@ def fltr_gen_workers(): if active_recv and not persistent: raise AllocException("Cannot ask for non-persistent active receive workers") - # If there are no zero resource workers - then ignore zrw (i.e., use only if they exist) - no_zrw = not any(self.W["zero_resource_worker"]) + # If there are no gen_workers - then ignore gen_workers no_gen_workers = not any(self.W["gen_worker"]) wrks = [] for wrk in self.W: - if fltr_recving() and fltr_persis() and fltr_zrw() and fltr_gen_workers(): + if fltr_recving() and fltr_persis() and fltr_gen_workers(): wrks.append(wrk["worker_id"]) return wrks @@ -153,18 +146,13 @@ def _req_resources_sim(self, libE_info, user_params, H, H_rows): use_gpus = None if "resource_sets" in H.dtype.names: num_rsets_req = np.max(H[H_rows]["resource_sets"]) # sim rsets - elif "num_procs" in H.dtype.names: + elif "num_procs" in H.dtype.names and self.resources: procs_per_rset = self.resources.resource_manager.procs_per_rset num_rsets_req = AllocSupport._convert_rows_to_rsets( libE_info, user_params, H, H_rows, procs_per_rset, "num_procs" ) else: num_rsets_req = 1 - if "use_gpus" in H.dtype.names: - if np.any(H[H_rows]["use_gpus"]): - use_gpus = True - else: - use_gpus = False if "num_gpus" in H.dtype.names: gpus_per_rset = self.resources.resource_manager.gpus_per_rset num_rsets_req_for_gpus = AllocSupport._convert_rows_to_rsets( @@ -183,13 +171,13 @@ def _req_resources_gen(self, libE_info, user_params): use_gpus = self.persis_info.get("gen_use_gpus", None) # can be overwritten below if not num_rsets_req: gen_nprocs = self.persis_info.get("gen_num_procs", self.def_gen_num_procs) - if gen_nprocs: + if gen_nprocs and self.resources: procs_per_rset = self.resources.resource_manager.procs_per_rset num_rsets_req = AllocSupport._convert_to_rsets( libE_info, user_params, procs_per_rset, gen_nprocs, "num_procs" ) gen_ngpus = self.persis_info.get("gen_num_gpus", self.def_gen_num_gpus) - if gen_ngpus: + if gen_ngpus and self.resources: gpus_per_rset = self.resources.resource_manager.gpus_per_rset num_rsets_req_for_gpus = AllocSupport._convert_to_rsets( libE_info, user_params, gpus_per_rset, gen_ngpus, "num_gpus" @@ -280,6 +268,7 @@ def gen_work(self, wid, H_fields, H_rows, persis_info, **libE_info): H_fields = AllocSupport._check_H_fields(H_fields) libE_info["H_rows"] = AllocSupport._check_H_rows(H_rows) + libE_info["batch_size"] = len(self.avail_worker_ids(gen_workers=False)) work = { "H_fields": H_fields, diff --git a/libensemble/tools/live_data/live_data.py b/libensemble/tools/live_data/live_data.py index 88d1cebcb2..7d50d75a8b 100644 --- a/libensemble/tools/live_data/live_data.py +++ b/libensemble/tools/live_data/live_data.py @@ -1,8 +1,6 @@ from abc import ABC, abstractmethod -from typing import TYPE_CHECKING -if TYPE_CHECKING: - import numpy.typing as npt +import numpy.typing as npt class LiveData(ABC): diff --git a/libensemble/tools/parse_args.py b/libensemble/tools/parse_args.py index 5e302c0ce0..9f52129d9b 100644 --- a/libensemble/tools/parse_args.py +++ b/libensemble/tools/parse_args.py @@ -1,6 +1,7 @@ import argparse import os import sys +from pathlib import Path # ==================== Command-line argument parsing =========================== @@ -32,13 +33,6 @@ parser.add_argument("--tester_args", type=str, nargs="*", help="Additional arguments for use by specific testers") -def _get_zrw(nworkers, nsim_workers): - """Determine zero resource workers from workers and sim workers""" - ngen_workers = nworkers - nsim_workers - assert ngen_workers > 0, "nsim_workers cannot be greater than number of workers" - return [i for i in range(1, ngen_workers + 1)] - - def mpi_init(mpi_comm): """Initialize MPI""" from mpi4py import MPI, rc @@ -64,7 +58,6 @@ def _mpi_parse_args(args): # Convenience option which sets other libE_specs options. nsim_workers = args.nsim_workers if nsim_workers is not None: - # libE_specs["zero_resource_workers"] = _get_zrw(nworkers, nsim_workers) libE_specs["num_resource_sets"] = libE_specs.get("num_resource_sets", nsim_workers) return nworkers, is_manager, libE_specs, args.tester_args @@ -82,7 +75,6 @@ def _local_parse_args(args): nsim_workers = args.nsim_workers if nsim_workers is not None: nworkers = nworkers or nsim_workers + 1 - # libE_specs["zero_resource_workers"] = _get_zrw(nworkers, nsim_workers) libE_specs["num_resource_sets"] = libE_specs.get("num_resource_sets", nsim_workers) nworkers = nworkers or 4 @@ -115,9 +107,9 @@ def _tcp_parse_args(args): def _ssh_parse_args(args): """Parses arguments for SSH with reverse tunnel.""" nworkers = len(args.workers) - worker_pwd = args.worker_pwd or os.getcwd() + worker_pwd = Path(args.worker_pwd) if args.worker_pwd else Path.cwd() script_dir, script_name = os.path.split(sys.argv[0]) - worker_script_name = os.path.join(worker_pwd, script_name) + worker_script_name = worker_pwd / script_name ssh = ["ssh", "-R", "{tunnel_port}:localhost:{manager_port}", "{worker_ip}"] cmd = [ args.worker_python, @@ -157,7 +149,7 @@ def _client_parse_args(args): def parse_args(): """ - Parses command-line arguments. Use in calling script. + Parses command-line arguments. .. code-block:: python @@ -197,7 +189,7 @@ def parse_args(): --nworkers/-n, (For 'local' or 'tcp' comms) Set number of workers. --nresource_sets, Explicitly set the number of resource sets. This sets libE_specs["num_resource_sets"]. By default, resources will be - divided by workers (excluding zero_resource_workers). + divided by workers. --nsim_workers, (For 'local" or 'mpi' comms) A convenience option for cases with persistent generators - sets the number of simulation workers. If used with no other criteria, one additional worker for running a @@ -213,7 +205,7 @@ def parse_args(): $ python calling_script --nworkers 4 $ python calling_script -n 4 - Run with 'local' comms and 5 workers - one gen worker (no resources), and 4 sim workers. + Run with 'local' comms and 5 workers - one gen worker and 4 sim workers. $ python calling_script --comms local --nsim_workers 4 Run with 'local' comms with 4 workers and 8 resource sets. The extra resource sets will @@ -234,7 +226,7 @@ def parse_args(): libE_specs: :obj:`dict` Settings and specifications for libEnsemble - :doc:`(example)` + :doc:`(example)` """ args, misc_args = parser.parse_known_args(sys.argv[1:]) diff --git a/libensemble/tools/test_support.py b/libensemble/tools/test_support.py index 9f62b1e121..bad9c2a789 100644 --- a/libensemble/tools/test_support.py +++ b/libensemble/tools/test_support.py @@ -242,6 +242,16 @@ def check_gpu_setting(task, assert_setting=True, print_setting=False, resources= print(f"Worker {task.workerID}: {desc}GPU setting ({stype}): {gpu_setting} {addon}", flush=True) if assert_setting: - assert ( - gpu_setting == expected - ), f"Worker {task.workerID}: Found GPU setting: {gpu_setting}, Expected: {expected}" + if isinstance(expected, dict): + for key, value in expected.items(): + assert key in gpu_setting, ( + f"Worker {task.workerID}: Expected env key '{key}' not found in GPU setting: {gpu_setting}" + ) + assert gpu_setting[key] == value, ( + f"Worker {task.workerID}: GPU setting key '{key}' has value '{gpu_setting[key]}', " + f"expected '{value}'" + ) + else: + assert ( + gpu_setting == expected + ), f"Worker {task.workerID}: Found GPU setting: {gpu_setting}, Expected: {expected}" diff --git a/libensemble/tools/tools.py b/libensemble/tools/tools.py index 8cb81f3af3..859664f5d3 100644 --- a/libensemble/tools/tools.py +++ b/libensemble/tools/tools.py @@ -1,5 +1,5 @@ """ -The libEnsemble utilities module assists in writing consistent calling scripts +The libEnsemble utilities module assists in writing consistent top-level scripts and user functions. """ @@ -8,6 +8,7 @@ import pickle import sys import time +from pathlib import Path import numpy as np import numpy.typing as npt @@ -61,20 +62,6 @@ + "\n\n" ) -# ==================== Warning that persistent return data is not used ========== - -_PERSIS_RETURN_WARNING = ( - "\n" - + 79 * "*" - + "\n" - + "A persistent worker has returned history data on shutdown. This data is\n" - + "not currently added to the manager's history to avoid possibly overwriting, but\n" - + "will be added to the manager's history in a future release. If you want to\n" - + "overwrite/append, you can set the libE_specs option ``use_persis_return_gen``\n" - + "or ``use_persis_return_sim``" - "\n" + 79 * "*" + "\n\n" -) - def _get_shortname(basename): script_name = os.path.splitext(os.path.basename(basename))[0] @@ -90,7 +77,7 @@ def save_libE_output( persis_info: dict, basename: str, nworkers: int, - dest_path: str = None, + dest_path: str | Path = "", mess: str = "Run completed", append_attrs: bool = True, ) -> str: @@ -140,8 +127,10 @@ def save_libE_output( Append run attributes to the base filename. """ - if dest_path is None: - dest_path = os.getcwd() + if not dest_path: + dest_path = Path.cwd() + else: + dest_path = Path(dest_path) short_name = _get_shortname(basename) @@ -151,73 +140,40 @@ def save_libE_output( hist_name = "_history_" + prob_str persis_name = "_persis_info_" + prob_str - h_filename = os.path.join(dest_path, short_name + hist_name) - p_filename = os.path.join(dest_path, short_name + persis_name) + h_filename = dest_path / (short_name + hist_name) + p_filename = dest_path / (short_name + persis_name) status_mess = " ".join(["------------------", mess, "-------------------"]) logger.info(f"{status_mess}\nSaving results to file: {h_filename}") np.save(h_filename, H) - with open(p_filename + ".pickle", "wb") as f: + with open(p_filename.with_suffix(".pickle"), "wb") as f: pickle.dump(persis_info, f) - return h_filename + ".npy" - - -# ===================== per-process numpy random-streams ======================= + return str(h_filename.with_suffix(".npy")) -def add_unique_random_streams(persis_info: dict, nstreams: int, seed: str = "") -> dict: +def get_rng(gen_specs: dict, libE_info: dict) -> np.random.Generator: """ - Creates nstreams random number streams for the libE manager and workers - when nstreams is num_workers + 1. Stream i is initialized with seed i by default. - Otherwise the streams can be initialized with a provided seed. + Returns a numpy random number generator. - The entries are appended to the provided persis_info dictionary. - - .. code-block:: python - - persis_info = add_unique_random_streams(old_persis_info, nworkers + 1) + If ``gen_seed`` is provided in ``gen_specs["user"]``, the generator is + initialized with ``gen_seed + libE_info["workerID"]``. Otherwise, the + generator is initialized with a random seed. Parameters ---------- - persis_info: :obj:`dict` - - Persistent information dictionary. - :ref:`(example)` - - nstreams: :obj:`int` - - Number of independent random number streams to produce. - - seed: :obj:`int` - - (Optional) Seed for identical random number streams for each worker. If - explicitly set to ``None``, random number streams are unique and seed - via other pseudorandom mechanisms. + gen_specs: :obj:`dict` + Generation specifications dictionary. + libE_info: :obj:`dict` + libEnsemble information dictionary. """ - - for i in range(nstreams): - if isinstance(seed, int) or seed is None: - random_seed = seed - else: - random_seed = i - - if i in persis_info: - persis_info[i].update( - { - "rand_stream": np.random.default_rng(random_seed), - "worker_num": i, - } - ) - else: - persis_info[i] = { - "rand_stream": np.random.default_rng(random_seed), - "worker_num": i, - } - return persis_info + seed = gen_specs.get("user", {}).get("gen_seed") + if seed is not None: + return np.random.default_rng(seed + libE_info.get("workerID", 0)) + return np.random.default_rng() def check_npy_file_exists(filename: str, basename: bool = False, max_wait: int = 3) -> bool: @@ -249,7 +205,7 @@ def check_basename_file_exists(): check_file_exists = check_basename_file_exists if basename else check_exact_file_exists sleep_interval = 0.1 - total_wait_time = 0 + total_wait_time = 0.0 file_exists = False while total_wait_time < max_wait: if check_file_exists(): diff --git a/libensemble/utils/__init__.py b/libensemble/utils/__init__.py index c9683a4a45..e69de29bb2 100644 --- a/libensemble/utils/__init__.py +++ b/libensemble/utils/__init__.py @@ -1,3 +0,0 @@ -from libensemble.utils import pydantic_bindings # noqa: F401 - -# The above needs to get run *somehow* diff --git a/libensemble/utils/misc.py b/libensemble/utils/misc.py index cfb4f4df20..da709f1c05 100644 --- a/libensemble/utils/misc.py +++ b/libensemble/utils/misc.py @@ -2,9 +2,12 @@ Misc internal functions """ -from itertools import groupby +from itertools import chain, groupby from operator import itemgetter +import numpy as np +import numpy.typing as npt + def extract_H_ranges(Work: dict) -> str: """Convert received H_rows into ranges for labeling""" @@ -14,8 +17,8 @@ def extract_H_ranges(Work: dict) -> str: else: # From https://stackoverflow.com/a/30336492 ranges = [] - for diff, group in groupby(enumerate(work_H_rows.tolist()), lambda x: x[0] - x[1]): - group = list(map(itemgetter(1), group)) + for diff, group_iter in groupby(enumerate(work_H_rows.tolist()), lambda x: x[0] - x[1]): + group = list(map(itemgetter(1), group_iter)) if len(group) > 1: ranges.append(str(group[0]) + "-" + str(group[-1])) else: @@ -57,3 +60,259 @@ def specs_checker_getattr(obj, key, default=None): def specs_checker_setattr(obj, key, value): obj.__dict__[key] = value + + +def _combine_names(names: list) -> list: + """Return unique field names without auto-combining""" + return list(dict.fromkeys(names)) # preserves order, removes duplicates + + +def _get_new_dtype_fields(first: dict, mapping: dict = {}) -> list: + """build list of fields that will be in the output numpy array""" + new_dtype_names = _combine_names([i for i in first.keys()]) # -> ['x', 'y'] + fields_to_convert = list( # combining all mapping lists + chain.from_iterable(list(mapping.values())) + ) # fields like ["beam_length", "beam_width"] that will become "x" + new_dtype_names = [i for i in new_dtype_names if i not in fields_to_convert] + return new_dtype_names + + +def _get_combinable_multidim_names(first: dict, new_dtype_names: list) -> list: + """Return each field name as a single-element list without auto-grouping""" + return [[name] for name in new_dtype_names] + + +def _decide_dtype(name: str, entry, size: int) -> tuple: + """decide dtype of field, and size if needed""" + output_type: str | type # str for numpy string type, type for python type + if isinstance(entry, str): # use numpy style for string type + output_type = "U" + str(len(entry) + 1) + else: + output_type = type(entry) # use default "python" type + if name == "sim_id": + output_type = int + if size == 1 or not size: + return (name, output_type) + else: + return (name, output_type, (size,)) # 3-tuple for multi-dimensional + + +def _start_building_dtype( + first: dict, new_dtype_names: list, combinable_names: list, dtype: list, mapping: dict +) -> list: + """parse out necessary components of dtype for output numpy array""" + for i, entry in enumerate(combinable_names): + name = new_dtype_names[i] + size = len(combinable_names[i]) # e.g. 2 for [x0, x1] + if name not in mapping: # mapping keys are what we're converting *to* + dtype.append(_decide_dtype(name, first[entry[0]], size)) + return dtype + + +def _pack_field(input_dict: dict, field_names: list) -> tuple: + """pack dict data into tuple for slotting into numpy array""" + # {"x0": 1, "x1": 2} -> (1, 2) + return tuple(input_dict[name] for name in field_names) if len(field_names) > 1 else input_dict[field_names[0]] + + +def list_dicts_to_np(list_dicts: list, dtype: list | None = None, mapping: dict = {}) -> npt.NDArray: + """Convert list of dicts to numpy structured array""" + if list_dicts is None: + return None + + if not isinstance(list_dicts, list): + return list_dicts + + if not list_dicts: + return np.array([], dtype=dtype if dtype else []) + + # first entry is used to determine dtype + first = list_dicts[0] + + # build a presumptive dtype + new_dtype_names = _get_new_dtype_fields(first, mapping) + combinable_names = _get_combinable_multidim_names(first, new_dtype_names) # [['x0', 'x1'], ['z']] + + if dtype is None: # Default value gets set upon function instantiation (default is mutable). + dtype = [] + + # build dtype of non-mapped fields. appending onto empty dtype + if not len(dtype): + dtype = _start_building_dtype(first, new_dtype_names, combinable_names, dtype, mapping) + + # append dtype of mapped float fields + if len(mapping): + existing_names = [f[0] for f in dtype] + for name in mapping: + # If the field is already in the dtype, skip it. *And* the field is present in the input data + if name not in existing_names and all(src in first for src in mapping[name]): + size = len(mapping[name]) + dtype.append(_decide_dtype(name, 0.0, size)) # default to float + new_dtype_names.append(name) + combinable_names.append(mapping[name]) + + out = np.zeros(len(list_dicts), dtype=dtype) + + # starting packing data from list of dicts into array + for j, input_dict in enumerate(list_dicts): + for output_name, input_names in zip(new_dtype_names, combinable_names): # [('x', ['x0', 'x1']), ...] + if output_name not in mapping: + out[output_name][j] = _pack_field(input_dict, input_names) + else: + out[output_name][j] = _pack_field(input_dict, mapping[output_name]) + + return out + + +def _is_multidim(selection: npt.NDArray) -> bool: + return hasattr(selection, "__len__") and len(selection) > 1 and not isinstance(selection, str) + + +def _is_singledim(selection: npt.NDArray) -> bool: + return (hasattr(selection, "__len__") and len(selection) == 1) or selection.shape == () + + +def unmap_numpy_array(array: npt.NDArray, mapping: dict = {}) -> npt.NDArray: + """Convert numpy array with mapped fields back to individual scalar fields. + Parameters + ---------- + array : npt.NDArray + Input array with mapped fields like x = [x0, x1, x2] + mapping : dict + Mapping from field names to variable names + Returns + ------- + npt.NDArray + Array with unmapped fields like x0, x1, x2 as individual scalars + """ + if not mapping or array is None: + return array + + active_mapping = {field: mapping[field] for field in array.dtype.names if field in mapping} + if not active_mapping: + return array + + new_fields = [] + for field in array.dtype.names: + if field in active_mapping: + for var_name in active_mapping[field]: + new_fields.append((var_name, array[field].dtype.type)) + else: + # Preserve the original field structure including per-row shape + field_dtype = array.dtype[field] + new_fields.append((field, field_dtype)) + unmapped_array = np.zeros(len(array), dtype=new_fields) + for field in array.dtype.names: + if field in active_mapping: + # Unmap array fields + if len(array[field].shape) == 1: + # Scalar field mapped to single variable + unmapped_array[active_mapping[field][0]] = array[field] + else: + # Multi-dimensional field + for i, var_name in enumerate(active_mapping[field]): + unmapped_array[var_name] = array[field][:, i] + else: + # Copy non-mapped fields + unmapped_array[field] = array[field] + return unmapped_array + + +def map_numpy_array(array: npt.NDArray, mapping: dict = {}) -> npt.NDArray: + """Convert numpy array with individual scalar fields to mapped fields. + Parameters + ---------- + array : npt.NDArray + Input array with unmapped fields like x0, x1, x2 + mapping : dict + Mapping from field names to variable names + Returns + ------- + npt.NDArray + Array with mapped fields like x = [x0, x1, x2] + """ + if not mapping or array is None: + return array + + # Some mappings may apply only on ingest. For example, generator suggestions + # usually contain variables but not objective values. + active_mapping = { + mapped_name: val_list + for mapped_name, val_list in mapping.items() + if all(val in array.dtype.names for val in val_list) + } + if not active_mapping: + return array + + new_fields: list[tuple] = [] + + # Track fields processed by mapping to avoid duplication + mapped_source_fields = set() + for val_list in active_mapping.values(): + mapped_source_fields.update(val_list) + + # First add mapped fields from the mapping definition + for mapped_name, val_list in active_mapping.items(): + first_var = val_list[0] + # We assume all components have the same type, take from first + base_type = array.dtype[first_var] + size = len(val_list) + if size > 1: + new_fields.append((mapped_name, base_type, (size,))) + else: + new_fields.append((mapped_name, base_type)) + + # Then add any fields from the source array that were NOT part of a mapping + for field in array.dtype.names: + if field not in mapped_source_fields: + new_fields.append((field, array.dtype[field])) + + # remove duplicates from new_fields + new_fields = list(dict.fromkeys(new_fields)) + + mapped_array = np.zeros(len(array), dtype=new_fields) + + for field in mapped_array.dtype.names: + if field in active_mapping: + val_list = active_mapping[field] + if len(val_list) == 1: + mapped_array[field] = array[val_list[0]] + else: + mapped_array[field] = np.stack([array[val] for val in val_list], axis=1) + else: + mapped_array[field] = array[field] + + return mapped_array + + +def np_to_list_dicts(array: npt.NDArray, mapping: dict = {}) -> list[dict]: + """Convert numpy structured array to list of dicts""" + out = [] + + for row in array: + new_dict = {} + + for field in row.dtype.names: + if field not in list(mapping.keys()): + # Unmapped fields: copy directly (no auto-unpacking) + new_dict[field] = row[field] + else: # keys from mapping and array unpacked into corresponding fields in dicts + field_shape = array.dtype[field].shape[0] if len(array.dtype[field].shape) > 0 else 1 + assert field_shape == len(mapping[field]), ( + "dimension mismatch between mapping and array with field " + field + ) + + for i, name in enumerate(mapping[field]): + if _is_multidim(row[field]): + new_dict[name] = row[field][i] + elif _is_singledim(row[field]): + new_dict[name] = row[field] + + out.append(new_dict) + + # Remove _id from entries where it's -1 (unset) + for entry in out: + if entry.get("_id") == -1: + entry.pop("_id") + + return out diff --git a/libensemble/utils/pydantic_bindings.py b/libensemble/utils/pydantic_bindings.py deleted file mode 100644 index 6c297bb95d..0000000000 --- a/libensemble/utils/pydantic_bindings.py +++ /dev/null @@ -1,119 +0,0 @@ -import sys - -from pydantic import ConfigDict, Field, create_model -from pydantic import validate_call as libE_wrapper # noqa: F401 -from pydantic.fields import FieldInfo - -from libensemble import specs -from libensemble.resources import platforms -from libensemble.utils.validators import ( - check_any_workers_and_disable_rm_if_tcp, - check_exit_criteria, - check_gpu_setting_type, - check_H0, - check_input_dir_exists, - check_inputs_exist, - check_logical_cores, - check_mpi_runner_type, - check_output_fields, - check_provided_ufuncs, - check_valid_comms_type, - check_valid_in, - check_valid_out, - enable_save_H_when_every_K, - genf_set_in_out_from_attrs, - set_calc_dirs_on_input_dir, - set_default_comms, - set_platform_specs_to_class, - set_workflow_dir, - simf_set_in_out_from_attrs, -) - -model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid", validate_assignment=True) - -specs.SimSpecs.model_config = model_config -specs.GenSpecs.model_config = model_config -specs.AllocSpecs.model_config = model_config -specs.LibeSpecs.model_config = model_config -specs.ExitCriteria.model_config = model_config -specs._EnsembleSpecs.model_config = model_config -platforms.Platform.model_config = model_config - -model = specs.SimSpecs.model_fields -model["inputs"] = FieldInfo.merge_field_infos(model["inputs"], Field(alias="in")) -model["outputs"] = FieldInfo.merge_field_infos(model["outputs"], Field(alias="out")) - -model = specs.GenSpecs.model_fields -model["inputs"] = FieldInfo.merge_field_infos(model["inputs"], Field(alias="in")) -model["outputs"] = FieldInfo.merge_field_infos(model["outputs"], Field(alias="out")) - -model = specs.AllocSpecs.model_fields -model["outputs"] = FieldInfo.merge_field_infos(model["outputs"], Field(alias="out")) - -specs.SimSpecs.model_rebuild(force=True) -specs.GenSpecs.model_rebuild(force=True) -specs.AllocSpecs.model_rebuild(force=True) -specs.LibeSpecs.model_rebuild(force=True) -specs.ExitCriteria.model_rebuild(force=True) -specs._EnsembleSpecs.model_rebuild(force=True) -platforms.Platform.model_rebuild(force=True) - -# the create_model function removes fields for rendering in docs -if "sphinx" not in sys.modules: - - specs.SimSpecs = create_model( - "SimSpecs", - __base__=specs.SimSpecs, - __validators__={ - "check_valid_out": check_valid_out, - "check_valid_in": check_valid_in, - "simf_set_in_out_from_attrs": simf_set_in_out_from_attrs, - }, - ) - - specs.GenSpecs = create_model( - "GenSpecs", - __base__=specs.GenSpecs, - __validators__={ - "check_valid_out": check_valid_out, - "check_valid_in": check_valid_in, - "genf_set_in_out_from_attrs": genf_set_in_out_from_attrs, - }, - ) - - specs.LibeSpecs = create_model( - "LibeSpecs", - __base__=specs.LibeSpecs, - __validators__={ - "check_valid_comms_type": check_valid_comms_type, - "set_platform_specs_to_class": set_platform_specs_to_class, - "check_input_dir_exists": check_input_dir_exists, - "check_inputs_exist": check_inputs_exist, - "check_any_workers_and_disable_rm_if_tcp": check_any_workers_and_disable_rm_if_tcp, - "enable_save_H_when_every_K": enable_save_H_when_every_K, - "set_default_comms": set_default_comms, - "set_workflow_dir": set_workflow_dir, - "set_calc_dirs_on_input_dir": set_calc_dirs_on_input_dir, - }, - ) - - specs._EnsembleSpecs = create_model( - "_EnsembleSpecs", - __base__=specs._EnsembleSpecs, - __validators__={ - "check_exit_criteria": check_exit_criteria, - "check_output_fields": check_output_fields, - "check_H0": check_H0, - "check_provided_ufuncs": check_provided_ufuncs, - }, - ) - - platforms.Platform = create_model( - "Platform", - __base__=platforms.Platform, - __validators__={ - "check_gpu_setting_type": check_gpu_setting_type, - "check_mpi_runner_type": check_mpi_runner_type, - "check_logical_cores": check_logical_cores, - }, - ) diff --git a/libensemble/utils/runners.py b/libensemble/utils/runners.py index 9554b11769..72f8d1d4f7 100644 --- a/libensemble/utils/runners.py +++ b/libensemble/utils/runners.py @@ -1,22 +1,35 @@ import inspect import logging import logging.handlers +import time import numpy.typing as npt from libensemble.comms.comms import QCommThread +from libensemble.generators import LibensembleGenerator, PersistentGenInterfacer +from libensemble.message_numbers import EVAL_GEN_TAG, FINISHED_PERSISTENT_GEN_TAG, PERSIS_STOP, STOP_TAG +from libensemble.tools.persistent_support import PersistentSupport +from libensemble.utils.misc import list_dicts_to_np, map_numpy_array, np_to_list_dicts, unmap_numpy_array logger = logging.getLogger(__name__) class Runner: - def __new__(cls, specs): + @classmethod + def from_specs(cls, specs): if len(specs.get("globus_compute_endpoint", "")) > 0: - return super(Runner, GlobusComputeRunner).__new__(GlobusComputeRunner) - if specs.get("threaded"): # TODO: undecided interface - return super(Runner, ThreadRunner).__new__(ThreadRunner) + return GlobusComputeRunner(specs) + if specs.get("threaded"): + return ThreadRunner(specs) + if (generator := specs.get("generator")) is not None: + if isinstance(generator, PersistentGenInterfacer): + return LibensembleGenThreadRunner(specs) + if isinstance(generator, LibensembleGenerator): + return LibensembleGenRunner(specs) + else: + return StandardGenRunner(specs) else: - return super().__new__(Runner) + return Runner(specs) def __init__(self, specs): self.specs = specs @@ -38,7 +51,20 @@ def shutdown(self) -> None: def run(self, calc_in: npt.NDArray, Work: dict) -> (npt.NDArray, dict, int | None): if Work["persis_info"] is None: Work["persis_info"] = {} - return self._result(calc_in, Work["persis_info"], Work["libE_info"]) + out = self._result(calc_in, Work["persis_info"], Work["libE_info"]) + + # Help users who mixed up sim_f and simulator parameters + if isinstance(out, (tuple, list)): + calc_out = out[0] + else: + calc_out = out + + if isinstance(calc_out, dict): + raise AttributeError( + "Manager received a dictionary from a simulation. " + "Perhaps you meant to set `SimSpecs.simulator` instead of `SimSpecs.sim_f`?" + ) + return out class GlobusComputeRunner(Runner): @@ -85,3 +111,187 @@ def _result(self, calc_in: npt.NDArray, persis_info: dict, libE_info: dict) -> ( def shutdown(self) -> None: if self.thread_handle is not None: self.thread_handle.terminate() + + +class StandardGenRunner(Runner): + """Interact with suggest/ingest generator. Base class initialized for third-party generators.""" + + def __init__(self, specs): + super().__init__(specs) + self.gen = specs.get("generator") + + def _get_points_updates(self, batch_size: int) -> (npt.NDArray, npt.NDArray): + # no suggest_updates on external gens + return ( + list_dicts_to_np( + self.gen.suggest(batch_size), + dtype=self.specs.get("out"), + mapping=getattr(self.gen, "variables_mapping", {}), + ), + None, + ) + + def _convert_ingest(self, x: npt.NDArray) -> list: + self.gen.ingest(np_to_list_dicts(x)) + + def _convert_initial_ingest(self, x: npt.NDArray) -> list: + self.gen.ingest(np_to_list_dicts(x, mapping=getattr(self.gen, "variables_mapping", {}))) + + def _loop_over_gen(self, tag, Work, H_in): + """Interact with suggest/ingest generator that *does not* contain a background thread""" + while tag not in [PERSIS_STOP, STOP_TAG]: + batch_size = self.specs.get("batch_size") or len(H_in) + H_out, _ = self._get_points_updates(batch_size) + tag, Work, H_in = self.ps.send_recv(H_out) + if H_in is not None: + self._convert_ingest(H_in) + return H_in + + def _get_initial_suggest(self, libE_info) -> npt.NDArray: + """Get initial batch from generator based on generator type""" + initial_batch = self.specs.get("initial_batch_size") or self.specs.get("batch_size") or libE_info["batch_size"] + H_out = self.gen.suggest(initial_batch) + return H_out + + def _start_generator_loop(self, tag, Work, H_in): + """Start the generator loop after choosing best way of giving initial results to gen""" + self._convert_initial_ingest(H_in) + return self._loop_over_gen(tag, Work, H_in) + + def _create_initial_sample(self, sample_method, num_points): + """Create initial sample points using the specified sampling method. + + ``sample_method`` may be either a string naming a built-in sampler + (instantiated here with the VOCS), or a pre-constructed sampler + instance with a ``suggest()`` method (used directly). + """ + from libensemble.gen_classes.sampling import LatinHypercubeSample, UniformSample + + if isinstance(sample_method, str): + samplers = { + "uniform": UniformSample, + "latin_hypercube": LatinHypercubeSample, + } + if sample_method not in samplers: + raise ValueError( + f"Unknown initial_sample_method: {sample_method!r}. " f"Supported: {list(samplers.keys())}" + ) + sampler = samplers[sample_method](vocs=self.specs.get("vocs")) + else: + sampler = sample_method + if not hasattr(sampler, "suggest"): + raise TypeError( + "initial_sample_method must be a string name or an object " + f"with a suggest() method; got {type(sampler).__name__}" + ) + return sampler.suggest(num_points) + + def _persistent_result(self, calc_in, persis_info, libE_info): + """Setup comms with manager, setup gen, loop gen to completion, return gen's results""" + self.ps = PersistentSupport(libE_info, EVAL_GEN_TAG) + + # If H0 exists, ingest it into the generator before initial suggest + if calc_in is not None and len(calc_in) > 0: + self._convert_initial_ingest(calc_in) + + sample_method = self.specs.get("initial_sample_method") + if sample_method is not None: + # libEnsemble produces the initial sample, evaluates it, and + # ingests results into the generator before optimization begins. + initial_batch = self.specs.get("initial_batch_size") + if not initial_batch: + raise ValueError("initial_sample_method requires initial_batch_size to be set in GenSpecs.") + H_sample = list_dicts_to_np( + self._create_initial_sample(sample_method, initial_batch), + dtype=self.specs.get("out"), + mapping=getattr(self.gen, "variables_mapping", {}), + ) + tag, Work, H_in = self.ps.send_recv(H_sample) + self._convert_initial_ingest(H_in) + # Generator now has evaluated data — enter the normal loop + final_H_out = self._loop_over_gen(tag, Work, H_in) + else: + # Generator handles its own initial sampling + H_out = list_dicts_to_np( + self._get_initial_suggest(libE_info), + dtype=self.specs.get("out"), + mapping=getattr(self.gen, "variables_mapping", {}), + ) + tag, Work, H_in = self.ps.send_recv(H_out) # evaluate the initial sample + final_H_out = self._start_generator_loop(tag, Work, H_in) + + self.gen.finalize() + return final_H_out, FINISHED_PERSISTENT_GEN_TAG + + def _result(self, calc_in: npt.NDArray, persis_info: dict, libE_info: dict) -> (npt.NDArray, dict, int): + if libE_info.get("persistent"): + return self._persistent_result(calc_in, persis_info, libE_info) + raise ValueError( + "suggest/ingest generators must run in persistent mode. This may be the default in the future." + ) + + +class LibensembleGenRunner(StandardGenRunner): + def _get_initial_suggest(self, libE_info) -> npt.NDArray: + """Get initial batch from a LibensembleGenerator. + + LibensembleGenerator.suggest_numpy emits VOCS-field-named structured arrays + (e.g. x0/x1, energy). The manager-side history expects mapped fields (x, f) + unless the user explicitly requested otherwise. + """ + initial_batch = self.specs.get("initial_batch_size") or self.specs.get("batch_size") or libE_info["batch_size"] + H_out = self.gen.suggest_numpy(initial_batch) + return map_numpy_array(H_out, mapping=getattr(self.gen, "variables_mapping", {})) + + def _get_points_updates(self, batch_size: int) -> (npt.NDArray, list): + numpy_out = self.gen.suggest_numpy(batch_size) + numpy_out = map_numpy_array(numpy_out, mapping=getattr(self.gen, "variables_mapping", {})) + if callable(getattr(self.gen, "suggest_updates", None)): + updates = self.gen.suggest_updates() + else: + updates = None + return numpy_out, updates + + def _convert_ingest(self, x: npt.NDArray) -> list: + self.gen.ingest_numpy(unmap_numpy_array(x, mapping=getattr(self.gen, "variables_mapping", {}))) + + def _convert_initial_ingest(self, x: npt.NDArray) -> list: + self.gen.ingest_numpy(unmap_numpy_array(x, mapping=getattr(self.gen, "variables_mapping", {}))) + + +class LibensembleGenThreadRunner(StandardGenRunner): + def _get_initial_suggest(self, _) -> npt.NDArray: + """Get initial batch from generator based on generator type""" + return unmap_numpy_array(self.gen.suggest_numpy(), mapping=getattr(self.gen, "variables_mapping", {})) + + def _convert_initial_ingest(self, x: npt.NDArray) -> list: + self.gen.ingest_numpy(map_numpy_array(x, mapping=getattr(self.gen, "variables_mapping", {}))) + + def _suggest_and_send(self): + """Loop over generator's outbox contents, send to manager""" + while not self.gen._running_gen_f.outbox.empty(): # recv/send any outstanding messages + points = unmap_numpy_array(self.gen.suggest_numpy(), mapping=getattr(self.gen, "variables_mapping", {})) + if callable(getattr(self.gen, "suggest_updates", None)): + updates = self.gen.suggest_updates() + else: + updates = None + if updates is not None and len(updates): + self.ps.send(points) + for i in updates: + self.ps.send(i, keep_state=True) # keep_state since an update doesn't imply "new points" + else: + self.ps.send(points) + + def _loop_over_gen(self, *args): + """Cycle between moving all outbound / inbound messages between threaded gen and manager""" + while True: + time.sleep(0.0025) # dont need to ping the gen relentlessly. Let it calculate. 400hz + self._suggest_and_send() + while self.ps.comm.mail_flag(): # receive any new messages from Manager, give all to gen + tag, _, H_in = self.ps.recv() + if tag in [STOP_TAG, PERSIS_STOP]: + self.gen.ingest_numpy( + map_numpy_array(H_in, mapping=getattr(self.gen, "variables_mapping", {})), PERSIS_STOP + ) + return self.gen._running_gen_f.result() + self.gen.ingest_numpy(map_numpy_array(H_in, mapping=getattr(self.gen, "variables_mapping", {}))) diff --git a/libensemble/utils/specs_checkers.py b/libensemble/utils/specs_checkers.py deleted file mode 100644 index cf33d359f7..0000000000 --- a/libensemble/utils/specs_checkers.py +++ /dev/null @@ -1,120 +0,0 @@ -""" -Save some space in specs.py by moving some validation functions to here. -Reference the models in that file. -""" - -import logging -import secrets -from pathlib import Path - -import numpy as np - -from libensemble.tools.fields_keys import libE_fields -from libensemble.utils.misc import specs_checker_getattr as scg -from libensemble.utils.misc import specs_checker_setattr as scs - -logger = logging.getLogger(__name__) - - -def _check_exit_criteria(values): - if scg(values, "exit_criteria").stop_val is not None: - stop_name = scg(values, "exit_criteria").stop_val[0] - sim_out_names = [e[0] for e in scg(values, "sim_specs").outputs] - gen_out_names = [e[0] for e in scg(values, "gen_specs").outputs] - assert stop_name in sim_out_names + gen_out_names, f"Can't stop on {stop_name} if it's not in a sim/gen output" - return values - - -def _check_output_fields(values): - out_names = [e[0] for e in libE_fields] - if scg(values, "H0") is not None and scg(values, "H0").dtype.names is not None: - out_names += list(scg(values, "H0").dtype.names) - out_names += [e[0] for e in scg(values, "sim_specs").outputs] - if scg(values, "gen_specs"): - out_names += [e[0] for e in scg(values, "gen_specs").outputs] - if scg(values, "alloc_specs"): - out_names += [e[0] for e in scg(values, "alloc_specs").outputs] - - for name in scg(values, "sim_specs").inputs: - assert name in out_names, ( - name + " in sim_specs['in'] is not in sim_specs['out'], " - "gen_specs['out'], alloc_specs['out'], H0, or libE_fields." - ) - - if scg(values, "gen_specs"): - for name in scg(values, "gen_specs").inputs: - assert name in out_names, ( - name + " in gen_specs['in'] is not in sim_specs['out'], " - "gen_specs['out'], alloc_specs['out'], H0, or libE_fields." - ) - return values - - -def _check_H0(values): - if scg(values, "H0").size > 0: - H0 = scg(values, "H0") - specs = [scg(values, "sim_specs"), scg(values, "gen_specs")] - specs_dtype_list = list(set(libE_fields + sum([k.outputs or [] for k in specs if k], []))) - specs_dtype_fields = [i[0] for i in specs_dtype_list] - specs_inputs_list = list(set(sum([k.inputs + k.persis_in or [] for k in specs if k], []))) - Dummy_H = np.zeros(1 + len(H0), dtype=specs_dtype_list) - - # should check that new fields compatible with sim/gen specs, if any? - - for field in specs_inputs_list: - assert field in list(H0.dtype.names) + specs_dtype_fields, f"{field} not in H0 although expected as input" - - assert "sim_ended" not in H0.dtype.names or np.all( - H0["sim_started"] == H0["sim_ended"] - ), "H0 contains unreturned or invalid points" - - def _check_consistent_field(name, field0, field1): - """Checks that new field (field1) is compatible with an old field (field0).""" - assert field0.ndim == field1.ndim, f"H0 and H have different ndim for field {name}" - assert np.all( - np.array(field1.shape) >= np.array(field0.shape) - ), f"H too small to receive all components of H0 in field {name}" - - for field in H0.dtype.names: - if field in specs_dtype_list: - _check_consistent_field(field, H0[field], Dummy_H[field]) - return values - - -def _check_any_workers_and_disable_rm_if_tcp(values): - comms_type = scg(values, "comms") - if comms_type in ["local", "tcp"]: - if scg(values, "nworkers"): - assert scg(values, "nworkers") >= 1, "Must specify at least one worker" - else: - if comms_type == "tcp": - assert scg(values, "workers"), "Without nworkers, must specify worker hosts on TCP" - if comms_type == "tcp": - scs(values, "disable_resource_manager", True) # Resource management not supported with TCP - return values - - -def _check_set_workflow_dir(values): - if scg(values, "use_workflow_dir") and len(str(scg(values, "workflow_dir_path"))) <= 1: - scs(values, "workflow_dir_path", Path("./workflow_" + secrets.token_hex(3)).absolute()) - elif len(str(scg(values, "workflow_dir_path"))) > 1: - if not scg(values, "use_workflow_dir"): - scs(values, "use_workflow_dir", True) - scs(values, "workflow_dir_path", Path(scg(values, "workflow_dir_path")).absolute()) - return values - - -def _check_set_calc_dirs_on_input_dir(values): - if scg(values, "sim_input_dir") and not scg(values, "sim_dirs_make"): - scs(values, "sim_dirs_make", True) - if scg(values, "gen_input_dir") and not scg(values, "gen_dirs_make"): - scs(values, "gen_dirs_make", True) - return values - - -def _check_logical_cores(values): - if scg(values, "cores_per_node") and scg(values, "logical_cores_per_node"): - assert ( - scg(values, "logical_cores_per_node") % scg(values, "cores_per_node") == 0 - ), "Logical cores doesn't divide evenly into cores" - return values diff --git a/libensemble/utils/validators.py b/libensemble/utils/validators.py index e2fec4e13d..58cddd4adc 100644 --- a/libensemble/utils/validators.py +++ b/libensemble/utils/validators.py @@ -1,20 +1,17 @@ +import logging import os +import secrets from collections.abc import Callable from pathlib import Path import numpy as np -from pydantic import field_validator, model_validator from libensemble.resources.platforms import Platform -from libensemble.utils.specs_checkers import ( - _check_any_workers_and_disable_rm_if_tcp, - _check_exit_criteria, - _check_H0, - _check_logical_cores, - _check_output_fields, - _check_set_calc_dirs_on_input_dir, - _check_set_workflow_dir, -) +from libensemble.tools.fields_keys import libE_fields +from libensemble.utils.misc import specs_checker_getattr as scg +from libensemble.utils.misc import specs_checker_setattr as scs + +logger = logging.getLogger(__name__) _UNRECOGNIZED_ERR = "Unrecognized field. Check closely for typos, or libEnsemble's docs" _UFUNC_INVALID_ERR = "Specified sim_f or gen_f is not callable. It should be a user function" @@ -103,31 +100,23 @@ def check_mpi_runner_type(cls, value): return value -# SPECS VALIDATORS ##### - -check_valid_out = field_validator("outputs")(classmethod(check_valid_out)) -check_valid_in = field_validator("inputs", "persis_in")(classmethod(check_valid_in)) -check_valid_comms_type = field_validator("comms")(classmethod(check_valid_comms_type)) -set_platform_specs_to_class = field_validator("platform_specs")(classmethod(set_platform_specs_to_class)) -check_input_dir_exists = field_validator("sim_input_dir", "gen_input_dir")(classmethod(check_input_dir_exists)) -check_inputs_exist = field_validator( - "sim_dir_copy_files", "sim_dir_symlink_files", "gen_dir_copy_files", "gen_dir_symlink_files" -)(classmethod(check_inputs_exist)) -check_gpu_setting_type = field_validator("gpu_setting_type")(classmethod(check_gpu_setting_type)) -check_mpi_runner_type = field_validator("mpi_runner")(classmethod(check_mpi_runner_type)) - - -@model_validator(mode="after") -def check_any_workers_and_disable_rm_if_tcp(self): - return _check_any_workers_and_disable_rm_if_tcp(self) +def check_any_workers_and_disable_rm_if_tcp(values): + comms_type = scg(values, "comms") + if comms_type in ["local", "tcp"]: + if scg(values, "nworkers"): + assert scg(values, "nworkers") >= 1, "Must specify at least one worker" + else: + if comms_type == "tcp": + assert scg(values, "workers"), "Without nworkers, must specify worker hosts on TCP" + if comms_type == "tcp": + scs(values, "disable_resource_manager", True) # Resource management not supported with TCP + return values -@model_validator(mode="before") def set_default_comms(cls, values): return default_comms(values) -@model_validator(mode="after") def enable_save_H_when_every_K(self): if not self.__dict__.get("save_H_on_completion") and ( self.__dict__.get("save_every_k_sims", 0) > 0 or self.__dict__.get("save_every_k_gens", 0) > 0 @@ -136,68 +125,90 @@ def enable_save_H_when_every_K(self): return self -@model_validator(mode="after") -def set_workflow_dir(self): - return _check_set_workflow_dir(self) +def set_workflow_dir(values): + if scg(values, "use_workflow_dir") and len(str(scg(values, "workflow_dir_path"))) <= 1: + scs(values, "workflow_dir_path", Path("./workflow_" + secrets.token_hex(3)).absolute()) + elif len(str(scg(values, "workflow_dir_path"))) > 1: + if not scg(values, "use_workflow_dir"): + scs(values, "use_workflow_dir", True) + scs(values, "workflow_dir_path", Path(scg(values, "workflow_dir_path")).absolute()) + return values -@model_validator(mode="after") -def set_calc_dirs_on_input_dir(self): - return _check_set_calc_dirs_on_input_dir(self) +def set_calc_dirs_on_input_dir(values): + if scg(values, "sim_input_dir") and not scg(values, "sim_dirs_make"): + scs(values, "sim_dirs_make", True) + if scg(values, "gen_input_dir") and not scg(values, "gen_dirs_make"): + scs(values, "gen_dirs_make", True) + return values + + +def check_exit_criteria(values): + if scg(values, "exit_criteria").stop_val is not None: + stop_name = scg(values, "exit_criteria").stop_val[0] + sim_out_names = [e[0] for e in scg(values, "sim_specs").outputs] + gen_out_names = [e[0] for e in scg(values, "gen_specs").outputs] + assert stop_name in sim_out_names + gen_out_names, f"Can't stop on {stop_name} if it's not in a sim/gen output" + return values + +def check_H0(values): + if scg(values, "H0").size > 0: + H0 = scg(values, "H0") + specs = [scg(values, "sim_specs"), scg(values, "gen_specs")] + specs_dtype_list = list(set(libE_fields + sum([k.outputs or [] for k in specs if k], []))) + specs_dtype_fields = [i[0] for i in specs_dtype_list] + specs_inputs_list = list(set(sum([k.inputs + k.persis_in or [] for k in specs if k], []))) + Dummy_H = np.zeros(1 + len(H0), dtype=specs_dtype_list) -@model_validator(mode="after") -def check_exit_criteria(self): - return _check_exit_criteria(self) + # should check that new fields compatible with sim/gen specs, if any? + for field in specs_inputs_list: + assert field in list(H0.dtype.names) + specs_dtype_fields, f"{field} not in H0 although expected as input" -@model_validator(mode="after") -def check_output_fields(self): - return _check_output_fields(self) + assert "sim_ended" not in H0.dtype.names or np.all( + H0["sim_started"] == H0["sim_ended"] + ), "H0 contains unreturned or invalid points" + def _check_consistent_field(name, field0, field1): + """Checks that new field (field1) is compatible with an old field (field0).""" + assert field0.ndim == field1.ndim, f"H0 and H have different ndim for field {name}" + assert np.all( + np.array(field1.shape) >= np.array(field0.shape) + ), f"H too small to receive all components of H0 in field {name}" -@model_validator(mode="after") -def check_H0(self): - return _check_H0(self) + for field in H0.dtype.names: + if field in specs_dtype_list: + _check_consistent_field(field, H0[field], Dummy_H[field]) + return values + + +def check_set_gen_specs_from_variables(values): + if not len(scg(values, "outputs")): + generator = scg(values, "generator") + if generator and hasattr(generator, "gen_specs"): + out = generator.gen_specs.get("out", []) + if len(out): + scs(values, "outputs", out) + return values -@model_validator(mode="after") def check_provided_ufuncs(self): assert hasattr(self.sim_specs, "sim_f"), "Simulation function not provided to SimSpecs." assert isinstance(self.sim_specs.sim_f, Callable), "Simulation function is not callable." if self.alloc_specs.alloc_f.__name__ != "give_pregenerated_sim_work": assert hasattr(self.gen_specs, "gen_f"), "Generator function not provided to GenSpecs." - assert isinstance(self.gen_specs.gen_f, Callable), "Generator function is not callable." - - return self - + assert ( + isinstance(self.gen_specs.gen_f, Callable) if self.gen_specs.gen_f is not None else True + ), "Generator function is not callable." -@model_validator(mode="after") -def simf_set_in_out_from_attrs(self): - if hasattr(self.__dict__.get("sim_f"), "inputs") and not self.__dict__.get("inputs"): - self.__dict__["inputs"] = self.__dict__.get("sim_f").inputs - if hasattr(self.__dict__.get("sim_f"), "outputs") and not self.__dict__.get("outputs"): - self.__dict__["outputs"] = self.__dict__.get("sim_f").outputs - if hasattr(self.__dict__.get("sim_f"), "persis_in") and not self.__dict__.get("persis_in"): - self.__dict__["persis_in"] = self.__dict__.get("sim_f").persis_in return self -@model_validator(mode="after") -def genf_set_in_out_from_attrs(self): - if hasattr(self.__dict__.get("gen_f"), "inputs") and not self.__dict__.get("inputs"): - self.__dict__["inputs"] = self.__dict__.get("gen_f").inputs - if hasattr(self.__dict__.get("gen_f"), "outputs") and not self.__dict__.get("outputs"): - self.__dict__["outputs"] = self.__dict__.get("gen_f").outputs - if hasattr(self.__dict__.get("gen_f"), "persis_in") and not self.__dict__.get("persis_in"): - self.__dict__["persis_in"] = self.__dict__.get("gen_f").persis_in - return self - - -# RESOURCES VALIDATORS ##### - - -@model_validator(mode="after") -def check_logical_cores(self): - return _check_logical_cores(self) +def check_logical_cores(values): + if scg(values, "cores_per_node") and scg(values, "logical_cores_per_node"): + assert ( + scg(values, "logical_cores_per_node") % scg(values, "cores_per_node") == 0 + ), "Logical cores doesn't divide evenly into cores" + return values diff --git a/libensemble/version.py b/libensemble/version.py index f507afd208..e3457841ef 100644 --- a/libensemble/version.py +++ b/libensemble/version.py @@ -1 +1 @@ -__version__ = "1.5.0+dev" +__version__ = "1.6.0+dev" diff --git a/libensemble/worker.py b/libensemble/worker.py index 44d5f0ddeb..c6ef5fcb3a 100644 --- a/libensemble/worker.py +++ b/libensemble/worker.py @@ -7,6 +7,7 @@ import logging import logging.handlers +import os import socket from itertools import count from pathlib import Path @@ -52,7 +53,7 @@ def worker_main( sim_specs: dict, gen_specs: dict, libE_specs: dict, - workerID: int = None, + workerID: int | None = None, log_comm: bool = True, resources: Resources = None, executor: Executor = None, @@ -110,16 +111,19 @@ def worker_main( worker_logging_config(comm, workerID) LS = LocationStack() - LS.register_loc("workflow", Path(libE_specs.get("workflow_dir_path"))) + LS.register_loc("workflow", Path(libE_specs.get("workflow_dir_path", "."))) # Set up and run worker + assert workerID is not None worker = Worker(comm, dtypes, workerID, sim_specs, gen_specs, libE_specs) with LS.loc("workflow"): + if Executor.executor is not None: + Executor.executor.base_dir = os.getcwd() worker.run() if libE_specs.get("profile"): pr.disable() - profile_state_fname = "worker_%d.prof" % (workerID) + profile_state_fname = "worker_%d.prof" % workerID pr.dump_stats(profile_state_fname) @@ -129,7 +133,7 @@ def worker_main( class WorkerErrMsg: - def __init__(self, msg, exc): + def __init__(self, msg: str, exc: str) -> None: self.msg = msg self.exc = exc @@ -172,12 +176,12 @@ def __init__( self.workerID = workerID self.libE_specs = libE_specs self.stats_fmt = libE_specs.get("stats_fmt", {}) - self.sim_runner = Runner(sim_specs) - self.gen_runner = Runner(gen_specs) + self.sim_runner = Runner.from_specs(sim_specs) + self.gen_runner = Runner.from_specs(gen_specs) self.runners = {EVAL_SIM_TAG: self.sim_runner.run, EVAL_GEN_TAG: self.gen_runner.run} self.calc_iter = {EVAL_SIM_TAG: 0, EVAL_GEN_TAG: 0} Worker._set_executor(self.workerID, self.comm) - Worker._set_resources(self.workerID, self.comm) + Worker._set_resources(self.workerID, self.comm, self.libE_specs) self.EnsembleDirectory = EnsembleDirectory(libE_specs=libE_specs) @staticmethod @@ -215,7 +219,7 @@ def _set_executor(workerID: int, comm: Comm) -> bool: return False @staticmethod - def _set_resources(workerID, comm: Comm) -> bool: + def _set_resources(workerID, comm: Comm, libE_specs) -> bool: """Sets worker ID in the resources, return True if set""" resources = Resources.resources if isinstance(resources, Resources): @@ -237,7 +241,7 @@ def _extract_debug_data(self, calc_type, Work): calc_id = calc_id.rjust(5, " ") return enum_desc, calc_id - def _handle_calc(self, Work: dict, calc_in: npt.NDArray) -> (npt.NDArray, dict, int): + def _handle_calc(self, Work: dict, calc_in: npt.NDArray) -> tuple[npt.NDArray | None, dict, int | str]: """Runs a calculation on this worker object. This routine calls the user calculations. Exceptions are caught, @@ -262,6 +266,7 @@ def _handle_calc(self, Work: dict, calc_in: npt.NDArray) -> (npt.NDArray, dict, try: logger.debug(f"Starting {enum_desc}: {calc_id}") + out = None calc = self.runners[calc_type] with timer: if self.EnsembleDirectory.use_calc_dirs(calc_type): @@ -278,15 +283,15 @@ def _handle_calc(self, Work: dict, calc_in: npt.NDArray) -> (npt.NDArray, dict, logger.debug(f"Returned from user function for {enum_desc} {calc_id}") - calc_status = UNSET_TAG + calc_status: int | str = UNSET_TAG # Check for buffered receive if self.comm.recv_buffer: tag, message = self.comm.recv() if tag in [STOP_TAG, PERSIS_STOP] and message is MAN_SIGNAL_FINISH: calc_status = MAN_SIGNAL_FINISH - if out: - if len(out) >= 3: # Out, persis_info, calc_status + if out is not None: + if not isinstance(out, np.ndarray) and len(out) >= 3: # Out, persis_info, calc_status calc_status = out[2] return out elif len(out) == 2: # Out, persis_info OR Out, calc_status @@ -312,7 +317,7 @@ def _handle_calc(self, Work: dict, calc_in: npt.NDArray) -> (npt.NDArray, dict, logging.getLogger(LogConfig.config.stats_name).info(calc_msg) - def _get_calc_msg(self, enum_desc: str, calc_id: int, calc_type: int, timer: Timer, status: str) -> str: + def _get_calc_msg(self, enum_desc: str, calc_id: str, calc_type: str, timer: Timer, status: int | str) -> str: """Construct line for libE_stats.txt file""" calc_msg = f"{enum_desc} {calc_id}: {calc_type} {timer}" @@ -328,7 +333,7 @@ def _get_calc_msg(self, enum_desc: str, calc_id: int, calc_type: int, timer: Tim return calc_msg - def _recv_H_rows(self, Work: dict) -> (dict, int, npt.NDArray): + def _recv_H_rows(self, Work: dict) -> tuple[dict, int, npt.NDArray]: """Unpacks Work request and receives any history rows""" libE_info = Work["libE_info"] calc_type = Work["tag"] @@ -342,7 +347,7 @@ def _recv_H_rows(self, Work: dict) -> (dict, int, npt.NDArray): return libE_info, calc_type, calc_in - def _handle(self, Work: dict) -> dict: + def _handle(self, Work: dict) -> dict | None: """Handles a work request from the manager""" # Check work request and receive second message (if needed) libE_info, calc_type, calc_in = self._recv_H_rows(Work) diff --git a/pixi.lock b/pixi.lock new file mode 100644 index 0000000000..c4714fc82a --- /dev/null +++ b/pixi.lock @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dd8596dc0210788ddfda1f23f01aa4a83aaf85d1242d5b68a530e350e14c174b +size 1084218 diff --git a/pyproject.toml b/pyproject.toml index 80535d8cbc..45bb84e22f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,34 +1,38 @@ [project] -authors = [{name = "Jeffrey Larson"}, {name = "Stephen Hudson"}, - {name = "Stefan M. Wild"}, {name = "David Bindel"}, - {name = "John-Luke Navarro"}] +authors = [ + { name = "Jeffrey Larson" }, + { name = "Stephen Hudson" }, + { name = "Stefan M. Wild" }, + { name = "David Bindel" }, + { name = "John-Luke Navarro" }, +] -dependencies = [ "numpy", "psutil", "pydantic", "pyyaml", "tomli"] +dependencies = ["numpy", "psutil", "pydantic", "gest-api>=0.1,<0.2"] description = "A Python toolkit for coordinating asynchronous and dynamic ensembles of calculations." name = "libensemble" -requires-python = ">=3.10" -license = {file = "LICENSE"} +requires-python = ">=3.11" +license = { file = "LICENSE" } readme = "README.rst" classifiers = [ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", - "Intended Audience :: Science/Research", - "License :: OSI Approved :: BSD License", - "Natural Language :: English", - "Operating System :: POSIX :: Linux", - "Operating System :: Unix", - "Operating System :: MacOS", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: Implementation :: CPython", - "Topic :: Scientific/Engineering", - "Topic :: Software Development :: Libraries :: Python Modules", + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: BSD License", + "Natural Language :: English", + "Operating System :: POSIX :: Linux", + "Operating System :: Unix", + "Operating System :: MacOS", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: Implementation :: CPython", + "Topic :: Scientific/Engineering", + "Topic :: Software Development :: Libraries :: Python Modules", ] dynamic = ["version"] @@ -39,73 +43,183 @@ Issues = "https://github.com/Libensemble/libensemble/issues" [build-system] build-backend = "setuptools.build_meta" -requires = ["setuptools", "wheel", "pip>=24.3.1,<26", "setuptools>=75.1.0,<81", ] +requires = ["setuptools", "wheel", "pip>=24.3.1,<27", "setuptools>=75.1.0,<83"] [tool.setuptools.packages.find] where = ["."] include = ["libensemble*"] [tool.setuptools.dynamic] -version = {attr = "libensemble.version.__version__"} +version = { attr = "libensemble.version.__version__" } -[tool.pixi.project] +[tool.pixi.workspace] channels = ["conda-forge"] -platforms = ["osx-arm64", "linux-64", "osx-64"] +platforms = ["osx-arm64", "linux-64"] [tool.pixi.pypi-dependencies] libensemble = { path = ".", editable = true } [tool.pixi.environments] default = [] -dev = ["dev"] +basic = ["basic"] +extra = ["basic", "extra"] +docs = ["docs", "basic"] + +dev = ["dev", "basic", "extra", "docs"] + +# CI environments +py311 = ["py311", "basic"] +py312 = ["py312", "basic"] +py313 = ["py313", "basic"] +py314 = ["py314", "basic"] + +py311e = ["py311", "py311e", "basic", "extra"] +py312e = ["py312", "py312e", "basic", "extra"] +py313e = ["py313", "py313e", "basic", "extra"] +py314e = ["py314", "py314e", "basic", "extra"] + +# Extra tools for dev environment [tool.pixi.feature.dev.dependencies] +pre-commit = ">=4.5.1,<5" +git-lfs = ">=3.7.1,<4" +black = ">=25.12.0,<26" + + +# Basic dependencies for basic CI +[tool.pixi.feature.basic.dependencies] mpi = ">=1.0.1,<2" -mpich = ">=4.3.0,<5" -mpi4py = ">=4.0.3,<5" -flake8 = ">=7.2.0,<8" -coverage = ">=7.8.0,<8" -pytest = ">=8.3.5,<9" -pytest-cov = ">=6.1.1,<7" -pytest-timeout = ">=2.3.1,<3" +mpich = ">=4.3.2,<5" +mpi4py = ">=4.1.1,<5" +scipy = ">=1.15.2,<2" +mpmath = "<=1.3.0" +nlopt = ">=2.10.0,<3" + +[tool.pixi.feature.basic.pypi-dependencies] +ibcdfo = { git = "https://github.com/POptUS/IBCDFO.git", subdirectory = "ibcdfo_pypkg" } + +# "dev" dependencies needed for basic CI +flake8 = ">=7.3.0,<8" +coverage = ">=7.13.0,<8" +pytest = ">=9.0.3, <10" + +pytest-cov = ">=7.0.0,<8" +pytest-timeout = ">=2.4.0,<3" mock = ">=5.2.0,<6" python-dateutil = ">=2.9.0.post0,<3" -anyio = ">=4.9.0,<5" -matplotlib = ">=3.10.1,<4" -mpmath = ">=1.3.0,<2" -rich = ">=14.0.0,<15" +rich = ">=14.2.0,<15" +matplotlib = ">=3.10.8,<4" + +# Extra dependencies for extra CI +[tool.pixi.feature.extra.dependencies] +superlu_dist = ">=9.1.0,<10" +hypre = ">=2.32.0,<3" +mumps-mpi = ">=5.8.1,<6" +dfo-ls = ">=1.3.0,<2" +petsc = "==3.24.2" +petsc4py = "==3.24.2" +pandas = "<3" +numpy = "<2.4" +proxystore = ">=0.7.1,<0.9" + +[tool.pixi.feature.docs.dependencies] sphinx = ">=8.2.3,<9" -sphinxcontrib-bibtex = ">=2.6.3,<3" +sphinxcontrib-bibtex = ">=2.6.5,<3" sphinx-design = ">=0.6.1,<0.7" -sphinx_rtd_theme = ">=3.0.1,<4" +sphinx_rtd_theme = ">=3.0.2,<4" sphinx-copybutton = ">=0.5.2,<0.6" -pre-commit = ">=4.2.0,<5" -nlopt = ">=2.10.0,<3" +pre-commit = ">=4.5.1,<5" scipy = ">=1.15.2,<2" -ax-platform = ">=0.5.0,<0.6" -sphinxcontrib-spelling = ">=8.0.1,<9" +ax-platform = ">=1.2.1,<2" +sphinxcontrib-spelling = ">=8.0.2,<9" autodoc-pydantic = ">=2.1.0,<3" ipdb = ">=0.13.13,<0.14" -mypy = ">=1.15.0,<2" +mypy = ">=1.19.1,<2" types-psutil = ">=6.1.0.20241221,<7" -types-pyyaml = ">=6.0.12.20250402,<7" +types-pyyaml = ">=6.0.12.20250915,<7" +furo = ">=2025.12.19,<2026" +latexcodec = ">=2.0.1,<3" +latexmk = ">=4.88,<5" +fonts-conda-forge = ">=1,<2" +imagemagick = ">=7.1.2_16,<8" + +[tool.pixi.tasks.build-docs] +cmd = "cd docs && make html" + +[tool.pixi.tasks.build-pdf] +cmd = "cd docs && make latexpdf" + +# Linux dependencies, only for extra tests +[tool.pixi.feature.extra.target.linux-64.dependencies] +scikit-build = "*" +packaging = "*" +octave = ">=9.4.0,<11" +pyzmq = ">=26.4.0,<28" +# Python versions +[tool.pixi.feature.py311.dependencies] +python = "3.11.*" +[tool.pixi.feature.py312.dependencies] +python = "3.12.*" +[tool.pixi.feature.py313.dependencies] +python = "3.13.*" +[tool.pixi.feature.py314.dependencies] +python = "3.14.*" + +# ax-platform only works up to 3.13 on Linux + +[tool.pixi.feature.py311e.target.linux-64.dependencies] +ax-platform = "==0.5.0" + +[tool.pixi.feature.py311e.dependencies] +globus-compute-sdk = ">=4.10.2,<5" + +[tool.pixi.feature.py312e.target.linux-64.dependencies] +ax-platform = "==0.5.0" + +[tool.pixi.feature.py312e.dependencies] +globus-compute-sdk = ">=4.10.2,<5" + +[tool.pixi.feature.py313e.target.linux-64.dependencies] +ax-platform = "==0.5.0" + +[tool.pixi.feature.py314e] + + +# Dependencies for libEnsemble [tool.pixi.dependencies] -python = ">=3.10,<3.14" -pip = ">=24.3.1,<25" -setuptools = ">=75.6.0,<76" -numpy = ">=1.21,<3" -pydantic = ">=1.10,<3" -pyyaml = ">=6.0,<7" -tomli = ">=1.2.1,<3" -psutil = ">=5.9.4,<7" +python = ">=3.11,<3.15" +pip = ">=25.2,<26" +setuptools = ">=80.8.0,<81" +numpy = ">=2.2.6,<3" +pydantic = ">=2.12.4,<3" +gest-api = ">=0.1,<0.2" + +# macOS dependencies [tool.pixi.target.osx-arm64.dependencies] -clang_osx-arm64 = ">=19.1.2,<20" +clang_osx-arm64 = ">=22.1.0,<23" + +# Linux dependencies +[tool.pixi.target.linux-64.dependencies] +gxx_linux-64 = ">=15.2.0,<16" + +# Extra dependencies, from pypi +[dependency-groups] +extra = [ + "pyenchant>=3.2.2", + "enchant>=0.0.1,<0.0.2", + "redis>=7.1.0,<8", + "surmise>=0.3.0,<0.4", + "optimas @ git+https://github.com/optimas-org/optimas@multitask_uses_id", +] +dev = ["wat>=0.7.0,<0.8"] +docs = ["pyenchant", "enchant>=0.0.1,<0.0.2", "sphinx-lfs-content>=1.1.10,<2"] +# Various config from here onward [tool.black] line-length = 120 -target-version = ['py39', 'py310', 'py311', 'py312', 'py313'] +target-version = ["py311", "py312", "py313", "py314"] force-exclude = ''' ( /( @@ -117,10 +231,7 @@ force-exclude = ''' ''' [tool.typos.default] -extend-ignore-identifiers-re = [ - ".*NDArray.*", - "8ba9de56.*" -] +extend-ignore-identifiers-re = [".*NDArray.*", "8ba9de56.*"] [tool.typos.default.extend-words] als = "als" @@ -140,7 +251,13 @@ inpt = "inpt" extend-exclude = ["*.bib", "*.xml", "docs/nitpicky"] [tool.mypy] +# Initial, permissive mypy configuration for libensemble. +# Allows incremental adoption. To be tightened in future releases. +packages = ["libensemble.utils"] +exclude = 'docs/conf.py$|libensemble/utils/(launcher|loc_stack|runners|pydantic|output_directory)\.py$|libensemble/tests/(regression_tests|functionality_tests|unit_tests|scaling_tests)/.*' disable_error_code = ["import-not-found", "import-untyped"] - -[dependency-groups] -dev = ["pyenchant", "enchant>=0.0.1,<0.0.2", "flake8-modern-annotations>=1.6.0,<2", "flake8-type-checking>=3.0.0,<4"] +ignore_missing_imports = true +follow_imports = "skip" +check_untyped_defs = false +disallow_untyped_defs = false +warn_unused_ignores = false