Parallel Solvers using the libE_MOOP Class

Solve MOOPs in parallel using ParMOO together with libEnsemble.

from parmoo.extras import libE_MOOP

The libE_MOOP.solve(...) method will distribute simulations across parallel resources using libEnsemble for this class.

Contains the libE_MOOP class and parmoo_persis_gen function.

Use the libE_MOOP class to define and solve multiobjective optimization problems (MOOPs) with parallel simulation evaluations. The libE_MOOP class extends the base class parmoo.moop.MOOP for defining and solving MOOPs.

The parmoo_persis_gen function can be used as a generator function in libEnsemble. To do so, create a regular parmoo.MOOP object and add it to the gen_specs dict, then import and use parmoo_persis_gen as the libE gen func.

class extras.libe.libE_MOOP(opt_func, hyperparams=None)

Class for solving a MOOP using libEnsemble to manage parallelism.

Upon initialization, supply a scalar optimization procedure and dictionary of hyperparameters using the default constructor:

  • moop = libE_MOOP.__init__(ScalarOpt, [hyperparams={}])

New: To fix the random seed, use the hyperparameter key “np_random_gen” and set either an int or numpy.random.Generator instance as the corresponding value.

In addition to other hyperparameters used by the base MOOP class (such as “np_random_gen”), a libE_MOOP uses the reserved hyperparameter key sim_dirs_make, which can be set to either True or False. When unset, it defaults to False. When set to True, libEnsemble will create a separate work directory for each sim and the sim will automatically run from inside this private workspace. The work directories will be created inside the ensemble subdirectory with the naming convention sim{SID}_worker{WID}, where SID and WID refer to the simulation and worker IDs, respectively.

Class methods are summarized below.

To define the MOOP, add each design variable, simulation, objective, and constraint by using the following functions:

  • libE_MOOP.addDesign(*args)

  • libE_MOOP.addSimulation(*args)

  • libE_MOOP.addObjective(*args)

  • libE_MOOP.addConstraint(*args)

Next, define your solver.

Acquisition functions (used for scalarizing problems/setting targets) are added using:

  • libE_MOOP.addAcquisition(*args)

When you are done defining a MOOP, it can be “compiled” to finalize the definition:

  • libE_MOOP.compile()

After creating a MOOP, the following methods may be useful for getting the numpy.dtype of the input/output arrays:

  • libE_MOOP.getDesignType()

  • libE_MOOP.getSimulationType()

  • libE_MOOP.getObjectiveType()

  • libE_MOOP.getConstraintType()

To turn on checkpointing use:
  • libE_MOOP.setCheckpoint(checkpoint, [filename="parmoo"])

ParMOO’s logging feature is not active for the libE_MOOP class since libEnsemble already provides this feature.

If there is any pre-existing simulation data, it can be added by calling the following method, where (x, sx) are the design, output pair for the simulation “s_name”:

  • libE_MOOP.updateSimDb(x, sx, s_name)

After defining the MOOP and setting up checkpointing and logging info, use the following method to solve the MOOP (serially):

  • libE_MOOP.solve(iter_max=None, sim_max=None)

The following methods are used for solving the MOOP and managing the internal simulation/objective databases:

  • libE_MOOP.checkSimDb(x, s_name)

  • libE_MOOP.evaluateSimulation(x, s_name)

  • libE_MOOP.addObjData(x, sx)

  • libE_MOOP.iterate(k, ib=None)

  • libE_MOOP.filterBatch(*args)

  • libE_MOOP.updateAll(k, batch)

Finally, the following methods are used to retrieve data after the problem has been solved:

  • libE_MOOP.getPF(format='ndarray')

  • libE_MOOP.getSimulationData(format='ndarray')

  • libE_MOOP.getObjectiveData(format='ndarray')

The following methods are used to save/load the current checkpoint (state):
  • libE_MOOP.save([filename="parmoo"])

  • libE_MOOP.load([filename="parmoo"])

Other private methods from the MOOP class are not accessible by a libE_MOOP.

__init__(opt_func, hyperparams=None)

Initializer for the libE interface to the MOOP class.

Parameters:
  • opt_func (SurrogateOptimizer) – A solver for the surrogate problems.

  • hyperparams (dict, optional) – A dictionary of hyperparameters for the opt_func, and any other procedures that will be used.

Returns:

A new libE_MOOP object with no design variables,

objectives, or constraints.

Return type:

libE_MOOP

addDesign(*args)

Add a new design variables to the libE_MOOP.

Parameters:

args (dict) –

