Write a ParMOO Script

The MOOP class

The MOOP class is the fundamental data structure in ParMOO.

To create an instance of the MOOP class, use the constructor.

from parmoo import MOOP
moop = MOOP(optimizer, hyperparams=hp)

In the above code snippet, optimizer should be an implementation of the SurrogateOptimizer Abstract-Base-Class (ABC), and the optional input hp is a dictionary of hyperparameters for the optimizer object. The optimizer is the surrogate optimization problem solver that will be used to generate candidate solutions for the MOOP. The choice of surrogate optimizer determines what information will be required when defining each objective and constraint.

  • If you use a derivative-free technique, such as GlobalSurrogate_PS, then you do not need to provide derivative information for your objective or constraint functions.

  • If you use a derivative-based technique, such as GlobalSurrogate_BFGS, then you need to provide an additional input to your objectives and constraint functions, which can be set to evaluate their derivatives with respect to design inputs and simulation outputs.

As of version 0.4.0: to fix the random seed, ParMOO no longer uses the global numpy random seed. Instead, pass an integer or numpy.random.Generator object using the key hp["np_random_gen"] when creating the MOOP. It will automatically be passed on to all subclasses and components.

To avoid issues, it is best to define your MOOP in the following order, but as of version 0.4.0, this is no longer a requirement.

  1. Add design variables using MOOP.addDesign(*args).

  2. Add simulations using MOOP.addSimulation(*args).

  3. Add objectives using MOOP.addObjective(*args).

  4. Add constraints using MOOP.addConstraint(*args).

  5. Add acquisitions using MOOP.addAcquisition(*args).

All of these methods accept one or more args, each of which is a dictionary, as detailed in the corresponding sections below.

The name Key and ParMOO Input/Output Types

Each of the design, simulation, objective, and constraint dictionaries may contain an optional name key. When omitted, the name of the design variables, simulations, objectives, and constraints default to {x|sim|f|c}i, where x is a design variable, sim is for a simulation, f is for an objective, c is for a constraint, and i=1,2,... is determined by the order in which each was added.

For example, if you add 3 simulations, then they will automatically be named sim1, sim2, and sim3 unless a different name, was specified for one or more by including the name key. Similarly, design variables are named x1, x2, …; objectives are named f1, f2, …; and constraints are named c1, c2, ….

Use of a repeated name will result in an error.

The inputs to any user-defined functions will be passed as Python dictionaries with keys corresponding to the name keys used above.

After solving, ParMOO formats its output in a numpy structured array, using the given or automatically assigned name keys to specify the name for each field.

As of version 0.4.0, this operation mode is required and all other naming conventions are no longer supported.

After adding all design variables, simulations, objectives, and constraints to the MOOP, you can check the numpy dtype for each of these by using


import numpy as np
from parmoo import MOOP
from parmoo.searches import LatinHypercube
from parmoo.surrogates import GaussRBF
from parmoo.optimizers import GlobalSurrogate_PS

my_moop = MOOP(GlobalSurrogate_PS)

# 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 result is the following.

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

Working with Unnamed Outputs [obsolete]

In older versions of ParMOO, it was possible to omit the name keys and use numpy.ndarray structures as inputs/outputs to ParMOO.

In an effort to simplify workflow, improve maintainability, and optimize iteration times when working with jax, this feature has been removed and all ParMOO scripts must use named inputs/outputs as of version 0.4.0.

Adding Design Variables

Design variables are added to your MOOP object using the addDesign(*args) method. ParMOO currently supports several types of design variables:

  • continuous (or real or cont),

  • integer (or int),

  • categorical (or cat),

  • custom,

  • raw – not recommended, for advanced users only.

To add a continuous variable, use the following format.

# Add a continuous design variable
moop.addDesign({'name': "MyContVar", # optional
                'des_type': "continuous",
                'lb': 0.0,
                'ub': 1.0,
                'des_tol': 1.0e-8})

  • Note that when the des_type key is omitted, its value defaults to continuous.

  • For continuous design variables, both a lower (lb) and upper (ub) bound must be specified. These bounds are hard constraints, meaning that no simulations or objectives will be evaluated outside of these bounds.

  • The optional key des_tol specifies a minimum step size between values for this design variable (default value is \(10^{-8}\)). For this design variable, any two values that are closer than des_tol will be treated as exactly equal.

