From 8f9fb1f8c28a47c5d3786246bcc04f5c2f75fbc5 Mon Sep 17 00:00:00 2001 From: gkennedy Date: Thu, 7 Dec 2023 09:26:21 -0500 Subject: [PATCH 01/25] fixed the convertJacobian call when jacType == "csr" so that it returns CSR data instead of passing through --- pyoptsparse/pyOpt_optimizer.py | 2 +- 1 file changed, 1 insertion(+), 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] From 57f3a495020e09ce0dac0492947c1d8f79723e27 Mon Sep 17 00:00:00 2001 From: gkennedy Date: Thu, 7 Dec 2023 10:41:04 -0500 Subject: [PATCH 02/25] added inform values to the ParOpt wrapper --- pyoptsparse/pyParOpt/ParOpt.py | 2 ++ 1 file changed, 2 insertions(+) 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 339d8cc382ac816c096a91510924ad4a08020aaa Mon Sep 17 00:00:00 2001 From: gkennedy Date: Thu, 7 Dec 2023 13:24:18 -0500 Subject: [PATCH 03/25] simplified ParOpt wrapper --- pyoptsparse/pyParOpt/ParOpt.py | 276 +-------------------------------- 1 file changed, 5 insertions(+), 271 deletions(-) diff --git a/pyoptsparse/pyParOpt/ParOpt.py b/pyoptsparse/pyParOpt/ParOpt.py index 044f359b..c36b4e7f 100644 --- a/pyoptsparse/pyParOpt/ParOpt.py +++ b/pyoptsparse/pyParOpt/ParOpt.py @@ -1,272 +1,6 @@ -# Standard Python modules -import datetime -import os -import time +try: + from paropt.paropt_pyoptsparse import ParOptSparse as ParOpt +except: -# External modules -import numpy as np - -# isort: off -# Attempt to import 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 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 - - -class ParOpt(Optimizer): - """ - ParOpt optimizer class - - ParOpt has the capability to handle distributed design vectors. - This is not replicated here since pyOptSparse does not have the - capability to handle this type of design problem. - """ - - 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") - - # Create and fill-in the dictionary of default option values - self.defOpts = {} - paropt_default_options = _ParOpt.getOptionsInfo() - # Manually override the options with missing default values - paropt_default_options["ip_checkpoint_file"].default = "default.out" - paropt_default_options["problem_name"].default = "problem" - for option_name in paropt_default_options: - # Get the type and default value of the named argument - _type = None - if paropt_default_options[option_name].option_type == "bool": - _type = bool - elif paropt_default_options[option_name].option_type == "int": - _type = int - elif paropt_default_options[option_name].option_type == "float": - _type = float - else: - _type = str - default_value = paropt_default_options[option_name].default - - # Set the entry into the dictionary - self.defOpts[option_name] = [_type, default_value] - - self.set_options = {} - self.informs = {} - super().__init__(name, category, defaultOptions=self.defOpts, informs=self.informs, options=options) - - # ParOpt requires a dense Jacobian format - self.jacType = "dense2d" - - return - - def __call__( - self, optProb, sens=None, sensStep=None, sensMode=None, storeHistory=None, hotStart=None, storeSens=True - ): - """ - This is the main routine used to solve the optimization - problem. - - Parameters - ---------- - optProb : Optimization or Solution class instance - This is the complete description of the optimization problem - to be solved by the optimizer - - sens : str or python Function. - Specifiy method to compute sensitivities. To - explictly use pyOptSparse gradient class to do the - derivatives with finite differenes use \'FD\'. \'sens\' - may also be \'CS\' which will cause pyOptSpare to compute - the derivatives using the complex step method. Finally, - \'sens\' may be a python function handle which is expected - to compute the sensitivities directly. For expensive - function evaluations and/or problems with large numbers of - design variables this is the preferred method. - - sensStep : float - Set the step size to use for design variables. Defaults to - 1e-6 when sens is \'FD\' and 1e-40j when sens is \'CS\'. - - sensMode : str - Use \'pgc\' for parallel gradient computations. Only - available with mpi4py and each objective evaluation is - otherwise serial - - storeHistory : str - File name of the history file into which the history of - this optimization will be stored - - hotStart : str - File name of the history file to "replay" for the - optimziation. The optimization problem used to generate - the history file specified in \'hotStart\' must be - **IDENTICAL** to the currently supplied \'optProb\'. By - identical we mean, **EVERY SINGLE PARAMETER MUST BE - IDENTICAL**. As soon as he requested evaluation point - from ParOpt does not match the history, function and - gradient evaluations revert back to normal evaluations. - - storeSens : bool - Flag sepcifying if sensitivities are to be stored in hist. - This is necessay for hot-starting only. - """ - self.startTime = time.time() - self.callCounter = 0 - self.storeSens = storeSens - - if len(optProb.constraints) == 0: - # If the problem is unconstrained, add a dummy constraint. - self.unconstrained = True - optProb.dummyConstraint = True - - # Save the optimization problem and finalize constraint - # Jacobian, in general can only do on root proc - self.optProb = optProb - self.optProb.finalize() - # Set history/hotstart - self._setHistory(storeHistory, hotStart) - self._setInitialCacheValues() - self._setSens(sens, sensStep, sensMode) - blx, bux, xs = self._assembleContinuousVariables() - xs = np.maximum(xs, blx) - xs = np.minimum(xs, bux) - - # The number of design variables - n = len(xs) - - oneSided = True - - if self.unconstrained: - m = 0 - else: - indices, blc, buc, fact = self.optProb.getOrdering(["ne", "le", "ni", "li"], oneSided=oneSided) - m = len(indices) - self.optProb.jacIndices = indices - self.optProb.fact = fact - self.optProb.offset = buc - - if self.optProb.comm.rank == 0: - - class Problem(_ParOpt.Problem): - def __init__(self, ptr, n, m, xs, blx, bux): - super().__init__(MPI.COMM_SELF, n, m) - self.ptr = ptr - self.n = n - self.m = m - self.xs = xs - self.blx = blx - self.bux = bux - self.fobj = 0.0 - return - - def getVarsAndBounds(self, x, lb, ub): - """Get the variable values and bounds""" - # Find the average distance between lower and upper bound - bound_sum = 0.0 - for i in range(len(x)): - if self.blx[i] <= -INFINITY or self.bux[i] >= INFINITY: - bound_sum += 1.0 - else: - bound_sum += self.bux[i] - self.blx[i] - bound_sum = bound_sum / len(x) - - for i in range(len(x)): - x[i] = self.xs[i] - lb[i] = self.blx[i] - ub[i] = self.bux[i] - if self.xs[i] <= self.blx[i]: - x[i] = self.blx[i] + 0.5 * np.min((bound_sum, self.bux[i] - self.blx[i])) - elif self.xs[i] >= self.bux[i]: - x[i] = self.bux[i] - 0.5 * np.min((bound_sum, self.bux[i] - self.blx[i])) - - return - - def evalObjCon(self, x): - """Evaluate the objective and constraint values""" - fobj, fcon, fail = self.ptr._masterFunc(x[:], ["fobj", "fcon"]) - self.fobj = fobj - return fail, fobj, -fcon - - def evalObjConGradient(self, x, g, A): - """Evaluate the objective and constraint gradients""" - gobj, gcon, fail = self.ptr._masterFunc(x[:], ["gobj", "gcon"]) - g[:] = gobj[:] - for i in range(self.m): - A[i][:] = -gcon[i][:] - return fail - - optTime = MPI.Wtime() - - # Optimize the problem - problem = Problem(self, n, m, xs, blx, bux) - optimizer = _ParOpt.Optimizer(problem, self.set_options) - optimizer.optimize() - x, z, zw, zl, zu = optimizer.getOptimizedPoint() - - # Set the total opt time - optTime = MPI.Wtime() - optTime - - # Get the obective function value - fobj = problem.fobj - - if self.storeHistory: - self.metadata["endTime"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - self.metadata["optTime"] = optTime - self.hist.writeData("metadata", self.metadata) - self.hist.close() - - # 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 - - # If number of constraints is zero, ParOpt returns z as None. - # Thus if there is no constraints, should pass an empty list - # to multipliers instead of z. - if z is not None: - sol = self._createSolution(optTime, sol_inform, fobj, x[:], multipliers=-z) - else: - sol = self._createSolution(optTime, sol_inform, fobj, x[:], multipliers=[]) - - # Indicate solution finished - self.optProb.comm.bcast(-1, root=0) - else: # We are not on the root process so go into waiting loop: - self._waitLoop() - sol = None - - # Communication solution and return - sol = self._communicateSolution(sol) - - return sol - - def _on_setOption(self, name, value): - """ - Add the value to the set_options dictionary. - """ - self.set_options[name] = value + class ParOpt: + pass From 8c5a27f772251bd114bfc6b217f3d019d8d50288 Mon Sep 17 00:00:00 2001 From: gkennedy Date: Thu, 7 Dec 2023 13:28:58 -0500 Subject: [PATCH 04/25] updated wrapper --- pyoptsparse/pyParOpt/ParOpt.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyoptsparse/pyParOpt/ParOpt.py b/pyoptsparse/pyParOpt/ParOpt.py index c36b4e7f..cc04bbfa 100644 --- a/pyoptsparse/pyParOpt/ParOpt.py +++ b/pyoptsparse/pyParOpt/ParOpt.py @@ -3,4 +3,8 @@ except: class ParOpt: - pass + def __init__(self, raiseError=True, options={}): + name = "ParOpt" + category = "Local Optimizer" + if raiseError: + raise Error("There was an error importing ParOpt") From e754550a1ac95565ebad05b4b6266beacf8be1be Mon Sep 17 00:00:00 2001 From: Alasdair Gray Date: Wed, 2 Oct 2024 14:08:49 -0400 Subject: [PATCH 05/25] Import error --- pyoptsparse/pyParOpt/ParOpt.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyoptsparse/pyParOpt/ParOpt.py b/pyoptsparse/pyParOpt/ParOpt.py index cc04bbfa..0e4bdac7 100644 --- a/pyoptsparse/pyParOpt/ParOpt.py +++ b/pyoptsparse/pyParOpt/ParOpt.py @@ -1,3 +1,5 @@ +from pyoptsparse.pyOpt_error import Error + try: from paropt.paropt_pyoptsparse import ParOptSparse as ParOpt except: From 0c4e33435ce5166c87682bbac899726ab693e1be Mon Sep 17 00:00:00 2001 From: Alasdair Gray Date: Thu, 20 Feb 2025 17:34:39 -0500 Subject: [PATCH 06/25] Add ParOpt to `test_large_sparse` --- tests/test_large_sparse.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_large_sparse.py b/tests/test_large_sparse.py index 5f88088c..edec544b 100644 --- a/tests/test_large_sparse.py +++ b/tests/test_large_sparse.py @@ -109,6 +109,7 @@ def setup_optProb(self, sparse=True): ("SNOPT", True), ("IPOPT", True), ("SNOPT", False), + ("ParOpt", False), ] ) def test_opt(self, optName, sparse): From 77109896e5e059dd0ca8a84b8040f14fb52c0a1c Mon Sep 17 00:00:00 2001 From: Alasdair Gray Date: Thu, 20 Feb 2025 18:25:43 -0500 Subject: [PATCH 07/25] Run tp109 test on more optimizers --- tests/test_tp109.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/test_tp109.py b/tests/test_tp109.py index 48a6b5d2..08b1a125 100644 --- a/tests/test_tp109.py +++ b/tests/test_tp109.py @@ -27,11 +27,13 @@ f*1 = 0.536206927538e+04 x*1 = [0.674888100445e+03, 0.113417039470e+04, 0.133569060261e+00, -0.371152592466e+00, 0.252e+03, 0.252e+03, 0.201464535316e+03, 0.426660777226e+03, 0.368494083867e+03] """ + # Standard Python modules import unittest # External modules import numpy as np +from parameterized import parameterized # First party modules from pyoptsparse import History, Optimization @@ -179,8 +181,9 @@ def test_snopt_informs(self): sol = self.optimize(optOptions={"Time Limit": 1e-15}) self.assert_inform_equal(sol, 34) - def test_slsqp(self): - self.optName = "SLSQP" + @parameterized.expand(["SLSQP", "PSQP", "NLPQLP"]) # ParOpt Can't solve this problem + def test_optimization(self, optName): + self.optName = optName self.setup_optProb() sol = self.optimize(sens="CS") self.assert_solution_allclose(sol, 1e-7) From be96f607ae91ab87e8782b4801c26f8d8317c894 Mon Sep 17 00:00:00 2001 From: Alasdair Gray Date: Thu, 20 Feb 2025 18:28:18 -0500 Subject: [PATCH 08/25] `isort .` --- pyoptsparse/pyParOpt/ParOpt.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyoptsparse/pyParOpt/ParOpt.py b/pyoptsparse/pyParOpt/ParOpt.py index 0e4bdac7..d57d7036 100644 --- a/pyoptsparse/pyParOpt/ParOpt.py +++ b/pyoptsparse/pyParOpt/ParOpt.py @@ -1,6 +1,8 @@ +# First party modules from pyoptsparse.pyOpt_error import Error try: + # External modules from paropt.paropt_pyoptsparse import ParOptSparse as ParOpt except: From 2a5225ef9a6deae630e6151b4226164b734deba0 Mon Sep 17 00:00:00 2001 From: Alasdair Gray Date: Thu, 20 Feb 2025 18:31:59 -0500 Subject: [PATCH 09/25] flake8 fixes --- pyoptsparse/pyParOpt/ParOpt.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/pyoptsparse/pyParOpt/ParOpt.py b/pyoptsparse/pyParOpt/ParOpt.py index d57d7036..4fc85c7e 100644 --- a/pyoptsparse/pyParOpt/ParOpt.py +++ b/pyoptsparse/pyParOpt/ParOpt.py @@ -1,14 +1,24 @@ # First party modules from pyoptsparse.pyOpt_error import Error +from pyoptsparse.pyOpt_optimizer import Optimizer try: # External modules from paropt.paropt_pyoptsparse import ParOptSparse as ParOpt -except: +except ImportError: - class ParOpt: + class ParOpt(Optimizer): def __init__(self, raiseError=True, options={}): name = "ParOpt" category = "Local Optimizer" + self.set_options = {} + self.informs = {} + super().__init__( + name, + category, + defaultOptions=self.defOpts, + informs=self.informs, + options=options, + ) if raiseError: raise Error("There was an error importing ParOpt") From b6b8d23079ff3893a633c19ce5b0ce079cb65ff2 Mon Sep 17 00:00:00 2001 From: Alasdair Gray Date: Wed, 23 Jul 2025 14:00:54 -0400 Subject: [PATCH 10/25] Allow instances of `OptTest` to implement a `setup_optimizer` method --- tests/testing_utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/testing_utils.py b/tests/testing_utils.py index 7fb041b5..5cc3ef1e 100644 --- a/tests/testing_utils.py +++ b/tests/testing_utils.py @@ -236,7 +236,10 @@ def optimize(self, sens=None, setDV=None, optOptions=None, storeHistory=False, h optOptions = self.update_OptOptions_output(optOptions) # Optimizer try: - opt = OPT(self.optName, options=optOptions) + if hasattr(self, "setup_optimizer"): + opt = self.setup_optimizer(optOptions=optOptions) + else: + opt = OPT(self.optName, options=optOptions) self.optVersion = opt.version except ImportError as e: if self.optName in DEFAULT_OPTIMIZERS: From 959f9d6142bf74ae69d18ac648e2fb66a0d2dba4 Mon Sep 17 00:00:00 2001 From: Alasdair Gray Date: Wed, 23 Jul 2025 14:01:13 -0400 Subject: [PATCH 11/25] Remove Error class that no longer exists --- pyoptsparse/pyParOpt/ParOpt.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyoptsparse/pyParOpt/ParOpt.py b/pyoptsparse/pyParOpt/ParOpt.py index 4fc85c7e..49b3cab8 100644 --- a/pyoptsparse/pyParOpt/ParOpt.py +++ b/pyoptsparse/pyParOpt/ParOpt.py @@ -1,5 +1,4 @@ # First party modules -from pyoptsparse.pyOpt_error import Error from pyoptsparse.pyOpt_optimizer import Optimizer try: @@ -21,4 +20,4 @@ def __init__(self, raiseError=True, options={}): options=options, ) if raiseError: - raise Error("There was an error importing ParOpt") + raise ImportError("There was an error importing ParOpt") From bb632d3cc0bd8c65eca8ce1be72272c36d1ecb6c Mon Sep 17 00:00:00 2001 From: Alasdair Gray Date: Wed, 23 Jul 2025 14:01:31 -0400 Subject: [PATCH 12/25] Add comprehensive testing of all ParOpt variants --- tests/test_paropt.py | 168 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 tests/test_paropt.py diff --git a/tests/test_paropt.py b/tests/test_paropt.py new file mode 100644 index 00000000..68f3c4aa --- /dev/null +++ b/tests/test_paropt.py @@ -0,0 +1,168 @@ +""" +============================================================================== +ParOpt Test +============================================================================== +@File : test_paropt.py +@Date : 2025/07/23 +@Author : Alasdair Christison Gray +@Description : +""" + +# ============================================================================== +# Standard Python modules +# ============================================================================== + +# ============================================================================== +# External Python modules +# ============================================================================== +import numpy as np +import scipy.sparse as sp +import parameterized + +# ============================================================================== +# Extension modules +# ============================================================================== +from pyoptsparse import Optimization, OPT +from testing_utils import OptTest + +# We want to test the following functionality in ParOpt: +# - Dense vs Sparse constraint treatment +# - Trust region, interiot point, MMA algorithms +# - Constrained and unconstrained problems + +test_params = [] +for sparse in [True, False]: + for constrained in [True, False]: + for algorithm in ["tr", "ip", "mma"]: + if not (sparse and algorithm == "tr"): + test_params.append((sparse, constrained, algorithm)) + + +def custom_name_func(testcase_func, param_num, param): + sparseString = "Sparse" if param["sparse"] else "Dense" + constrainedString = "Constrained" if param["constrained"] else "Unconstrained" + algorithmString = param["algorithm"].upper() + return f"{sparseString}_{constrainedString}_{algorithmString}" + + +@parameterized.parameterized_class( + ("sparse", "constrained", "algorithm"), + test_params, + custom_name_func, +) +class TestParOpt(OptTest): + def setUp(self): + super().setUp() + self.name = "Rosenbrock" if self.constrained else "Unconstrained Rosenbrock" + self.N = 50 + self.cons = {"normCon"} if self.constrained else set() + self.objs = {"obj"} + self.DVs = {"x"} + self.fStar = 214.380322 if self.constrained else 0.0 + self.xStar = {"x": np.ones(self.N) * 1 / np.sqrt(2.0) if self.constrained else np.ones(self.N)} + self.optName = "ParOpt" + + def objfunc(self, xdict): + x = xdict["x"] + + funcs = {} + + # for i in range(len(x) - 1): + # funcs["obj"] += 100 * (x[i + 1] - x[i] ** 2) ** 2 + (1 - x[i]) ** 2 + funcs["obj"] = np.sum(100 * (x[1:] - x[:-1] ** 2) ** 2 + (1 - x[:-1]) ** 2) + + if self.constrained: + # Nonlinear constraints, constraints are: + # x0^2 + x1^2 <= 1 + # x1^2 + x2^2 <= 1 + # ... + funcs["normCon"] = x[:-1] ** 2 + x[1:] ** 2 + + fail = False + return funcs, fail + + def sens(self, xdict, funcs): + x = xdict["x"] + funcsSens = {} + grads = np.zeros(len(x)) + + for i in range(len(x) - 1): + grads[i] += 2 * (200 * x[i] ** 3 - 200 * x[i] * x[i + 1] + x[i] - 1) + grads[i + 1] += 200 * (x[i + 1] - x[i] ** 2) + + funcsSens["obj"] = {"x": grads} + + if self.constrained: + # Nonlinear constraints, constraints are: + # x0^2 + x1^2 <= 1 + # x1^2 + x2^2 <= 1 + # ... + dgdx = np.zeros((self.N - 1, self.N)) + for i in range(self.N - 1): + dgdx[i, i] = 2 * x[i] + dgdx[i, i + 1] = 2 * x[i + 1] + dgdx = sp.coo_array(dgdx, shape=(self.N - 1, self.N)) + funcsSens["normCon"] = {"x": dgdx} + + fail = False + return funcsSens, fail + + def setup_optProb(self): + rng = np.random.default_rng(1234) + + optProb = Optimization("Rosenbrock Problem", self.objfunc) + optProb.addObj("obj") + optProb.addVarGroup("x", self.N, lower=0.5, upper=4.0, value=rng.uniform(0.5, 4.0, self.N)) + + if self.constrained: + # Linear constraints: x0 <= x1 <= x2 <= ... <= xN + # x0 - x1 <= 0 + # x1 - x2 <= 0 + # ... + rowInds = [] + colInds = [] + values = [] + for i in range(self.N - 1): + rowInds += [i, i] + colInds += [i, i + 1] + values += [1.0, -1.0] + jac = {"coo": [np.array(rowInds), np.array(colInds), np.array(values)], "shape": [self.N - 1, self.N]} + optProb.addConGroup( + "lincon", + self.N - 1, + upper=0.0, + lower=-1.0, + linear=True, + jac={"x": jac}, + ) + + # Nonlinear constraints have same sparsity structure as linear constraints + optProb.addConGroup("normCon", nCon=self.N - 1, upper=1.0, jac={"x": jac}) + + self.optProb = optProb + + def setup_optimizer(self, optOptions=None): + options = { + "algorithm": self.algorithm, + } + if optOptions is not None: + options.update(optOptions) + return OPT("ParOpt", sparse=self.sparse, options=options) + + def test_opt(self): + self.setup_optProb() + sol = self.optimize() + self.assert_solution_allclose(sol, 1e-5) + + +if __name__ == "__main__": + import unittest + + unittest.main() + # from pyoptsparse import OPT + + # optProb, sens = generateOptProb(100, True) # Example with 10 variables and constraints + # # opt = OPT("ParOpt", sparse=False, options={"algorithm": "ip"}) + # opt = OPT("SNOPT") + # sol = opt(optProb, sens=sens) + # print("Solution:", sol) From ea5d54b03a390c4d8e18e00d88121c10c5c3e14b Mon Sep 17 00:00:00 2001 From: Alasdair Gray Date: Wed, 23 Jul 2025 15:19:10 -0400 Subject: [PATCH 13/25] Improve tests --- tests/test_paropt.py | 80 ++++++++++++++++++++++++++++++++++++------ tests/testing_utils.py | 2 +- 2 files changed, 71 insertions(+), 11 deletions(-) diff --git a/tests/test_paropt.py b/tests/test_paropt.py index 68f3c4aa..eb57fa1b 100644 --- a/tests/test_paropt.py +++ b/tests/test_paropt.py @@ -11,6 +11,7 @@ # ============================================================================== # Standard Python modules # ============================================================================== +import unittest # ============================================================================== # External Python modules @@ -25,6 +26,11 @@ from pyoptsparse import Optimization, OPT from testing_utils import OptTest + +def rosenbrock(x): + return np.sum(100 * (x[1:] - x[:-1] ** 2) ** 2 + (1 - x[:-1]) ** 2) + + # We want to test the following functionality in ParOpt: # - Dense vs Sparse constraint treatment # - Trust region, interiot point, MMA algorithms @@ -34,7 +40,8 @@ for sparse in [True, False]: for constrained in [True, False]: for algorithm in ["tr", "ip", "mma"]: - if not (sparse and algorithm == "tr"): + # MMA cannot solve the unconstrained problem, so we skip it + if not (algorithm == "mma" and not constrained): test_params.append((sparse, constrained, algorithm)) @@ -51,15 +58,18 @@ def custom_name_func(testcase_func, param_num, param): custom_name_func, ) class TestParOpt(OptTest): + max_iter = 500 + tol = 1e-6 + def setUp(self): super().setUp() self.name = "Rosenbrock" if self.constrained else "Unconstrained Rosenbrock" - self.N = 50 + self.N = 20 self.cons = {"normCon"} if self.constrained else set() self.objs = {"obj"} self.DVs = {"x"} - self.fStar = 214.380322 if self.constrained else 0.0 self.xStar = {"x": np.ones(self.N) * 1 / np.sqrt(2.0) if self.constrained else np.ones(self.N)} + self.fStar = rosenbrock(self.xStar["x"]) self.optName = "ParOpt" def objfunc(self, xdict): @@ -69,7 +79,7 @@ def objfunc(self, xdict): # for i in range(len(x) - 1): # funcs["obj"] += 100 * (x[i + 1] - x[i] ** 2) ** 2 + (1 - x[i]) ** 2 - funcs["obj"] = np.sum(100 * (x[1:] - x[:-1] ** 2) ** 2 + (1 - x[:-1]) ** 2) + funcs["obj"] = rosenbrock(x) if self.constrained: # Nonlinear constraints, constraints are: @@ -112,7 +122,7 @@ def setup_optProb(self): optProb = Optimization("Rosenbrock Problem", self.objfunc) optProb.addObj("obj") - optProb.addVarGroup("x", self.N, lower=0.5, upper=4.0, value=rng.uniform(0.5, 4.0, self.N)) + optProb.addVarGroup("x", self.N, lower=0.5, upper=4.0, value=0.5) if self.constrained: # Linear constraints: x0 <= x1 <= x2 <= ... <= xN @@ -142,17 +152,67 @@ def setup_optProb(self): self.optProb = optProb def setup_optimizer(self, optOptions=None): - options = { - "algorithm": self.algorithm, - } + # Use recommended algorithm options from ParOpt documentation + if self.algorithm == "tr": + options = { + "algorithm": "tr", + "tr_max_iterations": self.max_iter, # Maximum number of trust region iterations + "tr_infeas_tol": self.tol, # Feasibility tolerace + "tr_l1_tol": self.tol, # l1 norm for the KKT conditions + "tr_linfty_tol": self.tol, # l-infinity norm for the KKT conditions + "tr_init_size": 0.05, # Initial trust region radius + "tr_min_size": 1e-6, # Minimum trust region radius size + "tr_max_size": 10.0, # Max trust region radius size + "tr_eta": 0.25, # Trust region step acceptance ratio + "tr_adaptive_gamma_update": True, # Use an adaptive update strategy for the penalty + "max_major_iters": 100, # Maximum number of iterations for the IP subproblem solver + "qn_subspace_size": 10, # Subspace size for the quasi-Newton method + "qn_type": "bfgs", # Type of quasi-Newton Hessian approximation + "abs_res_tol": 1e-8, # Tolerance for the subproblem + "starting_point_strategy": "affine_step", # Starting point strategy for the IP + "barrier_strategy": "mehrotra", # Barrier strategy for the IP + "use_line_search": False, # Don't useline searches for the subproblem + } + elif self.algorithm == "ip": + options = { + "algorithm": "ip", + "max_major_iters": self.max_iter, # Maximum number of iterations for the IP subproblem solver + "qn_subspace_size": 10, # Subspace size for the quasi-Newton method + "qn_type": "bfgs", # Type of quasi-Newton Hessian approximation + "abs_res_tol": 1e-8, # Tolerance for the subproblem + "starting_point_strategy": "affine_step", # Starting point strategy for the IP + "barrier_strategy": "mehrotra", # Barrier strategy for the IP + "use_line_search": True, + } + elif self.algorithm == "mma": + options = { + "algorithm": "mma", + "mma_output_file": "paropt.mma", # MMA output file name + "output_file": "paropt.out", # Interior point output file + "mma_max_iterations": self.max_iter, # Maximum number of iterations for MMA + "mma_infeas_tol": self.tol, # Feasibility tolerance for MMA + "mma_l1_tol": self.tol, # l1 tolerance on the on the KKT conditions for MMA + "mma_linfty_tol": self.tol, # l-infinity tolerance on the KKT conditions for MMA + "max_major_iters": 100, # Max iterations for each subproblem + "abs_res_tol": 1e-8, # Tolerance for each subproblem + "starting_point_strategy": "affine_step", # IP initialization strategy + "barrier_strategy": "mehrotra", # IP barrier strategy + "use_line_search": False, # Don't use line searches on the subproblem + } + options["gradient_verification_frequency"] = -1 if optOptions is not None: options.update(optOptions) return OPT("ParOpt", sparse=self.sparse, options=options) def test_opt(self): self.setup_optProb() - sol = self.optimize() - self.assert_solution_allclose(sol, 1e-5) + # If we try to use the sparse wrapper with the trust region algorithm, we should get an error, otherwise everything should work + if self.sparse and self.algorithm == "tr": + with self.assertRaises(ValueError): + sol = self.optimize() + else: + sol = self.optimize() + self.assert_solution_allclose(sol, 1e-5) if __name__ == "__main__": diff --git a/tests/testing_utils.py b/tests/testing_utils.py index 5cc3ef1e..c9535e39 100644 --- a/tests/testing_utils.py +++ b/tests/testing_utils.py @@ -53,7 +53,7 @@ def get_dict_distance(d, d2): "PSQP": {"IFILE": ".out"}, "CONMIN": {"IFILE": ".out"}, "NLPQLP": {"iFile": ".out"}, - "ParOpt": {"output_file": ".out"}, + "ParOpt": {"output_file": ".out", "tr_output_file": ".tr", "mma_output_file": ".mma"}, "ALPSO": {"filename": ".out"}, "NSGA2": {}, } From 9c4e2dd8eedbbe60700cb95ff8d61f07f7620992 Mon Sep 17 00:00:00 2001 From: Alasdair Gray Date: Wed, 23 Jul 2025 15:47:39 -0400 Subject: [PATCH 14/25] typo --- tests/test_paropt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_paropt.py b/tests/test_paropt.py index eb57fa1b..637e5056 100644 --- a/tests/test_paropt.py +++ b/tests/test_paropt.py @@ -126,8 +126,8 @@ def setup_optProb(self): if self.constrained: # Linear constraints: x0 <= x1 <= x2 <= ... <= xN - # x0 - x1 <= 0 - # x1 - x2 <= 0 + # -1 <= x0 - x1 <= 0 + # -1 <= x1 - x2 <= 0 # ... rowInds = [] colInds = [] From 32fb778b52e8f023af81a03a590b0fb448878e85 Mon Sep 17 00:00:00 2001 From: Alasdair Gray Date: Wed, 23 Jul 2025 16:45:33 -0400 Subject: [PATCH 15/25] Fix mpi import stuff --- pyoptsparse/pyParOpt/ParOpt.py | 62 ++++++++++++++++++++++++------- tests/test_require_mpi_env_var.py | 13 ++++--- 2 files changed, 56 insertions(+), 19 deletions(-) diff --git a/pyoptsparse/pyParOpt/ParOpt.py b/pyoptsparse/pyParOpt/ParOpt.py index 49b3cab8..40bd0934 100644 --- a/pyoptsparse/pyParOpt/ParOpt.py +++ b/pyoptsparse/pyParOpt/ParOpt.py @@ -1,23 +1,57 @@ +import os + # First party modules from pyoptsparse.pyOpt_optimizer import Optimizer -try: - # External modules - from paropt.paropt_pyoptsparse import ParOptSparse as ParOpt -except ImportError: +NAME = "ParOpt" +CATEGORY = "Local Optimizer" + +# Attempt to import ParOpt/mpi4py +# If PYOPTSPARSE_REQUIRE_MPI is not set to a recognized positive value, that means the user is explicitly requiring +# pyOptSparse not to use MPI. In this case, we cannot use ParOpt because it requires mpi4py. +if "PYOPTSPARSE_REQUIRE_MPI" in os.environ and os.environ["PYOPTSPARSE_REQUIRE_MPI"].lower() not in [ + "always", + "1", + "true", + "yes", +]: class ParOpt(Optimizer): def __init__(self, raiseError=True, options={}): - name = "ParOpt" - category = "Local Optimizer" - self.set_options = {} - self.informs = {} super().__init__( - name, - category, - defaultOptions=self.defOpts, - informs=self.informs, - options=options, + NAME, + CATEGORY, ) if raiseError: - raise ImportError("There was an error importing ParOpt") + raise ImportError( + "ParOpt was not imported, as requested by the environment variable 'PYOPTSPARSE_REQUIRE_MPI'" + ) + + +# In all other cases, we attempt to import ParOpt and mpi4py. If either import fails, we disable the optimizer. +else: + try: + from mpi4py import MPI + except ImportError: + + class ParOpt(Optimizer): + def __init__(self, raiseError=True, options={}): + super().__init__( + NAME, + CATEGORY, + ) + if raiseError: + raise ImportError("There was an error importing mpi4py, which is required for ParOpt") + + try: + from paropt.paropt_pyoptsparse import ParOptSparse as ParOpt + except ImportError: + + class ParOpt(Optimizer): + def __init__(self, raiseError=True, options={}): + super().__init__( + NAME, + CATEGORY, + ) + if raiseError: + raise ImportError("There was an error importing ParOpt") diff --git a/tests/test_require_mpi_env_var.py b/tests/test_require_mpi_env_var.py index ca95c0ed..0b7dce9a 100644 --- a/tests/test_require_mpi_env_var.py +++ b/tests/test_require_mpi_env_var.py @@ -45,32 +45,35 @@ def test_do_not_use_mpi(self): class TestRequireMPIEnvVarOnParOpt(unittest.TestCase): # Check how the environment variable affects using ParOpt def setUp(self): - # Just check to see if ParOpt is installed before doing any testing + # Skip these tests if ParOpt is not installed try: - from paropt import ParOpt as _ParOpt # noqa: F401 + from paropt import ParOpt except ImportError: raise unittest.SkipTest("Optimizer not available: paropt") def test_require_mpi_check_paropt(self): + """If we require MPI, we should be able to import and instantiate a ParOpt instance.""" os.environ["PYOPTSPARSE_REQUIRE_MPI"] = "1" import pyoptsparse.pyParOpt.ParOpt importlib.reload(pyoptsparse.pyParOpt.ParOpt) - self.assertIsNotNone(pyoptsparse.pyParOpt.ParOpt._ParOpt) + pyoptsparse.pyParOpt.ParOpt.ParOpt() def test_no_mpi_requirement_given_check_paropt(self): + """If the environment variable is not set, we should be able to import ParOpt if we have mpi4py.""" os.environ.pop("PYOPTSPARSE_REQUIRE_MPI", None) import pyoptsparse.pyParOpt.ParOpt importlib.reload(pyoptsparse.pyParOpt.ParOpt) - self.assertIsNotNone(pyoptsparse.pyParOpt.ParOpt._ParOpt) + pyoptsparse.pyParOpt.ParOpt.ParOpt() def test_do_not_use_mpi_check_paropt(self): + """If we explicitly require pyoptsparse not to use MPI then we should get an import error when when try to instantiate ParOpt.""" os.environ["PYOPTSPARSE_REQUIRE_MPI"] = "0" import pyoptsparse.pyParOpt.ParOpt importlib.reload(pyoptsparse.pyParOpt.ParOpt) - self.assertTrue(isinstance(pyoptsparse.pyParOpt.ParOpt._ParOpt, str)) + self.assertRaises(ImportError, pyoptsparse.pyParOpt.ParOpt.ParOpt) if __name__ == "__main__": From 63740e734e2d0793796dd173421b2b490b119ee1 Mon Sep 17 00:00:00 2001 From: Alasdair Gray Date: Wed, 23 Jul 2025 16:45:41 -0400 Subject: [PATCH 16/25] Reword docs --- doc/optimizers/ParOpt.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/optimizers/ParOpt.rst b/doc/optimizers/ParOpt.rst index 048f7acd..1f0b3e71 100644 --- a/doc/optimizers/ParOpt.rst +++ b/doc/optimizers/ParOpt.rst @@ -2,16 +2,16 @@ ParOpt ====== -ParOpt is a nonlinear interior point optimizer that is designed for large parallel design optimization problems with structured sparse constraints. +ParOpt is a nonlinear interior point optimizer that is designed for large parallel design optimization problems. ParOpt is open source and can be downloaded at `https://github.com/smdogroup/paropt `_. Documentation and examples for ParOpt can be found at `https://smdogroup.github.io/paropt/ `_. -The version of ParOpt supported is v2.0.2. +Unlike the other optimizers supported by pyOptSparse, the pyOptSparse interface to ParOpt is maintained as part of ParOpt itself. +We support ParOpt version 2.1.5 and later. Installation ------------ Please follow the instructions `here `_ to install ParOpt as a separate Python package. Make sure that the package is named ``paropt`` and the installation location can be found by Python, so that ``from paropt import ParOpt`` works within the pyOptSparse folder. -This typically requires installing it in a location which is already present under ``$PYTHONPATH`` environment variable, or you can modify the ``.bashrc`` file and manually append the path. Options ------- From 83588ccc3af124e7ee1316db434e3ff9a6564c68 Mon Sep 17 00:00:00 2001 From: Alasdair Gray Date: Wed, 23 Jul 2025 16:47:28 -0400 Subject: [PATCH 17/25] isort --- pyoptsparse/pyParOpt/ParOpt.py | 3 +++ tests/test_paropt.py | 9 +++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/pyoptsparse/pyParOpt/ParOpt.py b/pyoptsparse/pyParOpt/ParOpt.py index 40bd0934..7b984a18 100644 --- a/pyoptsparse/pyParOpt/ParOpt.py +++ b/pyoptsparse/pyParOpt/ParOpt.py @@ -1,3 +1,4 @@ +# Standard Python modules import os # First party modules @@ -31,6 +32,7 @@ def __init__(self, raiseError=True, options={}): # In all other cases, we attempt to import ParOpt and mpi4py. If either import fails, we disable the optimizer. else: try: + # External modules from mpi4py import MPI except ImportError: @@ -44,6 +46,7 @@ def __init__(self, raiseError=True, options={}): raise ImportError("There was an error importing mpi4py, which is required for ParOpt") try: + # External modules from paropt.paropt_pyoptsparse import ParOptSparse as ParOpt except ImportError: diff --git a/tests/test_paropt.py b/tests/test_paropt.py index 637e5056..72541954 100644 --- a/tests/test_paropt.py +++ b/tests/test_paropt.py @@ -13,17 +13,21 @@ # ============================================================================== import unittest +# External modules # ============================================================================== # External Python modules # ============================================================================== import numpy as np -import scipy.sparse as sp import parameterized +import scipy.sparse as sp +# First party modules # ============================================================================== # Extension modules # ============================================================================== -from pyoptsparse import Optimization, OPT +from pyoptsparse import OPT, Optimization + +# Local modules from testing_utils import OptTest @@ -216,6 +220,7 @@ def test_opt(self): if __name__ == "__main__": + # Standard Python modules import unittest unittest.main() From 4bd1816de1aca41e16a0cc74d677c0ed3d9467d1 Mon Sep 17 00:00:00 2001 From: Alasdair Gray Date: Wed, 23 Jul 2025 16:57:23 -0400 Subject: [PATCH 18/25] Fix init args --- pyoptsparse/pyParOpt/ParOpt.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyoptsparse/pyParOpt/ParOpt.py b/pyoptsparse/pyParOpt/ParOpt.py index 7b984a18..33294c75 100644 --- a/pyoptsparse/pyParOpt/ParOpt.py +++ b/pyoptsparse/pyParOpt/ParOpt.py @@ -18,7 +18,7 @@ ]: class ParOpt(Optimizer): - def __init__(self, raiseError=True, options={}): + def __init__(self, raiseError=True, options={}, sparse=True): super().__init__( NAME, CATEGORY, @@ -37,7 +37,7 @@ def __init__(self, raiseError=True, options={}): except ImportError: class ParOpt(Optimizer): - def __init__(self, raiseError=True, options={}): + def __init__(self, raiseError=True, options={}, sparse=True): super().__init__( NAME, CATEGORY, @@ -51,7 +51,7 @@ def __init__(self, raiseError=True, options={}): except ImportError: class ParOpt(Optimizer): - def __init__(self, raiseError=True, options={}): + def __init__(self, raiseError=True, options={}, sparse=True): super().__init__( NAME, CATEGORY, From fd585a0386da47d83a2b321074bfb7c3415e917d Mon Sep 17 00:00:00 2001 From: Alasdair Gray Date: Wed, 23 Jul 2025 17:24:20 -0400 Subject: [PATCH 19/25] Flake8 issues --- pyoptsparse/pyParOpt/ParOpt.py | 31 ++++++++++++++++--------------- tests/test_paropt.py | 20 -------------------- tests/test_require_mpi_env_var.py | 2 +- 3 files changed, 17 insertions(+), 36 deletions(-) diff --git a/pyoptsparse/pyParOpt/ParOpt.py b/pyoptsparse/pyParOpt/ParOpt.py index 33294c75..034ce75c 100644 --- a/pyoptsparse/pyParOpt/ParOpt.py +++ b/pyoptsparse/pyParOpt/ParOpt.py @@ -33,21 +33,22 @@ def __init__(self, raiseError=True, options={}, sparse=True): else: try: # External modules - from mpi4py import MPI - except ImportError: + from mpi4py import MPI # noqa:F401 + + try: + # External modules + from paropt.paropt_pyoptsparse import ParOptSparse as ParOpt + except ImportError: + + class ParOpt(Optimizer): + def __init__(self, raiseError=True, options={}, sparse=True): + super().__init__( + NAME, + CATEGORY, + ) + if raiseError: + raise ImportError("There was an error importing ParOpt") - class ParOpt(Optimizer): - def __init__(self, raiseError=True, options={}, sparse=True): - super().__init__( - NAME, - CATEGORY, - ) - if raiseError: - raise ImportError("There was an error importing mpi4py, which is required for ParOpt") - - try: - # External modules - from paropt.paropt_pyoptsparse import ParOptSparse as ParOpt except ImportError: class ParOpt(Optimizer): @@ -57,4 +58,4 @@ def __init__(self, raiseError=True, options={}, sparse=True): CATEGORY, ) if raiseError: - raise ImportError("There was an error importing ParOpt") + raise ImportError("There was an error importing mpi4py, which is required for ParOpt") diff --git a/tests/test_paropt.py b/tests/test_paropt.py index 72541954..36326c7a 100644 --- a/tests/test_paropt.py +++ b/tests/test_paropt.py @@ -8,23 +8,12 @@ @Description : """ -# ============================================================================== -# Standard Python modules -# ============================================================================== -import unittest - # External modules -# ============================================================================== -# External Python modules -# ============================================================================== import numpy as np import parameterized import scipy.sparse as sp # First party modules -# ============================================================================== -# Extension modules -# ============================================================================== from pyoptsparse import OPT, Optimization # Local modules @@ -122,8 +111,6 @@ def sens(self, xdict, funcs): return funcsSens, fail def setup_optProb(self): - rng = np.random.default_rng(1234) - optProb = Optimization("Rosenbrock Problem", self.objfunc) optProb.addObj("obj") optProb.addVarGroup("x", self.N, lower=0.5, upper=4.0, value=0.5) @@ -224,10 +211,3 @@ def test_opt(self): import unittest unittest.main() - # from pyoptsparse import OPT - - # optProb, sens = generateOptProb(100, True) # Example with 10 variables and constraints - # # opt = OPT("ParOpt", sparse=False, options={"algorithm": "ip"}) - # opt = OPT("SNOPT") - # sol = opt(optProb, sens=sens) - # print("Solution:", sol) diff --git a/tests/test_require_mpi_env_var.py b/tests/test_require_mpi_env_var.py index 0b7dce9a..1b93a8bf 100644 --- a/tests/test_require_mpi_env_var.py +++ b/tests/test_require_mpi_env_var.py @@ -47,7 +47,7 @@ class TestRequireMPIEnvVarOnParOpt(unittest.TestCase): def setUp(self): # Skip these tests if ParOpt is not installed try: - from paropt import ParOpt + from paropt import ParOpt # noqa:F401 except ImportError: raise unittest.SkipTest("Optimizer not available: paropt") From 38d505085ba5ea728f823b468f352924e4a8fe50 Mon Sep 17 00:00:00 2001 From: Alasdair Gray Date: Tue, 29 Jul 2025 11:39:20 -0400 Subject: [PATCH 20/25] Move testing utils to `pyoptsparse.testing` --- pyoptsparse/__init__.py | 1 + pyoptsparse/testing/__init__.py | 1 + .../testing_utils.py => pyoptsparse/testing/pyOpt_testing.py | 0 tests/test_hs015.py | 4 +--- tests/test_hs071.py | 4 +--- tests/test_large_sparse.py | 4 +--- tests/test_nsga2_multi_objective.py | 4 +--- tests/test_optProb.py | 2 +- tests/test_rosenbrock.py | 4 +--- tests/test_sphere.py | 4 +--- tests/test_tp109.py | 4 +--- 11 files changed, 10 insertions(+), 22 deletions(-) create mode 100644 pyoptsparse/testing/__init__.py rename tests/testing_utils.py => pyoptsparse/testing/pyOpt_testing.py (100%) diff --git a/pyoptsparse/__init__.py b/pyoptsparse/__init__.py index 74c88eaa..41e5487a 100644 --- a/pyoptsparse/__init__.py +++ b/pyoptsparse/__init__.py @@ -8,6 +8,7 @@ from .pyOpt_optimization import Optimization from .pyOpt_optimizer import Optimizer, OPT, Optimizers, list_optimizers from .pyOpt_solution import Solution +from . import testing # Now import all the individual optimizers from .pySNOPT.pySNOPT import SNOPT diff --git a/pyoptsparse/testing/__init__.py b/pyoptsparse/testing/__init__.py new file mode 100644 index 00000000..bb525cca --- /dev/null +++ b/pyoptsparse/testing/__init__.py @@ -0,0 +1 @@ +from .pyOpt_testing import * diff --git a/tests/testing_utils.py b/pyoptsparse/testing/pyOpt_testing.py similarity index 100% rename from tests/testing_utils.py rename to pyoptsparse/testing/pyOpt_testing.py diff --git a/tests/test_hs015.py b/tests/test_hs015.py index ffc15e7c..17ff260a 100644 --- a/tests/test_hs015.py +++ b/tests/test_hs015.py @@ -11,9 +11,7 @@ # First party modules from pyoptsparse import OPT, History, Optimization - -# Local modules -from testing_utils import OptTest +from pyoptsparse.testing import OptTest class TestHS15(OptTest): diff --git a/tests/test_hs071.py b/tests/test_hs071.py index f6ccb5b4..4dcf0139 100644 --- a/tests/test_hs071.py +++ b/tests/test_hs071.py @@ -10,9 +10,7 @@ # First party modules from pyoptsparse import History, Optimization - -# Local modules -from testing_utils import OptTest +from pyoptsparse.testing import OptTest class TestHS71(OptTest): diff --git a/tests/test_large_sparse.py b/tests/test_large_sparse.py index edec544b..9e287ef8 100644 --- a/tests/test_large_sparse.py +++ b/tests/test_large_sparse.py @@ -15,9 +15,7 @@ # First party modules from pyoptsparse import Optimization - -# Local modules -from testing_utils import OptTest +from pyoptsparse.testing import OptTest class TestLarge(OptTest): diff --git a/tests/test_nsga2_multi_objective.py b/tests/test_nsga2_multi_objective.py index 7cf0981e..1d2b3442 100644 --- a/tests/test_nsga2_multi_objective.py +++ b/tests/test_nsga2_multi_objective.py @@ -9,9 +9,7 @@ # First party modules from pyoptsparse import Optimization - -# Local modules -from testing_utils import OptTest +from pyoptsparse.testing import OptTest class TestNSGA2(OptTest): diff --git a/tests/test_optProb.py b/tests/test_optProb.py index a0a3c7ec..138f54ea 100644 --- a/tests/test_optProb.py +++ b/tests/test_optProb.py @@ -12,7 +12,7 @@ from pyoptsparse import OPT, Optimization # Local modules -from testing_utils import assert_optProb_size +from pyoptsparse.testing.pyOpt_testing import assert_optProb_size class TestOptProb(unittest.TestCase): diff --git a/tests/test_rosenbrock.py b/tests/test_rosenbrock.py index b590d605..0de7db20 100644 --- a/tests/test_rosenbrock.py +++ b/tests/test_rosenbrock.py @@ -11,9 +11,7 @@ # First party modules from pyoptsparse import History, Optimization - -# Local modules -from testing_utils import OptTest +from pyoptsparse.testing import OptTest class TestRosenbrock(OptTest): diff --git a/tests/test_sphere.py b/tests/test_sphere.py index 4901341e..34c8f0e2 100644 --- a/tests/test_sphere.py +++ b/tests/test_sphere.py @@ -9,9 +9,7 @@ # First party modules from pyoptsparse import Optimization - -# Local modules -from testing_utils import OptTest +from pyoptsparse.testing import OptTest class TestSphere(OptTest): diff --git a/tests/test_tp109.py b/tests/test_tp109.py index 08b1a125..6d5fc1c5 100644 --- a/tests/test_tp109.py +++ b/tests/test_tp109.py @@ -37,9 +37,7 @@ # First party modules from pyoptsparse import History, Optimization - -# Local modules -from testing_utils import OptTest +from pyoptsparse.testing import OptTest USE_LINEAR = True From 22f058044105cc9046c79ccdc81ec4f171ff2d42 Mon Sep 17 00:00:00 2001 From: Alasdair Gray Date: Tue, 29 Jul 2025 11:45:14 -0400 Subject: [PATCH 21/25] Remove ParOpt from optimizer testing --- tests/test_hs015.py | 3 +- tests/test_hs071.py | 7 +- tests/test_large_sparse.py | 1 - tests/test_paropt.py | 213 ------------------------------------- tests/test_rosenbrock.py | 3 +- tests/test_tp109.py | 2 +- 6 files changed, 6 insertions(+), 223 deletions(-) delete mode 100644 tests/test_paropt.py diff --git a/tests/test_hs015.py b/tests/test_hs015.py index 17ff260a..7f312a77 100644 --- a/tests/test_hs015.py +++ b/tests/test_hs015.py @@ -45,7 +45,6 @@ class TestHS15(OptTest): "SLSQP": 1e-5, "NLPQLP": 1e-12, "IPOPT": 1e-4, - "ParOpt": 1e-6, "CONMIN": 1e-10, "PSQP": 5e-12, } @@ -117,7 +116,7 @@ def test_snopt(self): # sol_xvars = [sol.variables["xvars"][i].value for i in range(2)] # assert_allclose(sol_xvars, dv["xvars"], atol=tol, rtol=tol) - @parameterized.expand(["SLSQP", "PSQP", "CONMIN", "NLPQLP", "ParOpt"]) + @parameterized.expand(["SLSQP", "PSQP", "CONMIN", "NLPQLP"]) def test_optimization(self, optName): self.optName = optName self.setup_optProb() diff --git a/tests/test_hs071.py b/tests/test_hs071.py index 4dcf0139..8ea94d1f 100644 --- a/tests/test_hs071.py +++ b/tests/test_hs071.py @@ -31,7 +31,6 @@ class TestHS71(OptTest): "SLSQP": 1e-6, "CONMIN": 1e-3, "PSQP": 1e-6, - "ParOpt": 1e-6, } optOptions = { "CONMIN": { @@ -201,7 +200,7 @@ def test_psqp_informs(self): sol = self.optimize(optOptions={"MIT": 1}) self.assert_inform_equal(sol, 11) - @parameterized.expand(["SNOPT", "IPOPT", "SLSQP", "PSQP", "CONMIN", "NLPQLP", "ParOpt"]) + @parameterized.expand(["SNOPT", "IPOPT", "SLSQP", "PSQP", "CONMIN", "NLPQLP"]) def test_optimization(self, optName): self.optName = optName self.setup_optProb() @@ -219,8 +218,8 @@ def test_optimization(self, optName): con2_line_num = constraint_header_line_num + 3 lambda_con1 = float(lines[con1_line_num].split()[-1]) lambda_con2 = float(lines[con2_line_num].split()[-1]) - if optName in ("IPOPT", "SNOPT", "ParOpt"): - # IPOPT returns Lagrange multipliers with opposite sign than SNOPT and ParOpt + if optName in ("IPOPT", "SNOPT"): + # IPOPT returns Lagrange multipliers with opposite sign than SNOPT lambda_sign = -1.0 if optName == "IPOPT" else 1.0 assert_allclose( [lambda_con1, lambda_con2], diff --git a/tests/test_large_sparse.py b/tests/test_large_sparse.py index 9e287ef8..04053050 100644 --- a/tests/test_large_sparse.py +++ b/tests/test_large_sparse.py @@ -107,7 +107,6 @@ def setup_optProb(self, sparse=True): ("SNOPT", True), ("IPOPT", True), ("SNOPT", False), - ("ParOpt", False), ] ) def test_opt(self, optName, sparse): diff --git a/tests/test_paropt.py b/tests/test_paropt.py deleted file mode 100644 index 36326c7a..00000000 --- a/tests/test_paropt.py +++ /dev/null @@ -1,213 +0,0 @@ -""" -============================================================================== -ParOpt Test -============================================================================== -@File : test_paropt.py -@Date : 2025/07/23 -@Author : Alasdair Christison Gray -@Description : -""" - -# External modules -import numpy as np -import parameterized -import scipy.sparse as sp - -# First party modules -from pyoptsparse import OPT, Optimization - -# Local modules -from testing_utils import OptTest - - -def rosenbrock(x): - return np.sum(100 * (x[1:] - x[:-1] ** 2) ** 2 + (1 - x[:-1]) ** 2) - - -# We want to test the following functionality in ParOpt: -# - Dense vs Sparse constraint treatment -# - Trust region, interiot point, MMA algorithms -# - Constrained and unconstrained problems - -test_params = [] -for sparse in [True, False]: - for constrained in [True, False]: - for algorithm in ["tr", "ip", "mma"]: - # MMA cannot solve the unconstrained problem, so we skip it - if not (algorithm == "mma" and not constrained): - test_params.append((sparse, constrained, algorithm)) - - -def custom_name_func(testcase_func, param_num, param): - sparseString = "Sparse" if param["sparse"] else "Dense" - constrainedString = "Constrained" if param["constrained"] else "Unconstrained" - algorithmString = param["algorithm"].upper() - return f"{sparseString}_{constrainedString}_{algorithmString}" - - -@parameterized.parameterized_class( - ("sparse", "constrained", "algorithm"), - test_params, - custom_name_func, -) -class TestParOpt(OptTest): - max_iter = 500 - tol = 1e-6 - - def setUp(self): - super().setUp() - self.name = "Rosenbrock" if self.constrained else "Unconstrained Rosenbrock" - self.N = 20 - self.cons = {"normCon"} if self.constrained else set() - self.objs = {"obj"} - self.DVs = {"x"} - self.xStar = {"x": np.ones(self.N) * 1 / np.sqrt(2.0) if self.constrained else np.ones(self.N)} - self.fStar = rosenbrock(self.xStar["x"]) - self.optName = "ParOpt" - - def objfunc(self, xdict): - x = xdict["x"] - - funcs = {} - - # for i in range(len(x) - 1): - # funcs["obj"] += 100 * (x[i + 1] - x[i] ** 2) ** 2 + (1 - x[i]) ** 2 - funcs["obj"] = rosenbrock(x) - - if self.constrained: - # Nonlinear constraints, constraints are: - # x0^2 + x1^2 <= 1 - # x1^2 + x2^2 <= 1 - # ... - funcs["normCon"] = x[:-1] ** 2 + x[1:] ** 2 - - fail = False - return funcs, fail - - def sens(self, xdict, funcs): - x = xdict["x"] - funcsSens = {} - grads = np.zeros(len(x)) - - for i in range(len(x) - 1): - grads[i] += 2 * (200 * x[i] ** 3 - 200 * x[i] * x[i + 1] + x[i] - 1) - grads[i + 1] += 200 * (x[i + 1] - x[i] ** 2) - - funcsSens["obj"] = {"x": grads} - - if self.constrained: - # Nonlinear constraints, constraints are: - # x0^2 + x1^2 <= 1 - # x1^2 + x2^2 <= 1 - # ... - dgdx = np.zeros((self.N - 1, self.N)) - for i in range(self.N - 1): - dgdx[i, i] = 2 * x[i] - dgdx[i, i + 1] = 2 * x[i + 1] - dgdx = sp.coo_array(dgdx, shape=(self.N - 1, self.N)) - funcsSens["normCon"] = {"x": dgdx} - - fail = False - return funcsSens, fail - - def setup_optProb(self): - optProb = Optimization("Rosenbrock Problem", self.objfunc) - optProb.addObj("obj") - optProb.addVarGroup("x", self.N, lower=0.5, upper=4.0, value=0.5) - - if self.constrained: - # Linear constraints: x0 <= x1 <= x2 <= ... <= xN - # -1 <= x0 - x1 <= 0 - # -1 <= x1 - x2 <= 0 - # ... - rowInds = [] - colInds = [] - values = [] - for i in range(self.N - 1): - rowInds += [i, i] - colInds += [i, i + 1] - values += [1.0, -1.0] - jac = {"coo": [np.array(rowInds), np.array(colInds), np.array(values)], "shape": [self.N - 1, self.N]} - optProb.addConGroup( - "lincon", - self.N - 1, - upper=0.0, - lower=-1.0, - linear=True, - jac={"x": jac}, - ) - - # Nonlinear constraints have same sparsity structure as linear constraints - optProb.addConGroup("normCon", nCon=self.N - 1, upper=1.0, jac={"x": jac}) - - self.optProb = optProb - - def setup_optimizer(self, optOptions=None): - # Use recommended algorithm options from ParOpt documentation - if self.algorithm == "tr": - options = { - "algorithm": "tr", - "tr_max_iterations": self.max_iter, # Maximum number of trust region iterations - "tr_infeas_tol": self.tol, # Feasibility tolerace - "tr_l1_tol": self.tol, # l1 norm for the KKT conditions - "tr_linfty_tol": self.tol, # l-infinity norm for the KKT conditions - "tr_init_size": 0.05, # Initial trust region radius - "tr_min_size": 1e-6, # Minimum trust region radius size - "tr_max_size": 10.0, # Max trust region radius size - "tr_eta": 0.25, # Trust region step acceptance ratio - "tr_adaptive_gamma_update": True, # Use an adaptive update strategy for the penalty - "max_major_iters": 100, # Maximum number of iterations for the IP subproblem solver - "qn_subspace_size": 10, # Subspace size for the quasi-Newton method - "qn_type": "bfgs", # Type of quasi-Newton Hessian approximation - "abs_res_tol": 1e-8, # Tolerance for the subproblem - "starting_point_strategy": "affine_step", # Starting point strategy for the IP - "barrier_strategy": "mehrotra", # Barrier strategy for the IP - "use_line_search": False, # Don't useline searches for the subproblem - } - elif self.algorithm == "ip": - options = { - "algorithm": "ip", - "max_major_iters": self.max_iter, # Maximum number of iterations for the IP subproblem solver - "qn_subspace_size": 10, # Subspace size for the quasi-Newton method - "qn_type": "bfgs", # Type of quasi-Newton Hessian approximation - "abs_res_tol": 1e-8, # Tolerance for the subproblem - "starting_point_strategy": "affine_step", # Starting point strategy for the IP - "barrier_strategy": "mehrotra", # Barrier strategy for the IP - "use_line_search": True, - } - elif self.algorithm == "mma": - options = { - "algorithm": "mma", - "mma_output_file": "paropt.mma", # MMA output file name - "output_file": "paropt.out", # Interior point output file - "mma_max_iterations": self.max_iter, # Maximum number of iterations for MMA - "mma_infeas_tol": self.tol, # Feasibility tolerance for MMA - "mma_l1_tol": self.tol, # l1 tolerance on the on the KKT conditions for MMA - "mma_linfty_tol": self.tol, # l-infinity tolerance on the KKT conditions for MMA - "max_major_iters": 100, # Max iterations for each subproblem - "abs_res_tol": 1e-8, # Tolerance for each subproblem - "starting_point_strategy": "affine_step", # IP initialization strategy - "barrier_strategy": "mehrotra", # IP barrier strategy - "use_line_search": False, # Don't use line searches on the subproblem - } - options["gradient_verification_frequency"] = -1 - if optOptions is not None: - options.update(optOptions) - return OPT("ParOpt", sparse=self.sparse, options=options) - - def test_opt(self): - self.setup_optProb() - # If we try to use the sparse wrapper with the trust region algorithm, we should get an error, otherwise everything should work - if self.sparse and self.algorithm == "tr": - with self.assertRaises(ValueError): - sol = self.optimize() - else: - sol = self.optimize() - self.assert_solution_allclose(sol, 1e-5) - - -if __name__ == "__main__": - # Standard Python modules - import unittest - - unittest.main() diff --git a/tests/test_rosenbrock.py b/tests/test_rosenbrock.py index 0de7db20..f58c22e8 100644 --- a/tests/test_rosenbrock.py +++ b/tests/test_rosenbrock.py @@ -47,7 +47,6 @@ class TestRosenbrock(OptTest): "SLSQP": 1e-6, "CONMIN": 1e-9, "PSQP": 1e-8, - "ParOpt": 1e-8, } optOptions = { "SLSQP": {"ACC": 1e-10}, @@ -144,7 +143,7 @@ def test_snopt_hotstart_starting_from_grad(self): # The first is from a call we deleted and the second is the call after 'last' self.assertEqual(self.ng, 2) - @parameterized.expand(["IPOPT", "SLSQP", "PSQP", "CONMIN", "NLPQLP", "ParOpt"]) + @parameterized.expand(["IPOPT", "SLSQP", "PSQP", "CONMIN", "NLPQLP"]) def test_optimization(self, optName): self.optName = optName if optName == "IPOPT" and sys.platform == "win32": diff --git a/tests/test_tp109.py b/tests/test_tp109.py index 6d5fc1c5..8d7195c0 100644 --- a/tests/test_tp109.py +++ b/tests/test_tp109.py @@ -179,7 +179,7 @@ def test_snopt_informs(self): sol = self.optimize(optOptions={"Time Limit": 1e-15}) self.assert_inform_equal(sol, 34) - @parameterized.expand(["SLSQP", "PSQP", "NLPQLP"]) # ParOpt Can't solve this problem + @parameterized.expand(["SLSQP", "PSQP", "NLPQLP"]) def test_optimization(self, optName): self.optName = optName self.setup_optProb() From d36d430a85e3f48b641528ceacb3e33486e9c9b9 Mon Sep 17 00:00:00 2001 From: Alasdair Gray Date: Tue, 29 Jul 2025 12:13:34 -0400 Subject: [PATCH 22/25] `isort .` --- tests/test_optProb.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_optProb.py b/tests/test_optProb.py index 138f54ea..1e0d401c 100644 --- a/tests/test_optProb.py +++ b/tests/test_optProb.py @@ -10,8 +10,6 @@ # First party modules from pyoptsparse import OPT, Optimization - -# Local modules from pyoptsparse.testing.pyOpt_testing import assert_optProb_size From d7dcad8c80c3136ccbb1c21ce74f696a8006cb7d Mon Sep 17 00:00:00 2001 From: Alasdair Gray Date: Wed, 30 Jul 2025 13:48:33 -0400 Subject: [PATCH 23/25] Revert "Fix mpi import stuff" This reverts commit 32fb778b52e8f023af81a03a590b0fb448878e85. --- pyoptsparse/pyParOpt/ParOpt.py | 68 +++++++------------------------ tests/test_require_mpi_env_var.py | 34 ---------------- 2 files changed, 15 insertions(+), 87 deletions(-) diff --git a/pyoptsparse/pyParOpt/ParOpt.py b/pyoptsparse/pyParOpt/ParOpt.py index 034ce75c..49b3cab8 100644 --- a/pyoptsparse/pyParOpt/ParOpt.py +++ b/pyoptsparse/pyParOpt/ParOpt.py @@ -1,61 +1,23 @@ -# Standard Python modules -import os - # First party modules from pyoptsparse.pyOpt_optimizer import Optimizer -NAME = "ParOpt" -CATEGORY = "Local Optimizer" - -# Attempt to import ParOpt/mpi4py -# If PYOPTSPARSE_REQUIRE_MPI is not set to a recognized positive value, that means the user is explicitly requiring -# pyOptSparse not to use MPI. In this case, we cannot use ParOpt because it requires mpi4py. -if "PYOPTSPARSE_REQUIRE_MPI" in os.environ and os.environ["PYOPTSPARSE_REQUIRE_MPI"].lower() not in [ - "always", - "1", - "true", - "yes", -]: +try: + # External modules + from paropt.paropt_pyoptsparse import ParOptSparse as ParOpt +except ImportError: class ParOpt(Optimizer): - def __init__(self, raiseError=True, options={}, sparse=True): + def __init__(self, raiseError=True, options={}): + name = "ParOpt" + category = "Local Optimizer" + self.set_options = {} + self.informs = {} super().__init__( - NAME, - CATEGORY, + name, + category, + defaultOptions=self.defOpts, + informs=self.informs, + options=options, ) if raiseError: - raise ImportError( - "ParOpt was not imported, as requested by the environment variable 'PYOPTSPARSE_REQUIRE_MPI'" - ) - - -# In all other cases, we attempt to import ParOpt and mpi4py. If either import fails, we disable the optimizer. -else: - try: - # External modules - from mpi4py import MPI # noqa:F401 - - try: - # External modules - from paropt.paropt_pyoptsparse import ParOptSparse as ParOpt - except ImportError: - - class ParOpt(Optimizer): - def __init__(self, raiseError=True, options={}, sparse=True): - super().__init__( - NAME, - CATEGORY, - ) - if raiseError: - raise ImportError("There was an error importing ParOpt") - - except ImportError: - - class ParOpt(Optimizer): - def __init__(self, raiseError=True, options={}, sparse=True): - super().__init__( - NAME, - CATEGORY, - ) - if raiseError: - raise ImportError("There was an error importing mpi4py, which is required for ParOpt") + raise ImportError("There was an error importing ParOpt") diff --git a/tests/test_require_mpi_env_var.py b/tests/test_require_mpi_env_var.py index 1b93a8bf..b2a0d6a2 100644 --- a/tests/test_require_mpi_env_var.py +++ b/tests/test_require_mpi_env_var.py @@ -42,39 +42,5 @@ def test_do_not_use_mpi(self): self.assertFalse(inspect.ismodule(pyoptsparse.pyOpt_MPI.MPI)) -class TestRequireMPIEnvVarOnParOpt(unittest.TestCase): - # Check how the environment variable affects using ParOpt - def setUp(self): - # Skip these tests if ParOpt is not installed - try: - from paropt import ParOpt # noqa:F401 - except ImportError: - raise unittest.SkipTest("Optimizer not available: paropt") - - def test_require_mpi_check_paropt(self): - """If we require MPI, we should be able to import and instantiate a ParOpt instance.""" - os.environ["PYOPTSPARSE_REQUIRE_MPI"] = "1" - import pyoptsparse.pyParOpt.ParOpt - - importlib.reload(pyoptsparse.pyParOpt.ParOpt) - pyoptsparse.pyParOpt.ParOpt.ParOpt() - - def test_no_mpi_requirement_given_check_paropt(self): - """If the environment variable is not set, we should be able to import ParOpt if we have mpi4py.""" - os.environ.pop("PYOPTSPARSE_REQUIRE_MPI", None) - import pyoptsparse.pyParOpt.ParOpt - - importlib.reload(pyoptsparse.pyParOpt.ParOpt) - pyoptsparse.pyParOpt.ParOpt.ParOpt() - - def test_do_not_use_mpi_check_paropt(self): - """If we explicitly require pyoptsparse not to use MPI then we should get an import error when when try to instantiate ParOpt.""" - os.environ["PYOPTSPARSE_REQUIRE_MPI"] = "0" - import pyoptsparse.pyParOpt.ParOpt - - importlib.reload(pyoptsparse.pyParOpt.ParOpt) - self.assertRaises(ImportError, pyoptsparse.pyParOpt.ParOpt.ParOpt) - - if __name__ == "__main__": unittest.main() From 541e9b86ab45a6573c4a003c6b4b7f799417f181 Mon Sep 17 00:00:00 2001 From: Alasdair Gray Date: Wed, 30 Jul 2025 14:11:40 -0400 Subject: [PATCH 24/25] Small fix --- 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 49b3cab8..ddb72bb3 100644 --- a/pyoptsparse/pyParOpt/ParOpt.py +++ b/pyoptsparse/pyParOpt/ParOpt.py @@ -10,7 +10,7 @@ class ParOpt(Optimizer): def __init__(self, raiseError=True, options={}): name = "ParOpt" category = "Local Optimizer" - self.set_options = {} + self.defOpts = {} self.informs = {} super().__init__( name, From 9aa403f4148f868c8a8e5f5195bda50865a49994 Mon Sep 17 00:00:00 2001 From: Alasdair Gray Date: Wed, 30 Jul 2025 15:16:59 -0400 Subject: [PATCH 25/25] Update docs --- doc/optimizers/ParOpt.rst | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/doc/optimizers/ParOpt.rst b/doc/optimizers/ParOpt.rst index 1f0b3e71..c9ba5fab 100644 --- a/doc/optimizers/ParOpt.rst +++ b/doc/optimizers/ParOpt.rst @@ -2,11 +2,22 @@ ParOpt ====== -ParOpt is a nonlinear interior point optimizer that is designed for large parallel design optimization problems. -ParOpt is open source and can be downloaded at `https://github.com/smdogroup/paropt `_. -Documentation and examples for ParOpt can be found at `https://smdogroup.github.io/paropt/ `_. -Unlike the other optimizers supported by pyOptSparse, the pyOptSparse interface to ParOpt is maintained as part of ParOpt itself. -We support ParOpt version 2.1.5 and later. +ParOpt is an open source package that implements trust-region, interior points, and MMA optimization algorithms. +The ParOpt optimizers are themselves MPI parallel, which allows them to scale to large problems. +Unlike other optimizers supported by pyOptSparse, as of ParOpt version 2.1.5 and later, the pyOptSparse interface to ParOpt is a part of ParOpt itself. +Maintaining the wrapper, and control of which versions of pyOptSparse are compatible with which versions of ParOpt, is therefore the responsibility of the ParOpt developers. +ParOpt can be downloaded at ``_. +Documentation and examples can be found at ``_. +The wrapper code in pyOptSparse is minimal, simply allowing the ParOpt wrapper to be used in the same way as other optimizers in pyOptSparse, through the ``OPT`` method. + +The ParOpt wrapper takes a ``sparse`` argument, which controls whether ParOpt uses sparse or dense storage for the constraint Jacobian. +The default is ``True``, which uses sparse storage, but is incompatible with ParOpt's trust-region algorithm. +If you want to use the trust-region algorithm, you must set ``sparse=False``, e.g.: + +.. code-block:: python + + from pyoptsparse import OPT + opt = OPT("ParOpt", sparse=False) Installation ------------