Basic Tutorials

This is a collection of all tutorials demonstrating basic ParMOO functionality (collected from throughout the ParMOO User Guide).

Quickstart Demo

This is a basic example (see quickstart.py) of how to build and solve a MOOP with ParMOO, taken from the Quickstart guide.


import numpy as np
import pandas as pd
from parmoo import MOOP
from parmoo.searches import LatinHypercube
from parmoo.surrogates import GaussRBF
from parmoo.acquisitions import RandomConstraint
from parmoo.optimizers import LocalGPS

# Fix the random seed for reproducibility
np.random.seed(0)

my_moop = MOOP(LocalGPS)

my_moop.addDesign({'name': "x1",
                   'des_type': "continuous",
                   'lb': 0.0, 'ub': 1.0})
my_moop.addDesign({'name': "x2", 'des_type': "categorical",
                   'levels': ["good", "bad"]})

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

my_moop.addSimulation({'name': "MySim",
                       'm': 2,
                       'sim_func': sim_func,
                       'search': LatinHypercube,
                       'surrogate': GaussRBF,
                       'hyperparams': {'search_budget': 20}})

def f1(x, s): return s["MySim"][0]
def f2(x, s): return s["MySim"][1]
my_moop.addObjective({'name': "f1", 'obj_func': f1})
my_moop.addObjective({'name': "f2", 'obj_func': f2})

def c1(x, s): return 0.1 - x["x1"]
my_moop.addConstraint({'name': "c1", 'constraint': c1})

for i in range(3):
   my_moop.addAcquisition({'acquisition': RandomConstraint,
                           'hyperparams': {}})

my_moop.solve(5)
results = my_moop.getPF(format="pandas")

# Display solution
print(results)

# Plot results -- must have extra viz dependencies installed
from parmoo.viz import scatter
# The optional arg `output` exports directly to jpg instead of interactive mode
scatter(my_moop, output="jpeg")

The above code saves all (approximate) Pareto optimal solutions in the results dataframe, and prints the results dataframe to the standard output:

         x1    x2        f1        f2        c1
0  0.742840  good  0.294675  0.003267 -0.642840
1  0.726092  good  0.276773  0.005462 -0.626092
2  0.605914  good  0.164766  0.037669 -0.505914
3  0.548931  good  0.121753  0.063036 -0.448931
4  0.543499  good  0.117991  0.065793 -0.443499
5  0.401011  good  0.040405  0.159192 -0.301011
6  0.353552  good  0.023578  0.199316 -0.253552
7  0.328402  good  0.016487  0.222404 -0.228402
8  0.269175  good  0.004785  0.281775 -0.169175
9  0.248183  good  0.002322  0.304502 -0.148183

And produces the following figure of the Pareto points:

Scatter plot of the Pareto front after solving demo problem

Named Output Types

The following named_var_ex.py code demonstrates ParMOO’s output datatype when the MOOP object is defined using named variables.


import numpy as np
from parmoo import MOOP
from parmoo.searches import LatinHypercube
from parmoo.surrogates import GaussRBF
from parmoo.acquisitions import UniformWeights
from parmoo.optimizers import LocalGPS

my_moop = MOOP(LocalGPS)

# Define a simulation to use below
def sim_func(x):
    if x["MyCat"] == 0:
        return np.array([(x["MyDes"]) ** 2, (x["MyDes"] - 1.0) ** 2])
    else:
        return np.array([99.9, 99.9])

# Add a design variable, simulation, objective, and constraint.
# Note the 'name' keys for each
my_moop.addDesign({'name': "MyDes",
                   'des_type': "continuous",
                   'lb': 0.0, 'ub': 1.0})
my_moop.addDesign({'name': "MyCat",
                   'des_type': "categorical",
                   'levels': 2})

my_moop.addSimulation({'name': "MySim",
                       'm': 2,
                       'sim_func': sim_func,
                       'search': LatinHypercube,
                       'surrogate': GaussRBF,
                       'hyperparams': {'search_budget': 20}})

my_moop.addObjective({'name': "MyObj",
                      'obj_func': lambda x, s: sum(s["MySim"])})

my_moop.addConstraint({'name': "MyCon",
                       'constraint': lambda x, s: 0.1 - x["MyDes"]})

# Extract numpy dtypes for all of this MOOP's inputs/outputs
des_dtype = my_moop.getDesignType()
obj_dtype = my_moop.getObjectiveType()
sim_dtype = my_moop.getSimulationType()

