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/workflows/basic.yml b/.github/workflows/basic.yml index 482c638ac8..f9e9bb338b 100644 --- a/.github/workflows/basic.yml +++ b/.github/workflows/basic.yml @@ -17,29 +17,16 @@ jobs: matrix: os: [ubuntu-latest] mpi-version: [mpich] - python-version: ["3.10", "3.11", "3.12", "3.13"] - pydantic-version: ["2.10.6"] + python-version: ["py310", "py311", "py312", "py313", "py314"] comms-type: [m, l] include: - os: macos-latest - python-version: "3.11" + python-version: "py311" mpi-version: mpich - pydantic-version: "2.10.6" comms-type: m - os: macos-latest - python-version: "3.11" + python-version: "py311" mpi-version: mpich - pydantic-version: "2.10.6" - comms-type: l - - os: ubuntu-latest - mpi-version: mpich - python-version: "3.10" - pydantic-version: "1.10.21" - comms-type: m - - os: ubuntu-latest - mpi-version: mpich - python-version: "3.10" - pydantic-version: "1.10.21" comms-type: l env: @@ -53,66 +40,43 @@ jobs: steps: - uses: actions/checkout@v4 - - 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 + lfs: true - # 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: Checkout lockfile + run: git lfs checkout - - 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 + - uses: prefix-dev/setup-pixi@v0.9.2 + with: + pixi-version: v0.55.0 + cache: true + frozen: true + environments: ${{ matrix.python-version }} + activate-environment: ${{ matrix.python-version }} - - name: Install mpi4py and MPI from conda + - name: Install IBCDFO run: | - conda install mpi4py ${{ matrix.mpi-version }} + pixi run -e ${{ matrix.python-version }} ./install/install_ibcdfo.sh - name: Install libEnsemble, test flake8 run: | - pip install pydantic==${{ matrix.pydantic-version }} pip install -e . flake8 libensemble - name: Remove various tests on newer pythons - if: matrix.python-version >= '3.11' + 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 3.12 + 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 }} + ./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 }} + pixi run -e ${{ matrix.python-version }} ./libensemble/tests/run_tests.py -A "-W error" -${{ matrix.comms-type }} - name: Merge coverage run: | @@ -128,5 +92,5 @@ jobs: if: contains(github.base_ref, 'develop') runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: crate-ci/typos@v1.34.0 + - uses: actions/checkout@v6 + - uses: crate-ci/typos@v1.42.0 diff --git a/.github/workflows/extra.yml b/.github/workflows/extra.yml index 1cd9f857b0..84392f2cc7 100644 --- a/.github/workflows/extra.yml +++ b/.github/workflows/extra.yml @@ -12,39 +12,24 @@ jobs: os: [ubuntu-latest] mpi-version: [mpich] python-version: ['3.10', '3.11', '3.12', '3.13'] - pydantic-version: ['2.10.6'] comms-type: [m, l] include: - os: macos-latest python-version: '3.13' mpi-version: mpich - pydantic-version: '2.10.6' comms-type: m - os: macos-latest python-version: '3.13' mpi-version: mpich - pydantic-version: '2.10.6' comms-type: l - os: ubuntu-latest python-version: '3.12' mpi-version: mpich - pydantic-version: '2.10.6' comms-type: t - os: ubuntu-latest mpi-version: 'openmpi' - pydantic-version: '2.10.6' python-version: '3.12' comms-type: l - - os: ubuntu-latest - mpi-version: mpich - python-version: '3.12' - pydantic-version: '1.10.21' - comms-type: m - - os: ubuntu-latest - mpi-version: mpich - python-version: '3.12' - pydantic-version: '1.10.21' - comms-type: l env: HYDRA_LAUNCHER: 'fork' @@ -56,7 +41,7 @@ jobs: shell: bash -l {0} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup conda - Python ${{ matrix.python-version }} uses: conda-incubator/setup-miniconda@v3 with: @@ -110,14 +95,6 @@ jobs: run: | pip install scikit-build packaging - - name: Install Balsam on Pydantic 1 - if: matrix.pydantic-version == '1.10.21' - run: | - conda install pyzmq - git clone https://github.com/argonne-lcf/balsam.git - sed -i -e "s/pyzmq>=22.1.0,<23.0.0/pyzmq>=23.0.0,<24.0.0/" ./balsam/setup.cfg - cd balsam; pip install -e .; cd .. - - name: Install other testing dependencies run: | pip install -r install/testing_requirements.txt @@ -127,7 +104,6 @@ jobs: - name: Install libEnsemble, flake8, lock environment run: | - pip install pydantic==${{ matrix.pydantic-version }} pip install -e . flake8 libensemble @@ -138,26 +114,14 @@ jobs: 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 on Pydantic 2 - if: matrix.pydantic-version == '2.10.6' + - name: Install redis/proxystore run: | pip install redis pip install proxystore==0.7.0 - - name: Remove proxystore test on Pydantic 1 - if: matrix.pydantic-version == '1.10.21' - run: | - rm ./libensemble/tests/regression_tests/test_proxystore_integration.py - - - name: Remove Balsam/Globus-compute tests on Pydantic 2 - if: matrix.pydantic-version == '2.10.6' - run: | - rm ./libensemble/tests/unit_tests/test_ufunc_runners.py - rm ./libensemble/tests/unit_tests/test_executor_balsam.py - - name: Start Redis if: matrix.os == 'ubuntu-latest' - uses: supercharge/redis-github-action@1.8.0 + uses: supercharge/redis-github-action@v2 with: redis-version: 7 @@ -179,5 +143,5 @@ jobs: if: contains(github.base_ref, 'develop') runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: crate-ci/typos@v1.34.0 + - uses: actions/checkout@v6 + - uses: crate-ci/typos@v1.42.0 diff --git a/docs/advanced_installation.rst b/docs/advanced_installation.rst index ad3131ee15..c02a63ed63 100644 --- a/docs/advanced_installation.rst +++ b/docs/advanced_installation.rst @@ -173,11 +173,9 @@ Optional Dependencies for Additional Features The following packages may be installed separately to enable additional features: -* Balsam_ - Manage and submit applications to the Balsam service with our :ref:`BalsamExecutor` * pyyaml_ and tomli_ - Parameterize libEnsemble via yaml or toml * `Globus Compute`_ - Submit simulation or generator function instances to remote Globus Compute endpoints -.. _Balsam: https://balsam.readthedocs.io/en/latest/ .. _conda-forge: https://conda-forge.org/ .. _Conda: https://docs.conda.io/en/latest/ .. _GitHub: https://github.com/Libensemble/libensemble diff --git a/docs/conf.py b/docs/conf.py index 7686b741f8..0b7e2b3dd4 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", diff --git a/docs/executor/balsam_2_executor.rst b/docs/executor/balsam_2_executor.rst deleted file mode 100644 index 2e2c361792..0000000000 --- a/docs/executor/balsam_2_executor.rst +++ /dev/null @@ -1,16 +0,0 @@ -.. _balsam-exctr: - -Balsam Executor - Remote apps -============================= - -.. automodule:: libensemble.executors.balsam_executor - :no-undoc-members: - -.. autoclass:: libensemble.executors.balsam_executor.BalsamExecutor - :show-inheritance: - :members: __init__, register_app, submit_allocation, revoke_allocation, submit - -.. autoclass:: libensemble.executors.balsam_executor.BalsamTask - :show-inheritance: - :member-order: bysource - :members: poll, wait, kill diff --git a/docs/executor/ex_index.rst b/docs/executor/ex_index.rst index f2912a8f56..ee4698c21f 100644 --- a/docs/executor/ex_index.rst +++ b/docs/executor/ex_index.rst @@ -14,4 +14,3 @@ portable interface for running and managing user applications. overview executor mpi_executor - balsam_2_executor diff --git a/docs/executor/overview.rst b/docs/executor/overview.rst index e3d025dfc8..196ba38b8b 100644 --- a/docs/executor/overview.rst +++ b/docs/executor/overview.rst @@ -26,17 +26,12 @@ The **Executor** provides a portable interface for running applications on any s ``result()``, and ``exception()`` functions from the standard. The main ``Executor`` class can subprocess serial applications in place, - while the ``MPIExecutor`` is used for running MPI applications, and the - ``BalsamExecutor`` for submitting MPI run requests from a worker running on - a compute node to the Balsam service. This second approach is suitable for - systems that don't allow submitting MPI applications from compute nodes. + while the ``MPIExecutor`` is used for running MPI applications. Typically, users choose and parameterize their ``Executor`` objects in their calling scripts, where each executable generator or simulation application is - registered to it. If an alternative Executor like Balsam is used, then the - applications can be registered as in the example below. Once in the user-side - worker code (sim/gen func), the Executor can be retrieved without any need to - specify the type. + registered to it. Once in the user-side worker code (sim/gen func), the Executor + can be retrieved without any need to specify the type. Once the Executor is retrieved, tasks can be submitted by specifying the ``app_name`` from registration in the calling script alongside other optional @@ -177,11 +172,6 @@ may resemble: print(task.state) # state may be finished/failed/killed -.. note:: - Applications or tasks submitted via the Balsam Executor are referred to as - **"jobs"** within Balsam, including within Balsam's database and when - describing the state of a completed submission. - The ``MPIExecutor`` autodetects system criteria such as the appropriate MPI launcher and mechanisms to poll and kill tasks. It also has access to the resource manager, which partitions resources among workers, ensuring that runs utilize different @@ -189,10 +179,9 @@ resources (e.g., nodes). Furthermore, the ``MPIExecutor`` offers resilience via 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 such as -Balsam_. Currently, these Executors launch at the application level within +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. -.. _Balsam: https://balsam.readthedocs.io/en/latest/ .. _concurrent futures: https://docs.python.org/library/concurrent.futures.html 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/known_issues.rst b/docs/known_issues.rst index 3910ad210c..89c596aae5 100644 --- a/docs/known_issues.rst +++ b/docs/known_issues.rst @@ -15,7 +15,7 @@ may occur when using libEnsemble. we recommend using ``local`` comms in place of ``mpi4py``. * When using the Executor: Open-MPI does not work with direct MPI task submissions in mpi4py comms mode, since Open-MPI does not support nested MPI - executions. Use either ``local`` mode or the Balsam Executor instead. + executions. Use ``local`` mode instead. * 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. diff --git a/docs/overview_usecases.rst b/docs/overview_usecases.rst index 56ad05b6c9..6d77b197b0 100644 --- a/docs/overview_usecases.rst +++ b/docs/overview_usecases.rst @@ -118,8 +118,7 @@ its capabilities. * **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 ``MPIExecutor`` and ``BalsamExecutor``. - The base ``Executor`` class allows local sub-processing of serial tasks. + 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 diff --git a/docs/platforms/platforms_index.rst b/docs/platforms/platforms_index.rst index c06cdbe6fd..79285aa7b0 100644 --- a/docs/platforms/platforms_index.rst +++ b/docs/platforms/platforms_index.rst @@ -182,33 +182,15 @@ tasks. However, running libEnsemble on the compute nodes is potentially more scalable and will better manage simulation and generation functions that contain considerable -computational work or I/O. Therefore the second option is to use proxy task-execution -services like Balsam_. - -Balsam - Externally Managed Applications ----------------------------------------- - -Running libEnsemble on the compute nodes while still submitting additional applications -requires alternative Executors that connect to external services like Balsam_. Balsam -can take tasks submitted by workers and execute them on the remaining compute nodes, -or *to entirely different systems*. - - .. figure:: ../images/balsam2.png - :alt: balsam2 - :scale: 40 - :align: center - - (New) Multi-System: libEnsemble + BalsamExecutor - -Submission scripts for running on launch/MOM nodes and for using Balsam can be found in -the :doc:`examples`. +computational work or I/O. Therefore the second option is to use Globus Compute +to isolate this work from the workers. .. _globus_compute_ref: Globus Compute - Remote User Functions -------------------------------------- -*Alternatively to much of the above*, if libEnsemble is running on some resource with +If libEnsemble is running on some resource with internet access (laptops, login nodes, other servers, etc.), workers can be instructed to launch generator or simulator user function instances to separate resources from themselves via `Globus Compute`_ (formerly funcX), a distributed, high-performance function-as-a-service platform: @@ -278,7 +260,6 @@ libEnsemble on specific HPC systems. srun example_scripts -.. _Balsam: https://balsam.readthedocs.io/en/latest/ .. _Globus Compute: https://www.globus.org/compute .. _Globus Compute endpoints: https://globus-compute.readthedocs.io/en/latest/endpoints.html .. _Globus: https://www.globus.org/ diff --git a/docs/requirements.txt b/docs/requirements.txt index 58efae7694..7c68cd9a43 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,4 @@ -sphinx<9 +sphinx<10 sphinxcontrib-bibtex sphinxcontrib-spelling autodoc_pydantic diff --git a/docs/running_libE.rst b/docs/running_libE.rst index ae658e31c6..7b8b0532d8 100644 --- a/docs/running_libE.rst +++ b/docs/running_libE.rst @@ -100,9 +100,8 @@ supercomputers. **Limitations of MPI mode** 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 - (see :doc:`Balsam`). This nesting does work - with MPICH_ and its derivative MPI implementations. + supported with Open MPI**. This can be overcome by using a proxy launcher. + This nesting does work with MPICH_ and its derivative MPI implementations. 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. diff --git a/install/install_ibcdfo.sh b/install/install_ibcdfo.sh index efd5f6dcb5..0ed790f01a 100644 --- a/install/install_ibcdfo.sh +++ b/install/install_ibcdfo.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -git clone --recurse-submodules -b develop https://github.com/POptUS/IBCDFO.git +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 diff --git a/install/misc_feature_requirements.txt b/install/misc_feature_requirements.txt index ab774eae07..740786b670 100644 --- a/install/misc_feature_requirements.txt +++ b/install/misc_feature_requirements.txt @@ -1 +1 @@ -globus-compute-sdk==3.8.0 +globus-compute-sdk==4.3.0 diff --git a/install/testing_requirements.txt b/install/testing_requirements.txt index bfd9a0527e..fe937f6557 100644 --- a/install/testing_requirements.txt +++ b/install/testing_requirements.txt @@ -1,11 +1,11 @@ flake8==7.3.0 coverage>=7.5 -pytest==8.4.1 -pytest-cov==6.2.1 +pytest==9.0.2 +pytest-cov==7.0.0 pytest-timeout==2.4.0 mock==5.2.0 python-dateutil==2.9.0.post0 -anyio==4.9.0 -matplotlib==3.10.3 +anyio==4.12.0 +matplotlib==3.10.8 mpmath==1.3.0 -rich==14.0.0 +rich==14.2.0 diff --git a/libensemble/executors/__init__.py b/libensemble/executors/__init__.py index 64e97746d6..563fa33525 100644 --- a/libensemble/executors/__init__.py +++ b/libensemble/executors/__init__.py @@ -1,12 +1,4 @@ -import importlib.util - from libensemble.executors.executor import Executor from libensemble.executors.mpi_executor import MPIExecutor -if importlib.util.find_spec("balsam"): - try: - from libensemble.executors.balsam_executor import BalsamExecutor # noqa: F401 - - __all__ = ["Executor", "MPIExecutor", "BalsamExecutor"] - except (ModuleNotFoundError, ImportError, AttributeError): - __all__ = ["Executor", "MPIExecutor"] +__all__ = ["Executor", "MPIExecutor"] diff --git a/libensemble/executors/balsam_executor.py b/libensemble/executors/balsam_executor.py deleted file mode 100644 index 54c9f78263..0000000000 --- a/libensemble/executors/balsam_executor.py +++ /dev/null @@ -1,580 +0,0 @@ -""" -This module launches and controls tasks via Balsam_, and -can submit tasks from any machine, to any machine running a Balsam site_. - -.. image:: ../images/balsam2.png - :alt: central_balsam - :scale: 40 - :align: center - -At this time, access to Balsam is limited to those with valid organizational logins -authenticated through Globus_. - -.. tab-set:: - - .. tab-item:: Initialization - - To initialize a Balsam executor:: - - from libensemble.executors.balsam_executors import BalsamExecutor - exctr = BalsamExecutor() - - .. tab-item:: App and Resource registration - - Note that - Balsam ``ApplicationDefinition`` instances are registered instead of paths and task - submissions will not run until Balsam reserves compute resources at a site:: - - from libensemble.executors.balsam_executors import BalsamExecutor - from balsam.api import ApplicationDefinition - - class HelloApp(ApplicationDefinition): - site = "my-balsam-site" - command_template = "/path/to/hello.app {{ my_name }}" - - exctr = BalsamExecutor() - exctr.register_app(HelloApp, app_name="hello") - - exctr.submit_allocation( - site_id=999, # corresponds to "my-balsam-site", found via ``balsam site ls`` - num_nodes=4, # Total number of nodes requested for *all jobs* - wall_time_min=30, - queue="debug-queue", - project="my-project", - ) - - .. tab-item:: Task Submission - - Task submissions of registered apps aren't too different from the other executors, - except Balsam expects application arguments in dictionary form. Note that these fields - must match the templating syntax in each ``ApplicationDefinition``'s ``command_template`` - field:: - - args = {"my_name": "World"} - - task = exctr.submit( - app_name="hello", - app_args=args, - num_procs=4, - num_nodes=1, - procs_per_node=4, - ) - -Application instances submitted by the executor to the Balsam service will get -scheduled within the reserved resource allocation. **Each Balsam app can only be -submitted to the site specified in its class definition.** Output files will appear -in the Balsam site's ``data`` directory, but can be automatically `transferred back`_ -via Globus. - -**Reading Balsam's documentation is highly recommended.** - -.. _site: https://balsam.readthedocs.io/en/latest/user-guide/site-config/ -.. _Balsam: https://balsam.readthedocs.io/en/latest/ -.. _`transferred back`: https://balsam.readthedocs.io/en/latest/user-guide/transfer/ -.. _Globus: https://www.globus.org/ -""" - -from __future__ import annotations - -import datetime -import logging -import os -import time - -from balsam import util - -from libensemble.executors import Executor -from libensemble.executors.executor import Application, ExecutorException, Task, TimeoutExpired, jassert - -util.config_root_logger("ERROR") # Balsam prevent auto-load client warning? - -from balsam.api import ApplicationDefinition, BatchJob, EventLog, Job # noqa: E402 - -logger = logging.getLogger(__name__) -# To change logging level for just this module -# logger.setLevel(logging.DEBUG) - - -class BalsamTask(Task): - """Wraps a Balsam ``Job`` from the Balsam service. - - The same attributes and query routines are implemented. Use ``task.process`` - to refer to the matching Balsam ``Job`` initialized by the ``BalsamExecutor``, - with every Balsam ``Job`` method invocable on it. Otherwise, libEnsemble task methods - like ``poll()`` can be used directly. - - """ - - def __init__( - self, - app: Application | None = None, - app_args: dict = None, - workdir: str | None = None, - stdout: str = None, - stderr: str = None, - workerid: int = None, - ) -> None: - """Instantiate a new ``BalsamTask`` instance. - - A new ``BalsamTask`` object is created with an id, status and - configuration attributes. This will normally be created by the - executor on a submission. - """ - # May want to override workdir with Balsam value when it exists - Task.__init__(self, app, app_args, workdir, stdout, stderr, workerid) - - def _get_time_since_balsam_submit(self) -> int: - """Return time since balsam task entered ``RUNNING`` state""" - event_query = EventLog.objects.filter(job_id=self.process.id, to_state="RUNNING") - if not len(event_query): - return 0 - balsam_launch_datetime = event_query[0].timestamp - current_datetime = datetime.datetime.now() - if balsam_launch_datetime: - return (current_datetime - balsam_launch_datetime).total_seconds() - else: - return 0 - - def calc_task_timing(self) -> None: - """Calculate timing information for this task""" - # Get runtime from Balsam - self.runtime = self._get_time_since_balsam_submit() - - if self.submit_time is None: - logger.warning("Cannot calc task total_time - submit time not set") - return - - if self.total_time is None: - self.total_time = time.time() - self.submit_time - - def _set_complete(self, dry_run: bool = False) -> None: - """Set task as complete""" - self.finished = True - if dry_run: - self.success = True - self.state = "FINISHED" - else: - balsam_state = self.process.state - self.workdir = self.workdir or self.process.working_directory - self.calc_task_timing() - if balsam_state in [ - "RUN_DONE", - "POSTPROCESSED", - "STAGED_OUT", - "JOB_FINISHED", - ]: - self.success = True - self.state = "FINISHED" - else: - self.state = balsam_state - - logger.info(f"Task {self.name} ended with state {self.state}") - - def poll(self) -> None: - """Polls and updates the status attributes of the supplied task. Requests - Job information from Balsam service.""" - if self.dry_run: - return - - if not self._check_poll(): - return - - # Get current state of tasks from Balsam database - self.process.refresh_from_db() - balsam_state = self.process.state - self.runtime = self._get_time_since_balsam_submit() - - if balsam_state in ["RUN_DONE", "POSTPROCESSED", "STAGED_OUT", "JOB_FINISHED"]: - self._set_complete() - - elif balsam_state in ["RUNNING"]: - self.state = "RUNNING" - self.workdir = self.workdir or self.process.working_directory - - elif balsam_state in [ - "CREATED", - "AWAITING_PARENTS", - "READY", - "STAGED_IN", - "PREPROCESSED", - ]: - self.state = "WAITING" - - elif balsam_state in ["RUN_ERROR", "RUN_TIMEOUT", "FAILED"]: - self.state = "FAILED" - self._set_complete() - - def wait(self, timeout: int | None = None) -> None: - """Waits on completion of the task or raises ``TimeoutExpired``. - - Status attributes of task are updated on completion. - - Parameters - ---------- - - timeout: int or float, Optional - Time in seconds after which a TimeoutExpired exception is raised. - If not set, then simply waits until completion. - Note that the task is not automatically killed on timeout. - """ - - if self.dry_run: - return - - if not self._check_poll(): - return - - # Wait on the task - start = time.time() - self.process.refresh_from_db() - while self.process.state not in [ - "RUN_DONE", - "POSTPROCESSED", - "STAGED_OUT", - "JOB_FINISHED", - "RUN_ERROR", - "RUN_TIMEOUT", - "FAILED", - ]: - time.sleep(0.2) - self.process.refresh_from_db() - if timeout and time.time() - start > timeout: - self.runtime = self._get_time_since_balsam_submit() - raise TimeoutExpired(self.name, timeout) - - self.runtime = self._get_time_since_balsam_submit() - self._set_complete() - - def kill(self) -> None: - """**Cancels** the task. Killing a running task is unsupported by Balsam at this time.""" - self.process.delete() - - logger.info(f"Killing task {self.name}") - self.state = "USER_KILLED" - self.finished = True - self.calc_task_timing() - - -class BalsamExecutor(Executor): - """Wraps the Balsam service. Via this Executor, - Balsam ``Jobs`` can be submitted to Balsam sites, either local or on remote machines. - - .. note:: Task kills are not configurable in the Balsam executor. - - """ - - def __init__(self) -> None: - """Instantiate a new ``BalsamExecutor`` instance.""" - super().__init__() - - self.workflow_name = "libe_workflow" - self.allocations = [] - - def serial_setup(self) -> None: - """Balsam serial setup includes emptying database and adding applications""" - pass - - def add_app(self, *args) -> None: - """Sync application with Balsam service""" - pass - - def register_app( - self, - BalsamApp: ApplicationDefinition, - app_name: str | None = None, - calc_type: str | None = None, - desc: str = None, - precedent: str | None = None, - ) -> None: - """Registers a Balsam ``ApplicationDefinition`` to libEnsemble. This class - instance *must* have a ``site`` and ``command_template`` specified. See - the Balsam docs for information on other optional fields. - - Parameters - ---------- - - BalsamApp: ``ApplicationDefinition`` object - A Balsam ``ApplicationDefinition`` instance. - - app_name: str, Optional - Name to identify this application. - - calc_type: str, Optional - Calculation type: Set this application as the default ``'sim'`` - or ``'gen'`` function. - - desc: str, Optional - Description of this application - - """ - - if precedent is not None: - logger.warning("precedent is ignored in Balsam executor - add to command template") - - if not app_name: - app_name = BalsamApp.command_template.split(" ")[0] - self.apps[app_name] = Application(" ", app_name, calc_type, desc, BalsamApp) - - # Default sim/gen apps will be deprecated. Just use names. - if calc_type is not None: - jassert( - calc_type in self.default_apps, - "Unrecognized calculation type", - calc_type, - ) - self.default_apps[calc_type] = self.apps[app_name] - - def submit_allocation( - self, - site_id: str, - num_nodes: int, - wall_time_min: int, - job_mode: str = "mpi", - queue: str = "local", - project: str = "local", - optional_params: dict = {}, - filter_tags: dict = {}, - partitions: list = [], - ) -> BatchJob: - """ - Submits a Balsam ``BatchJob`` machine allocation request to Balsam. - Corresponding Balsam applications with a matching site can be submitted to - this allocation. Effectively a wrapper for ``BatchJob.objects.create()``. - - Parameters - ---------- - - site_id: int - The corresponding ``site_id`` for a Balsam site. Retrieve via ``balsam site ls`` - - num_nodes: int - The number of nodes to request from a machine with a running Balsam site - - wall_time_min: int - The number of walltime minutes to request for the ``BatchJob`` allocation - - job_mode: str, Optional - Either ``"serial"`` or ``"mpi"``. Default: ``"mpi"`` - - queue: str, Optional - Specifies the queue from which the ``BatchJob`` should request nodes. Default: ``"local"`` - - project: str, Optional - Specifies the project that should be charged for the requested machine time. Default: ``"local"`` - - optional_params: dict, Optional - Additional system-specific parameters to set, based on fields in Balsam's ``job-template.sh`` - - filter_tags: dict, Optional - Directs the resultant ``BatchJob`` to only run Jobs with matching tags. - - partitions: List[dict], Optional - Divides the allocation into multiple launcher partitions, with differing - ``job_mode``, ``num_nodes``. ``filter_tags``, etc. See the Balsam docs. - - Returns - ------- - - The corresponding ``BatchJob`` object. - """ - - allocation = BatchJob.objects.create( - site_id=site_id, - num_nodes=num_nodes, - wall_time_min=wall_time_min, - job_mode=job_mode, - queue=queue, - project=project, - optional_params=optional_params, - filter_tags=filter_tags, - partitions=partitions, - ) - - self.allocations.append(allocation) - - logger.info( - f"Submitted Batch allocation to site {site_id}: " f"nodes {num_nodes} queue {queue} project {project}" - ) - - return allocation - - def revoke_allocation(self, allocation: BatchJob, timeout: int = 60) -> bool: - """ - Terminates a Balsam ``BatchJob`` machine allocation remotely. Balsam apps should - no longer be submitted to this allocation. Best to run after libEnsemble - completes, or after this ``BatchJob`` is no longer needed. Helps save machine time. - - Parameters - ---------- - - allocation: ``BatchJob`` object - a ``BatchJob`` with a corresponding machine allocation that should be cancelled. - - timeout: int, Optional - Timeout and warn user after this many seconds of attempting to revoke an allocation. - """ - allocation.refresh_from_db() - - start = time.time() - - while not allocation.scheduler_id: - time.sleep(1) - allocation.refresh_from_db() - if time.time() - start > timeout: - logger.warning( - "Unable to terminate Balsam BatchJob. You may need to login to the machine and manually remove it." - ) - return False - - batchjob = BatchJob.objects.get(scheduler_id=allocation.scheduler_id) - batchjob.state = "pending_deletion" - batchjob.save() - return True - - def set_resources(self, resources: str) -> None: - self.resources = resources - - def submit( - self, - calc_type: str | None = None, - app_name: str | None = None, - app_args: dict = None, - num_procs: int = None, - num_nodes: int = None, - procs_per_node: int = None, - max_tasks_per_node: int = None, - machinefile: str | None = None, - gpus_per_rank: int = 0, - transfers: dict = {}, - workdir: str = "", - dry_run: bool = False, - wait_on_start: bool = False, - extra_args: dict = {}, - tags: dict = {}, - ) -> BalsamTask: - """Initializes and submits a Balsam ``Job`` based on a registered ``ApplicationDefinition`` - and requested resources. A corresponding libEnsemble ``Task`` object is returned. - - Parameters - ---------- - - calc_type: str, Optional - The calculation type: ``'sim'`` or ``'gen'`` - Only used if ``app_name`` is not supplied. Uses default sim or gen application. - - app_name: str, Optional - The application name. Must be supplied if ``calc_type`` is not. - - app_args: dict - A dictionary of options that correspond to fields to template in the - ApplicationDefinition's ``command_template`` field. - - num_procs: int, Optional - The total number of MPI ranks on which to submit the task - - num_nodes: int, Optional - The number of nodes on which to submit the task - - procs_per_node: int, Optional - The processes per node for this task - - max_tasks_per_node: int - Instructs Balsam to schedule at most this many Jobs per node. - - machinefile: str, Optional - Name of a machinefile for this task to use. Unused by Balsam - - gpus_per_rank: int, Optional - Number of GPUs to reserve for each MPI rank - - transfers: dict, Optional - A Job-specific Balsam transfers dictionary that corresponds with an - ``ApplicationDefinition`` ``transfers`` field. See the Balsam docs for - more information. - - workdir: str - Specifies as name for the Job's output directory within the Balsam site's - data directory. Default: ``libe_workflow`` - - dry_run: bool, Optional - Whether this is a dry run - no task will be launched; instead - runline is printed to logger (at ``INFO`` level) - - wait_on_start: bool, Optional - Whether to block, and wait for task to be polled as ``RUNNING`` (or other - active/end state) before continuing - - extra_args: dict, Optional - Additional arguments to supply to MPI runner. - - tags: dict, Optional - Additional tags to organize the ``Job`` or restrict which ``BatchJobs`` run it. - - Returns - ------- - - task: BalsamTask - The launched task object - - - Note that since Balsam Jobs are often sent to entirely different machines - than where libEnsemble is running, how libEnsemble's resource manager - has divided local resources among workers doesn't impact what resources - can be requested for a Balsam ``Job`` running on an entirely different machine. - - """ - - if app_name is not None: - app = self.get_app(app_name) - elif calc_type is not None: - app = self.default_app(calc_type) - else: - raise ExecutorException("Either app_name or calc_type must be set") - - if len(workdir): - workdir = os.path.join(self.workflow_name, workdir) - else: - workdir = self.workflow_name - - if machinefile is not None: - logger.warning("machinefile arg ignored - not supported in Balsam") - - task = BalsamTask(app, app_args, workdir, None, None, self.workerID) - - if dry_run: - task.dry_run = True - logger.info(f"Test (No submit) Balsam app {app_name}") - task._set_complete(dry_run=True) - else: - App = app.pyobj - - try: - App.sync() # if App source-code available, send to Balsam service - except OSError: - pass # App retrieved from Balsam service, assume no access to source-code - - task.process = Job( - app_id=App, - workdir=workdir, - parameters=app_args, - num_nodes=num_nodes, - ranks_per_node=procs_per_node, - launch_params=extra_args, - gpus_per_rank=gpus_per_rank, - node_packing_count=max_tasks_per_node, - transfers=transfers, - ) - - task.process.save() - - if wait_on_start: - self._wait_on_start(task) - - if not task.timer.timing and not task.finished: - task.timer.start() - task.submit_time = task.timer.tstart # Time not date - may not need if using timer. - - logger.info(f"Submitted Balsam App to site {App.site}: " "nodes {num_nodes} ppn {procs_per_node}") - - self.list_of_tasks.append(task) - return task diff --git a/libensemble/executors/executor.py b/libensemble/executors/executor.py index d9cf6f428d..bd7e50704f 100644 --- a/libensemble/executors/executor.py +++ b/libensemble/executors/executor.py @@ -80,7 +80,7 @@ def __init__( name: str | None = None, calc_type: str | None = "sim", desc: str | None = None, - pyobj: Any | None = None, # used by balsam_executor to store ApplicationDefinition + pyobj: Any | None = None, precedent: str = "", ) -> None: """Instantiates a new Application instance.""" diff --git a/libensemble/gen_funcs/aposmm_localopt_support.py b/libensemble/gen_funcs/aposmm_localopt_support.py index 3545afd892..1bbe27fdfa 100644 --- a/libensemble/gen_funcs/aposmm_localopt_support.py +++ b/libensemble/gen_funcs/aposmm_localopt_support.py @@ -9,6 +9,7 @@ "run_local_tao", "run_local_dfols", "run_local_ibcdfo_pounders", + "run_local_ibcdfo_manifold_sampling", "run_local_scipy_opt", "run_external_localopt", ] @@ -26,7 +27,7 @@ class APOSMMException(Exception): """Raised for any exception in APOSMM""" -optimizer_list = ["petsc", "nlopt", "dfols", "scipy", "ibcdfo", "external"] +optimizer_list = ["petsc", "nlopt", "dfols", "scipy", "ibcdfo_pounders", "ibcdfo_manifold_sampling", "external"] optimizers = libensemble.gen_funcs.rc.aposmm_optimizers if optimizers is not None: @@ -42,8 +43,10 @@ class APOSMMException(Exception): import nlopt # noqa: F401 if "dfols" in optimizers: import dfols # noqa: F401 - if "ibcdfo" in optimizers: - from ibcdfo import pounders # noqa: F401 + if "ibcdfo_pounders" in optimizers: + from ibcdfo import run_pounders + if "ibcdfo_manifold_sampling" in optimizers: + 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: @@ -79,6 +82,7 @@ class LocalOptInterfacer(object): - PETSc/TAO [``'pounders'``, ``'blmvm'``, ``'nm'``] - SciPy [``'scipy_Nelder-Mead'``, ``'scipy_COBYLA'``, ``'scipy_BFGS'``] - DFOLS [``'dfols'``] + - IBCDFO [``'pounders'``, ``'manifold_sampling_primal'``] - External local optimizer [``'external_localopt'``] (which use files to pass/receive ``x/f`` values) """ @@ -123,6 +127,8 @@ def __init__(self, user_specs, x0, f0, grad0=None): run_local_opt = run_local_dfols elif user_specs["localopt_method"] in ["ibcdfo_pounders"]: run_local_opt = run_local_ibcdfo_pounders + elif user_specs["localopt_method"] in ["ibcdfo_manifold_sampling"]: + run_local_opt = run_local_ibcdfo_manifold_sampling elif user_specs["localopt_method"] in ["external_localopt"]: run_local_opt = run_external_localopt else: @@ -417,6 +423,60 @@ def run_local_dfols(user_specs, comm_queue, x0, f0, child_can_read, parent_can_r finish_queue(x_opt, opt_flag, comm_queue, parent_can_read, user_specs) +def run_local_ibcdfo_manifold_sampling(user_specs, comm_queue, x0, f0, child_can_read, parent_can_read): + """ + Runs a IBCDFO local optimization run starting at ``x0``, governed by the + parameters in ``user_specs``. + + Although IBCDFO methods can receive previous evaluations, few other methods + support that, so APOSMM assumes the first point will be re-evaluated (but + not be sent back to the manager). + """ + n = len(x0) + # Define bound constraints (lower <= x <= upper) + lb = np.zeros(n) + ub = np.ones(n) + + # Set random seed (for reproducibility) + np.random.seed(0) + + # dist_to_bound = min(min(ub - x0), min(x0 - lb)) + # assert dist_to_bound > np.finfo(np.float64).eps, "The distance to the boundary is too small" + + run_max_eval = user_specs.get("run_max_eval", 100 * (n + 1)) + # g_tol = 1e-8 + # delta_0 = 0.5 * dist_to_bound + # m = len(f0) + subprob_switch = "linprog" + + [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, + lb, + ub, + run_max_eval, + subprob_switch, + ) + + assert flag >= 0 or flag == -6, "IBCDFO errored" + + x_opt = X[xkin] + + if flag > 0: + opt_flag = 1 + else: + print( + "[APOSMM] The IBCDFO run started from " + str(x0) + " stopped with an exit " + "flag of " + str(flag) + ". No point from this run will be " + "ruled as a minimum! APOSMM may start a new run from some point " + "in this run." + ) + opt_flag = 0 + + finish_queue(x_opt, opt_flag, comm_queue, parent_can_read, user_specs) + + def run_local_ibcdfo_pounders(user_specs, comm_queue, x0, f0, child_can_read, parent_can_read): """ Runs a IBCDFO local optimization run starting at ``x0``, governed by the @@ -447,7 +507,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.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, diff --git a/libensemble/gen_funcs/persistent_aposmm.py b/libensemble/gen_funcs/persistent_aposmm.py index b1e3a3d9a5..685fa4021d 100644 --- a/libensemble/gen_funcs/persistent_aposmm.py +++ b/libensemble/gen_funcs/persistent_aposmm.py @@ -739,7 +739,7 @@ def initialize_children(user_specs): "nm", ]: fields_to_pass = ["x_on_cube", "f"] - elif user_specs["localopt_method"] in ["pounders", "ibcdfo_pounders", "dfols"]: + elif user_specs["localopt_method"] in ["pounders", "ibcdfo_pounders", "ibcdfo_manifold_sampling", "dfols"]: fields_to_pass = ["x_on_cube", "fvec"] else: raise NotImplementedError(f"Unknown local optimization method {user_specs['localopt_method']}.") diff --git a/libensemble/gen_funcs/persistent_ax_multitask.py b/libensemble/gen_funcs/persistent_ax_multitask.py index ede50e46b2..0a2e07f209 100644 --- a/libensemble/gen_funcs/persistent_ax_multitask.py +++ b/libensemble/gen_funcs/persistent_ax_multitask.py @@ -376,9 +376,9 @@ def max_utility_from_GP(n, m, gr, hifi_task): f, cov = m.predict(obsf) # Compute expected utility u = -np.array(f["hifi_metric"]) - best_arm_indx = np.flip(np.argsort(u))[:n] + best_arm_index = np.flip(np.argsort(u))[:n] gr_new = GeneratorRun( - arms=[gr.arms[i] for i in best_arm_indx], + arms=[gr.arms[i] for i in best_arm_index], weights=[1.0] * n, ) return gr_new diff --git a/libensemble/gen_funcs/persistent_gpCAM.py b/libensemble/gen_funcs/persistent_gpCAM.py index 05b08bb5ed..262ca2d6b0 100644 --- a/libensemble/gen_funcs/persistent_gpCAM.py +++ b/libensemble/gen_funcs/persistent_gpCAM.py @@ -158,7 +158,7 @@ def persistent_gpCAM(H_in, persis_info, gen_specs, libE_info): """ 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 subequent iterations, it calls an optimization method to produce the next + 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. diff --git a/libensemble/gen_funcs/persistent_sampling.py b/libensemble/gen_funcs/persistent_sampling.py index 401ccdaa94..375d7f4387 100644 --- a/libensemble/gen_funcs/persistent_sampling.py +++ b/libensemble/gen_funcs/persistent_sampling.py @@ -30,7 +30,7 @@ def _get_user_params(user_specs): @persistent_input_fields(["sim_id"]) -@output_data([("x", float, (2,))]) # The dimesion of 2 is a default and can be overwritten +@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 diff --git a/libensemble/tests/functionality_tests/check_libE_stats.py b/libensemble/tests/functionality_tests/check_libE_stats.py index 424c07d8b1..304925dc1e 100644 --- a/libensemble/tests/functionality_tests/check_libE_stats.py +++ b/libensemble/tests/functionality_tests/check_libE_stats.py @@ -1,4 +1,4 @@ -""" Script to check format of libE_stats.txt +"""Script to check format of libE_stats.txt Checks matching start and end times existing for calculation and tasks if required. Checks that dates/times are in a valid format. diff --git a/libensemble/tests/regression_tests/declare_hfun_and_combine_model_with_jax.py b/libensemble/tests/regression_tests/declare_hfun_and_combine_model_with_jax.py new file mode 100644 index 0000000000..0e49363e25 --- /dev/null +++ b/libensemble/tests/regression_tests/declare_hfun_and_combine_model_with_jax.py @@ -0,0 +1,50 @@ +# This declares the hfun for Test_compare_pounder_pounders_with_jax.py and +# then used jax to combine the quadratic models of each component of the +# inputs to the hfun. +# +# For other general use cases of pounders on smooth hfuns, only the hfun below +# needs to be changed (and combinemodels_jax can be given to pounders) + + +import jax +import numpy + +jax.config.update("jax_enable_x64", True) + + +def hfun(z): + res = z[0] * z[1] - z[2] ** 2 + return res + + +@jax.jit +def hfun_d(z, zd): + resd = jax.jvp(hfun, (z,), (zd,)) + return resd + + +@jax.jit +def hfun_dd(z, zd, zdt, zdd): + _, resdd = jax.jvp(hfun_d, (z, zd), (zdt, zdd)) + return resdd + + +def G_combine(Cres, Gres): + n, m = Gres.shape + G = numpy.zeros(n) + for i in range(n): + _, G[i] = hfun_d(Cres, Gres[i, :]) + return G + + +def H_combine(Cres, Gres, Hres): + n, _, m = Hres.shape + H = numpy.zeros((n, n)) + for i in range(n): + for j in range(n): + _, H[i, j] = hfun_dd(Cres, Gres[i, :], Gres[j, :], Hres[i, j, :]) + return H + + +def combinemodels_jax(Cres, Gres, Hres): + return G_combine(Cres, Gres), H_combine(Cres, Gres, Hres) diff --git a/libensemble/tests/regression_tests/support.py b/libensemble/tests/regression_tests/support.py index 4189bcfe48..cd6386aa36 100644 --- a/libensemble/tests/regression_tests/support.py +++ b/libensemble/tests/regression_tests/support.py @@ -1,7 +1,6 @@ import copy import numpy as np - from libensemble.specs import input_fields, output_data branin_vals_and_minima = np.array( diff --git a/libensemble/tests/regression_tests/test_1d_sampling.py b/libensemble/tests/regression_tests/test_1d_sampling.py index edecabb668..71ee298f62 100644 --- a/libensemble/tests/regression_tests/test_1d_sampling.py +++ b/libensemble/tests/regression_tests/test_1d_sampling.py @@ -14,7 +14,6 @@ # TESTSUITE_NPROCS: 2 4 import numpy as np - from libensemble import Ensemble from libensemble.gen_funcs.sampling import latin_hypercube_sample as gen_f diff --git a/libensemble/tests/regression_tests/test_2d_sampling.py b/libensemble/tests/regression_tests/test_2d_sampling.py index 8164c2844a..32844ae57d 100644 --- a/libensemble/tests/regression_tests/test_2d_sampling.py +++ b/libensemble/tests/regression_tests/test_2d_sampling.py @@ -14,7 +14,6 @@ # TESTSUITE_NPROCS: 2 4 import numpy as np - from libensemble import Ensemble from libensemble.gen_funcs.sampling import latin_hypercube_sample as gen_f diff --git a/libensemble/tests/regression_tests/test_GPU_variable_resources.py b/libensemble/tests/regression_tests/test_GPU_variable_resources.py index b6b3197f90..914beee471 100644 --- a/libensemble/tests/regression_tests/test_GPU_variable_resources.py +++ b/libensemble/tests/regression_tests/test_GPU_variable_resources.py @@ -26,7 +26,6 @@ # TESTSUITE_NPROCS: 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 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..5abb42b69a 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 @@ -35,7 +35,6 @@ # TESTSUITE_NPROCS: 10 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 diff --git a/libensemble/tests/regression_tests/test_evaluate_mixed_sample.py b/libensemble/tests/regression_tests/test_evaluate_mixed_sample.py index 481db84191..98e3ce8ece 100644 --- a/libensemble/tests/regression_tests/test_evaluate_mixed_sample.py +++ b/libensemble/tests/regression_tests/test_evaluate_mixed_sample.py @@ -17,7 +17,6 @@ import warnings import numpy as np - from libensemble import Ensemble from libensemble.alloc_funcs.give_pregenerated_work import give_pregenerated_sim_work as alloc_f diff --git a/libensemble/tests/regression_tests/test_gpCAM.py b/libensemble/tests/regression_tests/test_gpCAM.py index 218ecfc918..ea2c3c216e 100644 --- a/libensemble/tests/regression_tests/test_gpCAM.py +++ b/libensemble/tests/regression_tests/test_gpCAM.py @@ -27,7 +27,6 @@ import warnings 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 diff --git a/libensemble/tests/regression_tests/test_inverse_bayes_example.py b/libensemble/tests/regression_tests/test_inverse_bayes_example.py index f1e5d1cc3a..31f0c632fd 100644 --- a/libensemble/tests/regression_tests/test_inverse_bayes_example.py +++ b/libensemble/tests/regression_tests/test_inverse_bayes_example.py @@ -19,7 +19,6 @@ # TESTSUITE_NPROCS: 3 4 import numpy as np - from libensemble import Ensemble from libensemble.alloc_funcs.inverse_bayes_allocf import only_persistent_gens_for_inverse_bayes as alloc_f from libensemble.gen_funcs.persistent_inverse_bayes import persistent_updater_after_likelihood as gen_f diff --git a/libensemble/tests/regression_tests/test_persistent_aposmm_dfols.py b/libensemble/tests/regression_tests/test_persistent_aposmm_dfols.py index 6e19930691..cd40d59826 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_dfols.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_dfols.py @@ -21,9 +21,8 @@ import multiprocessing import sys -import numpy as np - import libensemble.gen_funcs +import numpy as np # Import libEnsemble items for this test from libensemble.libE import libE diff --git a/libensemble/tests/regression_tests/test_persistent_aposmm_exception.py b/libensemble/tests/regression_tests/test_persistent_aposmm_exception.py index b197dc3f07..60e4ef1a12 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_exception.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_exception.py @@ -18,9 +18,8 @@ import multiprocessing import sys -import numpy as np - import libensemble.gen_funcs +import numpy as np # Import libEnsemble items for this test from libensemble.libE import libE 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..a76268a857 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_external_localopt.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_external_localopt.py @@ -26,9 +26,8 @@ import shutil # For copying the external_localopt script import sys -import numpy as np - import libensemble.gen_funcs +import numpy as np # Import libEnsemble items for this test from libensemble.libE import libE 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 new file mode 100644 index 0000000000..0fcc011fb2 --- /dev/null +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_ibcdfo_manifold_sampling.py @@ -0,0 +1,127 @@ +""" +Runs libEnsemble with APOSMM+IBCDFO on two test problems. Only a single +optimization run is being performed for the below setup. + +The first case uses POUNDERS to solve the chwirut least-squares problem. For +this case, all chwirut 214 residual calculations for a given point are +performed as a single simulation evaluation. + +The second case uses the generalized POUNDERS to minimize normalized beamline +emittance. The "beamline simulation" is a synthetic polynomial test function +that takes in 4 variables and returning 3 outputs. These outputs represent +position , momentum , and the correlation between them . + +These values are then mapped to the normalized emittance - . + +Execute via one of the following commands: + mpiexec -np 3 python test_persistent_aposmm_ibcdfo_pounders.py + python test_persistent_aposmm_ibcdfo_pounders.py --nworkers 2 +Both will run with 1 manager, 1 worker running APOSMM+IBCDFO, and 1 worker +doing the simulation evaluations. +""" + +# Do not change these lines - they are parsed by run-tests.sh +# TESTSUITE_COMMS: local mpi +# TESTSUITE_NPROCS: 3 + +import multiprocessing +import sys + +import libensemble.gen_funcs +import numpy as np +from libensemble.libE import libE + +libensemble.gen_funcs.rc.aposmm_optimizers = "ibcdfo_manifold_sampling" + +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 + +try: + import ibcdfo # noqa: F401 + +except ModuleNotFoundError: + sys.exit("Please 'pip install ibcdfo'") + +try: + from minqsw import minqsw # noqa: F401 + +except ModuleNotFoundError: + sys.exit("Ensure https://github.com/POptUS/minq has been cloned and that minq/py/minq5/ is on the PYTHONPATH") + + +def synthetic_beamline_mapping(H, _, sim_specs): + x = H["x"][0] + assert len(x) == 4, "Assuming 4 inputs to this function" + y = np.zeros(3) # Synthetic beamline outputs + y[0] = x[0] ** 2 + 1.0 + y[1] = x[1] ** 2 + 2.0 + y[2] = x[2] * x[3] + 0.5 + + Out = np.zeros(1, dtype=sim_specs["out"]) + Out["fvec"] = y + Out["f"] = np.max(y) + return Out + + +# Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). +if __name__ == "__main__": + multiprocessing.set_start_method("fork", force=True) + + nworkers, is_manager, libE_specs, _ = parse_args() + + assert nworkers == 2, "This test is just for two workers" + + m = 3 + n = 4 + sim_f = synthetic_beamline_mapping + + sim_specs = { + "sim_f": sim_f, + "in": ["x"], + "out": [("f", float), ("fvec", float, m)], + } + + gen_out = [ + ("x", float, n), + ("x_on_cube", float, n), + ("sim_id", int), + ("local_min", bool), + ("local_pt", bool), + ("started_run", bool), + ] + + gen_specs = { + "gen_f": gen_f, + "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, + "sample_points": np.atleast_2d(0.1 * (np.arange(n) + 1)), + "localopt_method": "ibcdfo_manifold_sampling", + "run_max_eval": 100 * (n + 1), + "components": m, + "lb": -1 * np.ones(n), + "ub": np.ones(n), + }, + } + + 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) + + 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 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 7523704a0b..5013944449 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_ibcdfo_pounders.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_ibcdfo_pounders.py @@ -27,21 +27,19 @@ import multiprocessing import sys -import numpy as np - import libensemble.gen_funcs +import numpy as np from libensemble.libE import libE from libensemble.sim_funcs.chwirut1 import chwirut_eval -libensemble.gen_funcs.rc.aposmm_optimizers = "ibcdfo" +libensemble.gen_funcs.rc.aposmm_optimizers = "ibcdfo_pounders" 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 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'") @@ -122,8 +120,8 @@ 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} 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 new file mode 100644 index 0000000000..8379d0844c --- /dev/null +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_ibcdfo_pounders_jax.py @@ -0,0 +1,134 @@ +""" +Runs libEnsemble with APOSMM+IBCDFO on two test problems. Only a single +optimization run is being performed for the below setup. + +The first case uses POUNDERS to solve the chwirut least-squares problem. For +this case, all chwirut 214 residual calculations for a given point are +performed as a single simulation evaluation. + +The second case uses the generalized POUNDERS to minimize normalized beamline +emittance. The "beamline simulation" is a synthetic polynomial test function +that takes in 4 variables and returning 3 outputs. These outputs represent +position , momentum , and the correlation between them . + +These values are then mapped to the normalized emittance - . + +Execute via one of the following commands: + mpiexec -np 3 python test_persistent_aposmm_ibcdfo_pounders.py + python test_persistent_aposmm_ibcdfo_pounders.py --nworkers 2 +Both will run with 1 manager, 1 worker running APOSMM+IBCDFO, and 1 worker +doing the simulation evaluations. +""" + +# Do not change these lines - they are parsed by run-tests.sh +# TESTSUITE_COMMS: local mpi +# TESTSUITE_NPROCS: 3 + +import multiprocessing +import sys + +import libensemble.gen_funcs +import numpy as np +from libensemble.libE import libE + +libensemble.gen_funcs.rc.aposmm_optimizers = "ibcdfo_pounders" + +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 + +try: + import ibcdfo # noqa: F401 + + from declare_hfun_and_combine_model_with_jax import combinemodels_jax, hfun + +except ModuleNotFoundError: + sys.exit("Please 'pip install ibcdfo'") + +try: + from minqsw import minqsw # noqa: F401 + +except ModuleNotFoundError: + 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" + y = np.zeros(3) # Synthetic beamline outputs + y[0] = x[0] ** 2 + 1.0 + y[1] = x[1] ** 2 + 2.0 + y[2] = x[2] * x[3] + 0.5 + + Out = np.zeros(1, dtype=sim_specs["out"]) + Out["fvec"] = y + Out["f"] = y[0] * y[1] - y[2] ** 2 + return Out + + +# Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). +if __name__ == "__main__": + multiprocessing.set_start_method("fork", force=True) + + nworkers, is_manager, libE_specs, _ = parse_args() + + assert nworkers == 2, "This test is just for two workers" + + m = 3 + n = 4 + sim_f = synthetic_beamline_mapping + + sim_specs = { + "sim_f": sim_f, + "in": ["x"], + "out": [("f", float), ("fvec", float, m)], + } + + gen_out = [ + ("x", float, n), + ("x_on_cube", float, n), + ("sim_id", int), + ("local_min", bool), + ("local_pt", bool), + ("started_run", bool), + ] + + gen_specs = { + "gen_f": gen_f, + "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, + "sample_points": np.atleast_2d(0.1 * (np.arange(n) + 1)), + "localopt_method": "ibcdfo_pounders", + "run_max_eval": 100 * (n + 1), + "components": m, + "lb": -1 * np.ones(n), + "ub": np.ones(n), + }, + } + + gen_specs["user"]["hfun"] = hfun + gen_specs["user"]["combinemodels"] = combinemodels_jax + + 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) + + if is_manager: + print(H[["x", "f", "local_min"]]) + assert persis_info[1].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..bfbc2facfa 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_nlopt.py @@ -14,13 +14,13 @@ # 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 -import numpy as np - import libensemble.gen_funcs +import numpy as np # Import libEnsemble items for this test from libensemble.libE import libE diff --git a/libensemble/tests/regression_tests/test_persistent_aposmm_periodic.py b/libensemble/tests/regression_tests/test_persistent_aposmm_periodic.py index d99e8802a0..653b79ac82 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_periodic.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_periodic.py @@ -19,9 +19,8 @@ import multiprocessing import sys -import numpy as np - import libensemble.gen_funcs +import numpy as np libensemble.gen_funcs.rc.aposmm_optimizers = ["nlopt", "scipy"] from libensemble.alloc_funcs.persistent_aposmm_alloc import persistent_aposmm_alloc as alloc_f diff --git a/libensemble/tests/regression_tests/test_persistent_aposmm_pounders.py b/libensemble/tests/regression_tests/test_persistent_aposmm_pounders.py index 5b038a0ce8..fdc6c8ef4d 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_pounders.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_pounders.py @@ -22,9 +22,8 @@ import sys from math import ceil, gamma, pi, sqrt -import numpy as np - import libensemble.gen_funcs +import numpy as np # Import libEnsemble items for this test from libensemble.libE import libE diff --git a/libensemble/tests/regression_tests/test_persistent_aposmm_scipy.py b/libensemble/tests/regression_tests/test_persistent_aposmm_scipy.py index ee4ec225b3..1712df7c2d 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_scipy.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_scipy.py @@ -14,14 +14,12 @@ # 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 -import numpy as np - import libensemble.gen_funcs +import numpy as np # Import libEnsemble items for this test from libensemble.libE import libE 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..19740010b5 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_tao_blmvm.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_tao_blmvm.py @@ -20,9 +20,8 @@ import sys from math import gamma, pi, sqrt -import numpy as np - import libensemble.gen_funcs +import numpy as np # Import libEnsemble items for this test from libensemble.libE import libE 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..350f06cf03 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_tao_nm.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_tao_nm.py @@ -19,9 +19,8 @@ import multiprocessing import sys -import numpy as np - import libensemble.gen_funcs +import numpy as np # Import libEnsemble items for this test from libensemble.libE import libE diff --git a/libensemble/tests/regression_tests/test_persistent_aposmm_timeout.py b/libensemble/tests/regression_tests/test_persistent_aposmm_timeout.py index e61843fd71..c20dfa9c6a 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_timeout.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_timeout.py @@ -20,9 +20,8 @@ import multiprocessing import sys -import numpy as np - import libensemble.gen_funcs +import numpy as np libensemble.gen_funcs.rc.aposmm_optimizers = "nlopt" 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..c5f0ba1e58 100644 --- a/libensemble/tests/regression_tests/test_persistent_aposmm_with_grad.py +++ b/libensemble/tests/regression_tests/test_persistent_aposmm_with_grad.py @@ -21,9 +21,8 @@ import sys from math import gamma, pi, sqrt -import numpy as np - import libensemble.gen_funcs +import numpy as np # Import libEnsemble items for this test from libensemble.libE import libE 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..632bc78c13 100644 --- a/libensemble/tests/regression_tests/test_persistent_fd_param_finder.py +++ b/libensemble/tests/regression_tests/test_persistent_fd_param_finder.py @@ -20,7 +20,6 @@ import shutil # For ECnoise.m import numpy as np - from libensemble import Ensemble from libensemble.alloc_funcs.start_fd_persistent import finite_diff_alloc as alloc_f from libensemble.gen_funcs.persistent_fd_param_finder import fd_param_finder as gen_f 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 8c589161ad..e67326ac32 100644 --- a/libensemble/tests/regression_tests/test_persistent_gp_multitask_ax.py +++ b/libensemble/tests/regression_tests/test_persistent_gp_multitask_ax.py @@ -24,7 +24,6 @@ import warnings import numpy as np - from libensemble import logger from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens from libensemble.libE import libE @@ -50,7 +49,7 @@ def run_simulation(H, persis_info, sim_specs, libE_info): z = 8 elif task == "cheap_model": z = 1 - print('in sim', task) + print("in sim", task) libE_output = np.zeros(1, dtype=sim_specs["out"]) calc_status = WORKER_DONE diff --git a/libensemble/tests/regression_tests/test_persistent_surmise_calib.py b/libensemble/tests/regression_tests/test_persistent_surmise_calib.py index 39cf11b5de..8701d58814 100644 --- a/libensemble/tests/regression_tests/test_persistent_surmise_calib.py +++ b/libensemble/tests/regression_tests/test_persistent_surmise_calib.py @@ -31,7 +31,6 @@ # Install Surmise package 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 diff --git a/libensemble/tests/regression_tests/test_persistent_surmise_killsims.py b/libensemble/tests/regression_tests/test_persistent_surmise_killsims.py index 11095f61f2..a4fad2ca9e 100644 --- a/libensemble/tests/regression_tests/test_persistent_surmise_killsims.py +++ b/libensemble/tests/regression_tests/test_persistent_surmise_killsims.py @@ -33,7 +33,6 @@ import os 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 diff --git a/libensemble/tests/regression_tests/test_persistent_tasmanian.py b/libensemble/tests/regression_tests/test_persistent_tasmanian.py index 269c4ba595..edaa7f8f90 100644 --- a/libensemble/tests/regression_tests/test_persistent_tasmanian.py +++ b/libensemble/tests/regression_tests/test_persistent_tasmanian.py @@ -21,7 +21,6 @@ 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 diff --git a/libensemble/tests/regression_tests/test_persistent_tasmanian_async.py b/libensemble/tests/regression_tests/test_persistent_tasmanian_async.py index f21544d185..3febdcdb1d 100644 --- a/libensemble/tests/regression_tests/test_persistent_tasmanian_async.py +++ b/libensemble/tests/regression_tests/test_persistent_tasmanian_async.py @@ -22,7 +22,6 @@ 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 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..77d6f96162 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 @@ -24,7 +24,6 @@ import sys import numpy as np - from libensemble.alloc_funcs.persistent_aposmm_alloc import persistent_aposmm_alloc as alloc_f from libensemble.executors import MPIExecutor from libensemble.gen_funcs.persistent_aposmm import aposmm as gen_f diff --git a/libensemble/tests/scaling_tests/forces/balsam_forces/cleanup.sh b/libensemble/tests/scaling_tests/forces/balsam_forces/cleanup.sh deleted file mode 100755 index 6f5720f967..0000000000 --- a/libensemble/tests/scaling_tests/forces/balsam_forces/cleanup.sh +++ /dev/null @@ -1 +0,0 @@ -rm -r ensemble* *.npy *.pickle ensemble.log lib*.txt *.stat diff --git a/libensemble/tests/scaling_tests/forces/balsam_forces/define_apps.py b/libensemble/tests/scaling_tests/forces/balsam_forces/define_apps.py deleted file mode 100644 index c232fdb062..0000000000 --- a/libensemble/tests/scaling_tests/forces/balsam_forces/define_apps.py +++ /dev/null @@ -1,61 +0,0 @@ -from balsam.api import ApplicationDefinition - -""" -This script uses the Balsam API to define and sync two types of Balsam apps: -a libEnsemble app, and a Forces app: - - - The libEnsemble app runs the calling script ``run_libe_forces_balsam.py``. - An input transfer is also specified, but parameterized in - ``submit_libe_forces_balsam.py`` as part of the Job specification process. - - - The Forces app is defined and synced with Balsam. The libEnsemble app - will submit instances of the Forces app to the Balsam service for scheduling - on a running batch session at its site. An optional output transfer is defined; - forces.stat files are transferred back to the Globus endpoint defined in - run_libe_forces_balsam.py - -Unless changes are made to these Apps, this should only need to be run once to -register each of these apps with the Balsam service. - -If not running libEnsemble remotely, feel free to comment-out ``RemoteLibensembleApp.sync()`` -""" - - -class RemoteLibensembleApp(ApplicationDefinition): - site = "jln_theta" - command_template = ( - "/home/jnavarro/.conda/envs/again/bin/python /home/jnavarro" - + "/libensemble/libensemble/tests/scaling_tests/forces/balsam_forces/run_libe_forces_balsam.py" - + " > libe_out.txt 2>&1" - ) - - -print("Defined RemoteLibensembleApp Balsam ApplicationDefinition.") - - -class RemoteForces(ApplicationDefinition): - site = "jln_theta" - command_template = ( - "/home/jnavarro" - + "/libensemble/libensemble/tests/scaling_tests/forces/forces_app/forces.x" - + " {{sim_particles}} {{sim_timesteps}} {{seed}}" - + " > out.txt 2>&1" - ) - - transfers = { - "result": { - "required": True, - "direction": "out", - "local_path": "forces.stat", - "description": "Forces stat file", - "recursive": False, - } - } - - -print("Defined RemoteForces Balsam ApplicationDefinition.") - -RemoteLibensembleApp.sync() -RemoteForces.sync() - -print("Synced each app with the Balsam service.") diff --git a/libensemble/tests/scaling_tests/forces/balsam_forces/forces_simf.py b/libensemble/tests/scaling_tests/forces/balsam_forces/forces_simf.py deleted file mode 100644 index cdb141631e..0000000000 --- a/libensemble/tests/scaling_tests/forces/balsam_forces/forces_simf.py +++ /dev/null @@ -1,73 +0,0 @@ -import os -import time - -import numpy as np - -from libensemble.executors.executor import Executor -from libensemble.message_numbers import TASK_FAILED, WORKER_DONE - - -def run_forces_balsam(H, persis_info, sim_specs, libE_info): - calc_status = 0 - - particles = str(int(H["x"][0][0])) - - exctr = Executor.executor - - GLOBUS_ENDPOINT = sim_specs["user"]["globus_endpoint"] - GLOBUS_DEST_DIR = sim_specs["user"]["globus_dest_dir"] - THIS_SCRIPT_ON_THETA = sim_specs["user"]["this_script_on_theta"] - - args = { - "sim_particles": particles, - "sim_timesteps": str(10), - "seed": particles, - } - - workdir = "sim" + str(libE_info["H_rows"][0]) + "_worker" + str(libE_info["workerID"]) - - statfile = f"forces{particles}.stat" - - if THIS_SCRIPT_ON_THETA: - transfer_statfile_path = GLOBUS_DEST_DIR + statfile - local_statfile_path = "../" + workdir + "/" + transfer_statfile_path.split("/")[-1] - else: - transfer_statfile_path = os.getcwd() + "/" + statfile - local_statfile_path = transfer_statfile_path - - transfer = {"result": GLOBUS_ENDPOINT + ":" + transfer_statfile_path} - - task = exctr.submit( - app_name="forces", - app_args=args, - num_procs=4, - num_nodes=1, - procs_per_node=4, - max_tasks_per_node=1, - transfers=transfer, - workdir=workdir, - ) - - task.wait(timeout=300) - task.poll() - - print(f"Task {task.name} polled. state: {task.state}.") - - while True: - time.sleep(1) - if os.path.isfile(local_statfile_path) and os.path.getsize(local_statfile_path) > 0: - break - - try: - data = np.loadtxt(local_statfile_path) - final_energy = data[-1] - calc_status = WORKER_DONE - except Exception: - final_energy = np.nan - 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 diff --git a/libensemble/tests/scaling_tests/forces/balsam_forces/readme.md b/libensemble/tests/scaling_tests/forces/balsam_forces/readme.md deleted file mode 100644 index 807656c108..0000000000 --- a/libensemble/tests/scaling_tests/forces/balsam_forces/readme.md +++ /dev/null @@ -1,179 +0,0 @@ -## Running test run_libe_forces_balsam.py - -Naive Electrostatics Code Test - -This is a synthetic, highly configurable simulation function. Its primary use -is to test libEnsemble's capability to submit application instances via the Balsam service, -including to separate machines from libEnsemble's processes. This means that although -this is typically an HPC scaling test, this can be run on a laptop with the `forces.x` -simulation submitted to the remote machine, and the resulting data-files transferred -back to the machine that runs the libEnsemble calling script. - -Note that this test currently requires active ALCF credentials to authenticate with -the Balsam service. - -### 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** - -Choose or modify a build line from `build_forces.sh` for the target platform: - - cd libensemble/libensemble/tests/scaling_tests/forces/forces_app - ./build_forces.sh - -### Configuring Balsam - -On the remote machine (in a conda or other virtual environment): - - pip install balsam - balsam login - balsam site init ./my-site - -You may be asked to login and authenticate with the Balsam service. Do so with -your ALCF credentials. Now go into the site directory: - - cd my-site - -To see if the site is active, run: - - balsam site ls - -If the site is not active, run: - - balsam site start - -On any machine you've installed and logged into Balsam, you can run `balsam site ls` -to list your sites and `balsam job rm --all` to remove extraneous jobs between runs. - -### Configuring data-transfer via Balsam and Globus - -Although the raw results of forces runs are available in Balsam sites, -this is understandably insufficient for the simulation function's capability -to evaluate results and determine the final status of an app run if it's running -on another machine. - -Balsam can coordinate data transfers via Globus between Globus endpoints. Assuming -this test is being run on a personal device, do the following to configure Globus, -then Balsam to use Globus. - -- Login to [Globus](https://www.globus.org/) using ALCF or other approved organization credentials. -- Download and run [Globus Connect Personal](https://app.globus.org/file-manager/gcp) to register your device as a Globus endpoint. Note the initialized collection name, e.g. ``test_collection``. -- Once a Globus collection has been initialized in Globus Connect Personal, login to Globus, click "Endpoints" on the left. -- Click the collection that was created on your personal device. Copy the string after "Endpoint UUID". -- Login to the remote machine, switch to your Balsam site directory, and run ``balsam site globus-login``. -- Modify ``settings.yml`` to contain a new transfer_location that matches your device, with the copied endpoint UUID. e.g. ``test_collection: globus://19036a15-570a-12f8-bef8-22060b9b458d`` -- Run ``balsam site sync`` within the site directory to save these changes. -- Locally, in the calling script (``run_libe_forces_balsam.py``), set ``GLOBUS_ENDPOINT`` to the collection name for the previously-defined transfer_location. - -This should be sufficient for ``forces.stat`` files from remote Balsam app runs -to be transferred back to your personal device after every app run. The -simulation function will wait for Balsam to transfer back a stat file, then determine -the calc status based on the received output. - -*To transfer files to/from a system*, you will need to login to Globus and activate -that system's Managed Public Endpoint: - -- Check your system's documentation for your Globus endpoint ID -- Login to Globus, click "Endpoints" on the left. -- Search for the Globus endpoint ID, click on the result. -- On the right, click "Activate", then "Continue". Authenticate with your organization. - -### Configuring libEnsemble - -There are several scripts that each need to be adjusted. To explain each: - -1. ``define_apps.py``: - - About: - - This script defines and syncs each of our Balsam apps with the Balsam service. A Balsam - app is an ``ApplicationDefinition`` class with ``site`` and - ``command_template`` fields. ``site`` specifies to Balsam on which Balsam site - the app should be run, and ``command_template`` specifies the command (as a Jinja2 - string template) that should be executed. This script contains two apps, ``RemoteLibensembleApp`` - and ``RemoteForces``. If you're running libEnsemble on your personal machine and - only submitting the Forces app via Balsam, only ``RemoteForces`` needs adjusting. - - Configuring: - - Adjust the ``site`` field in each ``ApplicationDefinition`` to match your remote - Balsam site. Adjust the various paths in the ``command_template`` fields to match - your home directory and/or Python paths **on the remote machine**. If running - libEnsemble on your personal machine, feel free comment-out ``RemoteLibensembleApp.sync()``. - - **Run this script each time you edit it,** since changes to each - ``ApplicationDefinition`` needs to be synced with the Balsam service. - -2. ``run_libe_forces_balsam.py``: - - About: - - This is a typical libEnsemble plus Executor calling script, but instead of - registering paths to apps like with the MPI Executor, this script loads the - ``RemoteForces`` app synced with the Balsam service in ``define_apps.py`` - and registers it with libEnsemble's Balsam Executor. If running this - script on your personal machine, it also uses the Balsam Executor to reserve - resources at a Balsam site. - - Configuring: - - See the Globus instructions above for setting up Globus transfers within this script. - - Adjust the ``BALSAM_SITE`` field - to match your remote Balsam site, and fields in the in the - ``batch = exctr.submit_allocation()`` block further down. For ``site_id``, - retrieve the corresponding field with ``balsam site ls``. - -3. (optional) ``submit_libe_forces_balsam.py``: - - About: - - This Python script is effectively a batch submission script. It uses the Balsam API - to check out resources (a ``BatchJob``) at a Balsam site, and submits libEnsemble as - a Balsam Job onto those resources. If transferring statfiles back to your - personal machine, it also waits until they are all returned and cancels - the remote ``BatchJob``. *Probably only needed if running libEnsemble remotely.* - - Configuring: - - Every field in UPPER_CASE can be adjusted. ``BALSAM_SITE``, ``PROJECT``, - and ``QUEUE`` among others will probably need adjusting. ``LIBE_NODES`` and ``LIBE_RANKS`` - specify a subset of resources specifically for libEnsemble out of ``BATCH_NUM_NODES``. - -### Running libEnsemble locally - -First make sure that all Balsam apps are synced with the Balsam service: - - python define_apps.py - -Then run libEnsemble with multiprocessing comms, with one manager and `N` workers: - - python run_libe_forces_balsam.py --comms local --nworkers N - -Or, run with MPI comms using one manager and `N-1` workers: - - mpirun -np N python run_libe_forces_balsam.py - -To remove output before the next run, use: - - ./cleanup.sh - -**This runs libEnsemble itself in-place, with only Forces submitted to a Balsam site.** - -### (Optional) Running libEnsemble remotely - -The previous instructions for running libEnsemble are understandably insufficient -if running with lots of workers or if the simulation/generation -functions are computationally expensive. - -To run both libEnsemble and the Forces app on the compute nodes at Balsam site, use: - - python define_apps.py - python submit_libe_forces_balsam.py - -This routine will wait for corresponding statfiles to be transferred back from -the remote machine, then cancel the allocation. diff --git a/libensemble/tests/scaling_tests/forces/balsam_forces/run_libe_forces_balsam.py b/libensemble/tests/scaling_tests/forces/balsam_forces/run_libe_forces_balsam.py deleted file mode 100644 index 62f2404ed9..0000000000 --- a/libensemble/tests/scaling_tests/forces/balsam_forces/run_libe_forces_balsam.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env python -import os -import socket - -import numpy as np -from balsam.api import ApplicationDefinition -from forces_simf import run_forces_balsam - -from libensemble.executors import BalsamExecutor -from libensemble.gen_funcs.sampling import uniform_random_sample -from libensemble.libE import libE -from libensemble.tools import add_unique_random_streams, parse_args - -BALSAM_SITE = "three" - -# Is this running on a personal machine, or a compute node? -THIS_SCRIPT_ON_THETA = any([i in socket.gethostname() for i in ["theta", "nid0"]]) - -# Use Globus to transfer output forces.stat files back -GLOBUS_ENDPOINT = "jln_laptop" - -if not THIS_SCRIPT_ON_THETA: - GLOBUS_DEST_DIR_PREFIX = os.getcwd() + "/ensemble" -else: - GLOBUS_DEST_DIR_PREFIX = "/path/to/remote/ensemble/directory" - -# Parse number of workers, comms type, etc. from arguments -nworkers, is_manager, libE_specs, _ = parse_args() - -# State the sim_f, inputs, outputs -sim_specs = { - "sim_f": run_forces_balsam, # sim_f, imported above - "in": ["x"], # Name of input for sim_f - "out": [("energy", float)], # Name, type of output from sim_f - "user": { - "globus_endpoint": GLOBUS_ENDPOINT, - "globus_dest_dir": GLOBUS_DEST_DIR_PREFIX, - "this_script_on_theta": THIS_SCRIPT_ON_THETA, - }, -} - -# State the gen_f, inputs, outputs, additional parameters -gen_specs = { - "gen_f": uniform_random_sample, # Generator function - "out": [("x", float, (1,))], # Name, type and size of data from gen_f - "user": { - "lb": np.array([1000]), # User parameters for the gen_f - "ub": np.array([3000]), - "gen_batch_size": 8, - }, -} - -# Create and work inside separate per-simulation directories -libE_specs["sim_dirs_make"] = True - -# Instruct libEnsemble to exit after this many simulations -exit_criteria = {"sim_max": 8} - -persis_info = add_unique_random_streams({}, nworkers + 1) - -apps = ApplicationDefinition.load_by_site(BALSAM_SITE) -RemoteForces = apps["RemoteForces"] - -exctr = BalsamExecutor() -exctr.register_app(RemoteForces, app_name="forces") - -if not THIS_SCRIPT_ON_THETA: - batch = exctr.submit_allocation( - site_id=246, # Check if matches BALSAM_SITE with `balsam site ls` - num_nodes=4, - wall_time_min=30, - queue="debug-flat-quad", - project="CSC250STMS07", - ) - -# Launch libEnsemble -H, persis_info, flag = libE(sim_specs, gen_specs, exit_criteria, persis_info=persis_info, libE_specs=libE_specs) - -if not THIS_SCRIPT_ON_THETA: - exctr.revoke_allocation(batch) diff --git a/libensemble/tests/scaling_tests/forces/balsam_forces/submit_libe_forces_remotely.py b/libensemble/tests/scaling_tests/forces/balsam_forces/submit_libe_forces_remotely.py deleted file mode 100644 index e99d2e3aa5..0000000000 --- a/libensemble/tests/scaling_tests/forces/balsam_forces/submit_libe_forces_remotely.py +++ /dev/null @@ -1,74 +0,0 @@ -import glob -import os -import time - -from balsam.api import ApplicationDefinition, BatchJob - -""" -This file is roughly equivalent to a traditional batch submission shell script -that used legacy Balsam commands, except it uses the Balsam API to submit jobs -to the scheduler. It can also be run from anywhere and still submit jobs to -the same machine. It loads, parameterizes, and submits the LibensembleApp for -execution. Use this script to run libEnsemble as a Balsam Job on compute nodes. - -If running libEnsemble on a laptop, this script is not needed. Just run the -corresponding libEnsemble calling script as normal. -""" - -BALSAM_SITE = "jln_theta" - -# Batch Session Parameters -BATCH_NUM_NODES = 5 -BATCH_WALL_CLOCK_TIME = 60 -PROJECT = "CSC250STMS07" -QUEUE = "debug-flat-quad" - -# libEnsemble Job Parameters - A subset of above resources dedicated to libEnsemble -LIBE_NODES = 1 -LIBE_RANKS = 5 - -# This script cancels remote allocation once SIM_MAX statfiles transferred -TRANSFER_DESTINATION = "./ensemble" -SIM_MAX = 16 - -# Retrieve the libEnsemble app from the Balsam service -apps = ApplicationDefinition.load_by_site(BALSAM_SITE) -RemoteLibensembleApp = apps["RemoteLibensembleApp"] -RemoteLibensembleApp.resolve_site_id() - - -# Submit the libEnsemble app as a Job to the Balsam service. -# It will wait for a compatible, running BatchJob session (remote allocation) -libe_job = RemoteLibensembleApp.submit( - workdir="libe_workflow", - num_nodes=LIBE_NODES, - ranks_per_node=LIBE_RANKS, -) - -print("libEnsemble App retrieved and submitted as Job to Balsam service.") - -# Submit an allocation (BatchJob) request to the libEnsemble app's site -batch = BatchJob.objects.create( - site_id=libe_job.site_id, - num_nodes=BATCH_NUM_NODES, - wall_time_min=BATCH_WALL_CLOCK_TIME, - job_mode="mpi", - project=PROJECT, - queue=QUEUE, -) - -print("BatchJob session initialized. All Balsam apps will run in this BatchJob.") - -# Wait for all forces.stat files to be transferred back, then cancel the BatchJob -os.makedirs(TRANSFER_DESTINATION, exist_ok=True) -print("Waiting for all returned forces.stat files...") - -while len(glob.glob(os.path.abspath(TRANSFER_DESTINATION) + "/*.stat")) != SIM_MAX: - time.sleep(3) - -print("All forces.stat files returned. Cancelling BatchJob session.") - -batch.state = "pending_deletion" -batch.save() - -print("BatchJob session cancelled. Success!") diff --git a/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py b/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py index b08bc85fa3..19586a8a2d 100644 --- a/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py +++ b/libensemble/tests/unit_tests/RENAME_test_persistent_aposmm.py @@ -5,7 +5,7 @@ import libensemble.gen_funcs -libensemble.gen_funcs.rc.aposmm_optimizers = "nlopt" +libensemble.gen_funcs.rc.aposmm_optimizers = "scipy" if platform.system() in ["Linux", "Darwin"]: multiprocessing.set_start_method("fork", force=True) @@ -66,14 +66,13 @@ def combined_func(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" + 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} @@ -90,16 +89,16 @@ def test_standalone_persistent_aposmm(): "initial_sample_size": 100, # 'localopt_method': 'LD_MMA', # Needs gradients "sample_points": np.round(minima, 1), - "localopt_method": "LN_BOBYQA", + "localopt_method": "scipy_Nelder-Mead", "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, + "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]), @@ -124,7 +123,6 @@ def test_standalone_persistent_aposmm(): @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 @@ -147,12 +145,12 @@ def test_standalone_persistent_aposmm_combined_func(): "initial_sample_size": 100, # 'localopt_method': 'LD_MMA', # Needs gradients "sample_points": np.round(minima, 1), - "localopt_method": "LN_BOBYQA", + "localopt_method": "scipy_Nelder-Mead", "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, + "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]), diff --git a/libensemble/tests/unit_tests/test_ensemble.py b/libensemble/tests/unit_tests/test_ensemble.py index f956141c08..2719325739 100644 --- a/libensemble/tests/unit_tests/test_ensemble.py +++ b/libensemble/tests/unit_tests/test_ensemble.py @@ -3,7 +3,7 @@ import numpy as np import libensemble.tests.unit_tests.setup as setup -from libensemble.utils.misc import pydanticV1, specs_dump +from libensemble.utils.misc import specs_dump def test_ensemble_init(): @@ -127,10 +127,7 @@ def test_full_workflow(): def test_flakey_workflow(): """Test initializing a workflow via Specs and Ensemble.run()""" - if pydanticV1: - from pydantic.error_wrappers import ValidationError - else: - from pydantic import ValidationError + from pydantic import ValidationError from libensemble.ensemble import Ensemble from libensemble.gen_funcs.sampling import latin_hypercube_sample diff --git a/libensemble/tests/unit_tests/test_executor_balsam.py b/libensemble/tests/unit_tests/test_executor_balsam.py deleted file mode 100644 index 6c98381d0b..0000000000 --- a/libensemble/tests/unit_tests/test_executor_balsam.py +++ /dev/null @@ -1,265 +0,0 @@ -# !/usr/bin/env python -# Integration Test of executor module for libensemble -# Test does not require running full libensemble -import datetime -import os -import sys -import warnings -from dataclasses import dataclass - -import mock -import pytest - -warnings.filterwarnings("ignore", category=DeprecationWarning) - - -from libensemble.executors.executor import Application, Executor, ExecutorException, TimeoutExpired # noqa E402 - - -def setup_module(module): - try: - print(f"setup_module module:{module.__name__}") - except AttributeError: - print(f"setup_module (direct run) module:{module}") - if Executor.executor is not None: - del Executor.executor - Executor.executor = None - - -def teardown_module(module): - try: - print(f"teardown_module module:{module.__name__}") - except AttributeError: - print(f"teardown_module (direct run) module:{module}") - if Executor.executor is not None: - del Executor.executor - Executor.executor = None - - -# fake Balsam app -class TestLibeApp: - site = "libe-unit-test" - command_template = "python simdir/py_startup.py" - - def sync(): - pass - - -# fake EventLog object -@dataclass -class LogEventTest: - timestamp: datetime.datetime = None - - -# This would typically be in the user calling script -def setup_executor(): - """Set up a Balsam Executor with sim app""" - from libensemble.executors import BalsamExecutor - - exctr = BalsamExecutor() # noqa F841 - - -# Tests ======================================================================================== - - -@pytest.mark.extra -def test_register_app(): - """Test of registering an App""" - print(f"\nTest: {sys._getframe().f_code.co_name}\n") - setup_executor() - exctr = Executor.executor - - exctr.serial_setup() # does nothing, compatibility with legacy-balsam-exctr - exctr.add_app("hello", "world") # does nothing, compatibility with legacy-balsam-exctr - exctr.set_resources("hello") # does nothing, compatibility with other executors - - exctr.register_app(TestLibeApp, calc_type="sim", precedent="fake/dir") - assert isinstance( - exctr.apps["python"], Application - ), "Application object not created based on registered Balsam AppDef" - - exctr.register_app(TestLibeApp, app_name="test") - assert isinstance( - exctr.apps["test"], Application - ), "Application object not created based on registered Balsam AppDef" - - -@pytest.mark.extra -def test_submit_app_defaults(): - """Test of submitting an App""" - print(f"\nTest: {sys._getframe().f_code.co_name}\n") - setup_executor() - exctr = Executor.executor - exctr.register_app(TestLibeApp, app_name="test", calc_type="sim") - with mock.patch("libensemble.executors.balsam_executor.Job"): - task = exctr.submit(calc_type="sim") - task = exctr.submit(app_name="test") - - assert task in exctr.list_of_tasks, "new task not added to executor's list of tasks" - - assert task == exctr.get_task(task.id), "task retrieved via task ID doesn't match new task" - - with pytest.raises(ExecutorException): - task = exctr.submit() - pytest.fail("Expected exception") - - -@pytest.mark.extra -def test_submit_app_workdir(): - """Test of submitting an App with a workdir""" - print(f"\nTest: {sys._getframe().f_code.co_name}\n") - setup_executor() - exctr = Executor.executor - exctr.register_app(TestLibeApp, app_name="test", calc_type="sim") - with mock.patch("libensemble.executors.balsam_executor.Job"): - task = exctr.submit(calc_type="sim", workdir="output", machinefile="nope") - - assert task.workdir == os.path.join(exctr.workflow_name, "output"), "workdir not properly defined for new task" - - -@pytest.mark.extra -def test_submit_app_dry(): - """Test of dry-run submitting an App""" - print(f"\nTest: {sys._getframe().f_code.co_name}\n") - setup_executor() - exctr = Executor.executor - exctr.register_app(TestLibeApp, app_name="test", calc_type="sim") - task = exctr.submit(calc_type="sim", dry_run=True) - task.poll() - - assert all([task.dry_run, task.done()]), "new task from dry_run wasn't marked as such, or set as done" - - -@pytest.mark.extra -def test_submit_app_wait(): - """Test of exctr.submit blocking until app is running""" - print(f"\nTest: {sys._getframe().f_code.co_name}\n") - setup_executor() - exctr = Executor.executor - exctr.register_app(TestLibeApp, app_name="test", calc_type="sim") - with mock.patch("libensemble.executors.balsam_executor.Job") as job: - with mock.patch("libensemble.executors.balsam_executor.EventLog") as log: - job.return_value.state = "RUNNING" - log.objects.filter.return_value = [ - LogEventTest(timestamp=datetime.datetime(2022, 4, 21, 20, 29, 33, 455144)) - ] - task = exctr.submit(calc_type="sim", wait_on_start=True) - assert task.running(), "new task is not marked as running after wait_on_start" - - log.objects.filter.return_value = [LogEventTest(timestamp=None)] - task = exctr.submit(calc_type="sim", wait_on_start=True) - assert task.runtime == 0, "runtime should be 0 without Balsam timestamp evaluated" - - -@pytest.mark.extra -def test_submit_revoke_alloc(): - """Test creating and revoking BatchJob objects through the executor""" - print(f"\nTest: {sys._getframe().f_code.co_name}\n") - setup_executor() - exctr = Executor.executor - exctr.register_app(TestLibeApp, app_name="test", calc_type="sim") - with mock.patch("libensemble.executors.balsam_executor.BatchJob"): - alloc = exctr.submit_allocation(site_id="libe-unit-test", num_nodes=1, wall_time_min=30) - - assert alloc in exctr.allocations, "batchjob object not appended to executor's list of allocations" - - alloc.scheduler_id = None - assert not exctr.revoke_allocation( - alloc, timeout=3 - ), "unable to revoke allocation if Balsam never returns scheduler ID" - - alloc.scheduler_id = 1 - assert exctr.revoke_allocation( - alloc, timeout=3 - ), "should've been able to revoke allocation if scheduler ID available" - - -@pytest.mark.extra -def test_task_poll(): - """Test of killing (cancelling) a balsam app""" - print(f"\nTest: {sys._getframe().f_code.co_name}\n") - setup_executor() - exctr = Executor.executor - exctr.register_app(TestLibeApp, app_name="test", calc_type="sim") - with mock.patch("libensemble.executors.balsam_executor.Job") as job: - with mock.patch("libensemble.executors.balsam_executor.EventLog"): - task = exctr.submit(calc_type="sim") - - job.return_value.state = "PREPROCESSED" - task.poll() - assert task.state == "WAITING", "task should've been considered waiting based on balsam state" - - job.return_value.state = "FAILED" - task.poll() - assert task.state == "FAILED", "task should've been considered failed based on balsam state" - - task = exctr.submit(calc_type="sim") - - job.return_value.state = "JOB_FINISHED" - task.poll() - assert task.state == "FINISHED", "task was not finished after wait method" - - assert not task.running(), "task shouldn't be running after wait method returns" - - assert task.done(), "task should be 'done' after wait method" - - -@pytest.mark.extra -def test_task_wait(): - """Test of killing (cancelling) a balsam app""" - print(f"\nTest: {sys._getframe().f_code.co_name}\n") - setup_executor() - exctr = Executor.executor - exctr.register_app(TestLibeApp, app_name="test", calc_type="sim") - with mock.patch("libensemble.executors.balsam_executor.Job") as job: - with mock.patch("libensemble.executors.balsam_executor.EventLog"): # need to patch since wait polls - task = exctr.submit(calc_type="sim") - - job.return_value.state = "RUNNING" - with pytest.raises(TimeoutExpired): - task.wait(timeout=3) - pytest.fail("Expected exception") - - job.return_value.state = "JOB_FINISHED" - task.wait(timeout=3) - task.wait(timeout=3) # should return immediately since self._check_poll() should return False - assert task.state == "FINISHED", "task was not finished after wait method" - assert not task.running(), "task shouldn't be running after wait method returns" - assert task.done(), "task should be 'done' after wait method" - - task = exctr.submit(calc_type="sim", dry_run=True) - task.wait() # should also return immediately since dry_run - - task = exctr.submit(calc_type="sim") - job.return_value.state = "FAILED" - task.wait(timeout=3) - assert task.state == "FAILED", "Matching Balsam state should've been assigned to task" - - -@pytest.mark.extra -def test_task_kill(): - """Test of killing (cancelling) a balsam app""" - print(f"\nTest: {sys._getframe().f_code.co_name}\n") - setup_executor() - exctr = Executor.executor - exctr.register_app(TestLibeApp, app_name="test", calc_type="sim") - with mock.patch("libensemble.executors.balsam_executor.Job"): - task = exctr.submit(calc_type="sim") - - with mock.patch("libensemble.executors.balsam_executor.EventLog"): - task.kill() - assert task.finished and task.state == "USER_KILLED", "task not set as killed after kill method" - - -if __name__ == "__main__": - setup_module(__file__) - test_register_app() - test_submit_app_defaults() - test_submit_app_workdir() - test_submit_app_dry() - test_submit_app_wait() - test_submit_revoke_alloc() - test_task_poll() - test_task_wait() - test_task_kill() - teardown_module(__file__) diff --git a/libensemble/tests/unit_tests/test_models.py b/libensemble/tests/unit_tests/test_models.py index 7c11bfd9bb..8477ef6f62 100644 --- a/libensemble/tests/unit_tests/test_models.py +++ b/libensemble/tests/unit_tests/test_models.py @@ -1,13 +1,9 @@ import numpy as np +from pydantic import ValidationError import libensemble.tests.unit_tests.setup as setup from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs, _EnsembleSpecs -from libensemble.utils.misc import pydanticV1, specs_dump - -if pydanticV1: - from pydantic.error_wrappers import ValidationError -else: - from pydantic import ValidationError +from libensemble.utils.misc import specs_dump class Fake_MPI: @@ -51,10 +47,7 @@ def test_sim_gen_alloc_exit_specs_invalid(): } try: - if pydanticV1: - SimSpecs.parse_obj(bad_specs) - else: - SimSpecs.model_validate(bad_specs) + SimSpecs.model_validate(bad_specs) flag = 0 except ValidationError as e: assert len(e.errors()) > 1, "SimSpecs model should have detected multiple errors in specs" @@ -62,10 +55,7 @@ def test_sim_gen_alloc_exit_specs_invalid(): assert flag, "SimSpecs didn't raise ValidationError on invalid specs" try: - if pydanticV1: - GenSpecs.parse_obj(bad_specs) - else: - GenSpecs.model_validate(bad_specs) + GenSpecs.model_validate(bad_specs) flag = 0 except ValidationError as e: assert len(e.errors()) > 1, "Should've detected multiple errors in specs" @@ -75,10 +65,7 @@ def test_sim_gen_alloc_exit_specs_invalid(): bad_ec = {"stop_vals": 0.5} try: - if pydanticV1: - ExitCriteria.parse_obj(bad_ec) - else: - ExitCriteria.model_validate(bad_ec) + ExitCriteria.model_validate(bad_ec) flag = 0 except ValidationError: flag = 1 @@ -88,41 +75,26 @@ def test_sim_gen_alloc_exit_specs_invalid(): def test_libe_specs(): sim_specs, gen_specs, exit_criteria = setup.make_criteria_and_specs_0() libE_specs = {"mpi_comm": Fake_MPI(), "comms": "mpi"} - if pydanticV1: - ls = LibeSpecs.parse_obj(libE_specs) - else: - ls = LibeSpecs.model_validate(libE_specs) + ls = LibeSpecs.model_validate(libE_specs) libE_specs["sim_input_dir"] = "./simdir" libE_specs["sim_dir_copy_files"] = ["./simdir"] - if pydanticV1: - ls = LibeSpecs.parse_obj(libE_specs) - else: - ls = LibeSpecs.model_validate(libE_specs) + ls = LibeSpecs.model_validate(libE_specs) libE_specs = {"comms": "tcp", "nworkers": 4} - if pydanticV1: - ls = LibeSpecs.parse_obj(libE_specs) - else: - ls = LibeSpecs.model_validate(libE_specs) + ls = LibeSpecs.model_validate(libE_specs) assert ls.disable_resource_manager, "resource manager should be disabled when using tcp comms" libE_specs = {"comms": "tcp", "workers": ["hello.host"]} - if pydanticV1: - ls = LibeSpecs.parse_obj(libE_specs) - else: - ls = LibeSpecs.model_validate(libE_specs) + ls = LibeSpecs.model_validate(libE_specs) def test_libe_specs_invalid(): bad_specs = {"comms": "local", "zero_resource_workers": 2, "sim_input_dirs": ["obj"]} try: - if pydanticV1: - LibeSpecs.parse_obj(bad_specs) - else: - LibeSpecs.model_validate(bad_specs) + LibeSpecs.model_validate(bad_specs) flag = 0 except ValidationError: flag = 1 diff --git a/libensemble/tests/unit_tests_logger/test_logger.py b/libensemble/tests/unit_tests_logger/test_logger.py index e06331b3d2..fdf13725f9 100644 --- a/libensemble/tests/unit_tests_logger/test_logger.py +++ b/libensemble/tests/unit_tests_logger/test_logger.py @@ -124,7 +124,7 @@ def test_custom_log_levels(): logger_test.manager_warning("This manager_warning message should log") logger_test.vdebug("This vdebug message should log") - with open(LogConfig.config.filename, 'r') as f: + with open(LogConfig.config.filename, "r") as f: file_content = f.read() assert "This manager_warning message should log" in file_content assert "This vdebug message should log" in file_content diff --git a/libensemble/utils/misc.py b/libensemble/utils/misc.py index ca67095ac1..cfb4f4df20 100644 --- a/libensemble/utils/misc.py +++ b/libensemble/utils/misc.py @@ -5,16 +5,6 @@ from itertools import groupby from operator import itemgetter -import pydantic - -pydantic_version = pydantic.__version__[0] - -pydanticV1 = pydantic_version == "1" -pydanticV2 = pydantic_version == "2" - -if not pydanticV1 and not pydanticV2: - raise ModuleNotFoundError("Pydantic not installed or current version not supported. Install v1 or v2.") - def extract_H_ranges(Work: dict) -> str: """Convert received H_rows into ranges for labeling""" @@ -55,24 +45,15 @@ def __iter__(self): def specs_dump(specs, **kwargs): - if pydanticV1: - return specs.dict(**kwargs) - else: - return specs.model_dump(**kwargs) + return specs.model_dump(**kwargs) def specs_checker_getattr(obj, key, default=None): - if pydanticV1: # dict - return obj.get(key, default) - else: # actual obj - try: - return getattr(obj, key) - except AttributeError: - return default + try: + return getattr(obj, key) + except AttributeError: + return default def specs_checker_setattr(obj, key, value): - if pydanticV1: # dict - obj[key] = value - else: # actual obj - obj.__dict__[key] = value + obj.__dict__[key] = value diff --git a/libensemble/utils/pydantic_bindings.py b/libensemble/utils/pydantic_bindings.py index 7ceca9615e..6c297bb95d 100644 --- a/libensemble/utils/pydantic_bindings.py +++ b/libensemble/utils/pydantic_bindings.py @@ -1,13 +1,12 @@ import sys -from pydantic import Field, create_model +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.misc import pydanticV1 from libensemble.utils.validators import ( - _UFUNC_INVALID_ERR, - _UNRECOGNIZED_ERR, check_any_workers_and_disable_rm_if_tcp, check_exit_criteria, check_gpu_setting_type, @@ -30,60 +29,34 @@ simf_set_in_out_from_attrs, ) -if pydanticV1: - from pydantic import BaseConfig - from pydantic import validate_arguments as libE_wrapper # noqa: F401 - - BaseConfig.arbitrary_types_allowed = True - BaseConfig.allow_population_by_field_name = True - BaseConfig.extra = "allow" - BaseConfig.error_msg_templates = { - "value_error.extra": _UNRECOGNIZED_ERR, - "type_error.callable": _UFUNC_INVALID_ERR, - } - BaseConfig.validate_assignment = True - - class Config: - arbitrary_types_allowed = True - - specs.LibeSpecs.Config = Config - specs._EnsembleSpecs.Config = Config - -else: - from pydantic import ConfigDict - from pydantic import validate_call as libE_wrapper # noqa: F401 - from pydantic.fields import FieldInfo - - 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) +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: diff --git a/libensemble/utils/validators.py b/libensemble/utils/validators.py index 1782e011e8..e2fec4e13d 100644 --- a/libensemble/utils/validators.py +++ b/libensemble/utils/validators.py @@ -3,9 +3,9 @@ from pathlib import Path import numpy as np +from pydantic import field_validator, model_validator from libensemble.resources.platforms import Platform -from libensemble.utils.misc import pydanticV1 from libensemble.utils.specs_checkers import ( _check_any_workers_and_disable_rm_if_tcp, _check_exit_criteria, @@ -103,190 +103,101 @@ def check_mpi_runner_type(cls, value): return value -if pydanticV1: - from pydantic import root_validator, validator - - # SPECS VALIDATORS ##### - - check_valid_out = validator("outputs", pre=True)(check_valid_out) - check_valid_in = validator("inputs", "persis_in", pre=True)(check_valid_in) - check_valid_comms_type = validator("comms")(check_valid_comms_type) - set_platform_specs_to_class = validator("platform_specs")(set_platform_specs_to_class) - check_input_dir_exists = validator("sim_input_dir", "gen_input_dir")(check_input_dir_exists) - check_inputs_exist = validator( - "sim_dir_copy_files", "sim_dir_symlink_files", "gen_dir_copy_files", "gen_dir_symlink_files" - )(check_inputs_exist) - check_gpu_setting_type = validator("gpu_setting_type")(check_gpu_setting_type) - check_mpi_runner_type = validator("mpi_runner")(check_mpi_runner_type) - - @root_validator - def check_any_workers_and_disable_rm_if_tcp(cls, values): - return _check_any_workers_and_disable_rm_if_tcp(values) - - @root_validator(pre=True) - def set_default_comms(cls, values): - return default_comms(values) - - @root_validator(pre=True) - def enable_save_H_when_every_K(cls, values): - if "save_H_on_completion" not in values and ( - values.get("save_every_k_sims", 0) > 0 or values.get("save_every_k_gens", 0) > 0 - ): - values["save_H_on_completion"] = True - return values - - @root_validator - def set_workflow_dir(cls, values): - return _check_set_workflow_dir(values) - - @root_validator - def set_calc_dirs_on_input_dir(cls, values): - return _check_set_calc_dirs_on_input_dir(values) - - @root_validator - def check_exit_criteria(cls, values): - return _check_exit_criteria(values) - - @root_validator - def check_output_fields(cls, values): - return _check_output_fields(values) - - @root_validator - def check_H0(cls, values): - return _check_H0(values) - - @root_validator - def check_provided_ufuncs(cls, values): - sim_specs = values.get("sim_specs") - assert hasattr(sim_specs, "sim_f"), "Simulation function not provided to SimSpecs." - assert isinstance(sim_specs.sim_f, Callable), "Simulation function is not callable." - - if values.get("alloc_specs").alloc_f.__name__ != "give_pregenerated_sim_work": - gen_specs = values.get("gen_specs") - assert hasattr(gen_specs, "gen_f"), "Generator function not provided to GenSpecs." - assert isinstance(gen_specs.gen_f, Callable), "Generator function is not callable." - - return values - - @root_validator - def simf_set_in_out_from_attrs(cls, values): - if not values.get("sim_f"): - from libensemble.sim_funcs.simple_sim import norm_eval - - values["sim_f"] = norm_eval - if hasattr(values.get("sim_f"), "inputs") and not values.get("inputs"): - values["inputs"] = values.get("sim_f").inputs - if hasattr(values.get("sim_f"), "outputs") and not values.get("outputs"): - values["outputs"] = values.get("sim_f").outputs - if hasattr(values.get("sim_f"), "persis_in") and not values.get("persis_in"): - values["persis_in"] = values.get("sim_f").persis_in - return values - - @root_validator - def genf_set_in_out_from_attrs(cls, values): - if not values.get("gen_f"): - from libensemble.gen_funcs.sampling import latin_hypercube_sample - - values["gen_f"] = latin_hypercube_sample - if hasattr(values.get("gen_f"), "inputs") and not values.get("inputs"): - values["inputs"] = values.get("gen_f").inputs - if hasattr(values.get("gen_f"), "outputs") and not values.get("outputs"): - values["outputs"] = values.get("gen_f").outputs - if hasattr(values.get("gen_f"), "persis_in") and not values.get("persis_in"): - values["persis_in"] = values.get("gen_f").persis_in - return values - - # RESOURCES VALIDATORS ##### - - @root_validator - def check_logical_cores(cls, values): - return _check_logical_cores(values) - -else: - from pydantic import field_validator, model_validator - - # 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) - - @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 - ): - self.__dict__["save_H_on_completion"] = True - return self - - @model_validator(mode="after") - def set_workflow_dir(self): - return _check_set_workflow_dir(self) - - @model_validator(mode="after") - def set_calc_dirs_on_input_dir(self): - return _check_set_calc_dirs_on_input_dir(self) - - @model_validator(mode="after") - def check_exit_criteria(self): - return _check_exit_criteria(self) - - @model_validator(mode="after") - def check_output_fields(self): - return _check_output_fields(self) - - @model_validator(mode="after") - def check_H0(self): - return _check_H0(self) - - @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 - - @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) +# 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) + + +@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 + ): + self.__dict__["save_H_on_completion"] = True + return self + + +@model_validator(mode="after") +def set_workflow_dir(self): + return _check_set_workflow_dir(self) + + +@model_validator(mode="after") +def set_calc_dirs_on_input_dir(self): + return _check_set_calc_dirs_on_input_dir(self) + + +@model_validator(mode="after") +def check_exit_criteria(self): + return _check_exit_criteria(self) + + +@model_validator(mode="after") +def check_output_fields(self): + return _check_output_fields(self) + + +@model_validator(mode="after") +def check_H0(self): + return _check_H0(self) + + +@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 + + +@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) diff --git a/pixi.lock b/pixi.lock new file mode 100644 index 0000000000..c7ebed04ee --- /dev/null +++ b/pixi.lock @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:751e13cc32dc1d88c00f1e2e9c2b029d20a5884d43f86e1b67ced74b8015823b +size 1089301 diff --git a/pyproject.toml b/pyproject.toml index 68d5654da3..271c171b4b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,34 +1,39 @@ [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", "pyyaml", "tomli"] description = "A Python toolkit for coordinating asynchronous and dynamic ensembles of calculations." name = "libensemble" requires-python = ">=3.10" -license = {file = "LICENSE"} +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.10", + "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 +44,175 @@ 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,<26", "setuptools>=75.1.0,<81"] [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 +py310 = ["py310", "basic"] +py311 = ["py311", "basic"] +py312 = ["py312", "basic"] +py313 = ["py313", "basic"] +py314 = ["py314", "basic"] + +py310e = ["py310", "py310e", "basic", "extra"] +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,<2" + +# "dev" dependencies needed for basic CI +flake8 = ">=7.3.0,<8" +coverage = ">=7.13.0,<8" +pytest = ">=9.0.2,<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,<4" +petsc4py = ">=3.24.2,<4" +ninja = ">=1.13.2,<2" # for building Tasmanian from pypi +nlopt = ">=2.10.0,<3" + +[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" + +# 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.py310.dependencies] +python = "3.10.*" +[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.py310e.target.linux-64.dependencies] +ax-platform = ">=1.2.1,<2" + +[tool.pixi.feature.py310e.dependencies] +globus-compute-sdk = ">=4.3.0,<5" + +[tool.pixi.feature.py311e.target.linux-64.dependencies] +ax-platform = ">=1.2.1,<2" + +[tool.pixi.feature.py311e.dependencies] +globus-compute-sdk = ">=4.3.0,<5" + +[tool.pixi.feature.py312e.target.linux-64.dependencies] +ax-platform = ">=1.2.1,<2" + +[tool.pixi.feature.py312e.dependencies] +globus-compute-sdk = ">=4.3.0,<5" + +[tool.pixi.feature.py313e.target.linux-64.dependencies] +ax-platform = ">=0.5.0,<0.6" + +[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.10,<3.15" +pip = ">=25.2,<26" +setuptools = ">=80.8.0,<81" +numpy = ">=2.2.6,<3" +pydantic = ">=2.12.4,<3" +pyyaml = ">=6.0.2,<7" +tomli = ">=2.2.1,<3" + +# macOS dependencies [tool.pixi.target.osx-arm64.dependencies] -clang_osx-arm64 = ">=19.1.2,<20" +clang_osx-arm64 = ">=21.1.7,<22" + +# 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", + "proxystore>=0.8.3,<0.9", + "redis>=7.1.0,<8", +] +dev = ["wat>=0.7.0,<0.8"] + +# Various config from here onward [tool.black] line-length = 120 -target-version = ['py39', 'py310', 'py311', 'py312', 'py313'] +target-version = ['py310', 'py311', 'py312', 'py313', 'py314'] force-exclude = ''' ( /( @@ -117,10 +224,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" @@ -134,12 +238,10 @@ HPE = "HPE" RO = "RO" lst = "lst" noy = "noy" +inpt = "inpt" [tool.typos.files] extend-exclude = ["*.bib", "*.xml", "docs/nitpicky"] [tool.mypy] 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"] diff --git a/scripts/plot_libe_calcs_util_v_time.py b/scripts/plot_libe_calcs_util_v_time.py index 9f9f22edda..fc6750a10d 100755 --- a/scripts/plot_libe_calcs_util_v_time.py +++ b/scripts/plot_libe_calcs_util_v_time.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -""" User function utilization plot +"""User function utilization plot Script to produce utilization plot based on how many workers are running user functions (sim or gens) at any given time. The plot is written to a file. diff --git a/scripts/plot_libe_histogram.py b/scripts/plot_libe_histogram.py index e5145bc05d..9365571404 100755 --- a/scripts/plot_libe_histogram.py +++ b/scripts/plot_libe_histogram.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -""" Histogram of user function run-times (completed & killed). +"""Histogram of user function run-times (completed & killed). Script to produce a histogram plot giving a count of user function (sim or gen) calls by run-time intervals. Color shows completed versus killed versus diff --git a/scripts/plot_libe_tasks_util_v_time.py b/scripts/plot_libe_tasks_util_v_time.py index ece34bdafb..cb5ced7236 100644 --- a/scripts/plot_libe_tasks_util_v_time.py +++ b/scripts/plot_libe_tasks_util_v_time.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -""" User tasks utilization plot +"""User tasks utilization plot Script to produce utilisation plot based on how many workers are running user tasks (submitted via a libEnsemble executor) at any given time. This does not