"""
pySNOPT - A variation of the pySNOPT wrapper specifically designed to
work with sparse optimization problems.
"""
# Standard Python modules
import datetime
import os
import re
import sys
import time
from typing import Any, Dict, Optional, Tuple
# External modules
from baseclasses.utils import CaseInsensitiveSet, writePickle
import numpy as np
from numpy import ndarray
from pkg_resources import parse_version
# Local modules
from ..pyOpt_error import Error
from ..pyOpt_optimization import Optimization
from ..pyOpt_optimizer import Optimizer
from ..pyOpt_utils import (
ICOL,
IDATA,
INFINITY,
IROW,
extractRows,
mapToCSC,
scaleRows,
try_import_compiled_module_from_path,
)
# import the compiled module
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
_IMPORT_SNOPT_FROM = os.environ.get("PYOPTSPARSE_IMPORT_SNOPT_FROM", THIS_DIR)
snopt = try_import_compiled_module_from_path("snopt", _IMPORT_SNOPT_FROM)
[docs]
class SNOPT(Optimizer):
"""
SNOPT Optimizer Class
"""
def __init__(self, raiseError=True, options: Dict = {}):
name = "SNOPT"
category = "Local Optimizer"
defOpts = self._getDefaultOptions()
# these are SNOPT-related options that do not get set via snset
self.specialOptions = CaseInsensitiveSet(
{
"iPrint",
"iSumm",
"Start",
}
)
# this is purely within pySNOPT, nothing to do with SNOPT itself
self.pythonOptions = CaseInsensitiveSet(
{
"Save major iteration variables",
"Return work arrays",
"Work arrays save file",
"snSTOP function handle",
"snSTOP arguments",
}
)
informs = self._getInforms()
if isinstance(snopt, str):
if raiseError:
raise ImportError(snopt)
else:
version = None
else:
# extract SNOPT version
version_str = snopt.sntitle().decode("utf-8")
# The version_str is going to look like
# S N O P T 7.7.5 (Oct 2020)
# we search between "S N O P T" and "("
res = re.search(r"S N O P T(.*)\(", version_str)
if res is not None:
version = res.group(1).strip()
else:
version = None
super().__init__(
name,
category,
defaultOptions=defOpts,
informs=informs,
options=options,
checkDefaultOptions=False,
version=version,
)
# SNOPT need Jacobians in csc format
self.jacType = "csc"
# SNOPT specific Jacobian map
self._snopt_jac_map_csr_to_csc: Optional[Tuple[ndarray, ndarray, ndarray]] = None
@staticmethod
def _getDefaultOptions() -> Dict[str, Any]:
defOpts = {
"iPrint": [int, 18],
"iSumm": [int, 19],
"Print file": [str, "SNOPT_print.out"],
"Summary file": [str, "SNOPT_summary.out"],
"Minor print level": [int, 0],
"Problem Type": [str, ["Minimize", "Maximize", "Feasible point"]],
"Start": [str, ["Cold", "Hot"]],
"Derivative level": [int, 3],
"Iterations limit": [int, 10000000],
"Minor iterations limit": [int, 10000],
"Proximal iterations limit": [int, 10000],
"Total character workspace": [int, None],
"Total integer workspace": [int, None],
"Total real workspace": [int, None],
"Save major iteration variables": [list, []],
"Return work arrays": [bool, False],
"Work arrays save file": [(type(None), str), None],
"snSTOP function handle": [(type(None), type(lambda: None)), None],
"snSTOP arguments": [list, []],
}
return defOpts
@staticmethod
def _getInforms() -> Dict[int, str]:
# INFO exit codes for SNOPT 7.7
informs = {
0: "finished successfully",
1: "optimality conditions satisfied",
2: "feasible point found",
3: "requested accuracy could not be achieved",
5: "elastic objective minimized",
6: "elastic infeasibilities minimized",
10: "the problem appears to be infeasible",
11: "infeasible linear constraints",
12: "infeasible linear equalities",
13: "nonlinear infeasibilities minimized",
14: "infeasibilities minimized",
15: "infeasible linear constraints in QP subproblem",
16: "infeasible nonelastic constraints",
20: "the problem appears to be unbounded",
21: "unbounded objective",
22: "constraint violation limit reached",
30: "resource limit error",
31: "iteration limit reached",
32: "major iteration limit reached",
33: "the superbasics limit is too small",
34: "time limit reached",
40: "terminated after numerical difficulties",
41: "current point cannot be improved",
42: "singular basis",
43: "cannot satisfy the general constraints",
44: "ill-conditioned null-space basis",
45: "unable to compute acceptable LU factors",
50: "error in the user-supplied functions",
51: "incorrect objective derivatives",
52: "incorrect constraint derivatives",
56: "irregular or badly scaled problem functions",
60: "undefined user-supplied functions",
61: "undefined function at the first feasible point",
62: "undefined function at the initial point",
63: "unable to proceed into undefined region",
70: "user requested termination",
71: "terminated during function evaluation",
74: "terminated from monitor routine",
80: "insufficient storage allocated",
81: "work arrays must have at least 500 elements",
82: "not enough character storage",
83: "not enough integer storage",
84: "not enough real storage",
90: "input arguments out of range",
91: "invalid input argument",
92: "basis file dimensions do not match this problem",
140: "system error",
141: "wrong no of basic variables",
142: "error in basis package",
}
return informs
[docs]
def __call__(
self,
optProb: Optimization,
sens=None,
sensStep=None,
sensMode=None,
storeHistory=None,
hotStart=None,
storeSens=True,
timeLimit=None,
restartDict=None,
):
"""
This is the main routine used to solve the optimization
problem.
Parameters
----------
optProb : Optimization or Solution class instance
This is the complete description of the optimization problem
to be solved by the optimizer
sens : str or python Function.
Specify method to compute sensitivities. The default is
None which will use SNOPT's own finite differences which
are vastly superior to the pyOptSparse implementation. To
explicitly use pyOptSparse gradient class to do the
derivatives with finite differences use `FD`. `sens`
may also be `CS` which will cause pyOptSpare to compute
the derivatives using the complex step method. Finally,
`sens` may be a python function handle which is expected
to compute the sensitivities directly. For expensive
function evaluations and/or problems with large numbers of
design variables this is the preferred method.
sensStep : float
Set the step size to use for design variables. Defaults to
``1e-6`` when sens is `FD` and ``1e-40j`` when sens is `CS`.
sensMode : str
Use `pgc` for parallel gradient computations. Only
available with mpi4py and each objective evaluation is
otherwise serial
storeHistory : str
File name of the history file into which the history of
this optimization will be stored
hotStart : str
File name of the history file to "replay" for the
optimization. The optimization problem used to generate
the history file specified in `hotStart` must be
**IDENTICAL** to the currently supplied `optProb`. By
identical we mean, **EVERY SINGLE PARAMETER MUST BE
IDENTICAL**. As soon as he requested evaluation point
from SNOPT does not match the history, function and
gradient evaluations revert back to normal evaluations.
storeSens : bool
Flag specifying if sensitivities are to be stored in hist.
This is necessary for hot-starting only.
timeLimit : float
Specify the maximum amount of time for optimizer to run.
Must be in seconds. This can be useful on queue systems when
you want an optimization to cleanly finish before the
job runs out of time. From SNOPT 7.7, use the "Time limit" option instead.
restartDict : dict
A dictionary containing the necessary information for hot-starting SNOPT.
This is typically the same dictionary returned by this function on a previous invocation.
Returns
-------
sol : Solution object
The optimization solution
restartDict : dict
If `Return work arrays` is True, a dictionary of arrays is also returned
"""
self.startTime = time.time()
self.callCounter = 0
self.storeSens = storeSens
# flush the output streams
sys.stdout.flush()
sys.stderr.flush()
# Store the starting time if the keyword timeLimit is given:
self.timeLimit = timeLimit
if len(optProb.constraints) == 0:
# If the user *actually* has an unconstrained problem,
# snopt sort of chokes with that....it has to have at
# least one constraint. So we will add one
# automatically here:
self.unconstrained = True
optProb.dummyConstraint = True
self.optProb = optProb
self.optProb.finalize()
# Set history/hotstart
self._setHistory(storeHistory, hotStart)
self._setInitialCacheValues()
self._setSens(sens, sensStep, sensMode)
blx, bux, xs = self._assembleContinuousVariables()
oneSided = False
# Set the number of nonlinear constraints snopt *thinks* we have:
if self.unconstrained:
nnCon = 1
else:
indices, tmp1, tmp2, fact = self.optProb.getOrdering(["ne", "ni"], oneSided=oneSided)
nnCon = len(indices)
self.optProb.jacIndices = indices
self.optProb.fact = fact
self.optProb.offset = np.zeros_like(fact)
# Again, make SNOPT think we have a nonlinear constraint when all
# our constraints are linear
if nnCon == 0:
nnCon = 1
self.optProb.jacIndices = [0]
self.optProb.fact = np.array([1.0])
self.optProb.offset = np.zeros_like(self.optProb.fact)
# Make sure restartDict is provided if using hot start
if self.getOption("Start") == "Hot" and restartDict is None:
raise Error("restartDict must be provided if using a hot start")
# If user requested the work arrays, then we need to set sticky parameter
# to make sure that the work arrays are re-usable at the next hot start
if self.getOption("Return work arrays"):
self.setOption("Sticky parameters", "Yes")
sol = None
# We make a split here: If the rank is zero we setup the
# problem and run SNOPT, otherwise we go to the waiting loop:
if self.optProb.comm.rank == 0:
# Determine the sparsity structure of the full Jacobian
# -----------------------------------------------------
# Gather dummy data and process Jacobian:
gcon = {}
for iCon in self.optProb.constraints:
gcon[iCon] = self.optProb.constraints[iCon].jac
jac = self.optProb.processConstraintJacobian(gcon)
if self.optProb.nCon > 0:
# We need to reorder this full Jacobian...so get ordering:
indices, blc, buc, fact = self.optProb.getOrdering(["ne", "ni", "le", "li"], oneSided=oneSided)
jac = extractRows(jac, indices) # Does reordering
scaleRows(jac, fact) # Perform logical scaling
else:
blc = [-INFINITY]
buc = [INFINITY]
if self._snopt_jac_map_csr_to_csc is None:
self._snopt_jac_map_csr_to_csc = mapToCSC(jac)
# # CSC data is the csr data with the csc_indexing applied
Acol = jac["csr"][IDATA][self._snopt_jac_map_csr_to_csc[IDATA]]
# # CSC Row indices are just the row indices information from the map
indA = self._snopt_jac_map_csr_to_csc[IROW] + 1
# # CSC Column pointers are the column information from the map
locA = self._snopt_jac_map_csr_to_csc[ICOL] + 1
if self.optProb.nCon == 0:
ncon = 1
else:
ncon = len(indices)
# Initialize the Print and Summary files
# --------------------------------------
iPrint = self.getOption("iPrint")
PrintFile = os.path.join(self.getOption("Print file"))
if iPrint != 0 and iPrint != 6:
ierror = snopt.openunit(iPrint, PrintFile, "replace", "sequential")
if ierror != 0:
raise Error(f"Failed to properly open {PrintFile}, ierror = {ierror:3}")
iSumm = self.getOption("iSumm")
SummFile = os.path.join(self.getOption("Summary file"))
if iSumm != 0 and iSumm != 6:
ierror = snopt.openunit(iSumm, SummFile, "replace", "sequential")
if ierror != 0:
raise Error(f"Failed to properly open {SummFile}, ierror = {ierror:3}")
# Calculate the length of the work arrays
# ---------------------------------------
nvar = self.optProb.ndvs
lencw = self.getOption("Total character workspace")
leniw = self.getOption("Total integer workspace")
lenrw = self.getOption("Total real workspace")
# Set flags to avoid overwriting user-specified lengths
checkLencw = lencw is None
checkLeniw = leniw is None
checkLenrw = lenrw is None
# Set defaults
minWorkArrayLength = 500
if lencw is None:
lencw = minWorkArrayLength
self.setOption("Total character workspace", lencw)
if leniw is None:
leniw = minWorkArrayLength + 100 * (ncon + nvar)
self.setOption("Total integer workspace", leniw)
if lenrw is None:
lenrw = minWorkArrayLength + 200 * (ncon + nvar)
self.setOption("Total real workspace", lenrw)
cw = np.empty((lencw, 8), dtype="|S1")
cw[:] = " "
iw = np.zeros(leniw, np.intc)
rw = np.zeros(lenrw, float)
snopt.sninit(iPrint, iSumm, cw, iw, rw)
# Memory allocation
nnObj = nvar
nnJac = nvar
iObj = np.array(0, np.intc)
neA = len(indA)
neGcon = neA # The nonlinear Jacobian and A are the same
iExit = 0
# set the work arrays
if restartDict is not None:
rw = restartDict["rw"]
iw = restartDict["iw"]
cw = restartDict["cw"]
# Set the options into the SNOPT instance
self._set_snopt_options(iPrint, iSumm, cw, iw, rw)
# Estimate workspace storage requirement
mincw, miniw, minrw, cw = snopt.snmemb(iExit, ncon, nvar, neA, neGcon, nnCon, nnJac, nnObj, cw, iw, rw)
# This flag is set to True if any of the lengths are overwritten
lengthsChanged = False
# Overwrite lengths if the defaults are too small
if checkLencw and mincw > lencw:
lencw = mincw
self.setOption("Total character workspace", lencw)
cw = np.empty((lencw, 8), dtype="|S1")
cw[:] = " "
lengthsChanged = True
if checkLeniw and miniw > leniw:
leniw = miniw
self.setOption("Total integer workspace", leniw)
iw = np.zeros(leniw, np.intc)
lengthsChanged = True
if checkLenrw and minrw > lenrw:
lenrw = minrw
self.setOption("Total real workspace", lenrw)
rw = np.zeros(lenrw, float)
lengthsChanged = True
# Initialize SNOPT again if any of the lengths were overwritten
if lengthsChanged:
snopt.sninit(iPrint, iSumm, cw, iw, rw)
# snInit resets all the options to the defaults.
# Set them again! this includes the work arrays
if restartDict is not None:
rw = restartDict["rw"]
iw = restartDict["iw"]
cw = restartDict["cw"]
self._set_snopt_options(iPrint, iSumm, cw, iw, rw)
# Setup argument list values
start = np.array(self.getOption("Start"))
ObjAdd = np.array(0.0, float)
ProbNm = np.array(self.optProb.name, "c")
xs = np.concatenate((xs, np.zeros(ncon, float)))
bl = np.concatenate((blx, blc))
bu = np.concatenate((bux, buc))
leniu = 2
lenru = 3
cu = np.empty((1, 8), dtype="|S1")
iu = np.zeros(leniu, np.intc)
ru = np.zeros(lenru, float)
hs = np.zeros(nvar + ncon, np.intc)
pi = np.zeros(ncon, float)
Names = np.array([" "], "c")
if restartDict is not None:
# update the states with the information in the restartDict
hs = restartDict["hs"]
xs = restartDict["xs"]
pi = restartDict["pi"]
# The snopt c interface
timeA = time.time()
# fmt: off
hs, xs, pi, rc, inform, mincw, miniw, minrw, nS, ninf, sinf, obj = snopt.snkerc(
start, nnCon, nnObj, nnJac, iObj, ObjAdd, ProbNm,
self._userfg_wrap, snopt.snlog, snopt.snlog2, snopt.sqlog, self._snstop,
Acol, indA, locA, bl, bu, Names, hs, xs, pi, cu, iu, ru, cw, iw, rw,
)
# fmt: on
optTime = time.time() - timeA
# Indicate solution finished
self.optProb.comm.bcast(-1, root=0)
if self.storeHistory:
# Record the full state of variables, xs and hs such
# that we could perform a warm start.
self.hist.writeData("xs", xs)
self.hist.writeData("hs", hs)
self.metadata["endTime"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.metadata["optTime"] = optTime
self.hist.writeData("metadata", self.metadata)
self.hist.close()
if iPrint != 0 and iPrint != 6:
snopt.closeunit(self.getOption("iPrint"))
if iSumm != 0 and iSumm != 6:
snopt.closeunit(self.getOption("iSumm"))
# Store Results
sol_inform = {}
sol_inform["value"] = inform
sol_inform["text"] = self.informs[inform]
# Create the optimization solution
if parse_version(self.version) > parse_version("7.7.0") and parse_version(self.version) < parse_version(
"7.7.7"
):
# SNOPT obj value is buggy and returned as 0, its thus overwritten with the solution objective value
obj = np.array([obj.value * obj.scale for obj in self.optProb.objectives.values()])
sol = self._createSolution(optTime, sol_inform, obj, xs[:nvar], multipliers=pi)
restartDict = {
"cw": cw,
"iw": iw,
"rw": rw,
"xs": xs,
"hs": hs,
"pi": pi,
}
self._on_flushFiles()
else: # We are not on the root process so go into waiting loop:
self._waitLoop()
restartDict = None
# Communication solution and return
commSol = self._communicateSolution(sol)
if self.getOption("Return work arrays"):
restartDict = self.optProb.comm.bcast(restartDict, root=0)
return commSol, restartDict
else:
return commSol
def _userfg_wrap(self, mode, nnJac, x, fobj, gobj, fcon, gcon, nState, cu, iu, ru):
"""
The snopt user function. This is what is actually called from snopt.
Essentially nothing is done in this function, but this funcion
has to precisely match the signature from fortran so must look
EXACTLY like this.
All we do here is call the generic masterFunc in the baseclass
which will take care of everything else.
"""
# nState >=2 means this is the final call which is redundant
# here we just return without doing anything since we don't
# need to do any cleanup or anything
if nState >= 2:
return
fail = 0
if mode == 0 or mode == 2:
fobj, fcon, fail = self._masterFunc(x, ["fobj", "fcon"])
if fail == 0:
if mode == 1:
if self.getOption("Derivative level") != 0:
gobj, gcon, fail = self._masterFunc(x, ["gobj", "gcon"])
if mode == 2:
if self.getOption("Derivative level") != 0:
gobj, gcon, fail2 = self._masterFunc(x, ["gobj", "gcon"])
fail = max(fail, fail2)
if fail == 1:
mode = -1
elif fail == 2:
mode = -2
self._on_flushFiles()
# Check if we've exceeded the timeLimit
if self.timeLimit is not None:
if time.time() - self.startTime > self.timeLimit:
mode = -2 # User requested termination
return mode, fobj, gobj, fcon, gcon
def _getHessian(self, iw, rw):
"""
This function retrieves the approximate Hessian from the SNOPT workspace arrays
Call it for example from the _snstop routine or after SNOPT has finished, where iw and rw arrays are available
Currently only full memory Hessian mode is implemented, do not use this for limited-memory case.
The FM Hessian in SNOPT is stored with its Cholesky factor
which has been flattened to 1D
"""
lvlHes = iw[72 - 1] # 0,1,2 => LM, FM, Exact Hessian
if lvlHes != 1:
print("pyOptSparse Error! Limited-memory Hessian not supported for history file!")
return None
lU = iw[391 - 1] - 1 # U(lenU), BFGS Hessian H = U'U
lenU = iw[392 - 1]
Uvec = rw[lU : lU + lenU]
nnH = iw[24 - 1]
Umat = np.zeros((nnH, nnH))
Umat[np.triu_indices(nnH)] = Uvec
H = np.matmul(Umat.T, Umat)
return H
def _getPenaltyVector(self, iw, rw):
"""
Retrieves the full penalty parameter vector from the work arrays.
"""
nnCon = iw[23 - 1]
lxPen = iw[304 - 1] - 1
xPen = rw[lxPen : lxPen + nnCon]
return xPen
# fmt: off
def _snstop(self, ktcond, mjrprtlvl, minimize, n, nncon, nnobj, ns, itn, nmajor, nminor, nswap, condzhz, iobj,
scaleobj, objadd, fobj, fmerit, penparm, step, primalinf, dualinf, maxvi, maxvirel, hs, locj, indj,
jcol, scales, bl, bu, fx, fcon, gcon, gobj, ycon, pi, rc, rg, x, cu, iu, ru, cw, iw, rw):
# fmt: on
"""
This routine is called every major iteration in SNOPT, after solving QP but before line search
We use it to determine the correct major iteration counting, and save some parameters in the history file.
If `snSTOP function handle` is set to a function handle, then it is called at the end of this function.
Returns
-------
iabort : int
The return code expected by SNOPT. A non-zero value will terminate SNOPT immediately.
"""
iterDict = {
"isMajor": True,
"nMajor": nmajor,
"nMinor": nminor,
"step": step,
"feasibility": primalinf,
"optimality": dualinf,
"merit": fmerit,
"condZHZ": condzhz,
"penalty": penparm[2],
}
for saveVar in self.getOption("Save major iteration variables"):
if saveVar == "penalty_vector":
iterDict[saveVar] = self._getPenaltyVector(iw, rw),
elif saveVar == "Hessian":
H = self._getHessian(iw, rw)
iterDict[saveVar] = H
elif saveVar == "slack":
iterDict[saveVar] = x[n:]
elif saveVar == "lambda":
iterDict[saveVar] = pi
elif saveVar == "nS":
iterDict[saveVar] = ns
elif saveVar == "BSwap":
iterDict[saveVar] = nswap
elif saveVar == "maxVi":
iterDict[saveVar] = maxvi
else:
raise Error(f"Received unknown SNOPT save variable {saveVar}. "
+ "Please see 'Save major iteration variables' option in the pyOptSparse documentation "
+ "under 'SNOPT'.")
if self.storeHistory:
currX = x[:n] # only the first n component is x, the rest are the slacks
if nmajor == 0:
callCounter = 0
else:
xuser_vec = self.optProb._mapXtoUser(currX)
callCounter = self.hist._searchCallCounter(xuser_vec)
if callCounter is not None:
self.hist.write(callCounter, iterDict)
# this adds funcs etc. to the iterDict by fetching it from the history file
iterDict = self.hist.read(callCounter)
# update funcs with any additional entries that may be added
if "funcs" in self.cache.keys():
iterDict["funcs"].update(self.cache["funcs"])
# Create the restart dictionary to be passed to snstop_handle
restartDict = {
"cw": cw,
"iw": iw,
"rw": rw,
"xs": x, # x is the same as xs; we call it x here to be consistent with the SNOPT subroutine snSTOP
"hs": hs,
"pi": pi,
}
workArraysSave = self.getOption("Work arrays save file")
if workArraysSave is not None:
# Save the restart dictionary
writePickle(workArraysSave, restartDict)
# perform callback if requested
snstop_handle = self.getOption("snSTOP function handle")
if snstop_handle is not None:
# Get the arguments to pass in to snstop_handle
# iterDict is always included
snstopArgs = [iterDict]
for snstopArg in self.getOption("snSTOP arguments"):
if snstopArg == "restartDict":
snstopArgs.append(restartDict)
else:
raise Error(f"Received unknown snSTOP argument {snstopArg}. "
+ "Please see 'snSTOP arguments' option in the pyOptSparse documentation "
+ "under 'SNOPT'.")
if not self.storeHistory:
raise Error("snSTOP function handle must be used with storeHistory=True")
iabort = snstop_handle(*snstopArgs)
# write iterDict again if anything was inserted
if self.storeHistory and callCounter is not None:
self.hist.write(callCounter, iterDict)
# if no return, assume everything went fine
if iabort is None:
iabort = 0
else:
iabort = 0
return iabort
def _set_snopt_options(self, iPrint: int, iSumm: int, cw: ndarray, iw: ndarray, rw: ndarray):
"""
Set all the options into SNOPT that have been assigned
by the user
"""
# Set Options from the local options dictionary
# ---------------------------------------------
inform = np.array(-1, np.intc)
for name, value in self.options.items():
# these do not get set using snset
if name in self.specialOptions or name in self.pythonOptions:
continue
if isinstance(value, str):
if name == "Problem Type":
snopt.snset(value, iPrint, iSumm, inform, cw, iw, rw)
elif name == "Print file":
snopt.snset(f"{name} {iPrint}", iPrint, iSumm, inform, cw, iw, rw)
elif name == "Summary file":
snopt.snset(f"{name} {iSumm}", iPrint, iSumm, inform, cw, iw, rw)
else:
snopt.snset(f"{name} {value}", iPrint, iSumm, inform, cw, iw, rw)
elif isinstance(value, float):
snopt.snsetr(name, value, iPrint, iSumm, inform, cw, iw, rw)
elif isinstance(value, int):
snopt.snseti(name, value, iPrint, iSumm, inform, cw, iw, rw)
elif isinstance(value, type(None)):
snopt.snset(name, iPrint, iSumm, inform, cw, iw, rw)
def _on_flushFiles(self):
"""
Flush the Output Files (Optimizer Specific Routine)
"""
#
iPrint = self.getOption("iPrint")
iSumm = self.getOption("iSumm")
if iPrint != 0:
snopt.pyflush(iPrint)
if iSumm != 0:
snopt.pyflush(iSumm)