Source code for openmdao.drivers.pyoptsparse_driver

"""
OpenMDAO Wrapper for pyoptsparse.
pyoptsparse is based on pyOpt, which is an object-oriented framework for
formulating and solving nonlinear constrained optimization problems, with
additional MPI capability. Note: only SNOPT and SLSQP are currently supported.
"""

from __future__ import print_function

import traceback
from six import iteritems
from six.moves import range

import scipy as sp
import numpy as np

from pyoptsparse import Optimization

from openmdao.core.driver import Driver
from openmdao.util.record_util import create_local_meta, update_local_meta
from collections import OrderedDict

# names of optimizers that use gradients
grad_drivers = set(['CONMIN', 'FSQP', 'IPOPT', 'NLPQLP',
                    'PSQP', 'SLSQP', 'SNOPT', 'NLPY_AUGLAG'])

def _check_imports():
    """ Dynamically remove optimizers we don't have
    """

    optlist = ['ALPSO', 'CONMIN', 'FSQP', 'IPOPT', 'NLPQLP',
               'NSGA2', 'PSQP', 'SLSQP', 'SNOPT', 'NLPY_AUGLAG', 'NOMAD']

    for optimizer in optlist[:]:
        try:
            exec('from pyoptsparse import %s' % optimizer)
        except ImportError:
            optlist.remove(optimizer)

    return optlist


