Skip to content

Commit 22bdeb8

Browse files
gjkennedyA-CGray
andauthored
Add dense and sparse wrappers for ParOpt from ParOpt directly (#414)
* fixed the convertJacobian call when jacType == "csr" so that it returns CSR data instead of passing through * added inform values to the ParOpt wrapper * simplified ParOpt wrapper * updated wrapper * Import error * Add ParOpt to `test_large_sparse` * Run tp109 test on more optimizers * `isort .` * flake8 fixes * Allow instances of `OptTest` to implement a `setup_optimizer` method * Remove Error class that no longer exists * Add comprehensive testing of all ParOpt variants * Improve tests * typo * Fix mpi import stuff * Reword docs * isort * Fix init args * Flake8 issues * Move testing utils to `pyoptsparse.testing` * Remove ParOpt from optimizer testing * `isort .` * Revert "Fix mpi import stuff" This reverts commit 32fb778. * Small fix * Update docs --------- Co-authored-by: Alasdair Gray <alachris@umich.edu>
1 parent 9d417c7 commit 22bdeb8

14 files changed

Lines changed: 64 additions & 335 deletions

doc/optimizers/ParOpt.rst

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,27 @@
22

33
ParOpt
44
======
5-
ParOpt is a nonlinear interior point optimizer that is designed for large parallel design optimization problems with structured sparse constraints.
6-
ParOpt is open source and can be downloaded at `https://github.com/smdogroup/paropt <https://github.com/smdogroup/paropt>`_.
7-
Documentation and examples for ParOpt can be found at `https://smdogroup.github.io/paropt/ <https://smdogroup.github.io/paropt/>`_.
8-
The version of ParOpt supported is v2.0.2.
5+
ParOpt is an open source package that implements trust-region, interior points, and MMA optimization algorithms.
6+
The ParOpt optimizers are themselves MPI parallel, which allows them to scale to large problems.
7+
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.
8+
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.
9+
ParOpt can be downloaded at `<https://github.com/smdogroup/paropt>`_.
10+
Documentation and examples can be found at `<https://smdogroup.github.io/paropt/>`_.
11+
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.
12+
13+
The ParOpt wrapper takes a ``sparse`` argument, which controls whether ParOpt uses sparse or dense storage for the constraint Jacobian.
14+
The default is ``True``, which uses sparse storage, but is incompatible with ParOpt's trust-region algorithm.
15+
If you want to use the trust-region algorithm, you must set ``sparse=False``, e.g.:
16+
17+
.. code-block:: python
18+
19+
from pyoptsparse import OPT
20+
opt = OPT("ParOpt", sparse=False)
921
1022
Installation
1123
------------
1224
Please follow the instructions `here <https://smdogroup.github.io/paropt/>`_ to install ParOpt as a separate Python package.
1325
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.
14-
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.
1526

1627
Options
1728
-------

pyoptsparse/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from .pyOpt_optimization import Optimization
99
from .pyOpt_optimizer import Optimizer, OPT, Optimizers, list_optimizers
1010
from .pyOpt_solution import Solution
11+
from . import testing
1112

1213
# Now import all the individual optimizers
1314
from .pySNOPT.pySNOPT import SNOPT

pyoptsparse/pyParOpt/ParOpt.py

Lines changed: 23 additions & 263 deletions
Original file line numberDiff line numberDiff line change
@@ -1,263 +1,23 @@
1-
# Standard Python modules
2-
import datetime
3-
import os
4-
import time
5-
6-
# External modules
7-
import numpy as np
8-
9-
# Local modules
10-
from ..pyOpt_optimizer import Optimizer
11-
from ..pyOpt_utils import INFINITY, try_import_compiled_module_from_path
12-
13-
# Attempt to import ParOpt/mpi4py
14-
# If PYOPTSPARSE_REQUIRE_MPI is set to a recognized positive value, attempt import
15-
# and raise exception on failure. If set to anything else, no import is attempted.
16-
if "PYOPTSPARSE_REQUIRE_MPI" in os.environ and os.environ["PYOPTSPARSE_REQUIRE_MPI"].lower() not in [
17-
"always",
18-
"1",
19-
"true",
20-
"yes",
21-
]:
22-
_ParOpt = "ParOpt was not imported, as requested by the environment variable 'PYOPTSPARSE_REQUIRE_MPI'"
23-
MPI = "mpi4py was not imported, as requested by the environment variable 'PYOPTSPARSE_REQUIRE_MPI'"
24-
# If PYOPTSPARSE_REQUIRE_MPI is unset, attempt to import mpi4py.
25-
# Since ParOpt requires mpi4py, if either _ParOpt or mpi4py is unavailable
26-
# we disable the optimizer.
27-
else:
28-
_ParOpt = try_import_compiled_module_from_path("paropt.ParOpt")
29-
MPI = try_import_compiled_module_from_path("mpi4py.MPI")
30-
31-
32-
class ParOpt(Optimizer):
33-
"""
34-
ParOpt optimizer class
35-
36-
ParOpt has the capability to handle distributed design vectors.
37-
This is not replicated here since pyOptSparse does not have the
38-
capability to handle this type of design problem.
39-
"""
40-
41-
def __init__(self, raiseError=True, options={}):
42-
name = "ParOpt"
43-
category = "Local Optimizer"
44-
for mod in [_ParOpt, MPI]:
45-
if isinstance(mod, str) and raiseError:
46-
raise ImportError(mod)
47-
48-
# Create and fill-in the dictionary of default option values
49-
self.defOpts = {}
50-
paropt_default_options = _ParOpt.getOptionsInfo()
51-
# Manually override the options with missing default values
52-
paropt_default_options["ip_checkpoint_file"].default = "default.out"
53-
paropt_default_options["problem_name"].default = "problem"
54-
for option_name in paropt_default_options:
55-
# Get the type and default value of the named argument
56-
_type = None
57-
if paropt_default_options[option_name].option_type == "bool":
58-
_type = bool
59-
elif paropt_default_options[option_name].option_type == "int":
60-
_type = int
61-
elif paropt_default_options[option_name].option_type == "float":
62-
_type = float
63-
else:
64-
_type = str
65-
default_value = paropt_default_options[option_name].default
66-
67-
# Set the entry into the dictionary
68-
self.defOpts[option_name] = [_type, default_value]
69-
70-
self.set_options = {}
71-
self.informs = {}
72-
super().__init__(name, category, defaultOptions=self.defOpts, informs=self.informs, options=options)
73-
74-
# ParOpt requires a dense Jacobian format
75-
self.jacType = "dense2d"
76-
77-
return
78-
79-
def __call__(
80-
self, optProb, sens=None, sensStep=None, sensMode=None, storeHistory=None, hotStart=None, storeSens=True
81-
):
82-
"""
83-
This is the main routine used to solve the optimization
84-
problem.
85-
86-
Parameters
87-
----------
88-
optProb : Optimization or Solution class instance
89-
This is the complete description of the optimization problem
90-
to be solved by the optimizer
91-
92-
sens : str or python Function.
93-
Specifiy method to compute sensitivities. To
94-
explictly use pyOptSparse gradient class to do the
95-
derivatives with finite differenes use \'FD\'. \'sens\'
96-
may also be \'CS\' which will cause pyOptSpare to compute
97-
the derivatives using the complex step method. Finally,
98-
\'sens\' may be a python function handle which is expected
99-
to compute the sensitivities directly. For expensive
100-
function evaluations and/or problems with large numbers of
101-
design variables this is the preferred method.
102-
103-
sensStep : float
104-
Set the step size to use for design variables. Defaults to
105-
1e-6 when sens is \'FD\' and 1e-40j when sens is \'CS\'.
106-
107-
sensMode : str
108-
Use \'pgc\' for parallel gradient computations. Only
109-
available with mpi4py and each objective evaluation is
110-
otherwise serial
111-
112-
storeHistory : str
113-
File name of the history file into which the history of
114-
this optimization will be stored
115-
116-
hotStart : str
117-
File name of the history file to "replay" for the
118-
optimziation. The optimization problem used to generate
119-
the history file specified in \'hotStart\' must be
120-
**IDENTICAL** to the currently supplied \'optProb\'. By
121-
identical we mean, **EVERY SINGLE PARAMETER MUST BE
122-
IDENTICAL**. As soon as he requested evaluation point
123-
from ParOpt does not match the history, function and
124-
gradient evaluations revert back to normal evaluations.
125-
126-
storeSens : bool
127-
Flag sepcifying if sensitivities are to be stored in hist.
128-
This is necessay for hot-starting only.
129-
"""
130-
self.startTime = time.time()
131-
self.callCounter = 0
132-
self.storeSens = storeSens
133-
134-
if len(optProb.constraints) == 0:
135-
# If the problem is unconstrained, add a dummy constraint.
136-
self.unconstrained = True
137-
optProb.dummyConstraint = True
138-
139-
# Save the optimization problem and finalize constraint
140-
# Jacobian, in general can only do on root proc
141-
self.optProb = optProb
142-
self.optProb.finalize()
143-
# Set history/hotstart
144-
self._setHistory(storeHistory, hotStart)
145-
self._setInitialCacheValues()
146-
self._setSens(sens, sensStep, sensMode)
147-
blx, bux, xs = self._assembleContinuousVariables()
148-
xs = np.maximum(xs, blx)
149-
xs = np.minimum(xs, bux)
150-
151-
# The number of design variables
152-
n = len(xs)
153-
154-
oneSided = True
155-
156-
if self.unconstrained:
157-
m = 0
158-
else:
159-
indices, blc, buc, fact = self.optProb.getOrdering(["ne", "le", "ni", "li"], oneSided=oneSided)
160-
m = len(indices)
161-
self.optProb.jacIndices = indices
162-
self.optProb.fact = fact
163-
self.optProb.offset = buc
164-
165-
if self.optProb.comm.rank == 0:
166-
167-
class Problem(_ParOpt.Problem):
168-
def __init__(self, ptr, n, m, xs, blx, bux):
169-
super().__init__(MPI.COMM_SELF, nvars=n, ncon=m)
170-
self.ptr = ptr
171-
self.n = n
172-
self.m = m
173-
self.xs = xs
174-
self.blx = blx
175-
self.bux = bux
176-
self.fobj = 0.0
177-
return
178-
179-
def getVarsAndBounds(self, x, lb, ub):
180-
"""Get the variable values and bounds"""
181-
# Find the average distance between lower and upper bound
182-
bound_sum = 0.0
183-
for i in range(len(x)):
184-
if self.blx[i] <= -INFINITY or self.bux[i] >= INFINITY:
185-
bound_sum += 1.0
186-
else:
187-
bound_sum += self.bux[i] - self.blx[i]
188-
bound_sum = bound_sum / len(x)
189-
190-
for i in range(len(x)):
191-
x[i] = self.xs[i]
192-
lb[i] = self.blx[i]
193-
ub[i] = self.bux[i]
194-
if self.xs[i] <= self.blx[i]:
195-
x[i] = self.blx[i] + 0.5 * np.min((bound_sum, self.bux[i] - self.blx[i]))
196-
elif self.xs[i] >= self.bux[i]:
197-
x[i] = self.bux[i] - 0.5 * np.min((bound_sum, self.bux[i] - self.blx[i]))
198-
199-
return
200-
201-
def evalObjCon(self, x):
202-
"""Evaluate the objective and constraint values"""
203-
fobj, fcon, fail = self.ptr._masterFunc(x[:], ["fobj", "fcon"])
204-
self.fobj = fobj
205-
return fail, fobj, -fcon
206-
207-
def evalObjConGradient(self, x, g, A):
208-
"""Evaluate the objective and constraint gradients"""
209-
gobj, gcon, fail = self.ptr._masterFunc(x[:], ["gobj", "gcon"])
210-
g[:] = gobj[:]
211-
for i in range(self.m):
212-
A[i][:] = -gcon[i][:]
213-
return fail
214-
215-
optTime = MPI.Wtime()
216-
217-
# Optimize the problem
218-
problem = Problem(self, n, m, xs, blx, bux)
219-
optimizer = _ParOpt.Optimizer(problem, self.set_options)
220-
optimizer.optimize()
221-
x, z, zw, zl, zu = optimizer.getOptimizedPoint()
222-
223-
# Set the total opt time
224-
optTime = MPI.Wtime() - optTime
225-
226-
# Get the obective function value
227-
fobj = problem.fobj
228-
229-
if self.storeHistory:
230-
self.metadata["endTime"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
231-
self.metadata["optTime"] = optTime
232-
self.hist.writeData("metadata", self.metadata)
233-
self.hist.close()
234-
235-
# Create the optimization solution. Note that the signs on the multipliers
236-
# are switch since ParOpt uses a formulation with c(x) >= 0, while pyOpt
237-
# uses g(x) = -c(x) <= 0. Therefore the multipliers are reversed.
238-
sol_inform = {"value": "", "text": ""}
239-
240-
# If number of constraints is zero, ParOpt returns z as None.
241-
# Thus if there is no constraints, should pass an empty list
242-
# to multipliers instead of z.
243-
if z is not None:
244-
sol = self._createSolution(optTime, sol_inform, fobj, x[:], multipliers=-z)
245-
else:
246-
sol = self._createSolution(optTime, sol_inform, fobj, x[:], multipliers=[])
247-
248-
# Indicate solution finished
249-
self.optProb.comm.bcast(-1, root=0)
250-
else: # We are not on the root process so go into waiting loop:
251-
self._waitLoop()
252-
sol = None
253-
254-
# Communication solution and return
255-
sol = self._communicateSolution(sol)
256-
257-
return sol
258-
259-
def _on_setOption(self, name, value):
260-
"""
261-
Add the value to the set_options dictionary.
262-
"""
263-
self.set_options[name] = value
1+
# First party modules
2+
from pyoptsparse.pyOpt_optimizer import Optimizer
3+
4+
try:
5+
# External modules
6+
from paropt.paropt_pyoptsparse import ParOptSparse as ParOpt
7+
except ImportError:
8+
9+
class ParOpt(Optimizer):
10+
def __init__(self, raiseError=True, options={}):
11+
name = "ParOpt"
12+
category = "Local Optimizer"
13+
self.defOpts = {}
14+
self.informs = {}
15+
super().__init__(
16+
name,
17+
category,
18+
defaultOptions=self.defOpts,
19+
informs=self.informs,
20+
options=options,
21+
)
22+
if raiseError:
23+
raise ImportError("There was an error importing ParOpt")

pyoptsparse/testing/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .pyOpt_testing import *
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def get_dict_distance(d, d2):
5353
"PSQP": {"IFILE": ".out"},
5454
"CONMIN": {"IFILE": ".out"},
5555
"NLPQLP": {"iFile": ".out"},
56-
"ParOpt": {"output_file": ".out"},
56+
"ParOpt": {"output_file": ".out", "tr_output_file": ".tr", "mma_output_file": ".mma"},
5757
"ALPSO": {"filename": ".out"},
5858
"NSGA2": {},
5959
}
@@ -236,7 +236,10 @@ def optimize(self, sens=None, setDV=None, optOptions=None, storeHistory=False, h
236236
optOptions = self.update_OptOptions_output(optOptions)
237237
# Optimizer
238238
try:
239-
opt = OPT(self.optName, options=optOptions)
239+
if hasattr(self, "setup_optimizer"):
240+
opt = self.setup_optimizer(optOptions=optOptions)
241+
else:
242+
opt = OPT(self.optName, options=optOptions)
240243
self.optVersion = opt.version
241244
except ImportError as e:
242245
if self.optName in DEFAULT_OPTIMIZERS:

tests/test_hs015.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,7 @@
1111

1212
# First party modules
1313
from pyoptsparse import OPT, History, Optimization
14-
15-
# Local modules
16-
from testing_utils import OptTest
14+
from pyoptsparse.testing import OptTest
1715

1816

1917
class TestHS15(OptTest):
@@ -47,7 +45,6 @@ class TestHS15(OptTest):
4745
"SLSQP": 1e-5,
4846
"NLPQLP": 1e-12,
4947
"IPOPT": 1e-4,
50-
"ParOpt": 1e-6,
5148
"CONMIN": 1e-10,
5249
"PSQP": 5e-12,
5350
}
@@ -119,7 +116,7 @@ def test_snopt(self):
119116
# sol_xvars = [sol.variables["xvars"][i].value for i in range(2)]
120117
# assert_allclose(sol_xvars, dv["xvars"], atol=tol, rtol=tol)
121118

122-
@parameterized.expand(["SLSQP", "PSQP", "CONMIN", "NLPQLP", "ParOpt"])
119+
@parameterized.expand(["SLSQP", "PSQP", "CONMIN", "NLPQLP"])
123120
def test_optimization(self, optName):
124121
self.optName = optName
125122
self.setup_optProb()

0 commit comments

Comments
 (0)