Each argument is a dictionary representing one design variable. The dictionary contains information about that design variable, including:

  • ’name’ (str, optional): The unique name of this design variable, which ultimately serves as its primary key in all of ParMOO’s databases. This is also how users should index this variable in all user-defined functions passed to ParMOO. If left blank, it defaults to “xi” where i= 1, 2, 3,… corresponds to the order in which the design variables were added.

  • ’des_type’ (str): The type for this design variable. Currently supported options are:

    • ’continuous’ (or ‘cont’ or ‘real’)

    • ’categorical’ (or ‘cat’)

    • ’integer’ (or ‘int’)

    • ’custom’ – an Embedder class must be provided (below)

    • ’raw’ – no re-scaling is performed: NOT RECOMMENDED

  • ’lb’ (float): When des_type is ‘continuous’, ‘integer’, or ‘raw’ this specifies the lower bound for the range of values this design variable could take. This value must be specified, and must be strictly less than ‘ub’ (below) up to the tolerance (below).

  • ’ub’ (float): When des_type is ‘continuous’, ‘integer’, or ‘raw’ this specifies the upper bound for the range of values this design variable could take. This value must be specified, and must be strictly greater than ‘lb’ (above) up to the tolerance (below) or by a whole numer for integer variables.

  • ’des_tol’ (float): When des_type is ‘continuous’, this specifies the tolerance, i.e., the minimum spacing along this dimension, before two design values are considered to have equal values in this dimension. If not specified, the default value is epsilon * max(ub - lb, 1.0e-4).

  • ’levels’ (int or list): When des_type is ‘categorical’, this specifies the number of levels for the variable (when int) or the names of each valid category (when a list). WARNING: If a list is given and the entries in the list do not have numeric types, then ParMOO will not be able to jit the extractor which will lead to seriously degraded performance.

  • ’embedder’ (parmoo.structs.Embedder): When des_type is ‘custom’, this is a custom Embedder class, which maps the input to a point in the unit hypercube and reports the embedded dimension.

addSimulation(*args)

Add new simulations to the libE_MOOP.

Append new simulation functions to the problem.

Parameters:

args (dict) –

Each argument is a dictionary representing one simulation function. The dictionary must contain information about that simulation function, including:

  • name (str, optional): The name of this simulation (defaults to “sim” + str(i), where i = 1, 2, 3, … for the first, second, third, … simulation added to the MOOP).

  • m (int): The number of outputs for this simulation.

  • sim_func (function): An implementation of the simulation function, mapping from R^n -> R^m. The interface should match: sim_out = sim_func(x).

  • search (GlobalSearch): A GlobalSearch object for performing the initial search over this simulation’s design space.

  • surrogate (SurrogateFunction): A SurrogateFunction object specifying how this simulation’s outputs will be modeled.

  • hyperparams (dict): A dictionary of hyperparameters, which will be passed to the surrogate and search routines. Most notably, ‘search_budget’: (int) can be specified here.

addObjective(*args)

Add a new objective to the libE_MOOP.

Append a new objective to the problem. The objective must be an algebraic function of the design variables and simulation outputs. Note that all objectives must be specified before any acquisition functions can be added.

Parameters:

*args (dict) –

Python dictionary containing objective function information, including:

  • ’name’ (str, optional): The name of this objective (defaults to “obj” + str(i), where i = 1, 2, 3, … for the first, second, third, … simulation added to the MOOP).

  • ’obj_func’ (function): An algebraic objective function that maps from X, S –> R, where X is the design space and S is the space of simulation outputs. Interface should match: cost = obj_func(x, sx) where the value sx is given by sx = sim_func(x) at runtime.

  • ’obj_grad’ (function): Evaluates the gradients of obj_func wrt s and sx. Interface should match: dx, ds = obj_grad(x, sx) where the value sx is given by sx = sim_func(x) at runtime. The outputs dx and ds represent the gradients with respect to x and sx, respectively.

addConstraint(*args)

Add a new constraint to the libE_MOOP. :param args: Python dictionary containing constraint function

information, including:
  • ‘name’ (str, optional): The name of this constraint (defaults to “const” + str(i), where i = 1, 2, 3, … for the first, second, third, … constraint added to the MOOP).

  • ‘con_func’ or ‘constraint’ (function): An algebraic constraint function that maps from X, S –> R where X and S are the design space and space of aggregated simulation outputs, respectively. The constraint function should evaluate to zero or a negative number when feasible and positive otherwise. The interface should match: violation = con_func(x, sx) where the value sx is given by sx = sim_func(x) at runtime. Note that any constraint(x, sim_func(x), der=0) <= 0 indicates that x is feaseible, while constraint(x, sim_func(x), der=0) > 0 indicates that x is infeasible, violating the constraint by an amount proportional to the output. It is the user’s responsibility to ensure that after adding all constraints, the feasible region is nonempty and has nonzero measure in the design space.

  • ‘con_grad’ (function): Evaluates the gradients of con_func wrt s and sx. Interface should match: dx, ds = con_grad(x, sx) where the value sx is given by sx = sim_func(x) at runtime. The outputs dx and ds represent the gradients with respect to x and sx, respectively.