# Display the dtypes as strings
print("Design variable type:   " + str(des_dtype))
print("Simulation output type: " + str(sim_dtype))
print("Objective type:         " + str(obj_dtype))

The above code produces the following output.

Design variable type:   [('MyDes', '<f8'), ('MyCat', '<i4')]
Simulation output type: [('MySim', '<f8', (2,))]
Objective type:         [('MyObj', '<f8')]

Unnamed Output Types

The following unnamed_var_ex.py code demonstrates ParMOO’s output datatype when the MOOP object is defined using unnamed variables.


import numpy as np
from parmoo import MOOP
from parmoo.searches import LatinHypercube
from parmoo.surrogates import GaussRBF
from parmoo.acquisitions import UniformWeights
from parmoo.optimizers import LocalGPS

# Fix the random seed for reproducibility
np.random.seed(0)

my_moop = MOOP(LocalGPS)

# Define a simulation to use below
def sim_func(x):
    return np.array([(x[0]) ** 2, (x[0] - 1.0) ** 2])

# Add a design variable, simulation, objective, and constraint, w/o name key
my_moop.addDesign({'des_type': "continuous",
                   'lb': 0.0, 'ub': 1.0})

my_moop.addSimulation({'m': 2,
                       'sim_func': sim_func,
                       'search': LatinHypercube,
                       'surrogate': GaussRBF,
                       'hyperparams': {'search_budget': 20}})

my_moop.addObjective({'obj_func': lambda x, s: sum(s)})

my_moop.addConstraint({'constraint': lambda x, s: 0.1 - x[0]})

# Extract numpy dtypes for all of this MOOP's inputs/outputs
des_dtype = my_moop.getDesignType()
sim_dtype = my_moop.getSimulationType()
obj_dtype = my_moop.getObjectiveType()
const_dtype = my_moop.getConstraintType()

# Display the dtypes as strings
print("Design variable type:   " + str(des_dtype))
print("Simulation output type: " + str(sim_dtype))
print("Objective type:         " + str(obj_dtype))
print("Constraint type:        " + str(const_dtype))
print()

# Add one acquisition and solve with 0 iterations to initialize databases
my_moop.addAcquisition({'acquisition': UniformWeights})
my_moop.solve(0)

# Extract final objective and simulation databases
obj_db = my_moop.getObjectiveData()
sim_db = my_moop.getSimulationData()

# Print the objective database dtypes
print("Objective database keys: " + str([key for key in obj_db.keys()]))
for key in obj_db.keys():
    print("\t'" + key + "'" + " dtype: " + str(obj_db[key].dtype))
    print("\t'" + key + "'" + " shape: " + str(obj_db[key].shape))
print()

# Print the simulation database dtypes
print("Simulation database type: " + str(type(sim_db)))
print("Simulation database length: " + str(len(sim_db)))
for i, dbi in enumerate(sim_db):
    print("\tsim_db[" + str(i) + "] database keys: " +
          str([key for key in dbi.keys()]))
    for key in dbi.keys():
        print("\t\t'" + key + "'" + " dtype: " + str(dbi[key].dtype))
        print("\t\t'" + key + "'" + " shape: " + str(dbi[key].shape))

The above code produces the following output.

Design variable type:   ('<f8', (1,))
Simulation output type: ('<f8', (2,))
Objective type:         ('<f8', (1,))
Constraint type:        ('<f8', (1,))

Objective database keys: ['x_vals', 'f_vals', 'c_vals']
	'x_vals' dtype: float64
	'x_vals' shape: (20, 1)
	'f_vals' dtype: float64
	'f_vals' shape: (20, 1)
	'c_vals' dtype: float64
	'c_vals' shape: (20, 1)

Simulation database type: <class 'list'>
Simulation database length: 1
	sim_db[0] database keys: ['x_vals', 's_vals']
		'x_vals' dtype: float64
		'x_vals' shape: (20, 1)
		's_vals' dtype: float64
		's_vals' shape: (20, 2)

Adding Precomputed Simulation Values

The following precomputed_data.py code demonstrates how to add a precomputed simulation output to ParMOO’s internal simulation databases.


import numpy as np
from parmoo import MOOP
from parmoo.searches import LatinHypercube
from parmoo.surrogates import GaussRBF
from parmoo.acquisitions import UniformWeights
from parmoo.optimizers import LocalGPS

my_moop = MOOP(LocalGPS)