[docs]class pyOptSparseDriver(Driver): """ Driver wrapper for pyoptsparse. pyoptsparse is based on pyOpt, which is an object-oriented framework for formulating and solving nonlinear constrained optimization problems, with additional MPI capability. pypptsparse has interfaces to the following optimizers: ALPSO, CONMIN, FSQP, IPOPT, NLPQLP, NSGA2, PSQP, SLSQP, SNOPT, NLPY_AUGLAG, NOMAD. Note that some of these are not open source and therefore not included in the pyoptsparse source code. pyOptSparseDriver supports the following: equality_constraints inequality_constraints two_sided_constraints Options ------- options['exit_flag'] : int(0) 0 for fail, 1 for ok options['optimizer'] : str('SLSQP') Name of optimizers to use options['print_results'] : bool(True) Print pyOpt results if True options['pyopt_diff'] : bool(True) Set to True to let pyOpt calculate the gradient options['title'] : str('Optimization using pyOpt_sparse') Title of this optimization run """ def __init__(self): """Initialize pyopt""" super(pyOptSparseDriver, self).__init__() # What we support self.supports['inequality_constraints'] = True self.supports['equality_constraints'] = True self.supports['multiple_objectives'] = False self.supports['two_sided_constraints'] = True # TODO: Support these self.supports['linear_constraints'] = False self.supports['integer_design_vars'] = False # User Options self.options.add_option('optimizer', 'SLSQP', values=_check_imports(), desc='Name of optimizers to use') self.options.add_option('title', 'Optimization using pyOpt_sparse', desc='Title of this optimization run') self.options.add_option('print_results', True, desc='Print pyOpt results if True') self.options.add_option('pyopt_diff', False, desc='Set to True to let pyOpt calculate the gradient') # The user places optimizer-specific settings in here. self.opt_settings = {} # The user can set a file name here to store history self.hist_file = None self.pyopt_solution = None self.lin_jacs = OrderedDict() self.quantities = [] self.metadata = None self.exit_flag = 0 self._problem = None self.sparsity = OrderedDict() self.sub_sparsity = OrderedDict() def _setup(self): self.supports['gradients'] = self.options['optimizer'] in grad_drivers super(pyOptSparseDriver, self)._setup()
[docs] def run(self, problem): """pyOpt execution. Note that pyOpt controls the execution, and the individual optimizers (i.e., SNOPT) control the iteration. Args ---- problem : `Problem` Our parent `Problem`. """ self.pyopt_solution = None rel = problem.root._probdata.relevance # Metadata Setup self.metadata = create_local_meta(None, self.options['optimizer']) self.iter_count = 0 update_local_meta(self.metadata, (self.iter_count,)) # Initial Run with problem.root._dircontext: problem.root.solve_nonlinear(metadata=self.metadata) opt_prob = Optimization(self.options['title'], self._objfunc) # Add all parameters param_meta = self.get_desvar_metadata() self.indep_list = indep_list = list(param_meta) param_vals = self.get_desvars() for name, meta in iteritems(param_meta): opt_prob.addVarGroup(name, meta['size'], type='c', value=param_vals[name], lower=meta['lower'], upper=meta['upper']) opt_prob.finalizeDesignVariables() # Figure out parameter subsparsity for paramcomp index connections. # sub_param_conns is empty unless there are some index conns. # full_param_conns gets filled with the connections to the entire # parameter so that those params can be filtered out of the sparse # set if the full path is also relevant sub_param_conns = {} full_param_conns = {} for name in indep_list: pathname = problem.root.unknowns.metadata(name)['pathname'] sub_param_conns[name] = {} full_param_conns[name] = set() for target, info in iteritems(problem.root.connections): src, indices = info if src == pathname: if indices is not None: # Need to map the connection indices onto the desvar # indices if both are declared. dv_idx = param_meta[name].get('indices') indices = set(indices) if dv_idx is not None: indices.intersection_update(dv_idx) ldv_idx = list(dv_idx) mapped_idx = [ldv_idx.index(item) for item in indices] sub_param_conns[name][target] = mapped_idx else: sub_param_conns[name][target] = indices else: full_param_conns[name].add(target) # Add all objectives objs = self.get_objectives() self.quantities = list(objs) self.sparsity = OrderedDict() self.sub_sparsity = OrderedDict() for name in objs: opt_prob.addObj(name) self.sparsity[name] = self.indep_list # Calculate and save gradient for any linear constraints. lcons = self.get_constraints(lintype='linear').keys() if len(lcons) > 0: self.lin_jacs = problem.calc_gradient(indep_list, lcons, return_format='dict') #print("Linear Gradient") #print(self.lin_jacs) # Add all equality constraints econs = self.get_constraints(ctype='eq', lintype='nonlinear') con_meta = self.get_constraint_metadata() self.quantities += list(econs) for name in self.get_constraints(ctype='eq'): meta = con_meta[name] size = meta['size'] lower = upper = meta['equals'] # Sparsify Jacobian via relevance rels = rel.relevant[name] wrt = rels.intersection(indep_list) self.sparsity[name] = wrt if meta['linear']: opt_prob.addConGroup(name, size, lower=lower, upper=upper, linear=True, wrt=wrt, jac=self.lin_jacs[name]) else: jac = self._build_sparse(name, wrt, size, param_vals, sub_param_conns, full_param_conns, rels) opt_prob.addConGroup(name, size, lower=lower, upper=upper, wrt=wrt, jac=jac) # Add all inequality constraints incons = self.get_constraints(ctype='ineq', lintype='nonlinear') self.quantities += list(incons) for name in self.get_constraints(ctype='ineq'): meta = con_meta[name] size = meta['size'] # Bounds - double sided is supported lower = meta['lower'] upper = meta['upper'] # Sparsify Jacobian via relevance rels = rel.relevant[name] wrt = rels.intersection(indep_list) self.sparsity[name] = wrt if meta['linear']: opt_prob.addConGroup(name, size, upper=upper, lower=lower, linear=True, wrt=wrt, jac=self.lin_jacs[name]) else: jac = self._build_sparse(name, wrt, size, param_vals, sub_param_conns, full_param_conns, rels) opt_prob.addConGroup(name, size, upper=upper, lower=lower, wrt=wrt, jac=jac) # Instantiate the requested optimizer optimizer = self.options['optimizer'] try: exec('from pyoptsparse import %s' % optimizer) except ImportError: msg = "Optimizer %s is not available in this installation." % \ optimizer raise ImportError(msg) optname = vars()[optimizer] opt = optname() #Set optimization options for option, value in self.opt_settings.items(): opt.setOption(option, value) self._problem = problem # Execute the optimization problem if self.options['pyopt_diff']: # Use pyOpt's internal finite difference fd_step = problem.root.fd_options['step_size'] sol = opt(opt_prob, sens='FD', sensStep=fd_step, storeHistory=self.hist_file) else: # Use OpenMDAO's differentiator for the gradient sol = opt(opt_prob, sens=self._gradfunc, storeHistory=self.hist_file) self._problem = None # Print results if self.options['print_results']: print(sol) # Pull optimal parameters back into framework and re-run, so that # framework is left in the right final state dv_dict = sol.getDVs() for name in indep_list: val = dv_dict[name] self.set_desvar(name, val) with self.root._dircontext: self.root.solve_nonlinear(metadata=self.metadata) # Save the most recent solution. self.pyopt_solution = sol try: exit_status = sol.optInform['value'] self.exit_flag = 1 if exit_status > 2: # bad self.exit_flag = 0 except KeyError: #nothing is here, so something bad happened! self.exit_flag = 0
def _build_sparse(self, name, wrt, consize, param_vals, sub_param_conns, full_param_conns, rels): """ Build up the data structures that define a sparse Jacobian matrix. Called separately on each nonlinear constraint. Args ---- name : str Constraint name. wrt : list List of relevant param names. consize : int Width of this constraint. param_vals : dict Dictionary of parameter values; used for sizing. sub_param_conns : dict Parameter subindex connection info. full_param_conns : dict Parameter full connection info. rels : set Set of relevant nodes for this connstraint. Returns ------- pyoptsparse coo matrix or None """ jac = None # Additional sparsity for index connections for param in wrt: sub_conns = sub_param_conns.get(param) if not sub_conns: continue # If we have a simultaneous full connection, then we move on full_conns = full_param_conns.get(param) if full_conns.intersection(rels): continue rel_idx = set() for target, idx in iteritems(sub_conns): # If a target of the indexed desvar connection is # in the relevant path for this constraint, then # those indices are relevant. if target in rels: rel_idx.update(idx) nrel = len(rel_idx) if nrel > 0: if jac is None: jac = {} if param not in jac: # A coo matrix for the Jacobian # mat = {'coo':[row, col, data], # 'shape':[nrow, ncols]} coo = {} coo['shape'] = [consize, len(param_vals[param])] jac[param] = coo row = [] col = [] for i in range(consize): row.extend([i]*nrel) col.extend(rel_idx) data = np.ones((len(row), )) jac[param]['coo'] = [np.array(row), np.array(col), data] if name not in self.sub_sparsity: self.sub_sparsity[name] = {} self.sub_sparsity[name][param] = np.array(list(rel_idx)) return jac def _objfunc(self, dv_dict): """ Function that evaluates and returns the objective function and constraints. This function is passed to pyOpt's Optimization object and is called from its optimizers. Args ---- dv_dict : dict Dictionary of design variable values. Returns ------- func_dict : dict Dictionary of all functional variables evaluated at design point. fail : int 0 for successful function evaluation 1 for unsuccessful function evaluation """ fail = 1 metadata = self.metadata system = self.root try: for name in self.indep_list: self.set_desvar(name, dv_dict[name]) # Execute the model #print("Setting DV") #print(dv_dict) self.iter_count += 1 update_local_meta(metadata, (self.iter_count,)) with self.root._dircontext: system.solve_nonlinear(metadata=metadata) func_dict = self.get_objectives() # this returns a new OrderedDict func_dict.update(self.get_constraints()) # Record after getting obj and constraint to assure they have # been gathered in MPI. self.recorders.record_iteration(system, metadata) # Get the double-sided constraint evaluations #for key, con in iteritems(self.get_2sided_constraints()): # func_dict[name] = np.array(con.evaluate(self.parent)) fail = 0 except Exception as msg: tb = traceback.format_exc() # Exceptions seem to be swallowed by the C code, so this # should give the user more info than the dreaded "segfault" print("Exception: %s" % str(msg)) print(70*"=",tb,70*"=") fail = 1 func_dict = {} #print("Functions calculated") #print(func_dict) return func_dict, fail def _gradfunc(self, dv_dict, func_dict): """ Function that evaluates and returns the gradient of the objective function and constraints. This function is passed to pyOpt's Optimization object and is called from its optimizers. Args ---- dv_dict : dict Dictionary of design variable values. func_dict : dict Dictionary of all functional variables evaluated at design point. Returns ------- sens_dict : dict Dictionary of dictionaries for gradient of each dv/func pair fail : int 0 for successful function evaluation 1 for unsuccessful function evaluation """ fail = 1 try: sens_dict = self.calc_gradient(dv_dict, self.quantities, return_format='dict', sparsity=self.sparsity) # Support for sub-index sparsity by returning the Jacobian in a # pyopt sparse format. for con, val1 in iteritems(self.sub_sparsity): for desvar, rel_idx in iteritems(val1): coo = {} jac = sens_dict[con][desvar] nrow, ncol = jac.shape coo['shape'] = [nrow, ncol] row = [] col = [] data = [] ncol = len(rel_idx) for i in range(nrow): row.extend([i]*ncol) col.extend(rel_idx) data.extend(jac[i][rel_idx]) coo['coo'] = [np.array(row), np.array(col), np.array(data)] sens_dict[con][desvar] = coo fail = 0 except Exception as msg: tb = traceback.format_exc() # Exceptions seem to be swallowed by the C code, so this # should give the user more info than the dreaded "segfault" print("Exception: %s" % str(msg)) print(70*"=",tb,70*"=") sens_dict = {} #print("Derivatives calculated") #print(dv_dict) #print(sens_dict) return sens_dict, fail