To add an integer design variable, use the following format.

# Add an integer design variable
moop.addDesign({'name': "MyIntVar", # optional
                'des_type': "integer",
                'lb': 0,
                'ub': 100})

  • The lb and ub keys must be integer-valued, and serve the same purpose as with continuous design variables.

To add a categorical design variable, use the following format.

# Add a categorical design variable
moop.addDesign({'name': "MyCatVar", # optional
                'des_type': "categorical",
                'levels': 3})

  • The levels key is either an integer specifying the number of categories taken on by this design variable (ParMOO will index these levels by \(0, 1, \ldots, \text{levels}-1\)) or a list of level IDs specifying the ID for each category (ParMOO will use these names for the levels, e.g., ["first cat", "second cat", ... ] or [-1, 0, 1]).

Note because jax cannot jit functions with string-valued inputs and outputs, as of version 0.4.0 it is strongly recommended to only use integer-valued level names when specifying the levels key using the list syntax. While it is still possible to specify string-valued category IDs, doing so will cause jax.jit(...) to fail in several places, which may ultimately increase iteration times by up to 10x.

To add a custom design variable, use the following format.

# Add a custom design variable
moop.addDesign({'name': "MyCustomVar", # optional
                'des_type': "custom",
                'embedder': my_embedding_func,
                })

  • The embedder key should be an instance of the Embedder class, defining a user-provided embedding. Warning: if jax cannot jit the embed and extract methods for this class, ParMOO’s iterations may become extremely slow.

To add a raw design variable, use the following format. Please note that raw design variables are not recommended, and one will typically need to write custom search, surrogate, optimizer, and acquisition functions/classes to accommodate a raw variable. This feature is only included to allow flexibility for expert users.

# Add a raw design variable
moop.addDesign({'name': "MyRawVar", # optional
                'des_type': "raw"})

Note that for every MOOP, at least one design variable is required before solving.

Adding Simulations

Before you can add a simulation to your MOOP, you must define the simulation function.

The simulation function can be either a Python function or a callable object whose __call__ method matches the signature below.

The simulation should take a single Python dictionary as input, whose keys match the design variable names. The simulation function returns a numpy.ndarray (or array-like object) containing the simulation output(s).

For example, with three design variables named x1, x2, and x3, you might define the quadratic \({\bf S}({\bf x}) = \|{\bf x}\|^2\) as follows.

def quadratic_sim(x):
    return np.array([x["x1"] ** 2 + x["x2"] ** 2 + x["x3"] ** 2])

To add your simulation to the MOOP object, use the addSimulation(*args) method.

from parmoo.searches import LatinHypercube
from parmoo.surrogates import GaussRBF

moop.addSimulation({'name': "MySim", # optional
                    'm': 1, # number of outputs
                    'sim_func': quadratic_sim, # simulation function
                    'search': LatinHypercube, # search technique
                    'surrogate': GaussRBF, # surrogate model
                    'hyperparams': {'search_budget': 20}})
In the above example,
  • name is used as described in the section on name key;

  • m specifies the number of outputs for this simulation;

  • sim_func is given a reference to the simulation function;

  • search specifies the GlobalSearch that you will use when generating data for this particular simulation;

  • surrogate specifies the class of SurrogateFunction that you will use to model this particular simulation’s output;

  • hyperparams is a dictionary of hyperparameter values that will be passed to the surrogate and search technique objects. One particularly important key in the hyperparams dictionary is the search_budget key, which specifies how many simulation evaluations should be used during the initial search phase.

If you wish, you may create a MOOP without any simulations.

Adding Objectives

Objectives are algebraic functions of your design variables and simulation outputs. ParMOO always minimizes objectives. If you would like to maximize instead, re-define the problem by minimizing the negative-value of your objective. We provide a library of common built-in objectives, which can do this automatically.

Just like with simulation functions, ParMOO accepts either a Python function or a callable object for each objective. Make sure you match the expected signature, which depends on your choice of name keys.

In particular, your objective function should accept two Python dictionaries and return a single scalar output. The following objective minimizes the sum of all outputs of the simulation output named MySim.

def min_sim(x, sim):
    return sum(sim["MySim"])

Similarly, the following objective minimizes the squared value of the design variable named MyDes.

def min_des(x, sim):
    return x["MyDes"] ** 2

