From 874e821b43d6d853a39b608b742dbf9fcd7898f8 Mon Sep 17 00:00:00 2001 From: Andrew Ellis Date: Sat, 14 Sep 2024 13:35:57 -0400 Subject: [PATCH 1/9] Strongly enforce SLSQP Bounds --- pyoptsparse/pySLSQP/pySLSQP.py | 4 ++-- pyoptsparse/pySLSQP/source/lsq.f | 14 +++++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/pyoptsparse/pySLSQP/pySLSQP.py b/pyoptsparse/pySLSQP/pySLSQP.py index bb2431b4..c8da3852 100644 --- a/pyoptsparse/pySLSQP/pySLSQP.py +++ b/pyoptsparse/pySLSQP/pySLSQP.py @@ -166,7 +166,7 @@ def __call__( # SLSQP - Objective/Constraint Values Function # ================================================================= def slfunc(m, me, la, n, f, g, x): - fobj, fcon, fail = self._masterFunc(x, ["fobj", "fcon"]) + fobj, fcon, fail = self._masterFunc(np.clip(x, blx, bux), ["fobj", "fcon"]) f = fobj g[0:m] = -fcon slsqp.pyflush(self.getOption("IOUT")) @@ -176,7 +176,7 @@ def slfunc(m, me, la, n, f, g, x): # SLSQP - Objective/Constraint Gradients Function # ================================================================= def slgrad(m, me, la, n, f, g, df, dg, x): - gobj, gcon, fail = self._masterFunc(x, ["gobj", "gcon"]) + gobj, gcon, fail = self._masterFunc(np.clip(x, blx, bux), ["gobj", "gcon"]) df[0:n] = gobj.copy() dg[0:m, 0:n] = -gcon.copy() slsqp.pyflush(self.getOption("IOUT")) diff --git a/pyoptsparse/pySLSQP/source/lsq.f b/pyoptsparse/pySLSQP/source/lsq.f index 00a673d0..0ac509c2 100644 --- a/pyoptsparse/pySLSQP/source/lsq.f +++ b/pyoptsparse/pySLSQP/source/lsq.f @@ -177,8 +177,20 @@ SUBROUTINE LSQ(M,MEQ,N,NL,LA,L,G,A,B,XL,XU,X,Y,W,JW,MODE) CALL DCOPY (N3, W(IW+M+N), 1, Y(M+N3+1), 1) ENDIF + call bound(n, x, xl, xu) C END OF SUBROUTINE LSQ END - \ No newline at end of file + + subroutine bound(n, x, xl, xu) + integer n, i + double precision x(n), xl(n), xu(n) + do i = 1, n + if(x(i) < xl(i))then + x(i) = xl(i) + else if(x(i) > xu(i))then + x(i) = xu(i) + end if + end do + end subroutine bound \ No newline at end of file From 230f92fa8168a72cce25f0c335087d72b71ee3d4 Mon Sep 17 00:00:00 2001 From: Andrew Ellis Date: Sun, 15 Sep 2024 12:17:16 -0400 Subject: [PATCH 2/9] added test and clipping for final result --- pyoptsparse/pySLSQP/pySLSQP.py | 4 +++ tests/test_slsqp.py | 47 ++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 tests/test_slsqp.py diff --git a/pyoptsparse/pySLSQP/pySLSQP.py b/pyoptsparse/pySLSQP/pySLSQP.py index c8da3852..0a13b0d7 100644 --- a/pyoptsparse/pySLSQP/pySLSQP.py +++ b/pyoptsparse/pySLSQP/pySLSQP.py @@ -220,6 +220,10 @@ def slgrad(m, me, la, n, f, g, df, dg, x): # fmt: on optTime = time.time() - t0 + # Clip final result to user bounds (this occurs during the optimization as well + # so this just makes the output consistent with what the optimizer sees) + xs = np.clip(xs, blx, bux) + # some entries of W include the lagrange multipliers # for each constraint, there are two entries (lower, upper). # if only one is active, look for the nonzero. If both are active, take the first one diff --git a/tests/test_slsqp.py b/tests/test_slsqp.py new file mode 100644 index 00000000..e8911466 --- /dev/null +++ b/tests/test_slsqp.py @@ -0,0 +1,47 @@ +"""Test class for SLSQP specific tests""" + +# First party modules +from pyoptsparse import Optimization, OPT + +# Local modules +from testing_utils import OptTest + + +class TestSLSQP(OptTest): + + def test_slsqp_strong_bound_enforcement(self): + """ + Test that SLSQP will never evaluate the function or gradient outside + the design variable bounds. Without strong bound enforcement, the + optimizer will step outside the bounds and a ValueError will be raised. + With strong bound enforement, this code will run without raising any + errors + """ + + def objfunc(xdict): + x = xdict["xvars"] + funcs = {} + if x[0] < 0: + raise ValueError("Function cannot be evaluated below 0.") + funcs["obj"] = (x[0] - 1.0) ** 2 + fail = False + return funcs, fail + + def sens(xdict, funcs): + x = xdict["xvars"] + if x[0] < 0: + raise ValueError("Function cannot be evaluated below 0.") + funcsSens = { + "obj": {"xvars": [2 * (x[0] + 1.0)]}, + } + fail = False + return funcsSens, fail + + optProb = Optimization("Problem with Error Region", objfunc) + optProb.addVarGroup("xvars", 1, lower=[0], value=[2]) + optProb.addObj("obj") + opt = OPT("SLSQP") + sol = opt(optProb, sens=sens) + self.assertEqual(sol.optInform["value"], 0) + self.assertGreaterEqual(sol.xStar["xvars"][0], 0) + self.assertAlmostEqual(sol.xStar["xvars"][0], 0, places=9) From 8b86b822c94bf91e58f57100532a64f409bcef73 Mon Sep 17 00:00:00 2001 From: Andrew Ellis Date: Mon, 16 Sep 2024 08:46:57 -0400 Subject: [PATCH 3/9] update test --- tests/test_slsqp.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/test_slsqp.py b/tests/test_slsqp.py index e8911466..81cdf938 100644 --- a/tests/test_slsqp.py +++ b/tests/test_slsqp.py @@ -2,12 +2,10 @@ # First party modules from pyoptsparse import Optimization, OPT +import unittest -# Local modules -from testing_utils import OptTest - -class TestSLSQP(OptTest): +class TestSLSQP(unittest.TestCase): def test_slsqp_strong_bound_enforcement(self): """ From ee582f92d5d74ba0ce9d4fe42d6e53c486aa8f0b Mon Sep 17 00:00:00 2001 From: Andrew Ellis Date: Mon, 16 Sep 2024 08:55:11 -0400 Subject: [PATCH 4/9] fix typo --- tests/test_slsqp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_slsqp.py b/tests/test_slsqp.py index 81cdf938..dedbc5ea 100644 --- a/tests/test_slsqp.py +++ b/tests/test_slsqp.py @@ -21,7 +21,7 @@ def objfunc(xdict): funcs = {} if x[0] < 0: raise ValueError("Function cannot be evaluated below 0.") - funcs["obj"] = (x[0] - 1.0) ** 2 + funcs["obj"] = (x[0] + 1.0) ** 2 fail = False return funcs, fail From cd4747f4e43276068bbcc6c215fc4f28e464342e Mon Sep 17 00:00:00 2001 From: Marco Mangano Date: Mon, 16 Sep 2024 10:37:59 -0400 Subject: [PATCH 5/9] isort and black fixes --- tests/test_slsqp.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_slsqp.py b/tests/test_slsqp.py index dedbc5ea..b7fa70e8 100644 --- a/tests/test_slsqp.py +++ b/tests/test_slsqp.py @@ -1,12 +1,13 @@ """Test class for SLSQP specific tests""" -# First party modules -from pyoptsparse import Optimization, OPT +# Standard Python modules import unittest +# First party modules +from pyoptsparse import OPT, Optimization + class TestSLSQP(unittest.TestCase): - def test_slsqp_strong_bound_enforcement(self): """ Test that SLSQP will never evaluate the function or gradient outside From bbc1c6fda1f152bb17f0e02544dd45b3c7bad199 Mon Sep 17 00:00:00 2001 From: Andrew Ellis Date: Thu, 19 Sep 2024 20:44:52 -0400 Subject: [PATCH 6/9] added warning --- pyoptsparse/pySLSQP/pySLSQP.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyoptsparse/pySLSQP/pySLSQP.py b/pyoptsparse/pySLSQP/pySLSQP.py index 0a13b0d7..081f79fe 100644 --- a/pyoptsparse/pySLSQP/pySLSQP.py +++ b/pyoptsparse/pySLSQP/pySLSQP.py @@ -7,6 +7,7 @@ import datetime import os import time +import warnings # External modules import numpy as np @@ -166,6 +167,9 @@ def __call__( # SLSQP - Objective/Constraint Values Function # ================================================================= def slfunc(m, me, la, n, f, g, x): + if (x < blx).any() or (x > bux).any(): + warnings.warn("Values in x were outside bounds during a " + "minimize step, clipping to bounds", RuntimeWarning) fobj, fcon, fail = self._masterFunc(np.clip(x, blx, bux), ["fobj", "fcon"]) f = fobj g[0:m] = -fcon From bdb628394d86fc92719923a28fecb8781de4bbaa Mon Sep 17 00:00:00 2001 From: Andrew Ellis Date: Sun, 6 Oct 2024 17:13:47 -0400 Subject: [PATCH 7/9] use py opt warnings --- pyoptsparse/pySLSQP/pySLSQP.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyoptsparse/pySLSQP/pySLSQP.py b/pyoptsparse/pySLSQP/pySLSQP.py index 081f79fe..5a05f511 100644 --- a/pyoptsparse/pySLSQP/pySLSQP.py +++ b/pyoptsparse/pySLSQP/pySLSQP.py @@ -7,7 +7,7 @@ import datetime import os import time -import warnings +from ..pyOpt_error import pyOptSparseWarning # External modules import numpy as np @@ -168,8 +168,8 @@ def __call__( # ================================================================= def slfunc(m, me, la, n, f, g, x): if (x < blx).any() or (x > bux).any(): - warnings.warn("Values in x were outside bounds during a " - "minimize step, clipping to bounds", RuntimeWarning) + pyOptSparseWarning("Values in x were outside bounds during" + " a minimize step, clipping to bounds")YY fobj, fcon, fail = self._masterFunc(np.clip(x, blx, bux), ["fobj", "fcon"]) f = fobj g[0:m] = -fcon From 1cd1a924e6713b0fca09172d2b4c929fc6ba86c7 Mon Sep 17 00:00:00 2001 From: Andrew Ellis Date: Mon, 7 Oct 2024 07:12:26 -0400 Subject: [PATCH 8/9] typo --- pyoptsparse/pySLSQP/pySLSQP.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyoptsparse/pySLSQP/pySLSQP.py b/pyoptsparse/pySLSQP/pySLSQP.py index 5a05f511..65bd33b6 100644 --- a/pyoptsparse/pySLSQP/pySLSQP.py +++ b/pyoptsparse/pySLSQP/pySLSQP.py @@ -169,7 +169,7 @@ def __call__( def slfunc(m, me, la, n, f, g, x): if (x < blx).any() or (x > bux).any(): pyOptSparseWarning("Values in x were outside bounds during" - " a minimize step, clipping to bounds")YY + " a minimize step, clipping to bounds") fobj, fcon, fail = self._masterFunc(np.clip(x, blx, bux), ["fobj", "fcon"]) f = fobj g[0:m] = -fcon From 8f4150997eadfeb5c798ccc999e0ffb6c228eed2 Mon Sep 17 00:00:00 2001 From: Marco Mangano Date: Mon, 7 Oct 2024 11:55:13 -0400 Subject: [PATCH 9/9] formatting fixes --- pyoptsparse/pySLSQP/pySLSQP.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pyoptsparse/pySLSQP/pySLSQP.py b/pyoptsparse/pySLSQP/pySLSQP.py index 65bd33b6..2207239b 100644 --- a/pyoptsparse/pySLSQP/pySLSQP.py +++ b/pyoptsparse/pySLSQP/pySLSQP.py @@ -7,12 +7,12 @@ import datetime import os import time -from ..pyOpt_error import pyOptSparseWarning # External modules import numpy as np # Local modules +from ..pyOpt_error import pyOptSparseWarning from ..pyOpt_optimizer import Optimizer from ..pyOpt_utils import try_import_compiled_module_from_path @@ -168,8 +168,7 @@ def __call__( # ================================================================= def slfunc(m, me, la, n, f, g, x): if (x < blx).any() or (x > bux).any(): - pyOptSparseWarning("Values in x were outside bounds during" - " a minimize step, clipping to bounds") + pyOptSparseWarning("Values in x were outside bounds during" " a minimize step, clipping to bounds") fobj, fcon, fail = self._masterFunc(np.clip(x, blx, bux), ["fobj", "fcon"]) f = fobj g[0:m] = -fcon