From fc08914faee97aa799f7da9cc6efba9d8b7e14b0 Mon Sep 17 00:00:00 2001 From: kanekosh Date: Thu, 20 Jun 2024 11:35:07 -0400 Subject: [PATCH 1/5] updated linear constraint guide --- doc/guide.rst | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/doc/guide.rst b/doc/guide.rst index 0ed5d761..8b03bd40 100644 --- a/doc/guide.rst +++ b/doc/guide.rst @@ -22,13 +22,13 @@ The optimization class is created using the following call: .. code-block:: python - optProb = Optimization("name", objFun) + optProb = Optimization("name", objconFun) -The general template of the objective function is as follows: +The general template of the objective and constraint function is as follows: .. code-block:: python - def obj_fun(xdict): + def objconFun(xdict): funcs = {} funcs["obj_name"] = function(xdict) funcs["con_name"] = function(xdict) @@ -196,16 +196,29 @@ This argument is a dictionary, and the keys must match the design variable sets Essentially what we have done is specified the which blocks of the constraint rows are non-zero, and provided the sparsity structure of ones that are sparse. -For linear constraints the values in ``jac`` are meaningful: +Note that the ``wrt`` and ``jac`` keyword arguments are only supported when user-supplied sensitivity is used. +If automatic gradients from pyOptSparse are used, the constraint Jacobian will necessarily be dense. + +.. note:: + Currently, only the optimizers SNOPT and IPOPT support sparse Jacobians. + +Linear Constraints +~~~~~~~~~~~~~~~~~~ +Linear constraints in pyOptSparse are defined exclusively by ``jac``, ``lower``, and ``upper`` entries of the ``addConGroup`` method. +For linear constraint :math:`g_L \leq Ax + b \leq g_U`, the constraint definition would look like: + +.. code-block:: python + + optProb.addConGroup("con", num_cons, linear=True, wrt=["xvars"], jac={"xvars": A}, lower=g_L - b, upper=g_U - b) + +For linear constraints, the values in ``jac`` are meaningful: they must be the actual linear constraint Jacobian values (which do not change). For non-linear constraints, only the sparsity structure (i.e. which entries are nonzero) is significant. The values themselves will be determined by a call to the ``sens()`` function. -Also note, that the ``wrt`` and ``jac`` keyword arguments are only supported when user-supplied sensitivity is used. -If automatic gradients from pyOptSparse are used, the constraint Jacobian will necessarily be dense. +Users do not need to (and should not) provide the linear constraint values (i.e., :math:`g = Ax + b`) in a user-defined function such as ``objconFun``. +Even if you do compute the value in the function, pyOptSparse will ignore it and only refers to ``jac``, ``lower``, and ``upper`` entries. -.. note:: - Currently, only the optimizers SNOPT and IPOPT support sparse Jacobians. Objectives ++++++++++ From 46b2a31941ab98f25dd0444c225353b576e5238e Mon Sep 17 00:00:00 2001 From: Alasdair Gray Date: Wed, 26 Jun 2024 12:04:09 -0400 Subject: [PATCH 2/5] Raise error if user is returning linear constraint values --- pyoptsparse/pyOpt_optimizer.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pyoptsparse/pyOpt_optimizer.py b/pyoptsparse/pyOpt_optimizer.py index e65f5f15..38cc8c9e 100644 --- a/pyoptsparse/pyOpt_optimizer.py +++ b/pyoptsparse/pyOpt_optimizer.py @@ -367,6 +367,9 @@ def _masterFunc2(self, x, evaluate, writeHist=True): self.userObjTime += time.time() - timeA self.userObjCalls += 1 + if self.callCounter == 0: + self._checkLinearConstraints(funcs) + # Discard zero imaginary components in funcs for key, val in funcs.items(): funcs[key] = np.real(val) @@ -416,6 +419,9 @@ def _masterFunc2(self, x, evaluate, writeHist=True): self.userObjTime += time.time() - timeA self.userObjCalls += 1 + if self.callCounter == 0: + self._checkLinearConstraints(funcs) + # Discard zero imaginary components in funcs for key, val in funcs.items(): funcs[key] = np.real(val) @@ -856,6 +862,14 @@ def _on_setOption(self, name, value): """ pass + def _checkLinearConstraints(self, funcs): + for conName in self.optProb.constraints: + con = self.constraints[conName] + if con.linear and conName in funcs: + raise Error( + "Value for linear constraint returned from user obj function, linear constraints are evaluated internally and should be returned from the user's function." + ) + def setOption(self, name, value=None): """ Generic routine for all option setting. The routine does From 70d01eb3caae64f941fddc060729764dee571be8 Mon Sep 17 00:00:00 2001 From: kanekosh Date: Wed, 26 Jun 2024 14:16:02 -0400 Subject: [PATCH 3/5] minor edits --- doc/guide.rst | 8 ++++---- pyoptsparse/pyOpt_optimizer.py | 12 +++++++++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/doc/guide.rst b/doc/guide.rst index 8b03bd40..b7341eed 100644 --- a/doc/guide.rst +++ b/doc/guide.rst @@ -209,16 +209,16 @@ For linear constraint :math:`g_L \leq Ax + b \leq g_U`, the constraint definitio .. code-block:: python - optProb.addConGroup("con", num_cons, linear=True, wrt=["xvars"], jac={"xvars": A}, lower=g_L - b, upper=g_U - b) + optProb.addConGroup("con", num_cons, linear=True, wrt=["xvars"], jac={"xvars": A}, lower=gL - b, upper=gU - b) + +Users should not provide the linear constraint values (i.e., :math:`g = Ax + b`) in a user-defined objective/constraint function. +pyOptSparse will raise an error if you do so. For linear constraints, the values in ``jac`` are meaningful: they must be the actual linear constraint Jacobian values (which do not change). For non-linear constraints, only the sparsity structure (i.e. which entries are nonzero) is significant. The values themselves will be determined by a call to the ``sens()`` function. -Users do not need to (and should not) provide the linear constraint values (i.e., :math:`g = Ax + b`) in a user-defined function such as ``objconFun``. -Even if you do compute the value in the function, pyOptSparse will ignore it and only refers to ``jac``, ``lower``, and ``upper`` entries. - Objectives ++++++++++ diff --git a/pyoptsparse/pyOpt_optimizer.py b/pyoptsparse/pyOpt_optimizer.py index 38cc8c9e..abfafa3a 100644 --- a/pyoptsparse/pyOpt_optimizer.py +++ b/pyoptsparse/pyOpt_optimizer.py @@ -367,6 +367,7 @@ def _masterFunc2(self, x, evaluate, writeHist=True): self.userObjTime += time.time() - timeA self.userObjCalls += 1 + # Make sure the user-defined function does *not* return linear constraint values if self.callCounter == 0: self._checkLinearConstraints(funcs) @@ -419,6 +420,7 @@ def _masterFunc2(self, x, evaluate, writeHist=True): self.userObjTime += time.time() - timeA self.userObjCalls += 1 + # Make sure the user-defined function does *not* return linear constraint values if self.callCounter == 0: self._checkLinearConstraints(funcs) @@ -863,11 +865,15 @@ def _on_setOption(self, name, value): pass def _checkLinearConstraints(self, funcs): + """ + Makes sure that the user-defined obj/con function does not compute the linear constraint values + because the linear constraints are exclusively defined by jac and bounds in addConGroup. + """ for conName in self.optProb.constraints: - con = self.constraints[conName] - if con.linear and conName in funcs: + if self.optProb.constraints[conName].linear and conName in funcs: raise Error( - "Value for linear constraint returned from user obj function, linear constraints are evaluated internally and should be returned from the user's function." + "Value for linear constraint returned from user obj function. Linear constraints " + + "are evaluated internally and should not be returned from the user's function." ) def setOption(self, name, value=None): From 1fd95324a10d7f8f353355d580158b5ed463545e Mon Sep 17 00:00:00 2001 From: kanekosh Date: Wed, 26 Jun 2024 15:30:32 -0400 Subject: [PATCH 4/5] modified some tests and added a test --- tests/test_lincon_error.py | 48 ++++++++++++++++++++++++++++++++++ tests/test_snopt_bugfix.py | 33 ++--------------------- tests/test_user_termination.py | 2 -- 3 files changed, 50 insertions(+), 33 deletions(-) create mode 100644 tests/test_lincon_error.py diff --git a/tests/test_lincon_error.py b/tests/test_lincon_error.py new file mode 100644 index 00000000..4b94e928 --- /dev/null +++ b/tests/test_lincon_error.py @@ -0,0 +1,48 @@ +""" +Tests that pyOptSparse raises an error when a user-defined obj/con function returns a linear constraint value +(which should not because linear constraint is defined exclusively by jac and bounds) +""" + + +# Standard Python modules +import unittest + + +# First party modules +from pyoptsparse import Optimization, SLSQP +from pyoptsparse.pyOpt_error import Error + + +def objfunc(xdict): + """Evaluates the equation f(x,y) = (x-3)^2 + xy + (y+4)^2 - 3""" + x = xdict["x"] + funcs = {} + + funcs["obj"] = x**2 + funcs["con"] = x - 1 # falsely return a linear constraint value + + fail = False + return funcs, fail + + +class TestLinearConstraintCheck(unittest.TestCase): + + def test(self): + # define an optimization problem with a linear constraint + optProb = Optimization("test", objfunc) + optProb.addVarGroup("x", 1, value=1) + optProb.addObj("obj") + optProb.addConGroup("con", 1, lower=1.0, linear=True, wrt=["x"], jac={"x": [1.0]}) + + opt = SLSQP() + with self.assertRaises(Error) as context: + opt(optProb, sens="FD") + + # check if we get the expected error message + err_msg = "Value for linear constraint returned from user obj function. Linear constraints " \ + + "are evaluated internally and should not be returned from the user's function." + self.assertEqual(err_msg, str(context.exception)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_snopt_bugfix.py b/tests/test_snopt_bugfix.py index d3c8b598..12ffe28a 100644 --- a/tests/test_snopt_bugfix.py +++ b/tests/test_snopt_bugfix.py @@ -21,35 +21,6 @@ def objfunc(xdict): funcs = {} funcs["obj"] = (x - 3.0) ** 2 + x * y + (y + 4.0) ** 2 - 3.0 - conval = -x + y - funcs["con"] = conval - - fail = False - return funcs, fail - - -def objfunc_no_con(xdict): - """Evaluates the equation f(x,y) = (x-3)^2 + xy + (y+4)^2 - 3""" - x = xdict["x"] - y = xdict["y"] - funcs = {} - - funcs["obj"] = (x - 3.0) ** 2 + x * y + (y + 4.0) ** 2 - 3.0 - - fail = False - return funcs, fail - - -def objfunc_2con(xdict): - """Evaluates the equation f(x,y) = (x-3)^2 + xy + (y+4)^2 - 3""" - x = xdict["x"] - y = xdict["y"] - funcs = {} - - funcs["obj"] = (x - 3.0) ** 2 + x * y + (y + 4.0) ** 2 - 3.0 - conval = -x + y - funcs["con"] = conval * np.ones(2) - funcs["con2"] = (conval + 1) * np.ones(3) fail = False return funcs, fail @@ -115,7 +86,7 @@ def test_opt(self): def test_opt_bug1(self): # Due to a new feature, there is a TypeError when you optimize a model without a constraint. - optProb = Optimization("Paraboloid", objfunc_no_con) + optProb = Optimization("Paraboloid", objfunc) # Design Variables optProb.addVarGroup("x", 1, varType="c", lower=-50.0, upper=50.0, value=0.0) @@ -141,7 +112,7 @@ def test_opt_bug1(self): def test_opt_bug_print_2con(self): # Optimization Object - optProb = Optimization("Paraboloid", objfunc_2con) + optProb = Optimization("Paraboloid", objfunc) # Design Variables optProb.addVarGroup("x", 1, varType="c", lower=-50.0, upper=50.0, value=0.0) diff --git a/tests/test_user_termination.py b/tests/test_user_termination.py index 9aa4a980..017255f3 100644 --- a/tests/test_user_termination.py +++ b/tests/test_user_termination.py @@ -30,8 +30,6 @@ def objfunc(self, xdict): funcs = {} funcs["obj"] = (x - 3.0) ** 2 + x * y + (y + 4.0) ** 2 - 3.0 - conval = -x + y - funcs["con"] = conval if self.obj_count > self.max_obj: fail = 2 From 1e3ce7caa3fe8280670993dca44f33e0e51d20e3 Mon Sep 17 00:00:00 2001 From: kanekosh Date: Wed, 26 Jun 2024 16:08:21 -0400 Subject: [PATCH 5/5] format --- tests/test_lincon_error.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/test_lincon_error.py b/tests/test_lincon_error.py index 4b94e928..7af5723b 100644 --- a/tests/test_lincon_error.py +++ b/tests/test_lincon_error.py @@ -3,13 +3,11 @@ (which should not because linear constraint is defined exclusively by jac and bounds) """ - # Standard Python modules import unittest - # First party modules -from pyoptsparse import Optimization, SLSQP +from pyoptsparse import SLSQP, Optimization from pyoptsparse.pyOpt_error import Error @@ -26,7 +24,6 @@ def objfunc(xdict): class TestLinearConstraintCheck(unittest.TestCase): - def test(self): # define an optimization problem with a linear constraint optProb = Optimization("test", objfunc) @@ -39,8 +36,10 @@ def test(self): opt(optProb, sens="FD") # check if we get the expected error message - err_msg = "Value for linear constraint returned from user obj function. Linear constraints " \ + err_msg = ( + "Value for linear constraint returned from user obj function. Linear constraints " + "are evaluated internally and should not be returned from the user's function." + ) self.assertEqual(err_msg, str(context.exception))