Note: The following has been changed as of version 0.4.0 in order to offer better support for jax.jit() compilation.

If you are using a gradient-based SurrogateOptimizer then you are required to supply an additional function for evaluating the gradient of your objective with respect to both x and sim inputs.

Note that for categorical variables ParMOO does not use the partial derivatives given here, and it is acceptable to fill these keys with a garbage value or leave them uninitialized.

Modifying the above two objectives to support derivative-based solvers, we get the following.

def min_sim_grad(x, sim):
    dx = {}
    ds = {}
    for key in x:
        dx[key] = 0.0
    for key in sim:
        ds[key] = np.zeros(sim[key].size)
    ds["MySim"] = 1.0
    return dx, ds

def min_des_grad(x, sim):
    for key in x:
        dx[key] = 0.0
    for key in sim:
        ds[key] = np.zeros(sim[key].size)
    dx["MyDes"] = 2.0 * x["MyDes"]
    return dx, ds

For a full example showing how to solve a MOOP using a derivative-based solver, see Solving a MOOP with Derivative-Based Solvers in Basic Tutorials.

To add the objective(s), use the MOOP.addObjective(*args) method.

moop.addObjective({'name': "Min MySim",
                   'obj_func': min_sim,
                   # below is only needed for gradient-based solvers
                   'obj_grad': min_sim_grad
                   })

moop.addObjective({'name': "Min MyDes",
                   'obj_func': min_des,
                   # below is only needed for gradient-based solvers
                   'obj_grad': min_des_grad
                   })

Note that for every MOOP, at least one objective is required before solving.

Adding Constraints

Adding constraints is similar to adding objectives. The main difference is in how ParMOO treats constraint functions. Although ParMOO may evaluate infeasible design points along the way, ParMOO will search for solutions where all constraints are less than or equal to zero.

For example, to add the constraint that the simulation MySim must have output greater than or equal to 0 and that the design variable MyDes must be less than or equal to 0.9, you would define the following constraint functions.

def sim_constraint(x, sim):
    return -1.0 * sim["MySim"]

def des_constraint(x, sim):
    return x["MyDes"] - 0.9

As with objectives, if you want to use a gradient-based SurrogateOptimizer then you must modify the above constraint functions as follows.

def sim_constraint_grad(x, sim):
    dx, ds = {}, {}
    for key in x:
        dx[key] = 0.0
    for key in sim:
        ds[key] = np.zeros(sim[key].size)
    ds["MySim"] = -1.0
    return dx, ds

def des_constraint_grad(x, sim):
    dx, ds = {}, {}
    for key in x:
        dx[key] = 0.0
    for key in sim:
        ds[key] = np.zeros(sim[key].size)
    dx["MyDes"] = 1.0
    return dx, ds

To add the constraint(s), use the MOOP.addConstraint(*args) method.

moop.addConstraint({'name': "Constrain MySim",
                    'con_func': sim_constraint,
                    # below is only needed for gradient-based solvers
                    'con_grad': sim_constraint_grad
                    })

moop.addConstraint({'name': "Constrain MyDes",
                    'con_func': des_constraint,
                    # below is only needed for gradient-based solvers
                    'con_grad': des_constraint_grad
                    })

You are not required to add any constraints of this form to your MOOP before solving.

Adding Acquisitions

After you have added all of the design variables, simulations, objectives, and constraints to your MOOP, you must add one or more acquisitions using the MOOP.addAcquisition(*args) method.

from parmoo.acquisitions import RandomConstraint, FixedWeights

moop.addAcquisition({'acquisition': RandomConstraint})
moop.addAcquisition({'acquisition': FixedWeights,
                     'hyperparams': {'weights': np.array([0.5, 0.5])}})
The acquisition dictionary may contain two keys:
  • acquisition (required) specifies one AcquisitionFunction that you would like to use for this problem; and

  • hyperparams (optional) specifies a dictionary of hyperparameter values that are used by the specified AcquisitionFunction.

The number of acquisitions added determines the batch size for each of ParMOO’s batches of simulation evaluations (which could be done in parallel). In general, if there are q acquisition functions and s simulations, then ParMOO will generate batches of q*s simulations. In other words, each simulation is evaluated once per acquisition function in each iteration of ParMOO’s algorithm.

Using a Precomputed Simulation Database

