From bee20ffa2a9ed8072ffe8dec038e7cf17b56d450 Mon Sep 17 00:00:00 2001 From: Neil Wu <602725+nwu63@users.noreply.github.com> Date: Mon, 17 Oct 2022 21:07:41 -0400 Subject: [PATCH 01/44] Add enum for optimizers (#309) * minor string fixes * added enum as an option for OPT Co-authored-by: Marco Mangano <36549388+marcomangano@users.noreply.github.com> --- pyoptsparse/__init__.py | 3 +-- pyoptsparse/pyOpt_optimizer.py | 38 ++++++++++++++++++++-------------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/pyoptsparse/__init__.py b/pyoptsparse/__init__.py index 667a6b53..9961aa37 100644 --- a/pyoptsparse/__init__.py +++ b/pyoptsparse/__init__.py @@ -6,8 +6,7 @@ from .pyOpt_constraint import Constraint from .pyOpt_objective import Objective from .pyOpt_optimization import Optimization -from .pyOpt_optimizer import Optimizer -from .pyOpt_optimizer import OPT +from .pyOpt_optimizer import Optimizer, OPT, Optimizers # Now import all the individual optimizers from .pySNOPT.pySNOPT import SNOPT diff --git a/pyoptsparse/pyOpt_optimizer.py b/pyoptsparse/pyOpt_optimizer.py index 825104b6..8e312a71 100644 --- a/pyoptsparse/pyOpt_optimizer.py +++ b/pyoptsparse/pyOpt_optimizer.py @@ -2,6 +2,7 @@ from collections import OrderedDict import copy import datetime +from enum import Enum import os import shutil import tempfile @@ -923,6 +924,9 @@ def getInform(self, infocode=None): # Generic OPT Constructor # ============================================================================= +# List of optimizers as an enum +Optimizers = Enum("Optimizers", "SNOPT IPOPT SLSQP NLPQLP CONMIN NSGA2 PSQP ALPSO ParOpt") + def OPT(optName, *args, **kwargs): """ @@ -933,8 +937,9 @@ def OPT(optName, *args, **kwargs): Parameters ---------- - optName : str - String identifying the optimizer to create + optName : str or enum + Either a string identifying the optimizer to create, e.g. "SNOPT", or + an enum accessed via ``pyoptsparse.Optimizers``, e.g. ``Optimizers.SNOPT``. *args, **kwargs : varies Passed to optimizer creation. @@ -944,31 +949,32 @@ def OPT(optName, *args, **kwargs): opt : pyOpt_optimizer inherited optimizer The desired optimizer """ - - optName = optName.lower() - optList = ["snopt", "ipopt", "slsqp", "nlpqlp", "conmin", "nsga2", "psqp", "alpso", "paropt"] - if optName == "snopt": + if isinstance(optName, str): + optName = optName.lower() + if optName == "snopt" or optName == Optimizers.SNOPT: from .pySNOPT.pySNOPT import SNOPT as opt - elif optName == "ipopt": + elif optName == "ipopt" or optName == Optimizers.IPOPT: from .pyIPOPT.pyIPOPT import IPOPT as opt - elif optName == "slsqp": + elif optName == "slsqp" or optName == Optimizers.SLSQP: from .pySLSQP.pySLSQP import SLSQP as opt - elif optName == "nlpqlp": + elif optName == "nlpqlp" or optName == Optimizers.NLPQLP: from .pyNLPQLP.pyNLPQLP import NLPQLP as opt - elif optName == "psqp": + elif optName == "psqp" or optName == Optimizers.PSQP: from .pyPSQP.pyPSQP import PSQP as opt - elif optName == "conmin": + elif optName == "conmin" or optName == Optimizers.CONMIN: from .pyCONMIN.pyCONMIN import CONMIN as opt - elif optName == "nsga2": + elif optName == "nsga2" or optName == Optimizers.NSGA2: from .pyNSGA2.pyNSGA2 import NSGA2 as opt - elif optName == "alpso": + elif optName == "alpso" or optName == Optimizers.ALPSO: from .pyALPSO.pyALPSO import ALPSO as opt - elif optName == "paropt": + elif optName == "paropt" or optName == Optimizers.ParOpt: from .pyParOpt.ParOpt import ParOpt as opt else: raise Error( - "The optimizer specified in 'optName' was not recognized. " - + f"The current list of supported optimizers is: {optList}" + ( + "The optimizer specified in 'optName' was not recognized. " + + "The current list of supported optimizers is {}" + ).format(list(map(str, Optimizers))) ) # Create the optimizer and return it From 70bcf5a524c6d0353744a091ac4df0a7be0d4a6a Mon Sep 17 00:00:00 2001 From: Jack Myers Date: Tue, 18 Oct 2022 12:28:55 -0700 Subject: [PATCH 02/44] hard code meson version for Windows build so that tests pass (#310) --- .github/environment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/environment.yml b/.github/environment.yml index 95724351..5883a678 100644 --- a/.github/environment.yml +++ b/.github/environment.yml @@ -4,7 +4,7 @@ dependencies: - numpy >=1.16 - ipopt - swig - - meson >=0.60 + - meson =0.61 - compilers - pkg-config - pip From 09f4a19bcb7193c4ab13cda98db2a4830a0e6fc3 Mon Sep 17 00:00:00 2001 From: Neil Wu <602725+nwu63@users.noreply.github.com> Date: Tue, 18 Oct 2022 15:34:31 -0400 Subject: [PATCH 03/44] version bump --- pyoptsparse/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyoptsparse/__init__.py b/pyoptsparse/__init__.py index 9961aa37..42da9dbc 100644 --- a/pyoptsparse/__init__.py +++ b/pyoptsparse/__init__.py @@ -1,4 +1,4 @@ -__version__ = "2.9.0" +__version__ = "2.9.1" from .pyOpt_history import History from .pyOpt_variable import Variable From 03ad4404cee300229609b4b945968344cf61aef1 Mon Sep 17 00:00:00 2001 From: Neil Wu <602725+nwu63@users.noreply.github.com> Date: Mon, 24 Oct 2022 10:40:36 -0400 Subject: [PATCH 04/44] allow user to write stuff into iterDict (#311) --- pyoptsparse/pySNOPT/pySNOPT.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyoptsparse/pySNOPT/pySNOPT.py b/pyoptsparse/pySNOPT/pySNOPT.py index 3c6f3e63..1998c6df 100644 --- a/pyoptsparse/pySNOPT/pySNOPT.py +++ b/pyoptsparse/pySNOPT/pySNOPT.py @@ -673,6 +673,10 @@ def _snstop(self, ktcond, mjrprtlvl, minimize, n, nncon, nnobj, ns, itn, nmajor, if not self.storeHistory: raise Error("snSTOP function handle must be used with storeHistory=True") iabort = snstop_handle(iterDict) + # 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 From ceedd278fb20642a77f80996b7d558ac2ad14193 Mon Sep 17 00:00:00 2001 From: Eytan Adler <63426601+eytanadler@users.noreply.github.com> Date: Mon, 7 Nov 2022 11:30:43 -0500 Subject: [PATCH 05/44] Updated IPOPT build instructions for other linear solvers (#312) --- .isort.cfg | 2 +- doc/install.rst | 2 +- doc/optimizers/IPOPT.rst | 10 ---------- 3 files changed, 2 insertions(+), 12 deletions(-) diff --git a/.isort.cfg b/.isort.cfg index dd81fa93..ae58192d 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -6,5 +6,5 @@ import_heading_stdlib=Standard Python modules import_heading_thirdparty=External modules import_heading_firstparty=First party modules import_heading_localfolder=Local modules -skip_glob=**__init__.py,**setup.py +skip_glob=**__init__.py,setup.py known_local_folder=testing_utils diff --git a/doc/install.rst b/doc/install.rst index 4774a686..d4ad85fb 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -27,7 +27,7 @@ pyOptSparse has the following dependencies: Please make sure these are installed and available for use. In order to use NSGA2, SWIG (v1.3+) is also required, which can be installed via the package manager. If those optimizers are not needed, then you do not need to install SWIG. -Simply comment out the corresponding lines in ``pyoptsparse/pyoptsparse/setup.py`` so that they are not compiled. +Simply comment out the corresponding lines in ``pyoptsparse/pyoptsparse/meson.build`` so that they are not compiled. The corresponding lines in ``pyoptsparse/pyoptsparse/__init__.py`` must be commented out as well. Python dependencies are automatically handled by ``pip``, so they do not need to be installed separately. diff --git a/doc/optimizers/IPOPT.rst b/doc/optimizers/IPOPT.rst index 0446991d..ebd03bc0 100644 --- a/doc/optimizers/IPOPT.rst +++ b/doc/optimizers/IPOPT.rst @@ -68,16 +68,6 @@ Here we explain a basic setup using MUMPS as the linear solver, together with ME #. Now clean build pyOptSparse. Verify that IPOPT works by running the relevant tests. -.. note:: - - To get IPOPT working with pyOptSparse when using another linear solver, several things must be changed. - - #. The ``setup.py`` file located in ``pyoptsparse/pyIPOPT`` must be updated accordingly. - In particular, the ``libraries=`` line must be changed to reflect the alternate linear solver. - For example, for HSL you need to replace ``coinmumps`` and ``coinmetis`` with ``coinhsl``. - #. The option ``linear_solver`` in the options dictionary must be changed. - The default value can be changed in ``pyIPOPT.py`` so that this option does not need to be manually set in every run script. - Options ------- Please refer to the `IPOPT website `__ for complete listing of options. From 6e56cd82342f7e1d0bcfae63c6694810072fb39d Mon Sep 17 00:00:00 2001 From: Sabet Seraj <48863473+sseraj@users.noreply.github.com> Date: Wed, 21 Dec 2022 12:42:38 -0500 Subject: [PATCH 06/44] Replaced np.int with int (#319) --- pyoptsparse/__init__.py | 2 +- pyoptsparse/pySLSQP/pySLSQP.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyoptsparse/__init__.py b/pyoptsparse/__init__.py index 42da9dbc..cd558546 100644 --- a/pyoptsparse/__init__.py +++ b/pyoptsparse/__init__.py @@ -1,4 +1,4 @@ -__version__ = "2.9.1" +__version__ = "2.9.2" from .pyOpt_history import History from .pyOpt_variable import Variable diff --git a/pyoptsparse/pySLSQP/pySLSQP.py b/pyoptsparse/pySLSQP/pySLSQP.py index 0a283a21..d1d04684 100644 --- a/pyoptsparse/pySLSQP/pySLSQP.py +++ b/pyoptsparse/pySLSQP/pySLSQP.py @@ -204,13 +204,13 @@ def slgrad(m, me, la, n, f, g, df, dg, x): lsei = ((n + 1) + mineq) * ((n + 1) - meq) + 2 * meq + (n + 1) slsqpb = (n + 1) * (n / 2) + 2 * m + 3 * n + 3 * (n + 1) + 1 lwM = lsq + lsi + lsei + slsqpb + n + m - lw = np.array([lwM], np.int) + lw = np.array([lwM], int) w = np.zeros(lw, float) ljwM = max(mineq, (n + 1) - meq) - ljw = np.array([ljwM], np.int) + ljw = np.array([ljwM], int) jw = np.zeros(ljw, np.intc) - nfunc = np.array([0], np.int) - ngrad = np.array([0], np.int) + nfunc = np.array([0], int) + ngrad = np.array([0], int) # Run SLSQP t0 = time.time() From 656fce2b27eb57aafd9a227a2d99d16e8616ccb3 Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Fri, 23 Dec 2022 11:55:24 -0500 Subject: [PATCH 07/44] Added inform to the string representation of the solution. (#320) --- pyoptsparse/pyOpt_solution.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyoptsparse/pyOpt_solution.py b/pyoptsparse/pyOpt_solution.py index bacf3ae2..915b42d7 100644 --- a/pyoptsparse/pyOpt_solution.py +++ b/pyoptsparse/pyOpt_solution.py @@ -85,6 +85,13 @@ def __str__(self) -> str: for i in range(5, len(lines)): text1 += lines[i] + "\n" + inform_val = self.optInform["value"] + inform_text = self.optInform["text"] + text1 += "\n" + text1 += " Exit Status\n" + text1 += " Inform Description\n" + text1 += f" {inform_val:>6} {inform_text:<0}\n" + text1 += ("-" * 80) + "\n" return text1 From bc71de92027c453d939bcafb1712c215f2773882 Mon Sep 17 00:00:00 2001 From: ArshSaja <63115167+ArshSaja@users.noreply.github.com> Date: Mon, 20 Feb 2023 18:49:05 -0500 Subject: [PATCH 08/44] Black style fix for updated version (#327) * new black style fix * flake8 style fix * flake8 style fix --- examples/hs071_zero_return.py | 1 - pyoptsparse/postprocessing/OptView.py | 6 ------ pyoptsparse/postprocessing/OptView_baseclass.py | 11 ----------- pyoptsparse/pyALPSO/pyALPSO.py | 2 -- pyoptsparse/pyCONMIN/pyCONMIN.py | 1 - pyoptsparse/pyIPOPT/pyIPOPT.py | 1 - pyoptsparse/pyNSGA2/pyNSGA2.py | 1 - pyoptsparse/pyOpt_MPI.py | 2 +- pyoptsparse/pyOpt_optimization.py | 10 +++------- pyoptsparse/pyOpt_optimizer.py | 2 -- pyoptsparse/pyOpt_solution.py | 1 - pyoptsparse/pyOpt_utils.py | 3 ++- pyoptsparse/pySNOPT/pySNOPT.py | 1 - tests/test_hs015.py | 1 - tests/test_nsga2_multi_objective.py | 4 +++- tests/test_rosenbrock.py | 1 - tests/test_sphere.py | 1 - tests/test_user_termination.py | 1 - 18 files changed, 9 insertions(+), 41 deletions(-) diff --git a/examples/hs071_zero_return.py b/examples/hs071_zero_return.py index 2c5db681..80d34182 100644 --- a/examples/hs071_zero_return.py +++ b/examples/hs071_zero_return.py @@ -28,7 +28,6 @@ def objfunc(xdict): def sens(xdict, funcs): - x0 = xdict["x0"][0] x1 = xdict["x1"][0] x2 = xdict["x2"][0] diff --git a/pyoptsparse/postprocessing/OptView.py b/pyoptsparse/postprocessing/OptView.py index b096e9d9..6ec04619 100644 --- a/pyoptsparse/postprocessing/OptView.py +++ b/pyoptsparse/postprocessing/OptView.py @@ -42,7 +42,6 @@ class Display(OVBaseClass): """ def __init__(self, histList, outputDir, figsize): - # Initialize the Tkinter object, which will contain all graphical # elements. self.root = Tk.Tk() @@ -214,7 +213,6 @@ def orig_plot(self, dat, val, values, a, i=0): self.error_display("No bounds information") def color_plot(self, dat, labels, a): - # If the user wants the non-constraint colormap, use viridis if self.var_color.get(): cmap = plt.get_cmap("viridis") @@ -254,7 +252,6 @@ def color_plot(self, dat, labels, a): # Loop through the data sets selected by the user for label in labels: - # Get the subarray for this particular data set and record its size subarray = np.array(dat[label]).T sub_size = subarray.shape[0] @@ -438,7 +435,6 @@ def plot_selected(self, values, dat): # Plot on individual vertical axes elif self.var.get() == 1 and not fail: - # Set window sizing parameters for when additional axes are # added n = len(values) @@ -553,7 +549,6 @@ def plot_selected(self, values, dat): # Plot color plots of rectangular pixels showing values, # especially useful for constraints elif self.var.get() == 3 and not fail: - # Remove options that aren't relevant self.c4.grid_forget() self.c5.grid_forget() @@ -666,7 +661,6 @@ def update_graph(self): self.plot_selected(values, dat) def set_mask(self): - if self.var_mask.get(): self.func_data = self.func_data_major self.var_data = self.var_data_major diff --git a/pyoptsparse/postprocessing/OptView_baseclass.py b/pyoptsparse/postprocessing/OptView_baseclass.py index de5ad728..66bf73c6 100644 --- a/pyoptsparse/postprocessing/OptView_baseclass.py +++ b/pyoptsparse/postprocessing/OptView_baseclass.py @@ -44,7 +44,6 @@ def OptimizationHistory(self): # Loop over each history file name provided by the user. for histIndex, histFileName in enumerate(self.histList): - # If they only have one history file, we don't change the keys' names if len(self.histList) == 1: histIndex = "" @@ -71,7 +70,6 @@ def OptimizationHistory(self): # Specific instructions for OpenMDAO databases if OpenMDAO: - # Get the number of iterations by looking at the largest number # in the split string names for each entry in the db for string in db.keys(): @@ -108,7 +106,6 @@ def OptimizationHistory(self): pass else: - # Get the number of iterations nkey = int(db["last"]) + 1 self.nkey = nkey @@ -190,14 +187,11 @@ def OptimizationHistory(self): self.num_iter = length def DetermineMajorIterations(self, db, OpenMDAO): - if not OpenMDAO: - previousIterCounter = -1 # Loop over each optimization call for i, iter_type in enumerate(self.iter_type): - # If this is an OpenMDAO file, the keys are of the format # 'rank0:SNOPT|1', etc key = "%d" % i @@ -252,7 +246,6 @@ def SaveDBData(self, db, data_all, data_major, OpenMDAO, data_str): # Loop over each optimization iteration for i, iter_type in enumerate(self.iter_type): - # If this is an OpenMDAO file, the keys are of the format # 'rank0:SNOPT|1', etc if OpenMDAO: @@ -262,13 +255,11 @@ def SaveDBData(self, db, data_all, data_major, OpenMDAO, data_str): # Do this for both major and minor iterations if self.iter_type[i]: - # Get just the info in the dict for this iteration iter_data = db[key][data_str] # Loop through each key within this iteration for key in sorted(iter_data): - # Format a new_key string where we append a modifier # if we have multiple history files new_key = key + f"{self.histIndex}" @@ -294,11 +285,9 @@ def SaveOpenMDAOData(self, db): # Loop over each key in the metadata db for tag in db: - # Only look at variables and unknowns if tag in ["Unknowns", "Parameters"]: for old_item in db[tag]: - # We'll rename each item, so we need to get the old item # name and modify it item = old_item + f"{self.histIndex}" diff --git a/pyoptsparse/pyALPSO/pyALPSO.py b/pyoptsparse/pyALPSO/pyALPSO.py index 3ed091d2..6a8aef0b 100644 --- a/pyoptsparse/pyALPSO/pyALPSO.py +++ b/pyoptsparse/pyALPSO/pyALPSO.py @@ -26,7 +26,6 @@ class ALPSO(Optimizer): """ def __init__(self, raiseError=True, options={}): - from . import alpso self.alpso = alpso @@ -154,7 +153,6 @@ def objconfunc(x): me = len(indices) if self.optProb.comm.rank == 0: - # Setup argument list values opt = self.getOption diff --git a/pyoptsparse/pyCONMIN/pyCONMIN.py b/pyoptsparse/pyCONMIN/pyCONMIN.py index 897b49b8..21dd0c07 100644 --- a/pyoptsparse/pyCONMIN/pyCONMIN.py +++ b/pyoptsparse/pyCONMIN/pyCONMIN.py @@ -164,7 +164,6 @@ def cnmnfun(n1, n2, x, f, g): # CONMIN - Objective/Constraint Gradients Function # ================================================================= def cnmngrad(n1, n2, x, f, g, ct, df, a, ic, nac): - gobj, gcon, fail = self._masterFunc(x[0:ndv], ["gobj", "gcon"]) df[0:ndv] = gobj.copy() diff --git a/pyoptsparse/pyIPOPT/pyIPOPT.py b/pyoptsparse/pyIPOPT/pyIPOPT.py index a7f4638f..55aff372 100644 --- a/pyoptsparse/pyIPOPT/pyIPOPT.py +++ b/pyoptsparse/pyIPOPT/pyIPOPT.py @@ -201,7 +201,6 @@ def __call__( # problem and run IPOPT, otherwise we go to the waiting loop: if self.optProb.comm.rank == 0: - # Now what we need for IPOPT is precisely the .row and # .col attributes of the fullJacobian array matStruct = ( diff --git a/pyoptsparse/pyNSGA2/pyNSGA2.py b/pyoptsparse/pyNSGA2/pyNSGA2.py index d4f7c096..5e61ff52 100644 --- a/pyoptsparse/pyNSGA2/pyNSGA2.py +++ b/pyoptsparse/pyNSGA2/pyNSGA2.py @@ -24,7 +24,6 @@ class NSGA2(Optimizer): """ def __init__(self, raiseError=True, options={}): - name = "NSGA-II" category = "Global Optimizer" defOpts = self._getDefaultOptions() diff --git a/pyoptsparse/pyOpt_MPI.py b/pyoptsparse/pyOpt_MPI.py index 95b03b5c..d97bb008 100644 --- a/pyoptsparse/pyOpt_MPI.py +++ b/pyoptsparse/pyOpt_MPI.py @@ -65,5 +65,5 @@ def __init__(self): + "non-gradient based optimizers. Continuing using a dummy MPI module " + "from pyOptSparse." ) - warnings.warn(warn) + warnings.warn(warn, stacklevel=2) MPI = myMPI() diff --git a/pyoptsparse/pyOpt_optimization.py b/pyoptsparse/pyOpt_optimization.py index 9fcab5d1..cd54e3bd 100644 --- a/pyoptsparse/pyOpt_optimization.py +++ b/pyoptsparse/pyOpt_optimization.py @@ -231,7 +231,7 @@ def addVarGroup( if "type" in kwargs: varType = kwargs["type"] # but we also throw a deprecation warning - warnings.warn("The argument `type=` is deprecated. Use `varType` in the future.") + warnings.warn("The argument `type=` is deprecated. Use `varType` in the future.", stacklevel=2) # Check that the type is ok if varType not in ["c", "i", "d"]: raise Error("Type must be one of 'c' for continuous, 'i' for integer or 'd' for discrete.") @@ -729,7 +729,6 @@ def printSparsity(self, verticalPrint=False): # If we're printing vertically, add an additional text array on top # of the already created txt array if verticalPrint: - # It has the same width and a height corresponding to the length # of the longest design variable name newTxt = np.zeros((longestNameLength + 1, nCol), dtype=str) @@ -739,14 +738,12 @@ def printSparsity(self, verticalPrint=False): # Loop through the letters in the longest design variable name # and add the letters for each design variable for i in range(longestNameLength + 2): - # Make a space between the name and the size if i >= longestNameLength: txt[i, :] = " " # Loop through each design variable for j, dvGroup in enumerate(self.variables): - # Print a letter in the name if any remain if i < longestNameLength and i < len(dvGroup): txt[i, int(varCenters[j])] = dvGroup[i] @@ -820,7 +817,7 @@ def finalize(self): self.finalized = True def finalizeDesignVariables(self): - warnings.warn("finalizeDesignVariables() is deprecated, use _finalizeDesignVariables() instead.") + warnings.warn("finalizeDesignVariables() is deprecated, use _finalizeDesignVariables() instead.", stacklevel=2) self._finalizeDesignVariables() def _finalizeDesignVariables(self): @@ -848,7 +845,7 @@ def _finalizeDesignVariables(self): self.ndvs = dvCounter def finalizeConstraints(self): - warnings.warn("finalizeConstraints() is deprecated, use _finalizeConstraints() instead.") + warnings.warn("finalizeConstraints() is deprecated, use _finalizeConstraints() instead.", stacklevel=2) self._finalizeConstraints() def _finalizeConstraints(self): @@ -1235,7 +1232,6 @@ def processContoVec( for iCon in self.constraints: con = self.constraints[iCon] if iCon in fcon_in: - # Make sure it is at least 1-dimensional: c = np.atleast_1d(fcon_in[iCon]) if dtype == "d": diff --git a/pyoptsparse/pyOpt_optimizer.py b/pyoptsparse/pyOpt_optimizer.py index 8e312a71..03863801 100644 --- a/pyoptsparse/pyOpt_optimizer.py +++ b/pyoptsparse/pyOpt_optimizer.py @@ -230,7 +230,6 @@ def _masterFunc(self, x: ndarray, evaluate: List[str]): # fire it back to the specific optimizer timeA = time.time() if self.hotStart: - # This is a very inexpensive check to see if point exists if self.hotStart.pointExists(self.callCounter): # Read the actual data for this point: @@ -242,7 +241,6 @@ def _masterFunc(self, x: ndarray, evaluate: List[str]): # Validated x-point point to use: xuser_vec = self.optProb._mapXtoUser(x) if np.isclose(xuser_vec, xuser_ref, rtol=EPS, atol=EPS).all(): - # However, we may need a sens that *isn't* in the # the dictionary: funcs = None diff --git a/pyoptsparse/pyOpt_solution.py b/pyoptsparse/pyOpt_solution.py index 915b42d7..0e6a4547 100644 --- a/pyoptsparse/pyOpt_solution.py +++ b/pyoptsparse/pyOpt_solution.py @@ -7,7 +7,6 @@ class Solution(Optimization): def __init__(self, optProb, xStar, fStar, lambdaStar, optInform, info): - """ This class is used to describe the solution of an optimization problem. This class inherits from Optimization which enables a diff --git a/pyoptsparse/pyOpt_utils.py b/pyoptsparse/pyOpt_utils.py index 432344a3..b9f65c0f 100644 --- a/pyoptsparse/pyOpt_utils.py +++ b/pyoptsparse/pyOpt_utils.py @@ -224,7 +224,8 @@ def convertToCOO(mat: Union[dict, spmatrix, ndarray]): + "fixed sparsity structure and explicit zeros in the matrix. " + "There is no way to guarantee a fixed sparsity structure with scipy matrices " + "which is what the underlying optimizers require. " - + "Using scipy.sparse matrices may cause unexpected errors." + + "Using scipy.sparse matrices may cause unexpected errors.", + stacklevel=2, ) mat = mat.tocoo() diff --git a/pyoptsparse/pySNOPT/pySNOPT.py b/pyoptsparse/pySNOPT/pySNOPT.py index 1998c6df..27cf709f 100644 --- a/pyoptsparse/pySNOPT/pySNOPT.py +++ b/pyoptsparse/pySNOPT/pySNOPT.py @@ -323,7 +323,6 @@ def __call__( # 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 # ----------------------------------------------------- diff --git a/tests/test_hs015.py b/tests/test_hs015.py index 23552237..37ed87b6 100644 --- a/tests/test_hs015.py +++ b/tests/test_hs015.py @@ -15,7 +15,6 @@ class TestHS15(OptTest): - ## Solve test problem HS15 from the Hock & Schittkowski collection. # # min 100 (x2 - x1^2)^2 + (1 - x1)^2 diff --git a/tests/test_nsga2_multi_objective.py b/tests/test_nsga2_multi_objective.py index 1a530851..075b7c59 100644 --- a/tests/test_nsga2_multi_objective.py +++ b/tests/test_nsga2_multi_objective.py @@ -46,7 +46,9 @@ def test_opt(self): # 300 generations will find x=(0,0), 200 or less will find x=(1,1) optOptions = {"maxGen": 200} if sys.platform == "win32": - warnings.warn("test_nsga2_multi_objective.py fails on windows with two objectives! Skipping for now.") + warnings.warn( + "test_nsga2_multi_objective.py fails on windows with two objectives! Skipping for now.", stacklevel=2 + ) return sol = self.optimize(optOptions=optOptions) tol = 1e-2 diff --git a/tests/test_rosenbrock.py b/tests/test_rosenbrock.py index c21f9a41..c0f61055 100644 --- a/tests/test_rosenbrock.py +++ b/tests/test_rosenbrock.py @@ -17,7 +17,6 @@ class TestRosenbrock(OptTest): - ## Solve unconstrained Rosenbrock problem. # This problem is scalable w.r.t. design variables number. # We select a problem with 4 design variables, but the diff --git a/tests/test_sphere.py b/tests/test_sphere.py index 4d7c7e1d..4901341e 100644 --- a/tests/test_sphere.py +++ b/tests/test_sphere.py @@ -15,7 +15,6 @@ class TestSphere(OptTest): - ## Solve unconstrained Sphere problem. # This problem is scalable w.r.t. design variables number. # We select a problem with 4 design variables, but the diff --git a/tests/test_user_termination.py b/tests/test_user_termination.py index 04db837e..ca40bad9 100644 --- a/tests/test_user_termination.py +++ b/tests/test_user_termination.py @@ -79,7 +79,6 @@ def setup_optProb(termcomp): class TestUserTerminationStatus(unittest.TestCase): - optOptions = { "IPOPT": { "output_file": "{}.out", From 21a4f67123aba27a54f785fc91a4bbe9d57c8a37 Mon Sep 17 00:00:00 2001 From: Neil Wu <602725+nwu63@users.noreply.github.com> Date: Tue, 21 Feb 2023 12:50:25 -0500 Subject: [PATCH 09/44] Attempt to fix Windows build in GHA (#328) * attempt to fix Windows build in GHA * try to fix test failure * adding optName to NSGA2 test * fixing name * black fix * cleanup * maybe this works too? --------- Co-authored-by: Marco Mangano <36549388+marcomangano@users.noreply.github.com> Co-authored-by: Marco Mangano --- .github/workflows/windows-build.yml | 2 +- tests/test_require_mpi_env_var.py | 2 +- tests/test_rosenbrock.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml index 705c638b..c5123515 100644 --- a/.github/workflows/windows-build.yml +++ b/.github/workflows/windows-build.yml @@ -17,7 +17,7 @@ jobs: - uses: conda-incubator/setup-miniconda@v2 with: python-version: 3.8 - mamba-version: "*" + miniforge-variant: Mambaforge channels: conda-forge,defaults channel-priority: strict activate-environment: pyos-build diff --git a/tests/test_require_mpi_env_var.py b/tests/test_require_mpi_env_var.py index 778784f8..15e7ee25 100644 --- a/tests/test_require_mpi_env_var.py +++ b/tests/test_require_mpi_env_var.py @@ -54,7 +54,7 @@ def setUp(self): try: from paropt import ParOpt as _ParOpt # noqa: F401 except ImportError: - raise unittest.SkipTest("Optimizer not available:", "paropt") + raise unittest.SkipTest("Optimizer not available: paropt") def test_require_mpi_check_paropt(self): os.environ["PYOPTSPARSE_REQUIRE_MPI"] = "1" diff --git a/tests/test_rosenbrock.py b/tests/test_rosenbrock.py index c0f61055..cf31618b 100644 --- a/tests/test_rosenbrock.py +++ b/tests/test_rosenbrock.py @@ -150,9 +150,9 @@ def test_snopt_hotstart_starting_from_grad(self): @parameterized.expand(["IPOPT", "SLSQP", "PSQP", "CONMIN", "NLPQLP", "ParOpt"]) def test_optimization(self, optName): + self.optName = optName if optName == "IPOPT" and sys.platform == "win32": raise unittest.SkipTest() - self.optName = optName self.setup_optProb() optOptions = self.optOptions.pop(optName, None) self.optimize_with_hotstart(self.tol[optName], optOptions=optOptions) From bed467e9b7893e38e06cea762d1a25e9eac52e9c Mon Sep 17 00:00:00 2001 From: Neil Wu <602725+nwu63@users.noreply.github.com> Date: Mon, 27 Feb 2023 12:11:45 -0500 Subject: [PATCH 10/44] Fix `sol.fStar` for NSGA2 (#330) * fix sol.fStar for multi-objective optimizers * slightly cleaner accessing for xStar * added type checking in case fStar does not have a __len__ method * only fetch array if we actually have an array * add associated test * Update pyNSGA2.py * Update test_nsga2_multi_objective.py * fix bug for float fStar * replacing ff with fStar * updating docstring for _assembleObjective --------- Co-authored-by: Marco Mangano --- pyoptsparse/pyNSGA2/pyNSGA2.py | 9 ++++++++- pyoptsparse/pyOpt_optimizer.py | 8 +++++--- tests/test_nsga2_multi_objective.py | 25 ++++++++++++++++++------- tests/test_snopt_bugfix.py | 4 ++-- 4 files changed, 33 insertions(+), 13 deletions(-) diff --git a/pyoptsparse/pyNSGA2/pyNSGA2.py b/pyoptsparse/pyNSGA2/pyNSGA2.py index 5e61ff52..b6f048da 100644 --- a/pyoptsparse/pyNSGA2/pyNSGA2.py +++ b/pyoptsparse/pyNSGA2/pyNSGA2.py @@ -188,8 +188,15 @@ def objconfunc(nreal, nobj, ncon, x, f, g): for i in range(n): xstar[i] = nsga2.doubleArray_getitem(x, i) + fStar = np.zeros(len_ff) + if len_ff > 1: + for i in range(len_ff): + fStar[i] = nsga2.doubleArray_getitem(f, i) + else: + fStar = nsga2.doubleArray_getitem(f, 0) + # Create the optimization solution - sol = self._createSolution(optTime, sol_inform, ff, xstar) + sol = self._createSolution(optTime, sol_inform, fStar, xstar) else: # We are not on the root process so go into waiting loop: self._waitLoop() diff --git a/pyoptsparse/pyOpt_optimizer.py b/pyoptsparse/pyOpt_optimizer.py index 03863801..9fb20a87 100644 --- a/pyoptsparse/pyOpt_optimizer.py +++ b/pyoptsparse/pyOpt_optimizer.py @@ -758,9 +758,8 @@ def _assembleConstraints(self): def _assembleObjective(self): """ - Utility function for assembling the design variables. Most - optimizers here use continuous variables so this chunk of code - can be reused. + Utility function for assembling the objective, fetching the information in the Objective object within the Optimization class. + Most optimizers use a single objective. In that case, the function will return a 0-length array (not a scalar). """ nobj = len(self.optProb.objectives.keys()) @@ -778,6 +777,9 @@ def _createSolution(self, optTime, sol_inform, obj, xopt, multipliers=None) -> S finishes. """ fStar = self.optProb._mapObjtoUser(obj) + # optionally convert to dict for multi-objective problems + if isinstance(fStar, (list, np.ndarray)) and len(fStar) > 1: + fStar = self.optProb.processObjtoDict(fStar, scaled=False) xuser = self.optProb._mapXtoUser(xopt) xStar = self.optProb.processXtoDict(xuser) diff --git a/tests/test_nsga2_multi_objective.py b/tests/test_nsga2_multi_objective.py index 075b7c59..37c4016c 100644 --- a/tests/test_nsga2_multi_objective.py +++ b/tests/test_nsga2_multi_objective.py @@ -7,6 +7,7 @@ # External modules from numpy.testing import assert_allclose +from parameterized import parameterized # First party modules from pyoptsparse import Optimization @@ -24,24 +25,27 @@ def objfunc(self, xdict): y = xdict["y"] funcs = {} - funcs["obj1"] = (x - 0.0) ** 2 + (y - 0.0) ** 2 + # Adding an offset so that fStar != 0.0 + funcs["obj1"] = (x - 0.0) ** 2 + (y - 0.0) ** 2 + 10 funcs["obj2"] = (x - 1.0) ** 2 + (y - 1.0) ** 2 fail = False return funcs, fail - def setup_optProb(self): + def setup_optProb(self, n_obj): # Instantiate Optimization Problem self.optProb = Optimization("quadratic", self.objfunc) self.optProb.addVar("x", value=0, lower=-600, upper=600) self.optProb.addVar("y", value=0, lower=-600, upper=600) self.optProb.addObj("obj1") - self.optProb.addObj("obj2") + if n_obj == 2: + self.optProb.addObj("obj2") - def test_opt(self): - self.setup_optProb() + @parameterized.expand([(1,), (2,)]) + def test_opt(self, n_obj): + self.setup_optProb(n_obj) # 300 generations will find x=(0,0), 200 or less will find x=(1,1) optOptions = {"maxGen": 200} @@ -52,8 +56,15 @@ def test_opt(self): return sol = self.optimize(optOptions=optOptions) tol = 1e-2 - assert_allclose(sol.variables["x"][0].value, 1.0, atol=tol, rtol=tol) - assert_allclose(sol.variables["y"][0].value, 1.0, atol=tol, rtol=tol) + if n_obj == 1: + assert_allclose(sol.xStar["x"], 0.0, atol=tol, rtol=tol) + assert_allclose(sol.xStar["y"], 0.0, atol=tol, rtol=tol) + assert_allclose(sol.fStar, 10.0, atol=tol, rtol=tol) + elif n_obj == 2: + assert_allclose(sol.xStar["x"], 1.0, atol=tol, rtol=tol) + assert_allclose(sol.xStar["y"], 1.0, atol=tol, rtol=tol) + assert_allclose(sol.fStar["obj1"], 12.0, atol=tol, rtol=tol) + assert_allclose(sol.fStar["obj2"], 0.0, atol=tol, rtol=tol) if __name__ == "__main__": diff --git a/tests/test_snopt_bugfix.py b/tests/test_snopt_bugfix.py index 82f647e8..e6460452 100644 --- a/tests/test_snopt_bugfix.py +++ b/tests/test_snopt_bugfix.py @@ -113,8 +113,8 @@ def test_opt(self): # Check Solution 7.166667, -7.833334 tol = 1e-6 - assert_allclose(sol.variables["x"][0].value, 7.166667, atol=tol, rtol=tol) - assert_allclose(sol.variables["y"][0].value, -7.833333, atol=tol, rtol=tol) + assert_allclose(sol.xStar["x"], 7.166667, atol=tol, rtol=tol) + assert_allclose(sol.xStar["y"], -7.833333, atol=tol, rtol=tol) def test_opt_bug1(self): # Due to a new feature, there is a TypeError when you optimize a model without a constraint. From 862cd946c55c22481996e7a8883fc958689428eb Mon Sep 17 00:00:00 2001 From: Neil Wu <602725+nwu63@users.noreply.github.com> Date: Mon, 27 Feb 2023 14:29:19 -0500 Subject: [PATCH 11/44] remove commented code in NSGA2 --- pyoptsparse/pyNSGA2/pyNSGA2.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyoptsparse/pyNSGA2/pyNSGA2.py b/pyoptsparse/pyNSGA2/pyNSGA2.py index b6f048da..125e3b0f 100644 --- a/pyoptsparse/pyNSGA2/pyNSGA2.py +++ b/pyoptsparse/pyNSGA2/pyNSGA2.py @@ -181,8 +181,6 @@ def objconfunc(nreal, nobj, ncon, x, f, g): # Store Results sol_inform = {} - # sol_inform['value'] = inform - # sol_inform['text'] = self.informs[inform[0]] xstar = [0.0] * n for i in range(n): From 52097c652a559225b789574af3d1e5a75ab36f4d Mon Sep 17 00:00:00 2001 From: Neil Wu <602725+nwu63@users.noreply.github.com> Date: Mon, 27 Feb 2023 14:29:43 -0500 Subject: [PATCH 12/44] version bump --- pyoptsparse/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyoptsparse/__init__.py b/pyoptsparse/__init__.py index cd558546..8d3d7656 100644 --- a/pyoptsparse/__init__.py +++ b/pyoptsparse/__init__.py @@ -1,4 +1,4 @@ -__version__ = "2.9.2" +__version__ = "2.9.3" from .pyOpt_history import History from .pyOpt_variable import Variable From aa83d79817c826251abfcbfca60df6d6d638730c Mon Sep 17 00:00:00 2001 From: Marco Mangano <36549388+marcomangano@users.noreply.github.com> Date: Tue, 14 Mar 2023 10:46:02 -0400 Subject: [PATCH 13/44] Update informs list to be consistent with SNOPT 7.7 + updating docstrings for flake8 rst checks (#333) * updated informs dictionary * doc update * removed old informs * added test for time limit option * fixed flake8 rst issues? --- doc/advancedFeatures.rst | 7 +++++++ examples/tp109.py | 2 ++ pyoptsparse/pyOpt_optimization.py | 6 ++++-- pyoptsparse/pyOpt_optimizer.py | 2 +- pyoptsparse/pySNOPT/pySNOPT.py | 35 +++++++------------------------ tests/test_hs071.py | 2 ++ tests/test_tp109.py | 2 ++ 7 files changed, 26 insertions(+), 30 deletions(-) diff --git a/doc/advancedFeatures.rst b/doc/advancedFeatures.rst index 6c8c2f15..45484ad4 100644 --- a/doc/advancedFeatures.rst +++ b/doc/advancedFeatures.rst @@ -62,6 +62,12 @@ Because the hot start process will store all the previous "restarted" iterations Time limit (for SNOPT only) --------------------------- + +.. note:: + + Since SNOPT 7.7, the user should rely on the ``Time Limit`` SNOPT option instead of the ``timeLimit`` argument of the ``Optimizer`` instance to set the maximum optimization time. + + The :ref:`optimizer` class in pyOptSparse has an attribute used to set the maximum allowable wall time for optimizations using SNOPT. The code will exit gracefully when such time limit is reached. This feature is particularly useful when running a time-constrained job, as in the case of most HPC systems. @@ -79,5 +85,6 @@ Note that the attribute takes the maximum wall time *in seconds* as an integer n It will NOT interrupt an ongoing function or sensitivity evaluation. If your function evaluations are expensive, you should be more conservative when setting the ``timeLimit`` option for it to be effective. + .. Clean Optimization Termination .. ------------------------------ diff --git a/examples/tp109.py b/examples/tp109.py index c941cad5..2ec1f0bc 100644 --- a/examples/tp109.py +++ b/examples/tp109.py @@ -2,6 +2,7 @@ Solves Schittkowski's TP109 constraint problem. min 3.0*x1+1.*10**(-6)*x1**3+0.522074*10**(-6)*x2**3+2.0*x2 + s.t. -(x4-x3+0.550) <= 0 -(x3-x4+0.550) <= 0 -(2.25*10**(+6)-x1**2-x8**2) <= 0 @@ -16,6 +17,7 @@ -0.55 <= xi <= 0.55, i = 3,4 196.0 <= xi <= 252.0, i = 5,6,7 -400.0 <= xi <= 800.0, i = 8,9 + where a = 50.176 b = np.sin(0.25) c = np.cos(0.25) diff --git a/pyoptsparse/pyOpt_optimization.py b/pyoptsparse/pyOpt_optimization.py index cd54e3bd..607ca4bb 100644 --- a/pyoptsparse/pyOpt_optimization.py +++ b/pyoptsparse/pyOpt_optimization.py @@ -1197,7 +1197,8 @@ def processObjtoDict(self, fobj_in: NumpyType, scaled: bool = True) -> Dict1DTyp def processContoVec( self, fcon_in: Dict1DType, scaled: bool = True, dtype: str = "d", natural: bool = False ) -> ndarray: - """ + """A function that converts a dictionary of constraints into a vector + Parameters ---------- fcon_in : dict @@ -1267,7 +1268,8 @@ def processContoVec( def processContoDict( self, fcon_in: ndarray, scaled: bool = True, dtype: str = "d", natural: bool = False, multipliers: bool = False ) -> Dict1DType: - """ + """A function that converts an array of constraints into a dictionary + Parameters ---------- fcon_in : array diff --git a/pyoptsparse/pyOpt_optimizer.py b/pyoptsparse/pyOpt_optimizer.py index 9fb20a87..b4c848a2 100644 --- a/pyoptsparse/pyOpt_optimizer.py +++ b/pyoptsparse/pyOpt_optimizer.py @@ -941,7 +941,7 @@ def OPT(optName, *args, **kwargs): Either a string identifying the optimizer to create, e.g. "SNOPT", or an enum accessed via ``pyoptsparse.Optimizers``, e.g. ``Optimizers.SNOPT``. - *args, **kwargs : varies + \*args, \*\*kwargs : varies Passed to optimizer creation. Returns diff --git a/pyoptsparse/pySNOPT/pySNOPT.py b/pyoptsparse/pySNOPT/pySNOPT.py index 27cf709f..ed8749db 100644 --- a/pyoptsparse/pySNOPT/pySNOPT.py +++ b/pyoptsparse/pySNOPT/pySNOPT.py @@ -116,18 +116,21 @@ def _getDefaultOptions() -> Dict[str, Any]: @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", - 4: "weak QP minimizer", + 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", @@ -135,17 +138,16 @@ def _getInforms() -> Dict[int, str]: 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", - 53: "the QP Hessian is indefinite", - 54: "incorrect second derivatives", - 55: "incorrect derivatives", 56: "irregular or badly scaled problem functions", 60: "undefined user-supplied functions", 61: "undefined function at the first feasible point", @@ -153,8 +155,6 @@ def _getInforms() -> Dict[int, str]: 63: "unable to proceed into undefined region", 70: "user requested termination", 71: "terminated during function evaluation", - 72: "terminated during constraint evaluation", - 73: "terminated during objective evaluation", 74: "terminated from monitor routine", 80: "insufficient storage allocated", 81: "work arrays must have at least 500 elements", @@ -164,30 +164,11 @@ def _getInforms() -> Dict[int, str]: 90: "input arguments out of range", 91: "invalid input argument", 92: "basis file dimensions do not match this problem", - 93: "the QP Hessian is indefinite", - 100: "finished successfully", - 101: "SPECS file read", - 102: "Jacobian structure estimated", - 103: "MPS file read", - 104: "memory requirements estimated", - 105: "user-supplied derivatives appear to be correct", - 106: "no derivatives were checked", - 107: "some SPECS keywords were not recognized", - 110: "errors while processing MPS data", - 111: "no MPS file specified", - 112: "problem-size estimates too small", - 113: "fatal error in the MPS file", - 120: "errors while estimating Jacobian structure", - 121: "cannot find Jacobian structure at given point", - 130: "fatal errors while reading the SP", - 131: "no SPECS file (iSpecs le 0 or iSpecs gt 99)", - 132: "End-of-file while looking for a BEGIN", - 133: "End-of-file while reading SPECS file", - 134: "ENDRUN found before any valid SPECS", 140: "system error", 141: "wrong no of basic variables", 142: "error in basis package", } + return informs def __call__( @@ -256,7 +237,7 @@ def __call__( 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. + 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. diff --git a/tests/test_hs071.py b/tests/test_hs071.py index 9d22b1c1..67b6ddab 100644 --- a/tests/test_hs071.py +++ b/tests/test_hs071.py @@ -173,6 +173,8 @@ def test_snopt_informs(self): self.setup_optProb() sol = self.optimize(optOptions={"Major iterations limit": 1}) self.assert_inform_equal(sol, 32) + sol = self.optimize(optOptions={"Time Limit": 0.001}) + self.assert_inform_equal(sol, 34) def test_slsqp_informs(self): self.optName = "SLSQP" diff --git a/tests/test_tp109.py b/tests/test_tp109.py index efb724f2..4ad71dec 100644 --- a/tests/test_tp109.py +++ b/tests/test_tp109.py @@ -2,6 +2,7 @@ Test uses Schittkowski's TP109 constraint problem. min 3.0*x1+1.*10**(-6)*x1**3+0.522074*10**(-6)*x2**3+2.0*x2 + s.t. -(x4-x3+0.550) <= 0 -(x3-x4+0.550) <= 0 -(2.25*10**(+6)-x1**2-x8**2) <= 0 @@ -16,6 +17,7 @@ -0.55 <= xi <= 0.55, i = 3,4 196.0 <= xi <= 252.0, i = 5,6,7 -400.0 <= xi <= 800.0, i = 8,9 + where a = 50.176 b = np.sin(0.25) c = np.cos(0.25) From 2eab4e1c1f847b9900768e5a74bca5602d92cbe0 Mon Sep 17 00:00:00 2001 From: Sabet Seraj <48863473+sseraj@users.noreply.github.com> Date: Thu, 16 Mar 2023 05:23:55 -0400 Subject: [PATCH 14/44] Moved SNOPT time limit test to larger problem (#334) --- tests/test_hs071.py | 2 -- tests/test_tp109.py | 6 ++++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/test_hs071.py b/tests/test_hs071.py index 67b6ddab..9d22b1c1 100644 --- a/tests/test_hs071.py +++ b/tests/test_hs071.py @@ -173,8 +173,6 @@ def test_snopt_informs(self): self.setup_optProb() sol = self.optimize(optOptions={"Major iterations limit": 1}) self.assert_inform_equal(sol, 32) - sol = self.optimize(optOptions={"Time Limit": 0.001}) - self.assert_inform_equal(sol, 34) def test_slsqp_informs(self): self.optName = "SLSQP" diff --git a/tests/test_tp109.py b/tests/test_tp109.py index 4ad71dec..efaacbce 100644 --- a/tests/test_tp109.py +++ b/tests/test_tp109.py @@ -173,6 +173,12 @@ def test_snopt(self): self.assertTrue(np.isrealobj(val["obj"])) self.assertTrue(np.isrealobj(val["con"])) + def test_snopt_informs(self): + self.optName = "SNOPT" + self.setup_optProb() + sol = self.optimize(optOptions={"Time Limit": 1e-5}) + self.assert_inform_equal(sol, 34) + def test_slsqp(self): self.optName = "SLSQP" self.setup_optProb() From 40af7dcad39537f7d6ff26583ad2e9b7422bbcdf Mon Sep 17 00:00:00 2001 From: swryan <881430+swryan@users.noreply.github.com> Date: Tue, 25 Apr 2023 16:14:29 -0400 Subject: [PATCH 15/44] fix deprecation in alpso (#336) * fix deprecation in alpso * fix deprecations in alpso_ext.py --------- Co-authored-by: swryan --- pyoptsparse/pyALPSO/alpso.py | 6 +++--- pyoptsparse/pyALPSO/alpso_ext.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pyoptsparse/pyALPSO/alpso.py b/pyoptsparse/pyALPSO/alpso.py index bf0b2075..79153229 100644 --- a/pyoptsparse/pyALPSO/alpso.py +++ b/pyoptsparse/pyALPSO/alpso.py @@ -55,7 +55,7 @@ def alpso(dimensions, constraints, neqcons, xtype, x0, xmin, xmax, swarmsize, nh """ # - if x0 != []: + if x0.size > 0: if isinstance(x0, list): x0 = np.array(x0) elif not isinstance(x0, np.ndarray): @@ -110,7 +110,7 @@ def alpso(dimensions, constraints, neqcons, xtype, x0, xmin, xmax, swarmsize, nh else: diI = 0 - if x0 != []: + if x0.size > 0: if len(x0.shape) == 1: nxi = 1 else: @@ -175,7 +175,7 @@ def alpso(dimensions, constraints, neqcons, xtype, x0, xmin, xmax, swarmsize, nh v_k[i, j] = (xmin[j] + rand.random() * (xmax[j] - xmin[j])) / dt - if x0 != []: + if x0.size > 0: if len(x0.shape) == 1: if scale == 1: x_k[0, :] = (x0[:] - space_centre) / space_halflen diff --git a/pyoptsparse/pyALPSO/alpso_ext.py b/pyoptsparse/pyALPSO/alpso_ext.py index a58df5f0..f54a9560 100644 --- a/pyoptsparse/pyALPSO/alpso_ext.py +++ b/pyoptsparse/pyALPSO/alpso_ext.py @@ -55,7 +55,7 @@ def alpso(dimensions, constraints, neqcons, xtype, x0, xmin, xmax, swarmsize, nh """ # - if x0 != []: + if x0.size > 0: if isinstance(x0, list): x0 = np.array(x0) elif not isinstance(x0, np.ndarray): @@ -110,7 +110,7 @@ def alpso(dimensions, constraints, neqcons, xtype, x0, xmin, xmax, swarmsize, nh else: diI = 0 - if x0 != []: + if x0.size > 0: if len(x0.shape) == 1: nxi = 1 else: @@ -175,7 +175,7 @@ def alpso(dimensions, constraints, neqcons, xtype, x0, xmin, xmax, swarmsize, nh v_k[i, j] = (xmin[j] + rand.random() * (xmax[j] - xmin[j])) / dt - if x0 != []: + if x0.size > 0: if len(x0.shape) == 1: if scale == 1: x_k[0, :] = (x0[:] - space_centre) / space_halflen From 98a791062df90a16b5f7697328505a6f1e34e092 Mon Sep 17 00:00:00 2001 From: Eirikur Jonsson <36180221+eirikurj@users.noreply.github.com> Date: Mon, 1 May 2023 13:37:23 +0000 Subject: [PATCH 16/44] Loosen tolerance on tests to support Mac ARM (#339) --- tests/test_hs015.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_hs015.py b/tests/test_hs015.py index 37ed87b6..1bb5cbc7 100644 --- a/tests/test_hs015.py +++ b/tests/test_hs015.py @@ -42,7 +42,7 @@ class TestHS15(OptTest): {"xvars": (-0.79212322, -1.26242985)}, ] tol = { - "SLSQP": 1e-8, + "SLSQP": 1e-5, "NLPQLP": 1e-12, "IPOPT": 1e-4, "ParOpt": 1e-6, From bf254b958a289442c0dd68aae385a842f112fd2b Mon Sep 17 00:00:00 2001 From: Phil Chiu Date: Mon, 1 May 2023 10:59:02 -0600 Subject: [PATCH 17/44] Add ability to specify search path for SNOPT import with env var (#338) Co-authored-by: Marco Mangano <36549388+marcomangano@users.noreply.github.com> Co-authored-by: Eirikur Jonsson <36180221+eirikurj@users.noreply.github.com> --- doc/contribute.rst | 2 +- doc/optimizers/SNOPT.rst | 19 +++++++++++++++ pyoptsparse/pySNOPT/pySNOPT.py | 44 +++++++++++++++++++++++++++++----- tests/test_other.py | 17 +++++++++++++ 4 files changed, 75 insertions(+), 7 deletions(-) create mode 100644 tests/test_other.py diff --git a/doc/contribute.rst b/doc/contribute.rst index 40f2efea..f6fc4574 100644 --- a/doc/contribute.rst +++ b/doc/contribute.rst @@ -46,7 +46,7 @@ To install these dependencies, type .. prompt:: bash - pip install sphinx numpydoc sphinx-rtd-theme + pip install sphinx numpydoc sphinx-rtd-theme sphinx_mdolab_theme Testing ------- diff --git a/doc/optimizers/SNOPT.rst b/doc/optimizers/SNOPT.rst index 4e5b0492..f5257f1c 100644 --- a/doc/optimizers/SNOPT.rst +++ b/doc/optimizers/SNOPT.rst @@ -13,6 +13,9 @@ BFGS quasi-Newton update. Installation ------------ + +Building from source +******************** SNOPT is available for purchase `here `_. Upon purchase, you should receive a zip file. Within the zip file, there is a folder called ``src``. To use SNOPT with pyoptsparse, paste all files from ``src`` except snopth.f into ``pyoptsparse/pySNOPT/source``. @@ -20,6 +23,22 @@ From v2.0 onwards, only SNOPT v7.7.x is officially supported. To use pyOptSparse with previous versions of SNOPT, please checkout release v1.2. We currently test v7.7.7 and v7.7.1. +Installation by conda +********************* +When installing via conda, all pyoptsparse binaries are pre-compiled and installed as part of the package. +However, the `snopt` binding module cannot be included as part of the package due to license restrictions. + +If you are installing via conda and would like to use SNOPT, you will need to build the `snopt` binding module on your own, and inform `pyoptsparse` that it should use that library. + +Suppose you have built the binding file, producing ``snopt.cpython-310.so``, living in the folder ``~/snopt-bind``. + +To use this module, set the environment variable, ``PYOPTSPARSE_IMPORT_SNOPT_FROM``, e.g.: + +.. code-block:: bash + + PYOPTSPARSE_IMPORT_SNOPT_FROM=~/snopt-bind/ + +This will attempt to load the ``snopt`` binding module from ``~/snopt-bind``. If the module cannot be loaded from this path, a warning will be raised at import time, and an error will be raised if attempting to run the SNOPT optimizer. Options ------- diff --git a/pyoptsparse/pySNOPT/pySNOPT.py b/pyoptsparse/pySNOPT/pySNOPT.py index ed8749db..c2f37818 100644 --- a/pyoptsparse/pySNOPT/pySNOPT.py +++ b/pyoptsparse/pySNOPT/pySNOPT.py @@ -2,17 +2,14 @@ pySNOPT - A variation of the pySNOPT wrapper specificially designed to work with sparse optimization problems. """ -# Compiled module -try: - from . import snopt # isort: skip -except ImportError: - snopt = None # Standard Python modules import datetime import os import re +import sys import time from typing import Any, Dict, Optional, Tuple +import warnings # External modules from baseclasses.utils import CaseInsensitiveSet @@ -26,6 +23,38 @@ from ..pyOpt_utils import ICOL, IDATA, INFINITY, IROW, extractRows, mapToCSC, scaleRows +def _import_snopt_from_path(path): + """Attempt to import snopt from a specific path. Return the loaded module, or `None` if snopt cannot be imported.""" + path = os.path.abspath(os.path.expandvars(os.path.expanduser(path))) + orig_path = sys.path + sys.path = [path] + try: + import snopt # isort: skip + except ImportError: + warnings.warn( + f"`snopt` module could not be imported from {path}.", + ImportWarning, + stacklevel=2, + ) + snopt = None + finally: + sys.path = orig_path + return snopt + + +# Compiled module +_IMPORT_SNOPT_FROM = os.environ.get("PYOPTSPARSE_IMPORT_SNOPT_FROM", None) +if _IMPORT_SNOPT_FROM is not None: + # if a specific import path is specified, attempt to load SNOPT from it + snopt = _import_snopt_from_path(_IMPORT_SNOPT_FROM) +else: + # otherwise, load it relative to this file + try: + from . import snopt # isort: skip + except ImportError: + snopt = None + + class SNOPT(Optimizer): """ SNOPT Optimizer Class - Inherited from Optimizer Abstract Class @@ -108,7 +137,10 @@ def _getDefaultOptions() -> Dict[str, Any]: "Total character workspace": [int, None], "Total integer workspace": [int, None], "Total real workspace": [int, None], - "Save major iteration variables": [list, ["step", "merit", "feasibility", "optimality", "penalty"]], + "Save major iteration variables": [ + list, + ["step", "merit", "feasibility", "optimality", "penalty"], + ], "Return work arrays": [bool, False], "snSTOP function handle": [(type(None), type(lambda: None)), None], } diff --git a/tests/test_other.py b/tests/test_other.py new file mode 100644 index 00000000..dce516a2 --- /dev/null +++ b/tests/test_other.py @@ -0,0 +1,17 @@ +# Standard Python modules +import sys +import unittest + +# First party modules +from pyoptsparse.pySNOPT.pySNOPT import _import_snopt_from_path + + +class TestImportSnoptFromPath(unittest.TestCase): + def test_nonexistent_path(self): + with self.assertWarns(ImportWarning): + self.assertIsNone(_import_snopt_from_path("/a/nonexistent/path")) + + def test_sys_path_unchanged(self): + path = tuple(sys.path) + _import_snopt_from_path("/some/path") + self.assertEqual(tuple(sys.path), path) From 298c48cb06781ade51638dff919acfc0c1f4de44 Mon Sep 17 00:00:00 2001 From: Eirikur Jonsson Date: Mon, 1 May 2023 18:10:13 +0000 Subject: [PATCH 18/44] version bump --- pyoptsparse/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyoptsparse/__init__.py b/pyoptsparse/__init__.py index 8d3d7656..41f3088a 100644 --- a/pyoptsparse/__init__.py +++ b/pyoptsparse/__init__.py @@ -1,4 +1,4 @@ -__version__ = "2.9.3" +__version__ = "2.9.4" from .pyOpt_history import History from .pyOpt_variable import Variable From 6acea68b0cb78897b8f65aa1cc922145751563f4 Mon Sep 17 00:00:00 2001 From: Neil Wu <602725+nwu63@users.noreply.github.com> Date: Thu, 4 May 2023 20:22:05 -0400 Subject: [PATCH 19/44] Remove flaky tests (#342) * remove flaky timing-based test * skip multiobjective NSGA2 tests * skip only the n_obj=2 test --- tests/test_nsga2_multi_objective.py | 2 ++ tests/testing_utils.py | 3 --- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_nsga2_multi_objective.py b/tests/test_nsga2_multi_objective.py index 37c4016c..0fe7f57a 100644 --- a/tests/test_nsga2_multi_objective.py +++ b/tests/test_nsga2_multi_objective.py @@ -45,6 +45,8 @@ def setup_optProb(self, n_obj): @parameterized.expand([(1,), (2,)]) def test_opt(self, n_obj): + if n_obj == 2: + raise unittest.SkipTest("skip flaky NSGA2 tests") self.setup_optProb(n_obj) # 300 generations will find x=(0,0), 200 or less will find x=(1,1) diff --git a/tests/testing_utils.py b/tests/testing_utils.py index 21ef383f..b0bccd73 100644 --- a/tests/testing_utils.py +++ b/tests/testing_utils.py @@ -326,9 +326,6 @@ def check_hist_file(self, tol): times = hist.getValues(names="time", major=False)["time"] # the times should be monotonically increasing self.assertTrue(np.all(np.diff(times) > 0)) - # the final time should be close to the metadata time - # we only specify a relatively loose atol because of variations in overhead cost between machines - assert_allclose(times[-1], metadata["optTime"], atol=1.0) # this check is only used for optimizers that guarantee '0' and 'last' contain funcs if self.optName in ["SNOPT", "PSQP"]: From 33cca24ae9fa94ea1d4cde440f72465828a6b00b Mon Sep 17 00:00:00 2001 From: Neil Wu <602725+nwu63@users.noreply.github.com> Date: Tue, 9 May 2023 13:06:06 -0700 Subject: [PATCH 20/44] provide Solution in top-level init (#341) --- pyoptsparse/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyoptsparse/__init__.py b/pyoptsparse/__init__.py index 41f3088a..d38556e7 100644 --- a/pyoptsparse/__init__.py +++ b/pyoptsparse/__init__.py @@ -7,6 +7,7 @@ from .pyOpt_objective import Objective from .pyOpt_optimization import Optimization from .pyOpt_optimizer import Optimizer, OPT, Optimizers +from .pyOpt_solution import Solution # Now import all the individual optimizers from .pySNOPT.pySNOPT import SNOPT From cd1c2a981d2956ef29241a95c300ef9ac2a02f8e Mon Sep 17 00:00:00 2001 From: Neil Wu <602725+nwu63@users.noreply.github.com> Date: Tue, 9 May 2023 13:34:39 -0700 Subject: [PATCH 21/44] Remove some warnings (#343) * remove unnecessary warning about mpi4py * remove very old deprecation warnings * fix flake8 * really struggling today * version bump --- pyoptsparse/__init__.py | 2 +- pyoptsparse/pyOpt_MPI.py | 12 ++---------- pyoptsparse/pyOpt_optimization.py | 14 -------------- 3 files changed, 3 insertions(+), 25 deletions(-) diff --git a/pyoptsparse/__init__.py b/pyoptsparse/__init__.py index d38556e7..83810ef8 100644 --- a/pyoptsparse/__init__.py +++ b/pyoptsparse/__init__.py @@ -1,4 +1,4 @@ -__version__ = "2.9.4" +__version__ = "2.10.0" from .pyOpt_history import History from .pyOpt_variable import Variable diff --git a/pyoptsparse/pyOpt_MPI.py b/pyoptsparse/pyOpt_MPI.py index d97bb008..e1ac263d 100644 --- a/pyoptsparse/pyOpt_MPI.py +++ b/pyoptsparse/pyOpt_MPI.py @@ -8,9 +8,6 @@ # Standard Python modules import os -import warnings - -# isort: off class COMM: @@ -50,6 +47,7 @@ def __init__(self): # and raise exception on failure. If set to anything else, no import is attempted. if "PYOPTSPARSE_REQUIRE_MPI" in os.environ: if os.environ["PYOPTSPARSE_REQUIRE_MPI"].lower() in ["always", "1", "true", "yes"]: + # External modules from mpi4py import MPI else: MPI = myMPI() @@ -57,13 +55,7 @@ def __init__(self): # with a notification. else: try: + # External modules from mpi4py import MPI except ImportError: - warn = ( - "mpi4py could not be imported. mpi4py is required to use " - + "the parallel gradient analysis and parallel objective analysis for " - + "non-gradient based optimizers. Continuing using a dummy MPI module " - + "from pyOptSparse." - ) - warnings.warn(warn, stacklevel=2) MPI = myMPI() diff --git a/pyoptsparse/pyOpt_optimization.py b/pyoptsparse/pyOpt_optimization.py index 607ca4bb..32a55281 100644 --- a/pyoptsparse/pyOpt_optimization.py +++ b/pyoptsparse/pyOpt_optimization.py @@ -4,7 +4,6 @@ import os import pickle from typing import Callable, Dict, Iterable, List, Optional, Tuple, Union -import warnings # External modules import numpy as np @@ -227,11 +226,6 @@ def addVarGroup( f"The 'nVars' argument to addVarGroup must be greater than or equal to 1. The bad DV is {name}." ) - # we let type overwrite the newer varType option name - if "type" in kwargs: - varType = kwargs["type"] - # but we also throw a deprecation warning - warnings.warn("The argument `type=` is deprecated. Use `varType` in the future.", stacklevel=2) # Check that the type is ok if varType not in ["c", "i", "d"]: raise Error("Type must be one of 'c' for continuous, 'i' for integer or 'd' for discrete.") @@ -816,10 +810,6 @@ def finalize(self): self._finalizeConstraints() self.finalized = True - def finalizeDesignVariables(self): - warnings.warn("finalizeDesignVariables() is deprecated, use _finalizeDesignVariables() instead.", stacklevel=2) - self._finalizeDesignVariables() - def _finalizeDesignVariables(self): """ Communicate design variables potentially from different @@ -844,10 +834,6 @@ def _finalizeDesignVariables(self): dvCounter += n self.ndvs = dvCounter - def finalizeConstraints(self): - warnings.warn("finalizeConstraints() is deprecated, use _finalizeConstraints() instead.", stacklevel=2) - self._finalizeConstraints() - def _finalizeConstraints(self): """ There are several functions for this routine: From b029888d016e8d69c845cec5e9f3afdac2cfc10c Mon Sep 17 00:00:00 2001 From: Neil Wu <602725+nwu63@users.noreply.github.com> Date: Tue, 16 May 2023 11:20:02 -0700 Subject: [PATCH 22/44] Remove callables from optProb only when serializing in History file (#344) * remove callables from optProb only when serializing in History file * I lied about checking flake8 locally --- pyoptsparse/pyOpt_optimization.py | 11 +---------- pyoptsparse/pyOpt_optimizer.py | 8 ++++++-- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/pyoptsparse/pyOpt_optimization.py b/pyoptsparse/pyOpt_optimization.py index 32a55281..781455d6 100644 --- a/pyoptsparse/pyOpt_optimization.py +++ b/pyoptsparse/pyOpt_optimization.py @@ -2,7 +2,6 @@ from collections import OrderedDict import copy import os -import pickle from typing import Callable, Dict, Iterable, List, Optional, Tuple, Union # External modules @@ -1761,15 +1760,7 @@ def __getstate__(self) -> dict: The un-serializable fields are deleted first. """ d = copy.copy(self.__dict__) - keysToRemove = ["comm"] - try: - pickle.dumps(self.objFun) - except Exception: - # Use a blanket exception because pickle errors are unreliable - # Tests raise RecursionError - # mpi4py raises TypeError - keysToRemove.append("objFun") - for key in keysToRemove: + for key in ["comm"]: if key in d.keys(): del d[key] return d diff --git a/pyoptsparse/pyOpt_optimizer.py b/pyoptsparse/pyOpt_optimizer.py index b4c848a2..b3004102 100644 --- a/pyoptsparse/pyOpt_optimizer.py +++ b/pyoptsparse/pyOpt_optimizer.py @@ -592,14 +592,18 @@ def _masterFunc2(self, x, evaluate, writeHist=True): for objKey in self.optProb.objectives.keys(): objInfo[objKey] = {"scale": self.optProb.objectives[objKey].scale} - # There is a special write for the bounds data + # There is a special write for additional metadata if self.storeHistory: self.hist.writeData("varInfo", varInfo) self.hist.writeData("conInfo", conInfo) self.hist.writeData("objInfo", objInfo) self._setMetadata() self.hist.writeData("metadata", self.metadata) - self.hist.writeData("optProb", self.optProb) + # we have to get rid of some callables in optProb before serialization + optProb = copy.copy(self.optProb) + optProb.objFun = None + optProb.sens = None + self.hist.writeData("optProb", optProb) # Write history if necessary if self.optProb.comm.rank == 0 and writeHist and self.storeHistory: From c7b37848b356861ac33ffec6801430cc2153c715 Mon Sep 17 00:00:00 2001 From: Neil Wu <602725+nwu63@users.noreply.github.com> Date: Mon, 29 May 2023 02:55:42 -0700 Subject: [PATCH 23/44] Save more variables by default in SNOPT history file (#347) * fixed SNOPT import test * store more things from snstop by default * updated docs * address some comments * addressed comments * fix typo --- doc/optimizers/SNOPT_options.yaml | 24 +++++++++++++-- pyoptsparse/pySNOPT/pySNOPT.py | 51 +++++++++++++++++-------------- tests/test_hs015.py | 2 +- tests/test_other.py | 7 ++++- tests/test_rosenbrock.py | 6 ++-- 5 files changed, 58 insertions(+), 32 deletions(-) diff --git a/doc/optimizers/SNOPT_options.yaml b/doc/optimizers/SNOPT_options.yaml index 95ae03ce..2e043336 100644 --- a/doc/optimizers/SNOPT_options.yaml +++ b/doc/optimizers/SNOPT_options.yaml @@ -71,9 +71,27 @@ Total real workspace: Save major iteration variables: desc: > - This option is unique to the Python wrapper, and takes a list of values which can be saved at each iteration to the History file. - This specifies the list of major iteration variables to be stored in the history file. - ``Hessian``, ``slack``, ``lambda`` and ``condZHZ`` are also supported. + This option is unique to the Python wrapper, and takes a list of values which can be saved at each major iteration to the History file. + The possible values are + + - ``Hessian`` + - ``slack`` + - ``lambda`` + - ``nS`` + - ``BSwap`` + - ``maxVi`` + - ``penalty_vector`` + + In addition, a set of default parameters are saved to the history file and cannot be changed. These are + + - ``nMajor`` + - ``nMinor`` + - ``step`` + - ``feasibility`` + - ``optimality`` + - ``merit`` + - ``condZHZ`` + - ``penalty`` Return work arrays: desc: > diff --git a/pyoptsparse/pySNOPT/pySNOPT.py b/pyoptsparse/pySNOPT/pySNOPT.py index c2f37818..1a76b1a5 100644 --- a/pyoptsparse/pySNOPT/pySNOPT.py +++ b/pyoptsparse/pySNOPT/pySNOPT.py @@ -137,10 +137,7 @@ def _getDefaultOptions() -> Dict[str, Any]: "Total character workspace": [int, None], "Total integer workspace": [int, None], "Total real workspace": [int, None], - "Save major iteration variables": [ - list, - ["step", "merit", "feasibility", "optimality", "penalty"], - ], + "Save major iteration variables": [list, []], "Return work arrays": [bool, False], "snSTOP function handle": [(type(None), type(lambda: None)), None], } @@ -617,7 +614,7 @@ def _getHessian(self, iw, rw): H = np.matmul(Umat.T, Umat) return H - def _getPenaltyParam(self, iw, rw): + def _getPenaltyVector(self, iw, rw): """ Retrieves the full penalty parameter vector from the work arrays. """ @@ -627,43 +624,51 @@ def _getPenaltyParam(self, iw, rw): 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): + 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 the callback is performed at the end of this function. + If 'snSTOP function handle' is set to a function handle, then it is called at the end of this function. - returning with iabort != 0 will terminate SNOPT immediately + 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 == "merit": - iterDict[saveVar] = fmerit - elif saveVar == "feasibility": - iterDict[saveVar] = primalinf - elif saveVar == "optimality": - iterDict[saveVar] = dualinf - elif saveVar == "penalty": - penParam = self._getPenaltyParam(iw, rw) - iterDict[saveVar] = penParam + if saveVar == "penalty_vector": + iterDict[saveVar] = self._getPenaltyVector(iw, rw), elif saveVar == "Hessian": H = self._getHessian(iw, rw) iterDict[saveVar] = H - elif saveVar == "step": - iterDict[saveVar] = step - elif saveVar == "condZHZ": - iterDict[saveVar] = condzhz 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: diff --git a/tests/test_hs015.py b/tests/test_hs015.py index 1bb5cbc7..a7870044 100644 --- a/tests/test_hs015.py +++ b/tests/test_hs015.py @@ -98,7 +98,7 @@ def setup_optProb(self): def test_snopt(self): self.optName = "SNOPT" self.setup_optProb() - store_vars = ["step", "merit", "feasibility", "optimality", "penalty", "Hessian", "condZHZ", "slack", "lambda"] + store_vars = ["Hessian", "slack", "lambda", "penalty_vector", "nS", "BSwap", "maxVi"] optOptions = {"Save major iteration variables": store_vars} self.optimize_with_hotstart(1e-12, optOptions=optOptions) diff --git a/tests/test_other.py b/tests/test_other.py index dce516a2..98d0996d 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -1,9 +1,14 @@ # Standard Python modules +import os import sys import unittest +# we have to unset this environment variable because otherwise when we import `_import_snopt_from_path` +# the snopt module gets automatically imported, thus failing the import test below +os.environ.pop("PYOPTSPARSE_IMPORT_SNOPT_FROM", None) + # First party modules -from pyoptsparse.pySNOPT.pySNOPT import _import_snopt_from_path +from pyoptsparse.pySNOPT.pySNOPT import _import_snopt_from_path # noqa: E402 class TestImportSnoptFromPath(unittest.TestCase): diff --git a/tests/test_rosenbrock.py b/tests/test_rosenbrock.py index cf31618b..b590d605 100644 --- a/tests/test_rosenbrock.py +++ b/tests/test_rosenbrock.py @@ -101,10 +101,8 @@ def setup_optProb(self): def test_snopt(self): self.optName = "SNOPT" self.setup_optProb() - store_vars = ["step", "merit", "feasibility", "optimality", "penalty", "Hessian", "condZHZ", "slack", "lambda"] - optOptions = { - "Save major iteration variables": store_vars, - } + store_vars = ["Hessian", "slack", "lambda", "penalty_vector", "nS", "BSwap", "maxVi"] + optOptions = {"Save major iteration variables": store_vars} self.optimize_with_hotstart(1e-8, optOptions=optOptions) hist = History(self.histFileName, flag="r") From 71013501310421c2a3b48e527851a448e8125d8b Mon Sep 17 00:00:00 2001 From: Neil Wu <602725+nwu63@users.noreply.github.com> Date: Fri, 2 Jun 2023 01:54:42 -0700 Subject: [PATCH 24/44] More tolerant scalar check (#348) * more tolerant scalar check * refactor broadcast of value/lower/upper etc. * isort * change allow_none to default False * add missing backtick, good job flake8 * version bump --- pyoptsparse/__init__.py | 2 +- pyoptsparse/pyOpt_constraint.py | 40 ++------------- pyoptsparse/pyOpt_optimization.py | 83 +++++++------------------------ pyoptsparse/pyOpt_utils.py | 41 +++++++++++++++ pyoptsparse/types.py | 13 ++--- 5 files changed, 70 insertions(+), 109 deletions(-) diff --git a/pyoptsparse/__init__.py b/pyoptsparse/__init__.py index 83810ef8..1ea04cc7 100644 --- a/pyoptsparse/__init__.py +++ b/pyoptsparse/__init__.py @@ -1,4 +1,4 @@ -__version__ = "2.10.0" +__version__ = "2.10.1" from .pyOpt_history import History from .pyOpt_variable import Variable diff --git a/pyoptsparse/pyOpt_constraint.py b/pyoptsparse/pyOpt_constraint.py index 76444bd2..80cfe85c 100644 --- a/pyoptsparse/pyOpt_constraint.py +++ b/pyoptsparse/pyOpt_constraint.py @@ -8,7 +8,7 @@ # Local modules from .pyOpt_error import Error, pyOptSparseWarning -from .pyOpt_utils import INFINITY, convertToCOO +from .pyOpt_utils import INFINITY, _broadcast_to_array, convertToCOO from .types import Dict1DType @@ -43,41 +43,9 @@ def __init__( # Before we can do the processing below we need to have lower # and upper arguments expanded: - if lower is None: - lower = [None for i in range(self.ncon)] - elif np.isscalar(lower): - lower = lower * np.ones(self.ncon) - elif len(lower) == self.ncon: - pass # Some iterable object - else: - raise Error( - "The 'lower' argument to addCon or addConGroup is invalid. " - + f"It must be None, a scalar, or a list/array or length nCon={nCon}." - ) - - if upper is None: - upper = [None for i in range(self.ncon)] - elif np.isscalar(upper): - upper = upper * np.ones(self.ncon) - elif len(upper) == self.ncon: - pass # Some iterable object - else: - raise Error( - "The 'upper' argument to addCon or addConGroup is invalid. " - + f"It must be None, a scalar, or a list/array or length nCon={nCon}." - ) - - # ------ Process the scale argument - scale = np.atleast_1d(scale) - if len(scale) == 1: - scale = scale[0] * np.ones(nCon) - elif len(scale) == nCon: - pass - else: - raise Error( - f"The length of the 'scale' argument to addCon or addConGroup is {len(scale)}, " - + f"but the number of constraints is {nCon}." - ) + lower = _broadcast_to_array("lower", lower, nCon, allow_none=True) + upper = _broadcast_to_array("upper", upper, nCon, allow_none=True) + scale = _broadcast_to_array("scale", scale, nCon) # Save lower and upper...they are only used for printing however self.lower = lower diff --git a/pyoptsparse/pyOpt_optimization.py b/pyoptsparse/pyOpt_optimization.py index 781455d6..ac41174d 100644 --- a/pyoptsparse/pyOpt_optimization.py +++ b/pyoptsparse/pyOpt_optimization.py @@ -15,7 +15,18 @@ from .pyOpt_constraint import Constraint from .pyOpt_error import Error from .pyOpt_objective import Objective -from .pyOpt_utils import ICOL, IDATA, INFINITY, IROW, convertToCOO, convertToCSR, mapToCSR, scaleColumns, scaleRows +from .pyOpt_utils import ( + ICOL, + IDATA, + INFINITY, + IROW, + _broadcast_to_array, + convertToCOO, + convertToCSR, + mapToCSR, + scaleColumns, + scaleRows, +) from .pyOpt_variable import Variable from .types import Dict1DType, Dict2DType, NumpyType @@ -229,71 +240,11 @@ def addVarGroup( if varType not in ["c", "i", "d"]: raise Error("Type must be one of 'c' for continuous, 'i' for integer or 'd' for discrete.") - # ------ Process the value argument - value = np.atleast_1d(value).real - if len(value) == 1: - value = value[0] * np.ones(nVars) - elif len(value) == nVars: - pass - else: - raise Error( - f"The length of the 'value' argument to addVarGroup is {len(value)}, " - + f"but the number of variables in nVars is {nVars}." - ) - - if lower is None: - lower = [None for i in range(nVars)] - elif np.isscalar(lower): - lower = lower * np.ones(nVars) - elif len(lower) == nVars: - lower = np.atleast_1d(lower).real - else: - raise Error( - "The 'lower' argument to addVarGroup is invalid. " - + f"It must be None, a scalar, or a list/array or length nVars={nVars}." - ) - - if upper is None: - upper = [None for i in range(nVars)] - elif np.isscalar(upper): - upper = upper * np.ones(nVars) - elif len(upper) == nVars: - upper = np.atleast_1d(upper).real - else: - raise Error( - "The 'upper' argument to addVarGroup is invalid. " - + f"It must be None, a scalar, or a list/array or length nVars={nVars}." - ) - - # ------ Process the scale argument - if scale is None: - scale = np.ones(nVars) - else: - scale = np.atleast_1d(scale) - if len(scale) == 1: - scale = scale[0] * np.ones(nVars) - elif len(scale) == nVars: - pass - else: - raise Error( - f"The length of the 'scale' argument to addVarGroup is {len(scale)}, " - + f"but the number of variables in nVars is {nVars}." - ) - - # ------ Process the offset argument - if offset is None: - offset = np.ones(nVars) - else: - offset = np.atleast_1d(offset) - if len(offset) == 1: - offset = offset[0] * np.ones(nVars) - elif len(offset) == nVars: - pass - else: - raise Error( - f"The length of the 'offset' argument to addVarGroup is {len(offset)}, " - + f"but the number of variables is {nVars}." - ) + value = _broadcast_to_array("value", value, nVars) + lower = _broadcast_to_array("lower", lower, nVars, allow_none=True) + upper = _broadcast_to_array("upper", upper, nVars, allow_none=True) + scale = _broadcast_to_array("scale", scale, nVars) + offset = _broadcast_to_array("offset", offset, nVars) # Determine if scalar i.e. it was called from addVar(): scalar = kwargs.pop("scalar", False) diff --git a/pyoptsparse/pyOpt_utils.py b/pyoptsparse/pyOpt_utils.py index b9f65c0f..e67eabb7 100644 --- a/pyoptsparse/pyOpt_utils.py +++ b/pyoptsparse/pyOpt_utils.py @@ -20,6 +20,7 @@ # Local modules from .pyOpt_error import Error +from .types import ArrayType # Define index mnemonics IROW = 0 @@ -529,3 +530,43 @@ def _csc_to_coo(mat: dict) -> dict: coo_data = np.array(data) return {"coo": [coo_rows, coo_cols, coo_data], "shape": mat["shape"]} + + +def _broadcast_to_array(name: str, value: ArrayType, n_values: int, allow_none: bool = False): + """ + Broadcast an input to an array with a specified length + + Parameters + ---------- + name : str + The name of the input. This is only used in the error message emitted. + value : float, list[float], numpy array + The input value + n_values : int + The number of values + allow_none : bool, optional + Whether to allow `None` in the input/output, by default False + + Returns + ------- + NDArray + An array with the shape ``(n_values)`` + + Raises + ------ + Error + If either the input is not broadcastable, or if the input contains None and ``allow_none=False``. + + Warnings + -------- + Note that the default value for ``allow_none`` is False. + """ + try: + value = np.broadcast_to(value, n_values) + except ValueError: + raise Error( + f"The '{name}' argument is invalid. It must be None, a scalar, or a list/array or length {n_values}." + ) + if not allow_none and any([i is None for i in value]): + raise Error(f"The {name} argument cannot be 'None'.") + return value diff --git a/pyoptsparse/types.py b/pyoptsparse/types.py index 7315341e..e20513b9 100644 --- a/pyoptsparse/types.py +++ b/pyoptsparse/types.py @@ -1,14 +1,15 @@ # Standard Python modules -from typing import Dict, List, Union +from typing import Dict, Sequence, Union # External modules -from numpy import ndarray +import numpy as np +import numpy.typing as npt # Either ndarray or scalar -NumpyType = Union[float, ndarray] +NumpyType = Union[float, npt.NDArray[np.float_]] # ndarray, list of numbers, or scalar -ArrayType = Union[float, List[float], ndarray] +ArrayType = Union[NumpyType, Sequence[float]] # funcs -Dict1DType = Dict[str, ndarray] +Dict1DType = Dict[str, npt.NDArray[np.float_]] # funcsSens -Dict2DType = Dict[str, Dict[str, ndarray]] +Dict2DType = Dict[str, Dict[str, npt.NDArray[np.float_]]] From fe2a819596d7159bf31b13f01de0fd901503a1ae Mon Sep 17 00:00:00 2001 From: Neil Wu <602725+nwu63@users.noreply.github.com> Date: Thu, 8 Jun 2023 09:07:58 -0700 Subject: [PATCH 25/44] adjust SNOPT option formatting (#349) --- doc/optimizers/SNOPT_options.yaml | 2 +- pyoptsparse/pySNOPT/pySNOPT.py | 26 +++++++++++--------------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/doc/optimizers/SNOPT_options.yaml b/doc/optimizers/SNOPT_options.yaml index 2e043336..0c740234 100644 --- a/doc/optimizers/SNOPT_options.yaml +++ b/doc/optimizers/SNOPT_options.yaml @@ -70,7 +70,7 @@ Total real workspace: User-specified values are not overwritten. Save major iteration variables: - desc: > + desc: | This option is unique to the Python wrapper, and takes a list of values which can be saved at each major iteration to the History file. The possible values are diff --git a/pyoptsparse/pySNOPT/pySNOPT.py b/pyoptsparse/pySNOPT/pySNOPT.py index 1a76b1a5..0408521d 100644 --- a/pyoptsparse/pySNOPT/pySNOPT.py +++ b/pyoptsparse/pySNOPT/pySNOPT.py @@ -1,5 +1,5 @@ """ -pySNOPT - A variation of the pySNOPT wrapper specificially designed to +pySNOPT - A variation of the pySNOPT wrapper specifically designed to work with sparse optimization problems. """ # Standard Python modules @@ -57,14 +57,10 @@ def _import_snopt_from_path(path): class SNOPT(Optimizer): """ - SNOPT Optimizer Class - Inherited from Optimizer Abstract Class + SNOPT Optimizer Class """ def __init__(self, raiseError=True, options: Dict = {}): - """ - SNOPT Optimizer Class Initialization - """ - name = "SNOPT" category = "Local Optimizer" defOpts = self._getDefaultOptions() @@ -227,20 +223,20 @@ def __call__( 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 + 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 + `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'. + ``1e-6`` when sens is `FD` and ``1e-40j`` when sens is `CS`. sensMode : str - Use 'pgc' for parallel gradient computations. Only + Use `pgc` for parallel gradient computations. Only available with mpi4py and each objective evaluation is otherwise serial @@ -251,8 +247,8 @@ def __call__( 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 + 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 @@ -277,7 +273,7 @@ def __call__( sol : Solution object The optimization solution restartDict : dict - If 'Return work arrays' is True, a dictionary of arrays is also returned + If `Return work arrays` is True, a dictionary of arrays is also returned """ self.startTime = time.time() self.callCounter = 0 @@ -631,7 +627,7 @@ def _snstop(self, ktcond, mjrprtlvl, minimize, n, nncon, nnobj, ns, itn, nmajor, """ 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. + If `snSTOP function handle` is set to a function handle, then it is called at the end of this function. Returns ------- From 7e01f2cef20e7f5788750f997af6ea20523f3e90 Mon Sep 17 00:00:00 2001 From: Shugo Kaneko <49300827+kanekosh@users.noreply.github.com> Date: Fri, 30 Jun 2023 06:26:23 -0400 Subject: [PATCH 26/44] Installation docs update (#350) Co-authored-by: Marco Mangano <36549388+marcomangano@users.noreply.github.com> Co-authored-by: Eirikur Jonsson --- doc/install.rst | 12 +++++++++++- doc/optimizers/IPOPT.rst | 2 ++ doc/optimizers/SNOPT.rst | 3 +++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/doc/install.rst b/doc/install.rst index d4ad85fb..0cb472ff 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -12,7 +12,16 @@ Conda packages are available on ``conda-forge`` and can be installed via conda install -c conda-forge pyoptsparse This would install pyOptSparse with the built-in optimizers, as well as IPOPT. -If you wish to use optimizers not packaged by ``conda``, e.g. SNOPT, then you must build the package from source. +If you wish to use optimizers not packaged by ``conda``, e.g. SNOPT, then you must either build the package from source or use the installation script below. +If you have the SNOPT precompiled library available, it is possible to dynamically link it to pyOptSparse following the instructions on the :ref:`SNOPT installation page`. + +Using an installation script +---------------------------- +You can build and install pyOptsparse using a `Python script `_ developed by the OpenMDAO team. +For usage, see the instruction on the README of the repo. + +This script is particularly useful for installing :ref:`IPOPT` and its dependencies. +It can also support SNOPT installation if you have access to the SNOPT source code. Building from source -------------------- @@ -37,6 +46,7 @@ The only exception is ``numpy``, which is required as part of the build process * In Linux, the python header files (``python-dev``) are also required. * **We do not support operating systems other than Linux.** For macOS users, the conda package may work out of the box if you do not need any non-default optimizers. + Also, the installation script by OpenMDAO likely works on macOS. For Windows users, a conda package is on the way, if it's not already in the repos. This comes with the same disclaimer as the macOS conda package. Alternatively, follow the :ref:`conda build instructions` below as this will work on any platform. diff --git a/doc/optimizers/IPOPT.rst b/doc/optimizers/IPOPT.rst index ebd03bc0..dd2ff586 100644 --- a/doc/optimizers/IPOPT.rst +++ b/doc/optimizers/IPOPT.rst @@ -1,3 +1,5 @@ +.. _ipopt: + IPOPT ===== IPOPT (Interior Point OPTimizer) is an open source interior point optimizer, designed for large-scale nonlinear optimization. diff --git a/doc/optimizers/SNOPT.rst b/doc/optimizers/SNOPT.rst index f5257f1c..beeb42ec 100644 --- a/doc/optimizers/SNOPT.rst +++ b/doc/optimizers/SNOPT.rst @@ -25,6 +25,9 @@ We currently test v7.7.7 and v7.7.1. Installation by conda ********************* + +.. _snopt_by_conda: + When installing via conda, all pyoptsparse binaries are pre-compiled and installed as part of the package. However, the `snopt` binding module cannot be included as part of the package due to license restrictions. From 304177cc6f1af8085b3a71fabb087d73df63e35b Mon Sep 17 00:00:00 2001 From: Neil Wu <602725+nwu63@users.noreply.github.com> Date: Tue, 22 Aug 2023 10:05:45 -0700 Subject: [PATCH 27/44] Make nsga2 tests optional + docs update (#351) * update docs for NSGA2 * make nsga2 tests optional * add an nsga2 test to rosenbrock * fix tests * remove nsga2 from rosenbrock --- doc/install.rst | 5 ----- tests/test_nsga2_multi_objective.py | 18 ++++++++++++------ tests/testing_utils.py | 2 +- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/doc/install.rst b/doc/install.rst index 0cb472ff..4b1ea597 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -35,12 +35,7 @@ pyOptSparse has the following dependencies: Please make sure these are installed and available for use. In order to use NSGA2, SWIG (v1.3+) is also required, which can be installed via the package manager. -If those optimizers are not needed, then you do not need to install SWIG. -Simply comment out the corresponding lines in ``pyoptsparse/pyoptsparse/meson.build`` so that they are not compiled. -The corresponding lines in ``pyoptsparse/pyoptsparse/__init__.py`` must be commented out as well. - Python dependencies are automatically handled by ``pip``, so they do not need to be installed separately. -The only exception is ``numpy``, which is required as part of the build process and therefore must be present before installing. .. note:: * In Linux, the python header files (``python-dev``) are also required. diff --git a/tests/test_nsga2_multi_objective.py b/tests/test_nsga2_multi_objective.py index 0fe7f57a..614ff187 100644 --- a/tests/test_nsga2_multi_objective.py +++ b/tests/test_nsga2_multi_objective.py @@ -3,7 +3,6 @@ # Standard Python modules import sys import unittest -import warnings # External modules from numpy.testing import assert_allclose @@ -19,6 +18,14 @@ class TestNSGA2(OptTest): name = "quadratic" optName = "NSGA2" + histFileName = None + + def setUp(self): + try: + # First party modules + from pyoptsparse.pyNSGA2 import nsga2 # noqa: F401 + except ImportError: + raise unittest.SkipTest("Optimizer not available: NSGA2") def objfunc(self, xdict): x = xdict["x"] @@ -43,6 +50,9 @@ def setup_optProb(self, n_obj): if n_obj == 2: self.optProb.addObj("obj2") + @unittest.skipIf( + sys.platform == "win32", "test_nsga2_multi_objective.py fails on windows with two objectives! Skipping for now." + ) @parameterized.expand([(1,), (2,)]) def test_opt(self, n_obj): if n_obj == 2: @@ -51,11 +61,7 @@ def test_opt(self, n_obj): # 300 generations will find x=(0,0), 200 or less will find x=(1,1) optOptions = {"maxGen": 200} - if sys.platform == "win32": - warnings.warn( - "test_nsga2_multi_objective.py fails on windows with two objectives! Skipping for now.", stacklevel=2 - ) - return + sol = self.optimize(optOptions=optOptions) tol = 1e-2 if n_obj == 1: diff --git a/tests/testing_utils.py b/tests/testing_utils.py index b0bccd73..9d5a8a55 100644 --- a/tests/testing_utils.py +++ b/tests/testing_utils.py @@ -61,7 +61,7 @@ def get_dict_distance(d, d2): } # these are optimizers which are installed by default -DEFAULT_OPTIMIZERS = {"SLSQP", "PSQP", "CONMIN", "ALPSO", "NSGA2"} +DEFAULT_OPTIMIZERS = {"SLSQP", "PSQP", "CONMIN", "ALPSO"} # Define gradient-based optimizers GRAD_BASED_OPTIMIZERS = {"CONMIN", "IPOPT", "NLPQLP", "ParOpt", "PSQP", "SLSQP", "SNOPT"} From 01dece27ebadcdf9ab60382a1d1a7a4f85a051dc Mon Sep 17 00:00:00 2001 From: Eirikur Jonsson <36180221+eirikurj@users.noreply.github.com> Date: Tue, 22 Aug 2023 17:52:08 +0000 Subject: [PATCH 28/44] update .readthedocs.yaml (#352) Co-authored-by: Andrew Lamkin --- .readthedocs.yaml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..88e8046e --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,17 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +sphinx: + configuration: doc/conf.py + +python: + install: + - requirements: doc/requirements.txt From ccc40ca863c680c61e514062eeef1bcc0d3c834a Mon Sep 17 00:00:00 2001 From: Neil Wu <602725+nwu63@users.noreply.github.com> Date: Tue, 29 Aug 2023 08:45:47 -0700 Subject: [PATCH 29/44] test nsga2 single-objective case on Windows (#354) --- tests/test_nsga2_multi_objective.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/test_nsga2_multi_objective.py b/tests/test_nsga2_multi_objective.py index 614ff187..f41e6405 100644 --- a/tests/test_nsga2_multi_objective.py +++ b/tests/test_nsga2_multi_objective.py @@ -1,7 +1,6 @@ """ Test NSGA2.""" # Standard Python modules -import sys import unittest # External modules @@ -50,9 +49,6 @@ def setup_optProb(self, n_obj): if n_obj == 2: self.optProb.addObj("obj2") - @unittest.skipIf( - sys.platform == "win32", "test_nsga2_multi_objective.py fails on windows with two objectives! Skipping for now." - ) @parameterized.expand([(1,), (2,)]) def test_opt(self, n_obj): if n_obj == 2: From f56e159726ee8e413053503b2f058a7316934988 Mon Sep 17 00:00:00 2001 From: Ella Wu <602725+nwu63@users.noreply.github.com> Date: Mon, 20 Nov 2023 10:52:27 -0500 Subject: [PATCH 30/44] Remove deprecations (#362) * np.float_ -> np.float64 * fix numpy 1.25 deprecations * we now test everything * revert change to fact * trigger new build * remove multi-objective test instead of skipping --- .github/test_real.sh | 2 +- pyoptsparse/postprocessing/OptView.py | 2 +- pyoptsparse/pyALPSO/pyALPSO.py | 2 +- pyoptsparse/pyIPOPT/pyIPOPT.py | 4 ++-- pyoptsparse/pySLSQP/pySLSQP.py | 10 +++++----- pyoptsparse/pySNOPT/pySNOPT.py | 4 ++-- pyoptsparse/types.py | 6 +++--- tests/test_nsga2_multi_objective.py | 9 ++++++--- 8 files changed, 21 insertions(+), 18 deletions(-) diff --git a/.github/test_real.sh b/.github/test_real.sh index 9a4072a9..69eb3d54 100755 --- a/.github/test_real.sh +++ b/.github/test_real.sh @@ -3,7 +3,7 @@ set -e # all tests should pass on the private image # except for the Intel image, where IPOPT is not available -if [[ $IMAGE == "private" ]] && [[ $COMPILERS != "intel" ]]; then +if [[ $IMAGE == "private" ]]; then EXTRA_FLAGS='--disallow_skipped' fi diff --git a/pyoptsparse/postprocessing/OptView.py b/pyoptsparse/postprocessing/OptView.py index 6ec04619..472c4425 100644 --- a/pyoptsparse/postprocessing/OptView.py +++ b/pyoptsparse/postprocessing/OptView.py @@ -745,7 +745,7 @@ def save_tec(self): num_vars = len(keys) num_iters = len(dat[keys[0]]) - full_data = np.arange(num_iters, dtype=np.float_).reshape(num_iters, 1) + full_data = np.arange(num_iters, dtype=np.float64).reshape(num_iters, 1) var_names = ["Iteration"] for key in keys: small_data = np.asarray(dat[key]) diff --git a/pyoptsparse/pyALPSO/pyALPSO.py b/pyoptsparse/pyALPSO/pyALPSO.py index 6a8aef0b..465728ca 100644 --- a/pyoptsparse/pyALPSO/pyALPSO.py +++ b/pyoptsparse/pyALPSO/pyALPSO.py @@ -160,7 +160,7 @@ def objconfunc(x): if dyniI == 0: self.setOption("minInnerIter", opt("maxInnerIter")) - if not opt("stopCriteria") in [0, 1]: + if opt("stopCriteria") not in [0, 1]: raise Error("Incorrect Stopping Criteria Setting") if opt("fileout") not in [0, 1, 2, 3]: diff --git a/pyoptsparse/pyIPOPT/pyIPOPT.py b/pyoptsparse/pyIPOPT/pyIPOPT.py index 55aff372..f5233292 100644 --- a/pyoptsparse/pyIPOPT/pyIPOPT.py +++ b/pyoptsparse/pyIPOPT/pyIPOPT.py @@ -191,8 +191,8 @@ def __call__( jac = extractRows(jac, indices) # Does reordering scaleRows(jac, fact) # Perform logical scaling else: - blc = np.array([-INFINITY]) - buc = np.array([INFINITY]) + blc = np.array(-INFINITY) + buc = np.array(INFINITY) ncon = 1 jac = convertToCOO(jac) # Conver to coo format for IPOPT diff --git a/pyoptsparse/pySLSQP/pySLSQP.py b/pyoptsparse/pySLSQP/pySLSQP.py index d1d04684..58f3ee6e 100644 --- a/pyoptsparse/pySLSQP/pySLSQP.py +++ b/pyoptsparse/pySLSQP/pySLSQP.py @@ -188,7 +188,7 @@ def slgrad(m, me, la, n, f, g, df, dg, x): gg = np.zeros([la], float) df = np.zeros([n + 1], float) dg = np.zeros([la, n + 1], float) - acc = np.array([self.getOption("ACC")], float) + acc = np.array(self.getOption("ACC"), float) maxit = self.getOption("MAXIT") iprint = self.getOption("IPRINT") iout = self.getOption("IOUT") @@ -204,13 +204,13 @@ def slgrad(m, me, la, n, f, g, df, dg, x): lsei = ((n + 1) + mineq) * ((n + 1) - meq) + 2 * meq + (n + 1) slsqpb = (n + 1) * (n / 2) + 2 * m + 3 * n + 3 * (n + 1) + 1 lwM = lsq + lsi + lsei + slsqpb + n + m - lw = np.array([lwM], int) + lw = np.array(lwM, int) w = np.zeros(lw, float) ljwM = max(mineq, (n + 1) - meq) - ljw = np.array([ljwM], int) + ljw = np.array(ljwM, int) jw = np.zeros(ljw, np.intc) - nfunc = np.array([0], int) - ngrad = np.array([0], int) + nfunc = np.array(0, int) + ngrad = np.array(0, int) # Run SLSQP t0 = time.time() diff --git a/pyoptsparse/pySNOPT/pySNOPT.py b/pyoptsparse/pySNOPT/pySNOPT.py index 0408521d..bac40403 100644 --- a/pyoptsparse/pySNOPT/pySNOPT.py +++ b/pyoptsparse/pySNOPT/pySNOPT.py @@ -461,7 +461,7 @@ def __call__( # Setup argument list values start = np.array(self.getOption("Start")) - ObjAdd = np.array([0.0], float) + ObjAdd = np.array(0.0, float) ProbNm = np.array(self.optProb.name, "c") cdummy = -1111111 # this is a magic variable defined in SNOPT for undefined strings cw[51, :] = cdummy # we set these to cdummy so that a placeholder is used in printout @@ -704,7 +704,7 @@ def _set_snopt_options(self, iPrint: int, iSumm: int, cw: ndarray, iw: ndarray, """ # Set Options from the local options dictionary # --------------------------------------------- - inform = np.array([-1], np.intc) + 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: diff --git a/pyoptsparse/types.py b/pyoptsparse/types.py index e20513b9..b4ca9fb0 100644 --- a/pyoptsparse/types.py +++ b/pyoptsparse/types.py @@ -6,10 +6,10 @@ import numpy.typing as npt # Either ndarray or scalar -NumpyType = Union[float, npt.NDArray[np.float_]] +NumpyType = Union[float, npt.NDArray[np.float64]] # ndarray, list of numbers, or scalar ArrayType = Union[NumpyType, Sequence[float]] # funcs -Dict1DType = Dict[str, npt.NDArray[np.float_]] +Dict1DType = Dict[str, npt.NDArray[np.float64]] # funcsSens -Dict2DType = Dict[str, Dict[str, npt.NDArray[np.float_]]] +Dict2DType = Dict[str, Dict[str, npt.NDArray[np.float64]]] diff --git a/tests/test_nsga2_multi_objective.py b/tests/test_nsga2_multi_objective.py index f41e6405..5d9f2b7e 100644 --- a/tests/test_nsga2_multi_objective.py +++ b/tests/test_nsga2_multi_objective.py @@ -49,10 +49,13 @@ def setup_optProb(self, n_obj): if n_obj == 2: self.optProb.addObj("obj2") - @parameterized.expand([(1,), (2,)]) + @parameterized.expand( + [ + (1,), + # (2,), # skipping flaky multi-objective test + ] + ) def test_opt(self, n_obj): - if n_obj == 2: - raise unittest.SkipTest("skip flaky NSGA2 tests") self.setup_optProb(n_obj) # 300 generations will find x=(0,0), 200 or less will find x=(1,1) From 75e647938b4eee3f3620ea3b8756d52aa2c206f9 Mon Sep 17 00:00:00 2001 From: Ella Wu <602725+nwu63@users.noreply.github.com> Date: Mon, 4 Dec 2023 13:05:27 -0500 Subject: [PATCH 31/44] Bugfix for optimal objective value (#364) * set objectives.value to fStar * add test * Update testing_utils.py * addressed comment --- pyoptsparse/pyOpt_solution.py | 12 ++++++++++-- tests/testing_utils.py | 11 ++++------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/pyoptsparse/pyOpt_solution.py b/pyoptsparse/pyOpt_solution.py index 0e6a4547..1d41b342 100644 --- a/pyoptsparse/pyOpt_solution.py +++ b/pyoptsparse/pyOpt_solution.py @@ -34,7 +34,7 @@ def __init__(self, optProb, xStar, fStar, lambdaStar, optInform, info): in the Solution object. """ - Optimization.__init__(self, optProb.name, None) + super().__init__(optProb.name, None) # Copy over the variables, constraints, and objectives self.variables = copy.deepcopy(optProb.variables) @@ -43,11 +43,19 @@ def __init__(self, optProb, xStar, fStar, lambdaStar, optInform, info): xopt = optProb._mapXtoOpt(optProb.processXtoVec(xStar)) # Now set the x-values: i = 0 - for dvGroup in self.variables: + for dvGroup in self.variables.keys(): for var in self.variables[dvGroup]: var.value = xopt[i] i += 1 + # Now set the f-values + if isinstance(fStar, float) or len(fStar) == 1: + self.objectives[list(self.objectives.keys())[0]].value = float(fStar) + fStar = float(fStar) + else: + for f_name, f in self.objectives.items(): + f.value = fStar[f_name] + self.optTime = info["optTime"] self.userObjTime = info["userObjTime"] self.userSensTime = info["userSensTime"] diff --git a/tests/testing_utils.py b/tests/testing_utils.py index 9d5a8a55..84fc2d37 100644 --- a/tests/testing_utils.py +++ b/tests/testing_utils.py @@ -6,7 +6,6 @@ from baseclasses.testing.assertions import assert_dict_allclose, assert_equal import numpy as np from numpy.testing import assert_allclose -from pkg_resources import parse_version # First party modules from pyoptsparse import OPT, History @@ -121,16 +120,14 @@ def assert_solution_allclose(self, sol, tol, partial_x=False): self.sol_index = 0 # now we assert against the closest solution # objective - # sol.fStar was broken for earlier versions of SNOPT - if self.optName == "SNOPT" and parse_version(self.optVersion) < parse_version("7.7.7"): - sol_objectives = np.array([sol.objectives[key].value for key in sol.objectives]) - else: - sol_objectives = sol.fStar - assert_allclose(sol_objectives, self.fStar[self.sol_index], atol=tol, rtol=tol) + assert_allclose(sol.fStar, self.fStar[self.sol_index], atol=tol, rtol=tol) + # make sure fStar and sol.objectives values match + assert_allclose(sol.fStar, [obj.value for obj in sol.objectives.values()], rtol=1e-12) # x assert_dict_allclose(sol.xStar, self.xStar[self.sol_index], atol=tol, rtol=tol, partial=partial_x) dv = sol.getDVs() assert_dict_allclose(dv, self.xStar[self.sol_index], atol=tol, rtol=tol, partial=partial_x) + assert_dict_allclose(sol.xStar, dv, rtol=1e-12) # lambda if ( hasattr(self, "lambdaStar") From 82bbb68d7f1b63d930c570093401fddc1ea9a337 Mon Sep 17 00:00:00 2001 From: Ella Wu <602725+nwu63@users.noreply.github.com> Date: Wed, 13 Dec 2023 17:09:26 -0500 Subject: [PATCH 32/44] Fix test for earlier SNOPT versions (#370) * fix test for earlier SNOPT versions * don't store obj value for buggy SNOPT * update testing asserts * adjust for scaling * black --------- Co-authored-by: Eirikur Jonsson --- pyoptsparse/pySNOPT/pySNOPT.py | 7 +++++++ tests/testing_utils.py | 7 ++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/pyoptsparse/pySNOPT/pySNOPT.py b/pyoptsparse/pySNOPT/pySNOPT.py index bac40403..249f4ddd 100644 --- a/pyoptsparse/pySNOPT/pySNOPT.py +++ b/pyoptsparse/pySNOPT/pySNOPT.py @@ -15,6 +15,7 @@ from baseclasses.utils import CaseInsensitiveSet import numpy as np from numpy import ndarray +from pkg_resources import parse_version # Local modules from ..pyOpt_error import Error @@ -520,6 +521,12 @@ def __call__( 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, diff --git a/tests/testing_utils.py b/tests/testing_utils.py index 84fc2d37..08017b5f 100644 --- a/tests/testing_utils.py +++ b/tests/testing_utils.py @@ -118,11 +118,16 @@ def assert_solution_allclose(self, sol, tol, partial_x=False): else: # assume we have a single solution self.sol_index = 0 + # now we assert against the closest solution # objective assert_allclose(sol.fStar, self.fStar[self.sol_index], atol=tol, rtol=tol) # make sure fStar and sol.objectives values match - assert_allclose(sol.fStar, [obj.value for obj in sol.objectives.values()], rtol=1e-12) + # NOTE this is not true in general, but true for well-behaving optimizations + # which should be the case for all tests + sol_objectives = np.array([obj.value for obj in sol.objectives.values()]) + assert_allclose(sol.fStar, sol_objectives, rtol=1e-12) + # x assert_dict_allclose(sol.xStar, self.xStar[self.sol_index], atol=tol, rtol=tol, partial=partial_x) dv = sol.getDVs() From c233e8f6c89cbed8e715ed8ec728b8e6596929e4 Mon Sep 17 00:00:00 2001 From: Eirikur Jonsson <36180221+eirikurj@users.noreply.github.com> Date: Mon, 18 Dec 2023 16:07:41 +0000 Subject: [PATCH 33/44] Fix numpy 1.25 deprecation warnings (#372) * fix deprecation warnings * adding disallow_deprecations to tests * address comments --- .github/test_real.sh | 2 +- pyoptsparse/pyOpt_solution.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/test_real.sh b/.github/test_real.sh index 69eb3d54..4af0258e 100755 --- a/.github/test_real.sh +++ b/.github/test_real.sh @@ -11,4 +11,4 @@ cd tests # we have to copy over the coveragerc file to make sure it's in the # same directory where codecov is run cp ../.coveragerc . -testflo --pre_announce -v --coverage --coverpkg pyoptsparse $EXTRA_FLAGS +testflo --pre_announce --disallow_deprecations -v --coverage --coverpkg pyoptsparse $EXTRA_FLAGS diff --git a/pyoptsparse/pyOpt_solution.py b/pyoptsparse/pyOpt_solution.py index 1d41b342..f6fba48e 100644 --- a/pyoptsparse/pyOpt_solution.py +++ b/pyoptsparse/pyOpt_solution.py @@ -1,6 +1,9 @@ # Standard Python modules import copy +# External modules +import numpy as np + # Local modules from .pyOpt_optimization import Optimization @@ -49,9 +52,9 @@ def __init__(self, optProb, xStar, fStar, lambdaStar, optInform, info): i += 1 # Now set the f-values - if isinstance(fStar, float) or len(fStar) == 1: - self.objectives[list(self.objectives.keys())[0]].value = float(fStar) - fStar = float(fStar) + if isinstance(fStar, np.ndarray) and len(fStar) == 1: + self.objectives[list(self.objectives.keys())[0]].value = fStar.item() + fStar = fStar.item() else: for f_name, f in self.objectives.items(): f.value = fStar[f_name] From 5fef213c4d27917f1511f8cfd42c351be26a9f7f Mon Sep 17 00:00:00 2001 From: Ella Wu <602725+nwu63@users.noreply.github.com> Date: Fri, 22 Dec 2023 09:51:37 -0500 Subject: [PATCH 34/44] NSGA2 patch (#365) * add Phil's fix * version bump --- pyoptsparse/__init__.py | 2 +- pyoptsparse/pyNSGA2/source/nsga2.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyoptsparse/__init__.py b/pyoptsparse/__init__.py index 1ea04cc7..aa852adb 100644 --- a/pyoptsparse/__init__.py +++ b/pyoptsparse/__init__.py @@ -1,4 +1,4 @@ -__version__ = "2.10.1" +__version__ = "2.10.2" from .pyOpt_history import History from .pyOpt_variable import Variable diff --git a/pyoptsparse/pyNSGA2/source/nsga2.h b/pyoptsparse/pyNSGA2/source/nsga2.h index 3ed100b9..051382fa 100644 --- a/pyoptsparse/pyNSGA2/source/nsga2.h +++ b/pyoptsparse/pyNSGA2/source/nsga2.h @@ -127,7 +127,7 @@ void mutation_ind (individual *ind, Global global, int *nrealmut, int *nbinmut); void bin_mutate_ind (individual *ind, Global global, int *nbinmut); void real_mutate_ind (individual *ind, Global global, int *nrealmut); -//void nsga2func (int nreal, int nbin, int nobj, int ncon, double *xreal, double *xbin, int **gene, double *obj, double *constr); +void nsga2func (int nreal, int nbin, int nobj, int ncon, double *xreal, double *xbin, int **gene, double *obj, double *constr); void assign_rank_and_crowding_distance (population *new_pop, Global global); From 8d43112244f97c106da40209c327526ee84a32ee Mon Sep 17 00:00:00 2001 From: Graeme Kennedy Date: Fri, 22 Dec 2023 10:26:10 -0500 Subject: [PATCH 35/44] fixed convertJacobian (#371) * fixed the convertJacobian call when jacType == "csr" so that it returns CSR data instead of passing through * added inform values to the ParOpt wrapper --------- Co-authored-by: Ella Wu <602725+nwu63@users.noreply.github.com> Co-authored-by: Marco Mangano <36549388+marcomangano@users.noreply.github.com> --- pyoptsparse/pyOpt_optimizer.py | 2 +- pyoptsparse/pyParOpt/ParOpt.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pyoptsparse/pyOpt_optimizer.py b/pyoptsparse/pyOpt_optimizer.py index b3004102..e65f5f15 100644 --- a/pyoptsparse/pyOpt_optimizer.py +++ b/pyoptsparse/pyOpt_optimizer.py @@ -665,7 +665,7 @@ def _convertJacobian(self, gcon_csr_in): self._jac_map_csr_to_csc = mapToCSC(gcon_csr) gcon = gcon_csr["csr"][IDATA][self._jac_map_csr_to_csc[IDATA]] elif self.jacType == "csr": - pass + gcon = gcon_csr["csr"][IDATA] elif self.jacType == "coo": gcon = convertToCOO(gcon_csr) gcon = gcon["coo"][IDATA] diff --git a/pyoptsparse/pyParOpt/ParOpt.py b/pyoptsparse/pyParOpt/ParOpt.py index 47a7fadf..044f359b 100644 --- a/pyoptsparse/pyParOpt/ParOpt.py +++ b/pyoptsparse/pyParOpt/ParOpt.py @@ -243,6 +243,8 @@ def evalObjConGradient(self, x, g, A): # are switch since ParOpt uses a formulation with c(x) >= 0, while pyOpt # uses g(x) = -c(x) <= 0. Therefore the multipliers are reversed. sol_inform = {} + sol_inform["value"] = None + sol_inform["text"] = None # If number of constraints is zero, ParOpt returns z as None. # Thus if there is no constraints, should pass an empty list From eee3f7029095491fe2b1dd794b3a5586fc56fb07 Mon Sep 17 00:00:00 2001 From: Sabet Seraj <48863473+sseraj@users.noreply.github.com> Date: Mon, 15 Jan 2024 11:57:30 -0500 Subject: [PATCH 36/44] decreased time limit for SNOPT informs test (#376) --- tests/test_tp109.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_tp109.py b/tests/test_tp109.py index efaacbce..48a6b5d2 100644 --- a/tests/test_tp109.py +++ b/tests/test_tp109.py @@ -176,7 +176,7 @@ def test_snopt(self): def test_snopt_informs(self): self.optName = "SNOPT" self.setup_optProb() - sol = self.optimize(optOptions={"Time Limit": 1e-5}) + sol = self.optimize(optOptions={"Time Limit": 1e-15}) self.assert_inform_equal(sol, 34) def test_slsqp(self): From 7f26d4336a019671a94fa01efde4d99fc2377bad Mon Sep 17 00:00:00 2001 From: Eirikur Jonsson <36180221+eirikurj@users.noreply.github.com> Date: Wed, 31 Jan 2024 18:15:31 +0000 Subject: [PATCH 37/44] fix f77 formatting in slsqp output file (#378) --- pyoptsparse/pySLSQP/source/slsqp.f | 34 ++++++++++++++++++------------ 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/pyoptsparse/pySLSQP/source/slsqp.f b/pyoptsparse/pySLSQP/source/slsqp.f index 07c25a55..ba4c4b72 100644 --- a/pyoptsparse/pySLSQP/source/slsqp.f +++ b/pyoptsparse/pySLSQP/source/slsqp.f @@ -180,11 +180,11 @@ SUBROUTINE SLSQP (M,MEQ,LA,N,X,XL,XU,F,C,G,A,ACC,ITER, 1 JW(L_JW), LA, M, MEQ, MINEQ, MODE, N, N1, IPRINT, IOUT, 2 NFUNC, NGRAD, 3 IEXACT, INCONS, IRESET, ITERMX, LINE, N2, N3 - + DOUBLE PRECISION ACC, A(LA,N+1), C(LA), F, G(N+1), - * X(N), XL(N), XU(N), W(L_W), + * X(N), XL(N), XU(N), W(L_W), * ALPHA, F0, GS, H1, H2, H3, H4, T, T0, TOL - + EXTERNAL SLFUNC,SLGRAD CHARACTER*(*) IFILE @@ -279,9 +279,9 @@ SUBROUTINE SLSQP (M,MEQ,LA,N,X,XL,XU,F,C,G,A,ACC,ITER, * ITERMX,LINE,N1,N2,N3) C IF (ABS(MODE).EQ.1) GOTO 4 -C +C 3 CONTINUE - + C C PRINT FINAL C @@ -297,20 +297,26 @@ SUBROUTINE SLSQP (M,MEQ,LA,N,X,XL,XU,F,C,G,A,ACC,ITER, C ------------------------------------------------------------------ C FORMATS C ------------------------------------------------------------------ -C +C 1000 FORMAT(////,3X, 1 60H------------------------------------------------------------, 2 15H---------------, - 3 /,5X,59HSTART OF THE SEQUENTIAL LEAST SQUARES PROGRAMMING ALGORITHM, - 4 /,3X, - 5 60H------------------------------------------------------------, - 6 15H---------------) - 1100 FORMAT(/,5X,11HPARAMETERS:,/,8X,5HACC =,D13.4,/,8X,9HMAXITER =, - 1 I3,/,8X,8HIPRINT =,I4,/,6HIOUT =,I4//) + 3 /, + 4 5X,13HSTART OF THE , + 5 46HSEQUENTIAL LEAST SQUARES PROGRAMMING ALGORITHM, + 6 /,3X, + 7 60H------------------------------------------------------------, + 8 15H---------------) + 1100 FORMAT(/,5X, + 1 11HPARAMETERS:, + 2 /,8X,5HACC =,D13.4, + 3 /,8X,9HMAXITER =,I4, + 4 /,8X,8HIPRINT =,I4, + 5 /,8X,6HIOUT =,I4//) 1200 FORMAT(5X,6HITER =,I5,5X,5HOBJ =,7E16.8,5X,10HX-VECTOR =) - 1400 FORMAT (3X,7E13.4) + 1400 FORMAT(3X,7E13.4) 1450 FORMAT(8X,30HNUMBER OF FUNC-CALLS: NFUNC =,I4) 1460 FORMAT(8X,30HNUMBER OF GRAD-CALLS: NGRAD =,I4) C END - + From 604aedae79006d0b0b2ddc72ed8afe58002fcd2b Mon Sep 17 00:00:00 2001 From: Ella Wu <602725+nwu63@users.noreply.github.com> Date: Thu, 15 Feb 2024 07:15:24 -0800 Subject: [PATCH 38/44] Update meson version for windows (#375) * update meson version for windows * try lld * install flang * also need clang * back to compilers * one more time * try default linker * update to new meson and revert changes --------- Co-authored-by: Marco Mangano <36549388+marcomangano@users.noreply.github.com> --- .github/environment.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/environment.yml b/.github/environment.yml index 5883a678..2cac37e7 100644 --- a/.github/environment.yml +++ b/.github/environment.yml @@ -4,7 +4,7 @@ dependencies: - numpy >=1.16 - ipopt - swig - - meson =0.61 + - meson >=1.3.2 - compilers - pkg-config - pip @@ -15,4 +15,4 @@ dependencies: - testflo - scipy >1.2 - mdolab-baseclasses >=1.3.1 - - sqlitedict >=1.6 \ No newline at end of file + - sqlitedict >=1.6 From dbc5e3750c06999b7fe3bb9e76c181387362339f Mon Sep 17 00:00:00 2001 From: Eirikur Jonsson <36180221+eirikurj@users.noreply.github.com> Date: Wed, 21 Feb 2024 06:09:46 +0000 Subject: [PATCH 39/44] Update supported versions in NLPQLP documentation (#383) * Update NLPQLP.rst Update supported version in NLPQLP documentation. * typo --- doc/optimizers/NLPQLP.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/optimizers/NLPQLP.rst b/doc/optimizers/NLPQLP.rst index 8a928f21..9a15b2b1 100644 --- a/doc/optimizers/NLPQLP.rst +++ b/doc/optimizers/NLPQLP.rst @@ -12,8 +12,8 @@ solved. The line search can be performed with respect to two alternative merit functions, and the Hessian approximation is updated by a modified BFGS formula. -NLPQLP is a proprietary software, which can be obtained `here `_. -The latest version supported is v4.2.2. +NLPQLP is a proprietary software, which can be obtained `here `_. +The supported versions are v4.2.2 and v5.0.3, but other versions may work. Options ------- From cae6954e67ad58b03b535cc015e9d0118d89695e Mon Sep 17 00:00:00 2001 From: Ella Wu <602725+nwu63@users.noreply.github.com> Date: Thu, 22 Feb 2024 14:54:11 -0800 Subject: [PATCH 40/44] update paropt interface (#385) --- pyoptsparse/pyParOpt/ParOpt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyoptsparse/pyParOpt/ParOpt.py b/pyoptsparse/pyParOpt/ParOpt.py index 044f359b..3b64140b 100644 --- a/pyoptsparse/pyParOpt/ParOpt.py +++ b/pyoptsparse/pyParOpt/ParOpt.py @@ -173,7 +173,7 @@ def __call__( class Problem(_ParOpt.Problem): def __init__(self, ptr, n, m, xs, blx, bux): - super().__init__(MPI.COMM_SELF, n, m) + super().__init__(MPI.COMM_SELF, nvars=n, ncon=m) self.ptr = ptr self.n = n self.m = m From ab1618f2327c31964da2ce7087d630360c6fb760 Mon Sep 17 00:00:00 2001 From: Ella Wu <602725+ewu63@users.noreply.github.com> Date: Tue, 26 Mar 2024 06:49:06 -0700 Subject: [PATCH 41/44] Better import error (#389) * add util for importing module * use util function for importing * switch all import to util function * update tests to catch ImportError * cleanup paropt import * do not use pipe character due to old python version * fix tests * very hacky solution with sys.modules * fix linting * black * cast to list first * Use default UserWarning * Update test_other.py to match new warning --------- Co-authored-by: Ella Wu <602725+nwu63@users.noreply.github.com> Co-authored-by: Marco Mangano <36549388+marcomangano@users.noreply.github.com> --- pyoptsparse/pyALPSO/pyALPSO.py | 5 ++- pyoptsparse/pyCONMIN/pyCONMIN.py | 15 ++++----- pyoptsparse/pyIPOPT/pyIPOPT.py | 26 +++++++++------ pyoptsparse/pyNLPQLP/pyNLPQLP.py | 15 ++++----- pyoptsparse/pyNSGA2/pyNSGA2.py | 16 ++++----- pyoptsparse/pyOpt_utils.py | 41 ++++++++++++++++++++++- pyoptsparse/pyPSQP/pyPSQP.py | 15 ++++----- pyoptsparse/pyParOpt/ParOpt.py | 43 +++++++++++------------- pyoptsparse/pySLSQP/pySLSQP.py | 17 +++++----- pyoptsparse/pySNOPT/pySNOPT.py | 54 ++++++++++--------------------- tests/test_other.py | 19 +++++++---- tests/test_require_mpi_env_var.py | 19 ++++------- tests/test_snopt_bugfix.py | 19 ++++------- tests/test_user_termination.py | 13 +++----- tests/testing_utils.py | 3 +- 15 files changed, 160 insertions(+), 160 deletions(-) diff --git a/pyoptsparse/pyALPSO/pyALPSO.py b/pyoptsparse/pyALPSO/pyALPSO.py index 465728ca..f684e501 100644 --- a/pyoptsparse/pyALPSO/pyALPSO.py +++ b/pyoptsparse/pyALPSO/pyALPSO.py @@ -10,6 +10,7 @@ import numpy as np # Local modules +from . import alpso from ..pyOpt_error import Error from ..pyOpt_optimizer import Optimizer @@ -25,9 +26,7 @@ class ALPSO(Optimizer): - pll_type -> STR: ALPSO Parallel Implementation (None, SPM- Static, DPM- Dynamic, POA-Parallel Analysis), *Default* = None """ - def __init__(self, raiseError=True, options={}): - from . import alpso - + def __init__(self, options={}): self.alpso = alpso category = "Global Optimizer" diff --git a/pyoptsparse/pyCONMIN/pyCONMIN.py b/pyoptsparse/pyCONMIN/pyCONMIN.py index 21dd0c07..0fbac67d 100644 --- a/pyoptsparse/pyCONMIN/pyCONMIN.py +++ b/pyoptsparse/pyCONMIN/pyCONMIN.py @@ -2,11 +2,6 @@ pyCONMIN - A variation of the pyCONMIN wrapper specificially designed to work with sparse optimization problems. """ -# Compiled module -try: - from . import conmin # isort: skip -except ImportError: - conmin = None # Standard Python modules import datetime import os @@ -18,6 +13,11 @@ # Local modules from ..pyOpt_error import Error from ..pyOpt_optimizer import Optimizer +from ..pyOpt_utils import try_import_compiled_module_from_path + +# import the compiled module +THIS_DIR = os.path.dirname(os.path.abspath(__file__)) +conmin = try_import_compiled_module_from_path("conmin", THIS_DIR) class CONMIN(Optimizer): @@ -30,9 +30,8 @@ def __init__(self, raiseError=True, options={}): category = "Local Optimizer" defOpts = self._getDefaultOptions() informs = self._getInforms() - if conmin is None: - if raiseError: - raise Error("There was an error importing the compiled conmin module") + if isinstance(conmin, str) and raiseError: + raise ImportError(conmin) self.set_options = [] super().__init__(name, category, defaultOptions=defOpts, informs=informs, options=options) diff --git a/pyoptsparse/pyIPOPT/pyIPOPT.py b/pyoptsparse/pyIPOPT/pyIPOPT.py index f5233292..b009a4a4 100644 --- a/pyoptsparse/pyIPOPT/pyIPOPT.py +++ b/pyoptsparse/pyIPOPT/pyIPOPT.py @@ -1,24 +1,31 @@ """ pyIPOPT - A python wrapper to the core IPOPT compiled module. """ -# Compiled module -try: - from . import pyipoptcore # isort: skip -except ImportError: - pyipoptcore = None # Standard Python modules import copy import datetime +import os import time # External modules import numpy as np # Local modules -from ..pyOpt_error import Error from ..pyOpt_optimizer import Optimizer -from ..pyOpt_utils import ICOL, INFINITY, IROW, convertToCOO, extractRows, scaleRows +from ..pyOpt_utils import ( + ICOL, + INFINITY, + IROW, + convertToCOO, + extractRows, + scaleRows, + try_import_compiled_module_from_path, +) + +# import the compiled module +THIS_DIR = os.path.dirname(os.path.abspath(__file__)) +pyipoptcore = try_import_compiled_module_from_path("pyipoptcore", THIS_DIR) class IPOPT(Optimizer): @@ -36,9 +43,8 @@ def __init__(self, raiseError=True, options={}): defOpts = self._getDefaultOptions() informs = self._getInforms() - if pyipoptcore is None: - if raiseError: - raise Error("There was an error importing the compiled IPOPT module") + if isinstance(pyipoptcore, str) and raiseError: + raise ImportError(pyipoptcore) super().__init__( name, diff --git a/pyoptsparse/pyNLPQLP/pyNLPQLP.py b/pyoptsparse/pyNLPQLP/pyNLPQLP.py index 348f8e4f..c9f0fff2 100644 --- a/pyoptsparse/pyNLPQLP/pyNLPQLP.py +++ b/pyoptsparse/pyNLPQLP/pyNLPQLP.py @@ -2,11 +2,6 @@ pyNLPQLP - A pyOptSparse wrapper for Schittkowski's NLPQLP optimization algorithm. """ -# Compiled module -try: - from . import nlpqlp # isort: skip -except ImportError: - nlpqlp = None # Standard Python modules import datetime import os @@ -18,6 +13,11 @@ # Local modules from ..pyOpt_error import Error from ..pyOpt_optimizer import Optimizer +from ..pyOpt_utils import try_import_compiled_module_from_path + +# import the compiled module +THIS_DIR = os.path.dirname(os.path.abspath(__file__)) +nlpqlp = try_import_compiled_module_from_path("nlpqlp", THIS_DIR) class NLPQLP(Optimizer): @@ -30,9 +30,8 @@ def __init__(self, raiseError=True, options={}): category = "Local Optimizer" defOpts = self._getDefaultOptions() informs = self._getInforms() - if nlpqlp is None: - if raiseError: - raise Error("There was an error importing the compiled nlpqlp module") + if isinstance(nlpqlp, str) and raiseError: + raise ImportError(nlpqlp) super().__init__(name, category, defaultOptions=defOpts, informs=informs, options=options) # NLPQLP needs Jacobians in dense format diff --git a/pyoptsparse/pyNSGA2/pyNSGA2.py b/pyoptsparse/pyNSGA2/pyNSGA2.py index 125e3b0f..173703e9 100644 --- a/pyoptsparse/pyNSGA2/pyNSGA2.py +++ b/pyoptsparse/pyNSGA2/pyNSGA2.py @@ -2,12 +2,8 @@ pyNSGA2 - A variation of the pyNSGA2 wrapper specificially designed to work with sparse optimization problems. """ -# Compiled module -try: - from . import nsga2 # isort: skip -except ImportError: - nsga2 = None # Standard Python modules +import os import time # External modules @@ -16,6 +12,11 @@ # Local modules from ..pyOpt_error import Error from ..pyOpt_optimizer import Optimizer +from ..pyOpt_utils import try_import_compiled_module_from_path + +# import the compiled module +THIS_DIR = os.path.dirname(os.path.abspath(__file__)) +nsga2 = try_import_compiled_module_from_path("nsga2", THIS_DIR) class NSGA2(Optimizer): @@ -30,9 +31,8 @@ def __init__(self, raiseError=True, options={}): informs = self._getInforms() super().__init__(name, category, defaultOptions=defOpts, informs=informs, options=options) - if nsga2 is None: - if raiseError: - raise Error("There was an error importing the compiled nsga2 module") + if isinstance(nsga2, str) and raiseError: + raise ImportError(nsga2) @staticmethod def _getInforms(): diff --git a/pyoptsparse/pyOpt_utils.py b/pyoptsparse/pyOpt_utils.py index e67eabb7..35eb7400 100644 --- a/pyoptsparse/pyOpt_utils.py +++ b/pyoptsparse/pyOpt_utils.py @@ -9,7 +9,11 @@ mat = {'csc':[colp, rowind, data], 'shape':[nrow, ncols]} # A csc matrix """ # Standard Python modules -from typing import Tuple, Union +import importlib +import os +import sys +import types +from typing import Optional, Tuple, Union import warnings # External modules @@ -570,3 +574,38 @@ def _broadcast_to_array(name: str, value: ArrayType, n_values: int, allow_none: if not allow_none and any([i is None for i in value]): raise Error(f"The {name} argument cannot be 'None'.") return value + + +def try_import_compiled_module_from_path(module_name: str, path: Optional[str] = None) -> Union[types.ModuleType, str]: + """ + Attempt to import a module from a given path. + + Parameters + ---------- + module_name : str + The name of the module + path : Optional[str] + The path to import from. If None, the default ``sys.path`` is used. + + Returns + ------- + Union[types.ModuleType, str] + If importable, the imported module is returned. + If not importable, the error message is instead returned. + """ + orig_path = sys.path + if path is not None: + path = os.path.abspath(os.path.expandvars(os.path.expanduser(path))) + sys.path = [path] + try: + module = importlib.import_module(module_name) + except ImportError as e: + if path is not None: + warnings.warn( + f"{module_name} module could not be imported from {path}.", + stacklevel=2, + ) + module = str(e) + finally: + sys.path = orig_path + return module diff --git a/pyoptsparse/pyPSQP/pyPSQP.py b/pyoptsparse/pyPSQP/pyPSQP.py index ca495eb8..e69ed8de 100644 --- a/pyoptsparse/pyPSQP/pyPSQP.py +++ b/pyoptsparse/pyPSQP/pyPSQP.py @@ -1,11 +1,6 @@ """ pyPSQP - the pyPSQP wrapper """ -# Compiled module -try: - from . import psqp # isort: skip -except ImportError: - psqp = None # Standard Python modules import datetime import os @@ -17,6 +12,11 @@ # Local modules from ..pyOpt_error import Error from ..pyOpt_optimizer import Optimizer +from ..pyOpt_utils import try_import_compiled_module_from_path + +# import the compiled module +THIS_DIR = os.path.dirname(os.path.abspath(__file__)) +psqp = try_import_compiled_module_from_path("psqp", THIS_DIR) class PSQP(Optimizer): @@ -30,9 +30,8 @@ def __init__(self, raiseError=True, options={}): defOpts = self._getDefaultOptions() informs = self._getInforms() - if psqp is None: - if raiseError: - raise Error("There was an error importing the compiled psqp module") + if isinstance(psqp, str) and raiseError: + raise ImportError(psqp) super().__init__(name, category, defaultOptions=defOpts, informs=informs, options=options) diff --git a/pyoptsparse/pyParOpt/ParOpt.py b/pyoptsparse/pyParOpt/ParOpt.py index 3b64140b..f08e33b4 100644 --- a/pyoptsparse/pyParOpt/ParOpt.py +++ b/pyoptsparse/pyParOpt/ParOpt.py @@ -6,34 +6,27 @@ # External modules import numpy as np -# isort: off -# Attempt to import mpi4py. +# Local modules +from ..pyOpt_optimizer import Optimizer +from ..pyOpt_utils import INFINITY, try_import_compiled_module_from_path + +# Attempt to import ParOpt/mpi4py # If PYOPTSPARSE_REQUIRE_MPI is set to a recognized positive value, attempt import # and raise exception on failure. If set to anything else, no import is attempted. -if "PYOPTSPARSE_REQUIRE_MPI" in os.environ: - if os.environ["PYOPTSPARSE_REQUIRE_MPI"].lower() in ["always", "1", "true", "yes"]: - try: - from paropt import ParOpt as _ParOpt - from mpi4py import MPI - except ImportError: - _ParOpt = None - else: - _ParOpt = None +if "PYOPTSPARSE_REQUIRE_MPI" in os.environ and os.environ["PYOPTSPARSE_REQUIRE_MPI"].lower() not in [ + "always", + "1", + "true", + "yes", +]: + _ParOpt = "ParOpt was not imported, as requested by the environment variable 'PYOPTSPARSE_REQUIRE_MPI'" + MPI = "mpi4py was not imported, as requested by the environment variable 'PYOPTSPARSE_REQUIRE_MPI'" # If PYOPTSPARSE_REQUIRE_MPI is unset, attempt to import mpi4py. # Since ParOpt requires mpi4py, if either _ParOpt or mpi4py is unavailable # we disable the optimizer. else: - try: - from paropt import ParOpt as _ParOpt - from mpi4py import MPI - except ImportError: - _ParOpt = None -# isort: on - -# Local modules -from ..pyOpt_error import Error -from ..pyOpt_optimizer import Optimizer -from ..pyOpt_utils import INFINITY + _ParOpt = try_import_compiled_module_from_path("paropt.ParOpt") + MPI = try_import_compiled_module_from_path("mpi4py.MPI") class ParOpt(Optimizer): @@ -48,9 +41,9 @@ class ParOpt(Optimizer): def __init__(self, raiseError=True, options={}): name = "ParOpt" category = "Local Optimizer" - if _ParOpt is None: - if raiseError: - raise Error("There was an error importing ParOpt") + for mod in [_ParOpt, MPI]: + if isinstance(mod, str) and raiseError: + raise ImportError(mod) # Create and fill-in the dictionary of default option values self.defOpts = {} diff --git a/pyoptsparse/pySLSQP/pySLSQP.py b/pyoptsparse/pySLSQP/pySLSQP.py index 58f3ee6e..d7e6b367 100644 --- a/pyoptsparse/pySLSQP/pySLSQP.py +++ b/pyoptsparse/pySLSQP/pySLSQP.py @@ -2,11 +2,7 @@ pySLSQP - A variation of the pySLSQP wrapper specificially designed to work with sparse optimization problems. """ -# Compiled module -try: - from . import slsqp # isort: skip -except ImportError: - slsqp = None + # Standard Python modules import datetime import os @@ -16,8 +12,12 @@ import numpy as np # Local modules -from ..pyOpt_error import Error from ..pyOpt_optimizer import Optimizer +from ..pyOpt_utils import try_import_compiled_module_from_path + +# import the compiled module +THIS_DIR = os.path.dirname(os.path.abspath(__file__)) +slsqp = try_import_compiled_module_from_path("slsqp", THIS_DIR) class SLSQP(Optimizer): @@ -30,9 +30,8 @@ def __init__(self, raiseError=True, options={}): category = "Local Optimizer" defOpts = self._getDefaultOptions() informs = self._getInforms() - if slsqp is None: - if raiseError: - raise Error("There was an error importing the compiled slsqp module") + if isinstance(slsqp, str) and raiseError: + raise ImportError(slsqp) self.set_options = [] super().__init__(name, category, defaultOptions=defOpts, informs=informs, options=options) diff --git a/pyoptsparse/pySNOPT/pySNOPT.py b/pyoptsparse/pySNOPT/pySNOPT.py index 249f4ddd..63185e33 100644 --- a/pyoptsparse/pySNOPT/pySNOPT.py +++ b/pyoptsparse/pySNOPT/pySNOPT.py @@ -6,10 +6,8 @@ import datetime import os import re -import sys import time from typing import Any, Dict, Optional, Tuple -import warnings # External modules from baseclasses.utils import CaseInsensitiveSet @@ -21,39 +19,21 @@ 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 - - -def _import_snopt_from_path(path): - """Attempt to import snopt from a specific path. Return the loaded module, or `None` if snopt cannot be imported.""" - path = os.path.abspath(os.path.expandvars(os.path.expanduser(path))) - orig_path = sys.path - sys.path = [path] - try: - import snopt # isort: skip - except ImportError: - warnings.warn( - f"`snopt` module could not be imported from {path}.", - ImportWarning, - stacklevel=2, - ) - snopt = None - finally: - sys.path = orig_path - return snopt - - -# Compiled module -_IMPORT_SNOPT_FROM = os.environ.get("PYOPTSPARSE_IMPORT_SNOPT_FROM", None) -if _IMPORT_SNOPT_FROM is not None: - # if a specific import path is specified, attempt to load SNOPT from it - snopt = _import_snopt_from_path(_IMPORT_SNOPT_FROM) -else: - # otherwise, load it relative to this file - try: - from . import snopt # isort: skip - except ImportError: - snopt = None +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) class SNOPT(Optimizer): @@ -84,9 +64,9 @@ def __init__(self, raiseError=True, options: Dict = {}): informs = self._getInforms() - if snopt is None: + if isinstance(snopt, str): if raiseError: - raise Error("There was an error importing the compiled snopt module") + raise ImportError(snopt) else: version = None else: diff --git a/tests/test_other.py b/tests/test_other.py index 98d0996d..41435b8d 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -3,20 +3,25 @@ import sys import unittest -# we have to unset this environment variable because otherwise when we import `_import_snopt_from_path` +# First party modules +from pyoptsparse.pyOpt_utils import try_import_compiled_module_from_path + +# we have to unset this environment variable because otherwise # the snopt module gets automatically imported, thus failing the import test below os.environ.pop("PYOPTSPARSE_IMPORT_SNOPT_FROM", None) -# First party modules -from pyoptsparse.pySNOPT.pySNOPT import _import_snopt_from_path # noqa: E402 - class TestImportSnoptFromPath(unittest.TestCase): def test_nonexistent_path(self): - with self.assertWarns(ImportWarning): - self.assertIsNone(_import_snopt_from_path("/a/nonexistent/path")) + # first unload `snopt` from namespace + for key in list(sys.modules.keys()): + if "snopt" in key: + sys.modules.pop(key) + with self.assertWarns(UserWarning): + module = try_import_compiled_module_from_path("snopt", "/a/nonexistent/path") + self.assertTrue(isinstance(module, str)) def test_sys_path_unchanged(self): path = tuple(sys.path) - _import_snopt_from_path("/some/path") + try_import_compiled_module_from_path("snopt", "/some/path") self.assertEqual(tuple(sys.path), path) diff --git a/tests/test_require_mpi_env_var.py b/tests/test_require_mpi_env_var.py index 15e7ee25..ca95c0ed 100644 --- a/tests/test_require_mpi_env_var.py +++ b/tests/test_require_mpi_env_var.py @@ -2,14 +2,9 @@ import importlib import inspect import os -import sys import unittest # isort: off -if sys.version_info[0] == 2: - reload_func = reload # noqa: F821 -else: - reload_func = importlib.reload try: HAS_MPI = True @@ -26,14 +21,14 @@ def test_require_mpi(self): os.environ["PYOPTSPARSE_REQUIRE_MPI"] = "1" import pyoptsparse.pyOpt_MPI - reload_func(pyoptsparse.pyOpt_MPI) + importlib.reload(pyoptsparse.pyOpt_MPI) self.assertTrue(inspect.ismodule(pyoptsparse.pyOpt_MPI.MPI)) def test_no_mpi_requirement_given(self): os.environ.pop("PYOPTSPARSE_REQUIRE_MPI", None) import pyoptsparse.pyOpt_MPI - reload_func(pyoptsparse.pyOpt_MPI) + importlib.reload(pyoptsparse.pyOpt_MPI) if HAS_MPI: self.assertTrue(inspect.ismodule(pyoptsparse.pyOpt_MPI.MPI)) else: @@ -43,7 +38,7 @@ def test_do_not_use_mpi(self): os.environ["PYOPTSPARSE_REQUIRE_MPI"] = "0" import pyoptsparse.pyOpt_MPI - reload_func(pyoptsparse.pyOpt_MPI) + importlib.reload(pyoptsparse.pyOpt_MPI) self.assertFalse(inspect.ismodule(pyoptsparse.pyOpt_MPI.MPI)) @@ -60,22 +55,22 @@ def test_require_mpi_check_paropt(self): os.environ["PYOPTSPARSE_REQUIRE_MPI"] = "1" import pyoptsparse.pyParOpt.ParOpt - reload_func(pyoptsparse.pyParOpt.ParOpt) + importlib.reload(pyoptsparse.pyParOpt.ParOpt) self.assertIsNotNone(pyoptsparse.pyParOpt.ParOpt._ParOpt) def test_no_mpi_requirement_given_check_paropt(self): os.environ.pop("PYOPTSPARSE_REQUIRE_MPI", None) import pyoptsparse.pyParOpt.ParOpt - reload_func(pyoptsparse.pyParOpt.ParOpt) + importlib.reload(pyoptsparse.pyParOpt.ParOpt) self.assertIsNotNone(pyoptsparse.pyParOpt.ParOpt._ParOpt) def test_do_not_use_mpi_check_paropt(self): os.environ["PYOPTSPARSE_REQUIRE_MPI"] = "0" import pyoptsparse.pyParOpt.ParOpt - reload_func(pyoptsparse.pyParOpt.ParOpt) - self.assertIsNone(pyoptsparse.pyParOpt.ParOpt._ParOpt) + importlib.reload(pyoptsparse.pyParOpt.ParOpt) + self.assertTrue(isinstance(pyoptsparse.pyParOpt.ParOpt._ParOpt, str)) if __name__ == "__main__": diff --git a/tests/test_snopt_bugfix.py b/tests/test_snopt_bugfix.py index e6460452..d3c8b598 100644 --- a/tests/test_snopt_bugfix.py +++ b/tests/test_snopt_bugfix.py @@ -12,7 +12,6 @@ # First party modules from pyoptsparse import SNOPT, Optimization -from pyoptsparse.pyOpt_error import Error def objfunc(xdict): @@ -104,10 +103,8 @@ def test_opt(self): # Optimizer try: opt = SNOPT(options=optOptions) - except Error as e: - if "There was an error importing" in e.message: - raise unittest.SkipTest("Optimizer not available: SNOPT") - raise e + except ImportError: + raise unittest.SkipTest("Optimizer not available: SNOPT") sol = opt(optProb, sens=sens) @@ -137,10 +134,8 @@ def test_opt_bug1(self): # Optimizer try: opt = SNOPT(options=optOptions) - except Error as e: - if "There was an error importing" in e.message: - raise unittest.SkipTest("Optimizer not available: SNOPT") - raise e + except ImportError: + raise unittest.SkipTest("Optimizer not available: SNOPT") opt(optProb, sens=sens) @@ -180,10 +175,8 @@ def test_opt_bug_print_2con(self): # Optimizer try: opt = SNOPT(options=optOptions) - except Error as e: - if "There was an error importing" in e.message: - raise unittest.SkipTest("Optimizer not available: SNOPT") - raise e + except ImportError: + raise unittest.SkipTest("Optimizer not available: SNOPT") sol = opt(optProb, sens=sens) diff --git a/tests/test_user_termination.py b/tests/test_user_termination.py index ca40bad9..a857a9f1 100644 --- a/tests/test_user_termination.py +++ b/tests/test_user_termination.py @@ -14,7 +14,6 @@ # First party modules from pyoptsparse import OPT, Optimization -from pyoptsparse.pyOpt_error import Error class TerminateComp: @@ -105,10 +104,8 @@ def test_obj(self, optName): try: opt = OPT(optName, options=optOptions) - except Error as e: - if "There was an error importing" in e.message: - raise unittest.SkipTest(f"Optimizer not available: {optName}") - raise e + except ImportError: + raise unittest.SkipTest(f"Optimizer not available: {optName}") sol = opt(optProb, sens=termcomp.sens) @@ -128,10 +125,8 @@ def test_sens(self, optName): try: opt = OPT(optName, options=optOptions) - except Error as e: - if "There was an error importing" in e.message: - raise unittest.SkipTest("Optimizer not available: SNOPT") - raise e + except ImportError: + raise unittest.SkipTest("Optimizer not available: SNOPT") sol = opt(optProb, sens=termcomp.sens) diff --git a/tests/testing_utils.py b/tests/testing_utils.py index 08017b5f..fce54c26 100644 --- a/tests/testing_utils.py +++ b/tests/testing_utils.py @@ -9,7 +9,6 @@ # First party modules from pyoptsparse import OPT, History -from pyoptsparse.pyOpt_error import Error def assert_optProb_size(optProb, nObj, nDV, nCon): @@ -231,7 +230,7 @@ def optimize(self, sens=None, setDV=None, optOptions=None, storeHistory=False, h try: opt = OPT(self.optName, options=optOptions) self.optVersion = opt.version - except Error as e: + except ImportError as e: if self.optName in DEFAULT_OPTIMIZERS: raise e else: From 69c2a6f53eacfba99e7d25da754f665da7a26899 Mon Sep 17 00:00:00 2001 From: Ella Wu <602725+ewu63@users.noreply.github.com> Date: Wed, 24 Apr 2024 10:49:57 -0400 Subject: [PATCH 42/44] Fix SNOPT printout encoding and skip printing import warnings (#392) * initialize cw to spaces * optionally raise import warning * fix test * rename types.py to pyOpt_types.py * rename imports * version bump * bugfix version revert --- pyoptsparse/pyCONMIN/pyCONMIN.py | 2 +- pyoptsparse/pyNSGA2/pyNSGA2.py | 2 +- pyoptsparse/pyOpt_constraint.py | 2 +- pyoptsparse/pyOpt_gradient.py | 2 +- pyoptsparse/pyOpt_optimization.py | 2 +- pyoptsparse/{types.py => pyOpt_types.py} | 0 pyoptsparse/pyOpt_utils.py | 11 ++++++++--- pyoptsparse/pySLSQP/pySLSQP.py | 2 +- pyoptsparse/pySNOPT/pySNOPT.py | 12 ++++-------- tests/test_other.py | 2 +- 10 files changed, 19 insertions(+), 18 deletions(-) rename pyoptsparse/{types.py => pyOpt_types.py} (100%) diff --git a/pyoptsparse/pyCONMIN/pyCONMIN.py b/pyoptsparse/pyCONMIN/pyCONMIN.py index 0fbac67d..87d084ed 100644 --- a/pyoptsparse/pyCONMIN/pyCONMIN.py +++ b/pyoptsparse/pyCONMIN/pyCONMIN.py @@ -17,7 +17,7 @@ # import the compiled module THIS_DIR = os.path.dirname(os.path.abspath(__file__)) -conmin = try_import_compiled_module_from_path("conmin", THIS_DIR) +conmin = try_import_compiled_module_from_path("conmin", THIS_DIR, raise_warning=True) class CONMIN(Optimizer): diff --git a/pyoptsparse/pyNSGA2/pyNSGA2.py b/pyoptsparse/pyNSGA2/pyNSGA2.py index 173703e9..e5418f58 100644 --- a/pyoptsparse/pyNSGA2/pyNSGA2.py +++ b/pyoptsparse/pyNSGA2/pyNSGA2.py @@ -16,7 +16,7 @@ # import the compiled module THIS_DIR = os.path.dirname(os.path.abspath(__file__)) -nsga2 = try_import_compiled_module_from_path("nsga2", THIS_DIR) +nsga2 = try_import_compiled_module_from_path("nsga2", THIS_DIR, raise_warning=True) class NSGA2(Optimizer): diff --git a/pyoptsparse/pyOpt_constraint.py b/pyoptsparse/pyOpt_constraint.py index 80cfe85c..e85a850d 100644 --- a/pyoptsparse/pyOpt_constraint.py +++ b/pyoptsparse/pyOpt_constraint.py @@ -8,8 +8,8 @@ # Local modules from .pyOpt_error import Error, pyOptSparseWarning +from .pyOpt_types import Dict1DType from .pyOpt_utils import INFINITY, _broadcast_to_array, convertToCOO -from .types import Dict1DType class Constraint: diff --git a/pyoptsparse/pyOpt_gradient.py b/pyoptsparse/pyOpt_gradient.py index ec6e58a9..27083c64 100644 --- a/pyoptsparse/pyOpt_gradient.py +++ b/pyoptsparse/pyOpt_gradient.py @@ -8,7 +8,7 @@ # Local modules from .pyOpt_MPI import MPI from .pyOpt_optimization import Optimization -from .types import Dict1DType, Dict2DType +from .pyOpt_types import Dict1DType, Dict2DType class Gradient: diff --git a/pyoptsparse/pyOpt_optimization.py b/pyoptsparse/pyOpt_optimization.py index ac41174d..a15ffa93 100644 --- a/pyoptsparse/pyOpt_optimization.py +++ b/pyoptsparse/pyOpt_optimization.py @@ -15,6 +15,7 @@ from .pyOpt_constraint import Constraint from .pyOpt_error import Error from .pyOpt_objective import Objective +from .pyOpt_types import Dict1DType, Dict2DType, NumpyType from .pyOpt_utils import ( ICOL, IDATA, @@ -28,7 +29,6 @@ scaleRows, ) from .pyOpt_variable import Variable -from .types import Dict1DType, Dict2DType, NumpyType class Optimization: diff --git a/pyoptsparse/types.py b/pyoptsparse/pyOpt_types.py similarity index 100% rename from pyoptsparse/types.py rename to pyoptsparse/pyOpt_types.py diff --git a/pyoptsparse/pyOpt_utils.py b/pyoptsparse/pyOpt_utils.py index 35eb7400..7edba239 100644 --- a/pyoptsparse/pyOpt_utils.py +++ b/pyoptsparse/pyOpt_utils.py @@ -8,6 +8,7 @@ mat = {'csr':[rowp, colind, data], 'shape':[nrow, ncols]} # A csr matrix mat = {'csc':[colp, rowind, data], 'shape':[nrow, ncols]} # A csc matrix """ + # Standard Python modules import importlib import os @@ -24,7 +25,7 @@ # Local modules from .pyOpt_error import Error -from .types import ArrayType +from .pyOpt_types import ArrayType # Define index mnemonics IROW = 0 @@ -576,7 +577,9 @@ def _broadcast_to_array(name: str, value: ArrayType, n_values: int, allow_none: return value -def try_import_compiled_module_from_path(module_name: str, path: Optional[str] = None) -> Union[types.ModuleType, str]: +def try_import_compiled_module_from_path( + module_name: str, path: Optional[str] = None, raise_warning: bool = False +) -> Union[types.ModuleType, str]: """ Attempt to import a module from a given path. @@ -586,6 +589,8 @@ def try_import_compiled_module_from_path(module_name: str, path: Optional[str] = The name of the module path : Optional[str] The path to import from. If None, the default ``sys.path`` is used. + raise_warning : bool + If true, raise an import warning. By default false. Returns ------- @@ -600,7 +605,7 @@ def try_import_compiled_module_from_path(module_name: str, path: Optional[str] = try: module = importlib.import_module(module_name) except ImportError as e: - if path is not None: + if raise_warning: warnings.warn( f"{module_name} module could not be imported from {path}.", stacklevel=2, diff --git a/pyoptsparse/pySLSQP/pySLSQP.py b/pyoptsparse/pySLSQP/pySLSQP.py index d7e6b367..bb2431b4 100644 --- a/pyoptsparse/pySLSQP/pySLSQP.py +++ b/pyoptsparse/pySLSQP/pySLSQP.py @@ -17,7 +17,7 @@ # import the compiled module THIS_DIR = os.path.dirname(os.path.abspath(__file__)) -slsqp = try_import_compiled_module_from_path("slsqp", THIS_DIR) +slsqp = try_import_compiled_module_from_path("slsqp", THIS_DIR, raise_warning=True) class SLSQP(Optimizer): diff --git a/pyoptsparse/pySNOPT/pySNOPT.py b/pyoptsparse/pySNOPT/pySNOPT.py index 63185e33..c576a978 100644 --- a/pyoptsparse/pySNOPT/pySNOPT.py +++ b/pyoptsparse/pySNOPT/pySNOPT.py @@ -385,6 +385,7 @@ def __call__( 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) @@ -444,11 +445,6 @@ def __call__( start = np.array(self.getOption("Start")) ObjAdd = np.array(0.0, float) ProbNm = np.array(self.optProb.name, "c") - cdummy = -1111111 # this is a magic variable defined in SNOPT for undefined strings - cw[51, :] = cdummy # we set these to cdummy so that a placeholder is used in printout - cw[52, :] = cdummy - cw[53, :] = cdummy - cw[54, :] = cdummy xs = np.concatenate((xs, np.zeros(ncon, float))) bl = np.concatenate((blx, blc)) bu = np.concatenate((bux, buc)) @@ -701,11 +697,11 @@ def _set_snopt_options(self, iPrint: int, iSumm: int, cw: ndarray, iw: ndarray, if name == "Problem Type": snopt.snset(value, iPrint, iSumm, inform, cw, iw, rw) elif name == "Print file": - snopt.snset(name + " " + f"{iPrint}", iPrint, iSumm, inform, cw, iw, rw) + snopt.snset(f"{name} {iPrint}", iPrint, iSumm, inform, cw, iw, rw) elif name == "Summary file": - snopt.snset(name + " " + f"{iSumm}", iPrint, iSumm, inform, cw, iw, rw) + snopt.snset(f"{name} {iSumm}", iPrint, iSumm, inform, cw, iw, rw) else: - snopt.snset(name + " " + value, iPrint, iSumm, inform, cw, iw, rw) + 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): diff --git a/tests/test_other.py b/tests/test_other.py index 41435b8d..f0c59b2c 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -18,7 +18,7 @@ def test_nonexistent_path(self): if "snopt" in key: sys.modules.pop(key) with self.assertWarns(UserWarning): - module = try_import_compiled_module_from_path("snopt", "/a/nonexistent/path") + module = try_import_compiled_module_from_path("snopt", "/a/nonexistent/path", raise_warning=True) self.assertTrue(isinstance(module, str)) def test_sys_path_unchanged(self): From 01d511174ba141073c9b8ce51e7680f65995560a Mon Sep 17 00:00:00 2001 From: Ella Wu <602725+ewu63@users.noreply.github.com> Date: Wed, 24 Apr 2024 14:12:34 -0400 Subject: [PATCH 43/44] Populate inform dict for all optimizers (#394) * populate inform dict * bugfix version bump * revert version bump * use empty string instead of None * test print(sol) --- pyoptsparse/pyALPSO/pyALPSO.py | 5 ++--- pyoptsparse/pyCONMIN/pyCONMIN.py | 5 ++--- pyoptsparse/pyNSGA2/pyNSGA2.py | 3 ++- pyoptsparse/pyParOpt/ParOpt.py | 4 +--- tests/testing_utils.py | 3 +++ 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pyoptsparse/pyALPSO/pyALPSO.py b/pyoptsparse/pyALPSO/pyALPSO.py index f684e501..6268618c 100644 --- a/pyoptsparse/pyALPSO/pyALPSO.py +++ b/pyoptsparse/pyALPSO/pyALPSO.py @@ -2,6 +2,7 @@ pyALPSO - A pyOptSparse interface to ALPSO work with sparse optimization problems. """ + # Standard Python modules import datetime import time @@ -191,9 +192,7 @@ def objconfunc(x): self.optProb.comm.bcast(-1, root=0) # Store Results - sol_inform = {} - # sol_inform['value'] = inform - # sol_inform['text'] = self.informs[inform[0]] + sol_inform = {"value": "", "text": ""} # Create the optimization solution sol = self._createSolution(optTime, sol_inform, opt_f, opt_x) diff --git a/pyoptsparse/pyCONMIN/pyCONMIN.py b/pyoptsparse/pyCONMIN/pyCONMIN.py index 87d084ed..97b57930 100644 --- a/pyoptsparse/pyCONMIN/pyCONMIN.py +++ b/pyoptsparse/pyCONMIN/pyCONMIN.py @@ -2,6 +2,7 @@ pyCONMIN - A variation of the pyCONMIN wrapper specificially designed to work with sparse optimization problems. """ + # Standard Python modules import datetime import os @@ -240,9 +241,7 @@ def cnmngrad(n1, n2, x, f, g, ct, df, a, ic, nac): self.optProb.comm.bcast(-1, root=0) # Store Results - sol_inform = {} - # sol_inform['value'] = inform - # sol_inform['text'] = self.informs[inform[0]] + sol_inform = {"value": "", "text": ""} # Create the optimization solution sol = self._createSolution(optTime, sol_inform, ff, xs) diff --git a/pyoptsparse/pyNSGA2/pyNSGA2.py b/pyoptsparse/pyNSGA2/pyNSGA2.py index e5418f58..c6bd2e5c 100644 --- a/pyoptsparse/pyNSGA2/pyNSGA2.py +++ b/pyoptsparse/pyNSGA2/pyNSGA2.py @@ -2,6 +2,7 @@ pyNSGA2 - A variation of the pyNSGA2 wrapper specificially designed to work with sparse optimization problems. """ + # Standard Python modules import os import time @@ -180,7 +181,7 @@ def objconfunc(nreal, nobj, ncon, x, f, g): self.optProb.comm.bcast(-1, root=0) # Store Results - sol_inform = {} + sol_inform = {"value": "", "text": ""} xstar = [0.0] * n for i in range(n): diff --git a/pyoptsparse/pyParOpt/ParOpt.py b/pyoptsparse/pyParOpt/ParOpt.py index f08e33b4..e0e15be4 100644 --- a/pyoptsparse/pyParOpt/ParOpt.py +++ b/pyoptsparse/pyParOpt/ParOpt.py @@ -235,9 +235,7 @@ def evalObjConGradient(self, x, g, A): # Create the optimization solution. Note that the signs on the multipliers # are switch since ParOpt uses a formulation with c(x) >= 0, while pyOpt # uses g(x) = -c(x) <= 0. Therefore the multipliers are reversed. - sol_inform = {} - sol_inform["value"] = None - sol_inform["text"] = None + sol_inform = {"value": "", "text": ""} # If number of constraints is zero, ParOpt returns z as None. # Thus if there is no constraints, should pass an empty list diff --git a/tests/testing_utils.py b/tests/testing_utils.py index fce54c26..fa94cb06 100644 --- a/tests/testing_utils.py +++ b/tests/testing_utils.py @@ -141,6 +141,9 @@ def assert_solution_allclose(self, sol, tol, partial_x=False): ): assert_dict_allclose(sol.lambdaStar, self.lambdaStar[self.sol_index], atol=tol, rtol=tol) + # test printing solution + print(sol) + def assert_inform_equal(self, sol, optInform=None): """ Check that the optInform stored in the Solution object is as expected. From a988404bfaf6c51d3fafa74a7118d9421e17c9cf Mon Sep 17 00:00:00 2001 From: Marco Mangano <36549388+marcomangano@users.noreply.github.com> Date: Thu, 25 Apr 2024 06:48:57 -0400 Subject: [PATCH 44/44] Updating minimum dependency requirements (#388) * Update minimum scipy and numpy versions * Updated dependencies as per new policy * Specify just minor version * Dependencies consistent with stable docker image * test: enforcing minimum python version * missing comma * Actually updating minimum python version * Update environment.yml for windows build * Update windows-build.yml * Minor version bump --------- Co-authored-by: Ella Wu <602725+ewu63@users.noreply.github.com> --- .github/environment.yml | 6 +++--- .github/workflows/windows-build.yml | 2 +- pyoptsparse/__init__.py | 2 +- setup.py | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/environment.yml b/.github/environment.yml index 2cac37e7..10437bc1 100644 --- a/.github/environment.yml +++ b/.github/environment.yml @@ -1,7 +1,7 @@ dependencies: # build - - python >=3.8 - - numpy >=1.16 + - python >=3.9 + - numpy >=1.21 - ipopt - swig - meson >=1.3.2 @@ -13,6 +13,6 @@ dependencies: # testing - parameterized - testflo - - scipy >1.2 + - scipy >=1.7 - mdolab-baseclasses >=1.3.1 - sqlitedict >=1.6 diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml index c5123515..ecca3662 100644 --- a/.github/workflows/windows-build.yml +++ b/.github/workflows/windows-build.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/checkout@v2 - uses: conda-incubator/setup-miniconda@v2 with: - python-version: 3.8 + python-version: 3.9 miniforge-variant: Mambaforge channels: conda-forge,defaults channel-priority: strict diff --git a/pyoptsparse/__init__.py b/pyoptsparse/__init__.py index aa852adb..365985b2 100644 --- a/pyoptsparse/__init__.py +++ b/pyoptsparse/__init__.py @@ -1,4 +1,4 @@ -__version__ = "2.10.2" +__version__ = "2.11.0" from .pyOpt_history import History from .pyOpt_variable import Variable diff --git a/setup.py b/setup.py index d7860e7d..646d3408 100644 --- a/setup.py +++ b/setup.py @@ -103,8 +103,8 @@ def copy_shared_libraries(): keywords="optimization", install_requires=[ "sqlitedict>=1.6", - "numpy>=1.16", - "scipy>1.2", + "numpy>=1.21", + "scipy>=1.7", "mdolab-baseclasses>=1.3.1", ], extras_require={ @@ -134,7 +134,7 @@ def copy_shared_libraries(): package_data={ "": ["*.so", "*.lib", "*.pyd", "*.pdb", "*.dylib", "assets/*", "LICENSE"], }, - python_requires=">=3.7", + python_requires=">=3.9", entry_points={ "gui_scripts": [ "optview = pyoptsparse.postprocessing.OptView:main",