my_moop.addDesign({'name': "x1",
                   'des_type': "continuous",
                   'lb': 0.0, 'ub': 1.0})
my_moop.addDesign({'name': "x2", 'des_type': "categorical",
                   'levels': 3})

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])

my_moop.addSimulation({'name': "MySim",
                       'm': 2,
                       'sim_func': sim_func,
                       'search': LatinHypercube,
                       'surrogate': GaussRBF,
                       'hyperparams': {'search_budget': 20}})

my_moop.addObjective({'name': "f1", 'obj_func': lambda x, s: s["MySim"][0]})
my_moop.addObjective({'name': "f2", 'obj_func': lambda x, s: s["MySim"][1]})

my_moop.addAcquisition({'acquisition': UniformWeights})

# Precompute one simulation value for demo
des_val = np.zeros(1, dtype=[("x1", float), ("x2", int)])[0]
sim_val = sim_func(des_val)

# Add the precomputed simulation value from above
my_moop.update_sim_db(des_val, sim_val, "MySim")

# Get and display initial database
sim_db = my_moop.getSimulationData()
print(sim_db)

The above code produces the following output.

{'MySim': array([(0., 0, [0.04, 0.64])],
      dtype=[('x1', '<f8'), ('x2', '<i4'), ('out', '<f8', (2,))])}

Logging and Checkpointing

When solving a large or expensive problem, logging and checkpointing are recommended. The following code snippet shows how to solve the same problem from the Quickstart example, but with logging and checkpointing turned on. Then, another MOOP is created by reloading from the checkpoint file, and run for 1 extra iteration, in order to demonstrate how to reload from a saved checkpoint file.

Note how the lambda functions from Quickstart example have been explicitly defined below. This is because ParMOO reloads functions by name, which means that it does not support checkpointing for lambda functions and any named function must be defined with the same name in the global scope during reloading.


import numpy as np
from parmoo import MOOP
from parmoo.searches import LatinHypercube
from parmoo.surrogates import GaussRBF
from parmoo.acquisitions import UniformWeights
from parmoo.optimizers import LocalGPS
import logging

# Fix the random seed for reproducibility
np.random.seed(0)

# Create a new MOOP
my_moop = MOOP(LocalGPS)

# Add 1 continuous and 1 categorical design variable
my_moop.addDesign({'name': "x1",
                   'des_type': "continuous",
                   'lb': 0.0, 'ub': 1.0})
my_moop.addDesign({'name': "x2", 'des_type': "categorical",
                   'levels': 3})

# Create a simulation function
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])

# Add the simulation function to the MOOP
my_moop.addSimulation({'name': "MySim",
                       'm': 2,
                       'sim_func': sim_func,
                       'search': LatinHypercube,
                       'surrogate': GaussRBF,
                       'hyperparams': {'search_budget': 20}})

# Define the 2 objectives as named Python functions
def obj1(x, s): return s["MySim"][0]
def obj2(x, s): return s["MySim"][1]

# Define the constraint as a function
def const(x, s): return 0.1 - x["x1"]

# Add 2 objectives
my_moop.addObjective({'name': "f1", 'obj_func': obj1})
my_moop.addObjective({'name': "f2", 'obj_func': obj2})

# Add 1 constraint
my_moop.addConstraint({'name': "c1", 'constraint': const})

# Add 3 acquisition functions (generates batches of size 3)
for i in range(3):
   my_moop.addAcquisition({'acquisition': UniformWeights,
                           'hyperparams': {}})

# Turn on logging with timestamps
logging.basicConfig(level=logging.INFO,
                    format='%(asctime)s %(levelname)-8s %(message)s',
                    datefmt='%Y-%m-%d %H:%M:%S')

# Use checkpointing without saving a separate data file (in "parmoo.moop" file)
my_moop.setCheckpoint(True, checkpoint_data=False, filename="parmoo")

# Solve the problem with 4 iterations
my_moop.solve(4)

# Create a new MOOP object and reload the MOOP from parmoo.moop file
new_moop = MOOP(LocalGPS)
new_moop.load("parmoo")

# Do another iteration
new_moop.solve(5)

# Display the solution
results = new_moop.getPF()
print(results, "\n dtype=" + str(results.dtype))

The above checkpointing.py code produces the following output.

