|
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") |
0 commit comments