Skip to content

Commit f66e9fb

Browse files
IPOPT now returns Lagrange multpliers using the same sign convention as SNOPT. (#416)
* Restored lagrange multipliers in the string representation of the solution. Added lagrange multipliers to IPOPT's output. Note that in the call to _createSolution we negate the multiplier values so that they are consistent with those returned by SNOPT. My understanding is that this is a matter of convention (due to the internal representations of the problems in the two optimizers). * added ParOpt to optimizers which check lambdas * ran black on test_hs071.py * Reverted IPOPT to return is Lagrange multipliers unchanged. * formatting and comment for posterity --------- Co-authored-by: Marco Mangano <[email protected]>
1 parent f0db0af commit f66e9fb

File tree

5 files changed

+34
-8
lines changed

5 files changed

+34
-8
lines changed

pyoptsparse/pyIPOPT/pyIPOPT.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ def eval_intermediate_callback(*args, **kwargs):
296296
sol_inform["text"] = self.informs[status]
297297

298298
# Create the optimization solution
299-
sol = self._createSolution(optTime, sol_inform, obj, x)
299+
sol = self._createSolution(optTime, sol_inform, obj, x, multipliers=constraint_multipliers)
300300

301301
# Indicate solution finished
302302
self.optProb.comm.bcast(-1, root=0)

pyoptsparse/pyOpt_optimization.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -1577,7 +1577,7 @@ def _mapContoOpt_Dict(self, conDict: Dict1DType) -> Dict1DType:
15771577
con_opt = self._mapContoOpt(con)
15781578
return self.processContoDict(con_opt, scaled=False, natural=True)
15791579

1580-
def summary_str(self, minimal_print=False):
1580+
def summary_str(self, minimal_print=False, print_multipliers=False):
15811581
"""
15821582
Print Structured Optimization Problem
15831583
@@ -1588,6 +1588,8 @@ def summary_str(self, minimal_print=False):
15881588
variables and constraints with a non-empty status
15891589
(for example a violated bound).
15901590
This defaults to False, which will print all results.
1591+
print_multipliers : bool
1592+
If True, print the Lagrange multipliers associated with the constraints.
15911593
"""
15921594
TOL = 1.0e-6
15931595

@@ -1656,7 +1658,7 @@ def summary_str(self, minimal_print=False):
16561658

16571659
if len(self.constraints) > 0:
16581660
# must be an instance of the Solution class
1659-
if not isinstance(self, Optimization) and self.lambdaStar is not None:
1661+
if print_multipliers and self.lambdaStar is not None:
16601662
lambdaStar = self.lambdaStar
16611663
lambdaStar_label = "Lagrange Multiplier"
16621664
else:
@@ -1716,7 +1718,7 @@ def summary_str(self, minimal_print=False):
17161718
return text
17171719

17181720
def __str__(self):
1719-
return self.summary_str(minimal_print=False)
1721+
return self.summary_str(minimal_print=False, print_multipliers=False)
17201722

17211723
def __getstate__(self) -> dict:
17221724
"""

pyoptsparse/pyOpt_solution.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def __str__(self) -> str:
7575
"""
7676
Print Structured Solution
7777
"""
78-
text0 = super().__str__()
78+
text0 = self.summary_str(minimal_print=False, print_multipliers=True)
7979
text1 = ""
8080
lines = text0.split("\n")
8181
lines[1] = lines[1][len("Optimization Problem -- ") :]

tests/test_hs071.py

+20-1
Original file line numberDiff line numberDiff line change
@@ -210,9 +210,28 @@ def test_optimization(self, optName):
210210
optOptions = self.optOptions.pop(optName, None)
211211
sol = self.optimize(optOptions=optOptions)
212212
# Check Solution
213-
self.assert_solution_allclose(sol, self.tol[optName])
213+
lambda_sign = -1.0 if optName == "IPOPT" else 1.0
214+
self.assert_solution_allclose(sol, self.tol[optName], lambda_sign=lambda_sign)
214215
# Check informs
215216
self.assert_inform_equal(sol)
217+
# Check the lagrange multipliers in the solution text
218+
lines = str(sol).split("\n")
219+
constraint_header_line_num = [i for i, line in enumerate(lines) if "Constraints" in line][0]
220+
con1_line_num = constraint_header_line_num + 2
221+
con2_line_num = constraint_header_line_num + 3
222+
lambda_con1 = float(lines[con1_line_num].split()[-1])
223+
lambda_con2 = float(lines[con2_line_num].split()[-1])
224+
if optName in ("IPOPT", "SNOPT", "ParOpt"):
225+
# IPOPT returns Lagrange multipliers with opposite sign than SNOPT and ParOpt
226+
lambda_sign = -1.0 if optName == "IPOPT" else 1.0
227+
assert_allclose(
228+
[lambda_con1, lambda_con2],
229+
lambda_sign * np.asarray(self.lambdaStar[0]["con"]),
230+
rtol=1.0e-5,
231+
atol=1.0e-5,
232+
)
233+
else:
234+
assert_allclose([lambda_con1, lambda_con2], [9.0e100, 9.0e100], rtol=1.0e-5, atol=1.0e-5)
216235

217236

218237
if __name__ == "__main__":

tests/testing_utils.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ class OptTest(unittest.TestCase):
8585
def setUp(self):
8686
self.histFileName = None
8787

88-
def assert_solution_allclose(self, sol, tol, partial_x=False):
88+
def assert_solution_allclose(self, sol, tol, partial_x=False, lambda_sign=1.0):
8989
"""
9090
An assertion method to check that the solution object matches the expected
9191
optimum values defined in the class.
@@ -100,6 +100,10 @@ def assert_solution_allclose(self, sol, tol, partial_x=False):
100100
Whether partial assertion of the design variables ``x`` is allowed.
101101
For large problems, we may not have the full x vector available at the optimum,
102102
so we only check a few entries.
103+
lambda_sign : float
104+
The sign of the Lagrange multipliers returned by the optimizer. By convention,
105+
SNOPT and ParOpt return a sign that agrees with the test data, while IPOPT
106+
returns the opposite sign.
103107
"""
104108
if not isinstance(self.xStar, list):
105109
self.xStar = [self.xStar]
@@ -139,7 +143,8 @@ def assert_solution_allclose(self, sol, tol, partial_x=False):
139143
and self.lambdaStar is not None
140144
and sol.lambdaStar is not None
141145
):
142-
assert_dict_allclose(sol.lambdaStar, self.lambdaStar[self.sol_index], atol=tol, rtol=tol)
146+
lamStar = {con: lambda_sign * lam for con, lam in sol.lambdaStar.items()}
147+
assert_dict_allclose(lamStar, self.lambdaStar[self.sol_index], atol=tol, rtol=tol)
143148

144149
# test printing solution
145150
print(sol)

0 commit comments

Comments
 (0)