[(0.79013666, 0, 3.48261275e-01, 9.72855201e-05, -0.69013666)
 (0.7895263 , 0, 3.47541259e-01, 1.09698380e-04, -0.6895263 )
 (0.73488156, 0, 2.86098283e-01, 4.24041125e-03, -0.63488156)
 (0.70656124, 0, 2.56604287e-01, 8.73080244e-03, -0.60656124)
 (0.68409101, 0, 2.34344111e-01, 1.34348928e-02, -0.58409101)
 (0.67237225, 0, 2.23135544e-01, 1.62888423e-02, -0.57237225)
 (0.5784217 , 0, 1.43202981e-01, 4.90969442e-02, -0.4784217 )
 (0.53031761, 0, 1.09109723e-01, 7.27285920e-02, -0.43031761)
 (0.51322778, 0, 9.81116425e-02, 8.22383058e-02, -0.41322778)
 (0.49723345, 0, 8.83477213e-02, 9.16675863e-02, -0.39723345)
 (0.44987019, 0, 6.24351129e-02, 1.22590882e-01, -0.34987019)
 (0.40591372, 0, 4.24004606e-02, 1.55303995e-01, -0.30591372)
 (0.39028872, 0, 3.62097978e-02, 1.67863331e-01, -0.29028872)
 (0.38995793, 0, 3.60840145e-02, 1.68134501e-01, -0.28995793)
 (0.38287784, 0, 3.34443041e-02, 1.73990897e-01, -0.28287784)
 (0.25435646, 0, 2.95462529e-03, 2.97726867e-01, -0.15435646)
 (0.20137796, 0, 1.89878434e-06, 3.58348342e-01, -0.10137796)] 
 dtype=[('x1', '<f8'), ('x2', '<i4'), ('f1', '<f8'), ('f2', '<f8'), ('c1', '<f8')]

Solving a MOOP with Derivative-Based Solvers

This example shows how to minimize two conflicting quadratic functions of three variables (named x1, x2, and x3), under the constraint that an additional categorical variable (x4) must be fixed in class 0 \(^*\), using the derivative-based solver LBFGSB.

\(^*\) No, this constraint does not really affect the solution; it is just here to demonstrate how constraints/categorical variables are handled by derivative-based solvers. ParMOO does not use derivative information associated with any categorical variables, so the derivative w.r.t. x4 can be set to any value, without affecting the outcome.


import numpy as np
from parmoo import MOOP
from parmoo.acquisitions import RandomConstraint, FixedWeights
from parmoo.searches import LatinHypercube
from parmoo.surrogates import GaussRBF
from parmoo.optimizers import LBFGSB

# Fix the random seed for reproducibility
np.random.seed(0)

# Create a new MOOP with a derivative-based solver
my_moop = MOOP(LBFGSB, hyperparams={})

# Add 3 continuous variables named x1, x2, x3
for i in range(3):
    my_moop.addDesign({'name': "x" + str(i+1),
                       'des_type': "continuous",
                       'lb': 0.0,
                       'ub': 1.0,
                       'des_tol': 1.0e-8})
# Add one categorical variable named x4
my_moop.addDesign({'name': "x4",
                   'des_type': "categorical",
                   'levels': 3})

def quad_sim(x):
    """ A quadratic simulation function with 2 outputs.

    Returns:
        np.ndarray: simulation value (S) with 2 outputs
         * S_1(x) = <x, x>
         * S_2(x) = <x-1, x-1>

    """

    return np.array([x["x1"] ** 2 + x["x2"] ** 2 + x["x3"] ** 2,
                     (x["x1"] - 1.0) ** 2 + (x["x2"] - 1.0) ** 2 +
                     (x["x3"] - 1.0) ** 2])

# Add the quadratic simulation to the problem
# Use a 10 point LH search for ex design and a Gaussian RBF surrogate model
my_moop.addSimulation({'name': "f_conv",
                       'm': 2,
                       'sim_func': quad_sim,
                       'search': LatinHypercube,
                       'surrogate': GaussRBF,
                       'hyperparams': {'search_budget': 10}})

def obj_f1(x, sim, der=0):
    """ Minimize the first output from 'f_conv' """

    if der == 0:
        return sim['f_conv'][0]
    elif der == 1:
        return np.zeros(1, dtype=x.dtype)[0]
    elif der == 2:
        result = np.zeros(1, dtype=sim.dtype)[0]
        result['f_conv'][0] = 1.0
        return result

def obj_f2(x, sim, der=0):
    """ Minimize the second output from 'f_conv' """

    if der == 0:
        return sim['f_conv'][1]
    elif der == 1:
        return np.zeros(1, dtype=x.dtype)[0]
    elif der == 2:
        result = np.zeros(1, dtype=sim.dtype)[0]
        result['f_conv'][1] = 1.0
        return result