addAcquisition(*args)

Add an acquisition function to the libE_MOOP.

Parameters:

args (dict) –

Python dictionary of acquisition function info, including:

  • ’acquisition’ (AcquisitionFunction): An acquisition function that maps from R^o –> R for scalarizing outputs.

  • ’hyperparams’ (dict): A dictionary of hyperparameters for the acquisition functions. Can be omitted if no hyperparameters are needed.

compile()

Compile the MOOP object and initialize its components.

This locks the MOOP definition and jits all jit-able methods.

This must be done before adding any simulation or objective data to the internal database.

This cannot be done after simulation or objective data has been added to the internal database.

setCheckpoint(checkpoint, filename='parmoo')

Set ParMOO’s checkpointing feature.

Note that for checkpointing to work, all simulation, objective, and constraint functions must be defined in the global scope. ParMOO also cannot save lambda functions.

Parameters:
  • checkpoint (bool) – Turn checkpointing on (True) or off (False).

  • filename (str, optional) – Set the base checkpoint filename/path. The checkpoint file will have the JSON format and the extension “.moop” appended to the end of filename. Additional checkpoint files may be created with the same filename but different extensions, depending on the choice of AcquisitionFunction, SurrogateFunction, and GlobalSearch. When omitted, this parameter defaults to “parmoo” and is saved inside current working directory.

getDesignType()

Get the numpy dtype of all design points for this MOOP.

Returns:

The numpy dtype of this MOOP’s design points. If no design variables have yet been added, returns None.

Return type:

dtype

getSimulationType()

Get the numpy dtypes of the simulation outputs for this MOOP.

Returns:

The numpy dtype of this MOOP’s simulation outputs. If no simulations have been given, returns None.

Return type:

dtype

getObjectiveType()

Get the numpy dtype of an objective point for this MOOP.

Returns:

The numpy dtype of this MOOP’s objective points. If no objectives have yet been added, returns None.

Return type:

dtype

getConstraintType()

Get the numpy dtype of the constraint violations for this MOOP.

Returns:

The numpy dtype of this MOOP’s constraint violation output. If no constraints have been given, returns None.

Return type:

dtype

checkSimDb(x, s_name)

Check self.sim_db[s_name] to see if the design x was evaluated.

Parameters:
  • x (dict) – A Python dictionary specifying the keys/names and corresponding values of a design point to search for.

  • s_name (str) – The name of the simulation whose database will be searched.

Returns:

returns None if x is not in self.sim_db[s_name] (up to the design tolerance). Otherwise, returns the corresponding value of sx.

Return type:

None or numpy.ndarray

updateSimDb(x, sx, s_name)

Update sim_db[s_name] by adding a design/simulation output pair.

Parameters:
  • x (dict) – A Python dictionary specifying the keys/names and corresponding values of a design point to add.

  • sx (ndarray) – A 1D array containing the corresponding simulation output(s).

  • s_name (str) – The name of the simulation to whose database the pair (x, sx) will be added into.

evaluateSimulation(x, s_name)

Evaluate sim_func[s_name] and store the result in the database.

Parameters:
  • x (dict) – A Python dictionary with keys/names corresponding to the design variable names given and values containing the corresponding values of the design point to evaluate.

  • s_name (str) – The name of the simulation to evaluate.

Returns:

A 1D array containing the output from the evaluation sx = simulation[s_name](x).

Return type:

ndarray

addObjData(x, sx)

Update the internal objective database by truly evaluating x.

Parameters:
  • x (dict) – A Python dictionary containing the value of the design variable to add to ParMOO’s database.

  • sx (dict) – A Python dictionary containing the values of the corresponding simulation outputs for ALL simulations involved in this MOOP – sx[‘s_name’][:] contains the output(s) for sim_func[‘s_name’].

iterate(k, ib=None)

Perform an iteration of ParMOO’s solver and generate candidates.

Generates a batch of suggested candidate points (design points) or (candidate point, simulation name) pairs and returns to the user for further processing. Note, this method may produce duplicates.

Parameters:
  • k (int) – The iteration counter (corresponding to MOOP.iteration).

  • ib (int, optional) – The index of the acquisition function to optimize and add to the current batch. Defaults to None, which optimizes all acquisition functions and adds all resulting candidates to the batch.

Returns:

A list of tuples (design points, simulation name) specifying the unfiltered list of candidates that ParMOO recommends for true simulation evaluations. Specifically:

  • The first entry in each tuple is a Python dictionary specifying the design point to evaluate.

  • The second entry in the tuple is the (str) name of the simulation to evaluate at the design point specified above.

Return type:

(list)

filterBatch(*args)

Filter a batch produced by ParMOO’s MOOP.iterate method.

Accepts one or more batches of candidate design points, produced by the MOOP.iterate() method and checks both the batch and ParMOO’s database for redundancies. Any redundant points (up to the design tolerance) are replaced by model improving points, using each surrogate’s Surrogate.improve() method.

Parameters:
  • *args (list of tuples) – The list of unfiltered candidates

  • method. (returned by the MOOP.iterate())

Returns:

A filtered list of tuples, matching the format of the MOOP.iterate() output, but with redundant points removed and suitably replaced.

Return type:

(list)

updateAll(k, batch)

Update all surrogates given a batch of freshly evaluated data.

Parameters:
  • k (int) – The iteration counter (corresponding to MOOP.iteration).

  • batch (list) – A list of ordered pairs (tuples), each specifying a design point that was evaluated in this iteration, whose format matches the output of MOOP.iterate().

solve(iter_max=None, sim_max=None, wt_max=864000, profile=False)

Solve a MOOP using ParMOO + libEnsemble.

If desired, be sure to turn on checkpointing before starting the solve, using:

MOOP.setCheckpoint(checkpoint, [filename="parmoo"])

ParMOO will solve the MOOP and use libEnsemble to distribute simulations over available resources.

Parameters:
  • iter_max (int) – The max number of ParMOO iterations to be performed by libEnsemble (default is unlimited).

  • sim_max (int) – The max number of simulation to be performed by libEnsemble (default is unlimited).

  • wt_max (int) – The max number of seconds that the simulation may run for (the default is 864000 secs, i.e., 10 days).

  • profile (bool) – Specifies whether to run libE with the profiler.

getPF(format='ndarray')

Extract nondominated and efficient sets from internal databases.

Parameters:

format (str, optional) – Either ‘ndarray’ (default) or ‘pandas’, in order to produce output as a numpy structured array or pandas dataframe. Note: format=’pandas’ is only valid for named inputs.

Returns:

Either a structured array or dataframe (depending on the option selected above) whose column/key names match the names of the design variables, objectives, and constraints. It contains a discrete approximation of the Pareto front and efficient set.

Return type:

numpy structured array or pandas DataFrame

getSimulationData(format='ndarray')

Extract all computed simulation outputs from the MOOP’s database.

Parameters:

format (str, optional) – Either ‘ndarray’ (default) or ‘pandas’, in order to produce output as a numpy structured array or pandas dataframe. Note: format=’pandas’ is only valid for named inputs.

Returns:

A Python dictionary whose keys match the names of the simulations. Each value is either a numpy structured array or pandas dataframe (depending on the option selected above) whose column/key names match the names of the design variables plus either and ‘out’ field for single-output simulations, or ‘out_1’, ‘out_2’, … for multi-output simulations.

Return type:

dict

getObjectiveData(format='ndarray')

Extract all computed objective scores from this MOOP’s database.

Parameters:

format (str, optional) – Either ‘ndarray’ (default) or ‘pandas’, in order to produce output as a numpy structured array or pandas dataframe. Note: format=’pandas’ is only valid for named inputs.

Returns:

Either a structured array or dataframe (depending on the option selected above) whose column/key names match the names of the design variables, objectives, and constraints. It contains the results for every fully evaluated design point.

Return type:

numpy structured array or pandas DataFrame

save(filename='parmoo')

Serialize and save the MOOP object and all of its dependencies.

Parameters:

filename (str, optional) – The filepath to serialized checkpointing file(s). Do not include file extensions, they will be appended automatically. May create several save files with extensions of this name, in order to recursively save dependencies objects. Defaults to the value “parmoo” (filename will be “parmoo.moop”).

load(filename='parmoo')

Load a serialized MOOP object and all of its dependencies.

Parameters:

filename (str, optional) – The filepath to the serialized checkpointing file(s). Do not include file extensions, they will be appended automatically. This method may also load from other saved files with the same name, but different file extensions, in order to recursively load dependency objects (such as surrogate models) as needed. Defaults to the value “parmoo” (filename will be “parmoo.moop”).

savedata(x, sx, s_name, filename='parmoo')

Save the current simulation database for this MOOP.

Parameters:

filename (str, optional) – The filepath to the checkpointing file(s). Do not include file extensions, they will be appended automatically. Defaults to the value “parmoo” (filename will be “parmoo.simdb.json”).