If you would like to specify a precomputed database, use the MOOP.updateSimDb(x, sx, s_name) method to add all simulation data into ParMOO’s database after creating your MOOP but before solving. Be careful not to add duplicate points, because these could cause numerical issues when fitting surrogate models.

Before doing so, you must first call the MOOP.compile() method to “finalize” the definition of your MOOP. This can only be done once, so be sure that you are done defining the MOOP before doing so. If you turn on logging first (see below), ParMOO will attempt to jit all user-defined functions at this stage and log a warning to alert users if any methods failed to compile at this stage. Failure to compile does not prevent ParMOO from running, but may increase iteration times by 10x.

Note that the MOOP.compile() command is run automatically when calling MOOP.solve() (below), so you only needed to compile manually when adding precomputed simulation evaluations.


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 GlobalSurrogate_PS

my_moop = MOOP(GlobalSurrogate_PS)

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

# This step is needed to finalize the MOOP definition. If you are using the
# solve command it is done automatically, but it must be done manually before
# any pre-existing data can be added.
my_moop.compile()

# 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.updateSimDb(des_val, sim_val, "MySim")

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

The output of the above code is shown below.

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

Logging and Checkpointing

When solving large or expensive problems, it is often a good idea to activate ParMOO’s logging and/or checkpointing features.

Logging

For diagnostics, ParMOO logs its progress at the logging.INFO level. To display these log messages, turn on Python’s INFO-level logging.

import logging
logging.basicConfig(level=logging.INFO)

If you would like to also print a formatted timestamp, use the Python logger’s built-in formatting options.

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

Be aware that when using ParMOO together with libEnsemble, libE already comes with its own logging tools, which are recommended, and ParMOO’s logging tools will not work.

Checkpointing

A ParMOO can be run with checkpointing turned on, so that your MOOP can be paused and resumed later, and your simulation data can be recovered after a crash. Checkpointing is off by default. To turn it on, use the method:

moop.setCheckpoint(True, checkpoint_data=True, filename="parmoo")

The first argument tells ParMOO to save its internal class attributes and databases, so that they can be reloaded in the future. In the above example, this save data will be written to a file in the calling directory, with the name parmoo.moop.

In order to save the problem definition, ParMOO needs to store information for reloading all of your functions. For this to work:

  • All functions (such as simulation functions, objective functions, and constraint functions) are defined in the global scope;

  • All modules are reloaded before attempting to recover a previously-saved MOOP object (by calling the load(filename) method);

  • ParMOO cannot reload lambda functions. Use only regular functions and callable objects when checkpointing.

When checkpointins is active, ParMOO will also save a copy of all simulation evaluations in a human-readable JSON file in the same directory, with the name parmoo.simdb.json.

Reloading After Crash or Early Stop

After a crash or early termination, reload the saved .moop file to resume. Make sure that you first import any external modules and redefine any functions that are needed by ParMOO (with the exact same signatures).

from parmoo import MOOP
from optimizers import [optimizer]

# Create a new MOOP object
moop = MOOP([optimizer])
# Reload the old problem
moop.load(filename="parmoo") # Use your saved file name, omitting ".moop"

Then resume your solve with an increased budget.

# Resume solve with increased budget
moop.solve(6)

Example

The example below shows how the Quickstart demo can be modified to use logging and checkpointing, including an example of how to load a MOOP from a saved checkpoint file and resume running.


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 GlobalSurrogate_PS
import logging

# Create a new MOOP -- fix the random seed with the hyperparams
my_moop = MOOP(GlobalSurrogate_PS, hyperparams={'np_random_gen': 0})

# 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, 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(GlobalSurrogate_PS)
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 result is the following.

