.. _Quickstart: Quickstart ========== ParMOO is a parallel multiobjective optimization solver that seeks to exploit simulation-based structure in objective and constraint functions. To exploit structure, ParMOO models *simulations* separately from *objectives* and *constraints*. In our language: * a **design variable** is an input to the problem, which we can directly control; * a **simulation** is an expensive or time-consuming process, including real-world experimentation, which is treated as a blackbox function of the design variables and evaluated sparingly; * an **objective** is an algebraic function of the design variables and/or simulation outputs, which we would like to optimize; and * a **constraint** is an algebraic function of the design variables and/or simulation outputs, which cannot exceed a specified bound. .. figure:: img/des-sim-obj-space.png :alt: Designs, simulations, and objectives :align: center | To solve a multiobjective optimization problem (MOOP), we use surrogate models of the simulation outputs, together with the algebraic definition of the objectives and constraints. .. only:: html .. figure:: img/parmoo_movie.gif :alt: ParMOO animation :align: center | In order to achieve scalable parallelism, we use libEnsemble_ to distribute batches of simulation evaluations across parallel resources. Dependencies ------------ ParMOO has been tested on Unix/Linux and MacOS systems. ParMOO's base has the following dependencies: * Python_ 3.9+ * jax_ -- for algorithmic differentiation and just-in-time (jit) compilation * numpy_ -- for data structures and performant numerical linear algebra * scipy_ -- for scientific calculations needed for specific modules * pandas_ -- for exporting the resulting databases Additional dependencies are needed to use the additional features in ``parmoo.extras``: * libEnsemble_ -- for managing parallel simulation evaluations And for using the Pareto front visualization library in ``parmoo.viz``: * plotly_ -- for generating interactive plots * dash_ -- for hosting interactive plots in your browser * kaleido_ -- for exporting static plots post-interaction Installation ------------ The easiest way to install ParMOO is via the Python package index, PyPI (commonly called ``pip``): .. code-block:: bash pip install < --user > parmoo where the braces around ``< --user >`` indicate that the ``--user`` flag is optional. To install *all* dependencies (including libEnsemble) use: .. code-block:: bash pip install < --user > "parmoo[extras]" Note that the full feature set for libEnsemble_ and kaleido_ may require you to separately install an MPI implementation (such as Open_MPI_) and Google chrome (e.g., via kaleido_get_chrome_), respectively. You can also clone this project from our GitHub_ and ``pip`` install it in-place, so that you can easily pull the latest version or checkout the ``develop`` branch for pre-release features. On Debian-based systems with a bash shell, this looks like: .. code-block:: bash git clone https://github.com/parmoo/parmoo cd parmoo pip install -e . Alternatively, the latest release of ParMOO (including all required and optional dependencies) can be installed from the ``conda-forge`` channel using: .. code-block:: bash conda install --channel=conda-forge parmoo Before doing so, it is recommended to create a new conda environment using: .. code-block:: bash conda create --name channel-name conda activate channel-name For detailed instructions, see :doc:`install`. Testing ------- Note that in order to run the unit tests, you must first install the ``parmoo[extras]``, as described above. This may include the additional steps such as ``kaleido_get_chrome``. You can install pytest_ with the pytest-cov_ plugin and flake8_ using the ``tests`` extension, then you can lint the project with ``flake8`` and run the unit tests for your installation using ``pytest``. After running unit tests, you can view the coverage report using the ``coverage report`` command. .. code-block:: bash pip install -e ".[tests]" flake8 parmoo pytest coverage report Running the regression tests and libEnsemble tests is a bit more involved and is usually accomplished via the ``-l`` flag for the ``parmoo/tests/run-tests.sh`` script. To run all the linter, unit tests, regression tests, and generate the coverage report in a single command, run the script with all 4 flags set. .. code-block::bash ./parmoo/tests/run-tests.sh -curl These tests are run regularly using GitHub Actions_. Basic Usage ----------- ParMOO uses numpy_ and jax_ in an object-oriented design, based around the :class:`MOOP ` class. Before getting started, note that jax_ runs in single (32-bit) precision by default. To run in double precision, the following code is needed at startup: .. code-block:: python import jax jax.config.update("jax_enable_x64", True) This will be done automatically when importing certain modules in ParMOO, which are only compatible with double precision. However, in many use cases, 32-bit precision may be enough and provides substantial speedup in iteration tasks. Once the precision is set, to get started, create a :class:`MOOP ` object, using the :meth:`constructor `. .. code-block:: python from parmoo import MOOP from parmoo.optimizers import GlobalSurrogate_PS my_moop = MOOP(GlobalSurrogate_PS) To summarize the framework, in each iteration ParMOO models each simulation using a computationally cheap surrogate, then solves one or more scalarizations of the objectives, which are specified by acquisition functions. Read more about this framework at our :doc:`Learn About MOOPs ` page. In the above example, :mod:`optimizers.GlobalSurrogate_PS ` is the class of optimizers that the ``my_moop`` object will use to solve the scalarized surrogate problems. Next, add design variables to the problem as follows using the :meth:`MOOP.addDesign(*args) ` method. In this example, we define one continuous and one categorical design variable. Other options include integer, custom, and raw (using raw variables is not recommended except for expert users). .. code-block:: python # Add a single continuous design variable in the range [0.0, 1.0] my_moop.addDesign({'name': "x1", # optional, name 'des_type': "continuous", # optional, type of variable 'lb': 0.0, # required, lower bound 'ub': 1.0, # required, upper bound 'tol': 1.0e-8 # optional tolerance }) # Add a second categorical design variable with 3 levels my_moop.addDesign({'name': "x2", # optional, name 'des_type': "categorical", # required, type of variable 'levels': [-1, 1] # required, list of category labels }) Next, add simulations to the problem as follows using the :meth:`MOOP.addSimulation(*args) ` method. In this example, we define a toy simulation ``sim_func(x)``. .. code-block:: python import numpy as np from parmoo.searches import LatinHypercube from parmoo.surrogates import GaussRBF # Define a toy simulation for the problem, whose outputs are quadratic def sim_func(x): sx = np.array([(x["x1"] - 0.2) ** 2, (x["x1"] - 0.8) ** 2]) if x["x2"] != 1: sx += 99. return sx # Add the simulation to the problem my_moop.addSimulation({'name': "MySim", # Optional name for this simulation 'm': 2, # This simulation has 2 outputs 'sim_func': sim_func, # Our sample sim from above 'search': LatinHypercube, # Use a LHS search 'surrogate': GaussRBF, # Use a Gaussian RBF surrogate 'hyperparams': {}, # Hyperparams passed to internals 'sim_db': { # Optional dict of precomputed points 'search_budget': 10 # Set search budget }, }) Now we can add objectives and constraints using :meth:`MOOP.addObjective(*args) ` and :meth:`MOOP.addConstraint(*args) `. In this example, there are 2 objectives (each corresponding to a single simulation output) and one constraint. .. code-block:: python # First objective just returns the first simulation output def f1(x, s): return s["MySim"][0] my_moop.addObjective({'name': "f1", 'obj_func': f1}) # Second objective just returns the second simulation output def f2(x, s): return s["MySim"][1] my_moop.addObjective({'name': "f2", 'obj_func': f2}) # Add a single constraint, that x[0] >= 0.1 def c1(x, s): return 0.1 - x["x1"] my_moop.addConstraint({'name': "c1", 'con_func': c1}) Finally, we must add one or more acquisition functions using :meth:`MOOP.addAcquisition(*args) `. These are used to scalarize the surrogate problems. The number of acquisition functions typically determines the number of simulation evaluations per batch. This is useful to know if you are using a parallel solver. .. code-block:: python from parmoo.acquisitions import RandomConstraint # Add 3 acquisition functions for i in range(3): my_moop.addAcquisition({'acquisition': RandomConstraint, 'hyperparams': {}}) Finally, the MOOP is solved using the :meth:`MOOP.solve(budget) ` method, and the results can be viewed using :meth:`MOOP.getPF() `. .. code-block:: python import pandas as pd my_moop.solve(5) # Solve with 5 iterations of ParMOO algorithm results = my_moop.getPF(format="pandas") # Extract the results as pandas df After executing the above block of code, the ``results`` variable points to a pandas_ dataframe, each of whose rows corresponds to a nondominated objective value in the ``my_moop`` object's final database. You can reference individual columns in the ``results`` array by using the ``name`` keys that were assigned during ``my_moop``'s construction, or plot the results by using the :doc:`viz ` library. Congratulations, you now know enough to get started solving MOOPs! Minimal Working Example ----------------------- Putting it all together, we get the following minimal working example. .. literalinclude:: ../examples/quickstart.py :language: python The above code saves all (approximate) Pareto optimal solutions in the ``results`` variable, and prints the ``results`` variable to the standard output: .. literalinclude:: ../examples/quickstart.out And produces the following figure of the Pareto points: .. figure:: ../examples/quickstart.jpeg :alt: Scatter plot of the Pareto front after solving demo problem :align: center | Next Steps ---------- * If you want to take advantage of all that ParMOO has to offer, please see :doc:`Writing a ParMOO Script `. * If you would like more information on multiobjective optimization terminology and ParMOO's methodology, see the :doc:`Learn About MOOPs ` page. * For a full list of basic usage tutorials, see :doc:`More Tutorials `. * To start solving MOOPs on parallel hardware, install libEnsemble_ and see the :doc:`libEnsemble tutorial `. * See some of our pre-built solvers in the parmoo_solver_farm_. * To interactively explore your solutions, install its extra dependencies and use our built-in :doc:`viz ` tool. * For more advice, consult our :doc:`FAQs `. Resources --------- To seek support or report issues, e-mail: * ``parmoo@lbl.gov`` Our full documentation is hosted on: * ReadTheDocs_ Please read our LICENSE_ and CONTRIBUTING_ files. .. _Actions: https://github.com/parmoo/parmoo/actions .. _CONTRIBUTING: https://github.com/parmoo/parmoo/blob/main/CONTRIBUTING.rst .. _dash: https://dash.plotly.com .. _flake8: https://flake8.pycqa.org/en/latest .. _GitHub: https://github.com/parmoo/parmoo .. _jax: https://docs.jax.dev/en/latest/ .. _kaleido: https://github.com/plotly/Kaleido .. _kaleido_get_chrome: https://pypi.org/project/kaleido .. _libEnsemble: https://github.com/Libensemble/libensemble .. _LICENSE: https://github.com/parmoo/parmoo/blob/main/LICENSE .. _numpy: https://numpy.org .. _Open_MPI: https://docs.open-mpi.org/en/v5.0.x/installing-open-mpi/quickstart.html .. _pandas: https://pandas.pydata.org .. _parmoo_solver_farm: https://github.com/parmoo/parmoo-solver-farm .. _plotly: https://plotly.com/python .. _pytest: https://docs.pytest.org/en/7.0.x .. _pytest-cov: https://pytest-cov.readthedocs.io/en/latest .. _Python: https://www.python.org/downloads .. _ReadTheDocs: https://parmoo.readthedocs.org .. _scipy: https://scipy.org