Extras and Plugins

Running in Parallel using libEnsemble

libEnsemble is a Python library to “coordinate concurrent evaluation of dynamic ensembles of calculations.” Read more about libEnsemble by visiting the libEnsemble documentation.

The libE_MOOP class is used to solve MOOPs using libEnsemble. The libE_MOOP class inherits from MOOP and supports all of the public methods in its API.

To create an instance of the libE_MOOP class, import it from the extras.libe module and then create a MOOP, just as you normally would. The solve() method has been redefined to create a libEnsemble Persistent Generator function, which libEnsemble can call to generate batches of simulation evaluations, which it will distribute over available resources.

Below we reproduce the example from the Quickstart guide, using a libE_MOOP object.

Note that it is always recommended that you turn on checkpointing when using libEnsemble. Since setCheckpoint method does not support the usage of Python lambda functions, each of the objectives and constraints is explicitly defined.


import numpy as np
from parmoo.extras.libe import libE_MOOP
from parmoo.searches import LatinHypercube
from parmoo.surrogates import GaussRBF
from parmoo.acquisitions import UniformWeights
from parmoo.optimizers import LocalGPS

# When running with MPI, we need to keep track of which thread is the manager
# using libensemble.tools.parse_args()
from libensemble.tools import parse_args
_, is_manager, _, _ = parse_args()

# All functions are defined below.

def sim_func(x):
   if x["x2"] == 0:
      return np.array([(x["x1"] - 0.2) ** 2, (x["x1"] - 0.8) ** 2])
   else:
      return np.array([99.9, 99.9])

def obj_f1(x, s):
    return s["MySim"][0]

def obj_f2(x, s):
    return s["MySim"][1]

def const_c1(x, s):
    return 0.1 - x["x1"]

# When using libEnsemble with Python MP, the "solve" command must be enclosed
# in an "if __name__ == '__main__':" block, as shown below
if __name__ == "__main__":
    # Fix the random seed for reproducibility
    np.random.seed(0)

    # Create a libE_MOOP
    my_moop = libE_MOOP(LocalGPS)
    
    # Add 2 design variables (one continuous and one categorical)
    my_moop.addDesign({'name': "x1",
                       'des_type': "continuous",
                       'lb': 0.0, 'ub': 1.0})
    my_moop.addDesign({'name': "x2", 'des_type': "categorical",
                       'levels': 3})
    
    # Add the simulation (note the budget of 20 sim evals during search phase)
    my_moop.addSimulation({'name': "MySim",
                           'm': 2,
                           'sim_func': sim_func,
                           'search': LatinHypercube,
                           'surrogate': GaussRBF,
                           'hyperparams': {'search_budget': 20}})
    
    # Add the objectives
    my_moop.addObjective({'name': "f1", 'obj_func': obj_f1})
    my_moop.addObjective({'name': "f2", 'obj_func': obj_f2})
    
    # Add the constraint
    my_moop.addConstraint({'name': "c1", 'constraint': const_c1})
    
    # Add 3 acquisition functions
    for i in range(3):
       my_moop.addAcquisition({'acquisition': UniformWeights,
                               'hyperparams': {}})
    
    # Turn on checkpointing -- creates files parmoo.moop & parmoo.surrogate.1
    my_moop.setCheckpoint(True, checkpoint_data=False, filename="parmoo")
    
    # Use sim_max = 30 to perform just 30 simulations
    my_moop.solve(sim_max=30)
    
    # Display the solution -- this "if" clause is needed when running with MPI
    if is_manager:
        results = my_moop.getPF(format="pandas")
        print(results)

To run a ParMOO/libEnsemble script, first make sure that libEnsemble is installed. You can find instructions on how to do so under libEnsemble’s Advanced Installation documentation.

Next, run libEnsemble as described in the Running libEnsemble section. Common methods of running a libEnsemble script are with MPI

mpirun -np N python3 libe_basic_ex.py

and with Python’s built-in multiprocessing module.

python3 libe_basic_ex.py --comms local --nworkers N

  • Note: When running a libE_MOOP with Python multiprocessing, MacOS and Windows systems default to using the spawn method. When using the spawn method, one must enclose the libE_MOOP.solve() command inside an if __name__ == '__main__': block, as shown in the example above. Read more about the issue here:

The result from running the example is shown below.

         x1  x2        f1        f2        c1
0  0.742825   0  0.294659  0.003269 -0.642825
1  0.680283   0  0.230672  0.014332 -0.580283
2  0.616501   0  0.173473  0.033672 -0.516501
3  0.580369   0  0.144680  0.048238 -0.480369
4  0.555222   0  0.126183  0.059916 -0.455222
5  0.518980   0  0.101749  0.078972 -0.418980
6  0.475477   0  0.075888  0.105315 -0.375477
7  0.302503   0  0.010507  0.247503 -0.202503
8  0.201285   0  0.000002  0.358460 -0.101285