# Minimize each of the 2 outputs from the quadratic simulation
my_moop.addObjective({'name': "f1",
                      'obj_func': obj_f1})
my_moop.addObjective({'name': "f2",
                      'obj_func': obj_f2})

def const_x4(x, sim, der=0):
    """ Constrain x["x4"] = 0 """

    if der == 0:
        return 0.0 if (x["x4"] == 0) else 1.0
    elif der == 1:
        # No derivatives for categorical design var, just return all zeros
        return np.zeros(1, dtype=x.dtype)[0]
    elif der == 2:
        return np.zeros(1, dtype=sim.dtype)[0]

# Add the single constraint to the problem
my_moop.addConstraint({'name': "c_x4",
                       'constraint': const_x4})

# Add 2 different acquisition functions to the problem
my_moop.addAcquisition({'acquisition': RandomConstraint})
my_moop.addAcquisition({'acquisition': FixedWeights,
                        # Fixed weight with equal weight on both objectives
                        'hyperparams': {'weights': np.array([0.5, 0.5])}})

# Turn on checkpointing -- creates the files parmoo.moop and parmoo.surrogate.1
my_moop.setCheckpoint(True, checkpoint_data=False, filename="parmoo")

# Turn on logging
import logging
logging.basicConfig(level=logging.INFO,
                    format='%(asctime)s %(levelname)-8s %(message)s',
                    datefmt='%Y-%m-%d %H:%M:%S')

# Solve the problem
my_moop.solve(5)

# Get and print full simulation database
sim_db = my_moop.getSimulationData(format="pandas")
print("Simulation data:")
for key in sim_db.keys():
    print(f"\t{key}:")
    print(sim_db[key])

# Get and print results
soln = my_moop.getPF(format="pandas")
print("\n\n")
print("Solution points:")
print(soln)

The above advanced_ex.py code produces the following output.

Simulation data:
	f_conv:
          x1        x2        x3  x4     out_0     out_1
0   0.164589  0.071519  0.256804   0  0.098153  2.112328
1   0.279173  0.761210  0.446148   2  0.856425  0.883365
2   0.054881  0.143759  0.645615   1  0.440497  1.751987
3   0.761764  0.514335  0.869763   1  1.601312  0.309588
4   0.563992  0.252889  0.383262   1  0.528930  1.128643
5   0.626456  0.302022  0.761693   2  1.063841  0.683499
6   0.835951  0.921038  0.912893   1  2.380498  0.040735
7   0.308713  0.677423  0.189177   0  0.589994  1.239367
8   0.497862  0.843703  0.060276   0  0.963335  1.159652
9   0.967064  0.479916  0.594467   0  1.518922  0.436029
10  0.198616  0.279706  0.253141   0  0.181764  1.718838
11  0.104311  0.713583  0.360565   0  0.650089  1.293170
12  0.990617  0.496152  0.621227   2  1.613413  0.397420
13  0.322478  0.475506  0.173748   1  0.360287  1.416822
14  0.143465  0.160993  0.217433   0  0.093778  2.049997
15  0.317939  0.476173  0.171654   0  0.357290  1.425761
16  0.711117  0.366636  0.640507   0  1.050358  0.613839
17  0.373639  0.410939  0.465101   0  0.524796  1.025438
18  0.193532  0.456200  0.200312   0  0.285698  1.585610
19  0.448389  0.368552  0.567048   0  0.658426  0.890449



Solution points:
         x1        x2        x3  x4        f1        f2  c_x4
0  0.967064  0.479916  0.594467   0  1.518922  0.436029   0.0
1  0.711117  0.366636  0.640507   0  1.050358  0.613839   0.0
2  0.448389  0.368552  0.567048   0  0.658426  0.890449   0.0
3  0.373639  0.410939  0.465101   0  0.524796  1.025438   0.0
4  0.317939  0.476173  0.171654   0  0.357290  1.425761   0.0
5  0.193532  0.456200  0.200312   0  0.285698  1.585610   0.0
6  0.198616  0.279706  0.253141   0  0.181764  1.718838   0.0
7  0.143465  0.160993  0.217433   0  0.093778  2.049997   0.0

Note how in the full simulation database several of the design points violate the constraint (x4 != 0). But in the solution, the constraint is always satisfied.