[(0.78651355, 0, 0.34399814, 1.81884372e-04, -0.68651355)
 (0.72175941, 0, 0.27223289, 6.12158940e-03, -0.62175941)
 (0.71746254, 0, 0.26776748, 6.81243257e-03, -0.61746254)
 (0.71560707, 0, 0.26585065, 7.12216670e-03, -0.61560707)
 (0.71433754, 0, 0.2645431 , 7.33805733e-03, -0.61433754)
 (0.71033363, 0, 0.26044042, 8.04005753e-03, -0.61033363)
 (0.66769687, 0, 0.21874036, 1.75041176e-02, -0.56769687)
 (0.62648593, 0, 0.18189025, 3.01071308e-02, -0.52648593)
 (0.55285312, 0, 0.12450533, 6.10815791e-02, -0.45285312)
 (0.52941562, 0, 0.10851465, 7.32159054e-02, -0.42941562)
 (0.44156652, 0, 0.05835439, 1.28474557e-01, -0.34156652)
 (0.43785559, 0, 0.05657528, 1.31148576e-01, -0.33785559)
 (0.39723059, 0, 0.0388999 , 1.62223200e-01, -0.29723059)
 (0.34928137, 0, 0.02228493, 2.03147285e-01, -0.24928137)
 (0.32389074, 0, 0.01534892, 2.26680025e-01, -0.22389074)
 (0.30592199, 0, 0.01121947, 2.44113077e-01, -0.20592199)
 (0.29342199, 0, 0.00872767, 2.56621277e-01, -0.19342199)
 (0.28407396, 0, 0.00706843, 2.66179683e-01, -0.18407396)
 (0.24208177, 0, 0.00177088, 3.11272754e-01, -0.14208177)
 (0.22899583, 0, 0.00084076, 3.26045762e-01, -0.12899583)] 
 dtype=[('x1', '<f8'), ('x2', '<i4'), ('f1', '<f8'), ('f2', '<f8'), ('c1', '<f8')]

Methods for Solving

Once you have finished creating your MOOP object and adding all design variables, simulations, objectives, constraints, and acquisitions, you are ready to solve your problem.

The easiest way to solve is by using MOOP.solve(k). Here, k is the number of iterations of ParMOO’s algorithm that you would like to perform. Note that a value of k=0 is legal, and will result in ParMOO generating and evaluating an experimental design and fitting its surrogates, without ever attempting to solve a single scalarized surrogate problems.

# Evaluate an experimental design, then performing 5 iterations
moop.solve(5)

Note that the above command will perform all simulation evaluations serially. To generate a batch of simulations that you could evaluate in parallel, use MOOP.iterate(k), where k is the iteration index. You can let ParMOO handle the simulation evaluations with MOOP.evaluateSimulation(x, s_name), or you can evaluate the simulations yourself and add them to the simulation database using MOOP.updateSimDb(x, sx, s_name). Afterward, call MOOP.updateAll(k, batch) to update the surrogate models and objective database.

# Do 5 iterations letting ParMOO handle simulation evaluation
# Note that the i=0 iteration will just generate an experimental design
for i in range(5):
    # Get batch
    batch = moop.iterate(i)
    # Let ParMOO evaluate design point x for simulation s_name
    for (x, s_name) in batch:
        moop.evaluateSimulation(x, s_name)
    # Update ParMOO models
    moop.updateAll(i, batch)

or

# Solve another MOOP, doing simulation evaluation manually
for i in range(5):
    # Get batch
    batch = moop.iterate(i)
    # User evaluates design point x for simulation s_name
    for (x, s_name) in batch:
        ### User code to evaluate x with sim["s_name"] goes HERE ###
        ### Store results in variable sx ###
        moop.updateSimDb(x, sx, s_name)
    # Update ParMOO models
    moop.updateAll(i, batch)

Additional ParMOO solver execution paradigms (including those where ParMOO will handle parallel execution on the user’s behalf) are included under Additional ParMOO Plugins and Features.

Viewing Your Results

After solving the MOOP, you can view the results using MOOP.getPF().

soln = moop.getPF()

The output format defaults to a numpy structured array. However, you can change it to a pandas dataframe using the optional format argument.

soln = moop.getPF(format="pandas")

To get the full simulation and objective databases, you can also use MOOP.getSimulationData() and MOOP.getObjectiveData().

sim_db = moop.getSimulationData()
obj_db = moop.getObjectiveData()

To understand the format of these outputs, please revisit the section on The name Key and ParMOO Output Types.

Finally, if you have installed ParMOO with its extra dependencies (see the Advanced Installation), then you can visualize your results using any of the viz.scatter(), viz.parallel_coordinates(), or viz.radar() functions.

from parmoo.viz import scatter

scatter(moop)

Note that these plots are interactive and will render in a Dash app hosted locally on your computer. There are known issues when using the Chrome browser.

For more information, view the complete viz API page.

Built-in and Custom Components

By now you can see that the performance of ParMOO is determined by your choices of

You can find the current options for each of these in the following modules.

You can also create your own custom implementations for each of the above, by implementing the abstract base classes.