From dade9fcc1ddd2620da5a6626b473353fc454af23 Mon Sep 17 00:00:00 2001 From: zzl Date: Sun, 5 Nov 2023 01:12:22 +0800 Subject: [PATCH 1/8] fix bug: allow for print_level greater than 1 --- cyipopt/scipy_interface.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cyipopt/scipy_interface.py b/cyipopt/scipy_interface.py index 069fa3d..3733ead 100644 --- a/cyipopt/scipy_interface.py +++ b/cyipopt/scipy_interface.py @@ -582,9 +582,7 @@ def minimize_ipopt(fun, # Rename some default scipy options replace_option(options, b'disp', b'print_level') replace_option(options, b'maxiter', b'max_iter') - if getattr(options, 'print_level', False) is True: - options[b'print_level'] = 1 - else: + if b'print_level' not in options: options[b'print_level'] = 0 if b'tol' not in options: options[b'tol'] = tol or 1e-8 From d460d8c988afb0ffe6ed1714cccea4224670da17 Mon Sep 17 00:00:00 2001 From: zzl Date: Sun, 5 Nov 2023 01:57:27 +0800 Subject: [PATCH 2/8] allow warm start for dual variables --- cyipopt/scipy_interface.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/cyipopt/scipy_interface.py b/cyipopt/scipy_interface.py index 3733ead..15a4646 100644 --- a/cyipopt/scipy_interface.py +++ b/cyipopt/scipy_interface.py @@ -373,6 +373,9 @@ def _wrap_funs(fun, jac, hess, hessp, constraints, kwargs): def minimize_ipopt(fun, x0, + mult_g=[], + mult_x_L=[], + mult_x_U=[], args=(), kwargs=None, method=None, @@ -426,6 +429,16 @@ def minimize_ipopt(fun, x0 : array-like, shape(n, ) Initial guess. Array of real elements of shape (n,), where ``n`` is the number of independent variables. + mult_g : list, optional + Initial guess for the Lagrange multipliers of the constraints. A list + of real elements of length ``m``, where ``m`` is the number of + constraints. + mult_x_L : list, optional + Initial guess for the Lagrange multipliers of the lower bounds on the + variables. A list of real elements of length ``n``. + mult_x_U : list, optional + Initial guess for the Lagrange multipliers of the upper bounds on the + variables. A list of real elements of length ``n``. args : tuple, optional Extra arguments passed to the objective function and its derivatives (``fun``, ``jac``, and ``hess``). @@ -598,7 +611,17 @@ def minimize_ipopt(fun, msg = 'Invalid option for IPOPT: {0}: {1} (Original message: "{2}")' raise TypeError(msg.format(option, value, e)) - x, info = nlp.solve(x0) + if len(mult_g) > 0 and len(mult_g) != len(cl): + raise ValueError('`mult_g` must be empty or have length `m`, ' + 'where `m` is the number of constraints.') + if len(mult_x_L) > 0 and len(mult_x_L) != len(lb): + raise ValueError('`mult_x_L` must be empty or have length `n`, ' + 'where `n` is the number of decision variables.') + if len(mult_x_U) > 0 and len(mult_x_U) != len(ub): + raise ValueError('`mult_x_U` must be empty or have length `n`, ' + 'where `n` is the number of decision variables.') + + x, info = nlp.solve(x0, lagrange=mult_g, zl=mult_x_L, zu=mult_x_U) return OptimizeResult(x=x, success=info['status'] == 0, From e49fdcf6484485946633b5f6ece93351a0e01d84 Mon Sep 17 00:00:00 2001 From: zzl Date: Sun, 5 Nov 2023 18:41:12 +0800 Subject: [PATCH 3/8] change the order of kwargs --- cyipopt/scipy_interface.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/cyipopt/scipy_interface.py b/cyipopt/scipy_interface.py index 15a4646..6204eec 100644 --- a/cyipopt/scipy_interface.py +++ b/cyipopt/scipy_interface.py @@ -373,9 +373,6 @@ def _wrap_funs(fun, jac, hess, hessp, constraints, kwargs): def minimize_ipopt(fun, x0, - mult_g=[], - mult_x_L=[], - mult_x_U=[], args=(), kwargs=None, method=None, @@ -386,7 +383,10 @@ def minimize_ipopt(fun, constraints=(), tol=None, callback=None, - options=None): + options=None, + mult_g=[], + mult_x_L=[], + mult_x_U=[],): """ Minimization using Ipopt with an interface like :py:func:`scipy.optimize.minimize`. @@ -429,16 +429,6 @@ def minimize_ipopt(fun, x0 : array-like, shape(n, ) Initial guess. Array of real elements of shape (n,), where ``n`` is the number of independent variables. - mult_g : list, optional - Initial guess for the Lagrange multipliers of the constraints. A list - of real elements of length ``m``, where ``m`` is the number of - constraints. - mult_x_L : list, optional - Initial guess for the Lagrange multipliers of the lower bounds on the - variables. A list of real elements of length ``n``. - mult_x_U : list, optional - Initial guess for the Lagrange multipliers of the upper bounds on the - variables. A list of real elements of length ``n``. args : tuple, optional Extra arguments passed to the objective function and its derivatives (``fun``, ``jac``, and ``hess``). @@ -489,6 +479,16 @@ def minimize_ipopt(fun, callback : callable, optional This parameter is ignored unless `method` is one of the SciPy methods. + mult_g : list, optional + Initial guess for the Lagrange multipliers of the constraints. A list + of real elements of length ``m``, where ``m`` is the number of + constraints. + mult_x_L : list, optional + Initial guess for the Lagrange multipliers of the lower bounds on the + variables. A list of real elements of length ``n``. + mult_x_U : list, optional + Initial guess for the Lagrange multipliers of the upper bounds on the + variables. A list of real elements of length ``n``. References ---------- From 2d7cdb67e83b7d6bac3fc82484a6af602b2c9698 Mon Sep 17 00:00:00 2001 From: zzl Date: Sun, 5 Nov 2023 21:13:45 +0800 Subject: [PATCH 4/8] add more validation check for dual warm start --- cyipopt/scipy_interface.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/cyipopt/scipy_interface.py b/cyipopt/scipy_interface.py index 6204eec..6488e90 100644 --- a/cyipopt/scipy_interface.py +++ b/cyipopt/scipy_interface.py @@ -611,15 +611,9 @@ def minimize_ipopt(fun, msg = 'Invalid option for IPOPT: {0}: {1} (Original message: "{2}")' raise TypeError(msg.format(option, value, e)) - if len(mult_g) > 0 and len(mult_g) != len(cl): - raise ValueError('`mult_g` must be empty or have length `m`, ' - 'where `m` is the number of constraints.') - if len(mult_x_L) > 0 and len(mult_x_L) != len(lb): - raise ValueError('`mult_x_L` must be empty or have length `n`, ' - 'where `n` is the number of decision variables.') - if len(mult_x_U) > 0 and len(mult_x_U) != len(ub): - raise ValueError('`mult_x_U` must be empty or have length `n`, ' - 'where `n` is the number of decision variables.') + _dual_initial_guess_validation("mult_g", mult_g, len(cl)) + _dual_initial_guess_validation("mult_x_L", mult_x_L, len(lb)) + _dual_initial_guess_validation("mult_x_U", mult_x_U, len(ub)) x, info = nlp.solve(x0, lagrange=mult_g, zl=mult_x_L, zu=mult_x_U) @@ -696,3 +690,13 @@ def _minimize_ipopt_iv(fun, x0, args, kwargs, method, jac, hess, hessp, return (fun, x0, args, kwargs, method, jac, hess, hessp, bounds, constraints, tol, callback, options) + +def _dual_initial_guess_validation(dual_name: str, dual_value: list, length: int): + if not isinstance(dual_value, list): + raise TypeError(f'`{dual_name}` must be a list.') + if len(dual_value) > 0: + assert all(isinstance(x, (int, float)) for x in dual_value), \ + f'All elements of `{dual_name}` must be numeric.' + if len(dual_value) != length: + raise ValueError(f'`{dual_name}` must be empty or have length ' + f'`{length}`.') \ No newline at end of file From 811e3923da49d70835f13f75a1c9a636a9fb7152 Mon Sep 17 00:00:00 2001 From: zzl Date: Sun, 5 Nov 2023 21:16:32 +0800 Subject: [PATCH 5/8] add unit test for dual warm start --- cyipopt/tests/unit/test_dual_warm_start.py | 148 +++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 cyipopt/tests/unit/test_dual_warm_start.py diff --git a/cyipopt/tests/unit/test_dual_warm_start.py b/cyipopt/tests/unit/test_dual_warm_start.py new file mode 100644 index 0000000..ce1eef3 --- /dev/null +++ b/cyipopt/tests/unit/test_dual_warm_start.py @@ -0,0 +1,148 @@ +import pytest +import numpy as np +from numpy.testing import assert_, assert_allclose +from cyipopt import minimize_ipopt + + +class TestDualWarmStart: + atol = 1e-7 + + def setup_method(self): + self.opts = {'disp': False} + + def fun(self, d, sign=1.0): + """ + Arguments: + d - A list of two elements, where d[0] represents x and d[1] represents y + in the following equation. + sign - A multiplier for f. Since we want to optimize it, and the SciPy + optimizers can only minimize functions, we need to multiply it by + -1 to achieve the desired solution + Returns: + 2*x*y + 2*x - x**2 - 2*y**2 + + """ + x = d[0] + y = d[1] + return sign*(2*x*y + 2*x - x**2 - 2*y**2) + + def jac(self, d, sign=1.0): + """ + This is the derivative of fun, returning a NumPy array + representing df/dx and df/dy. + + """ + x = d[0] + y = d[1] + dfdx = sign*(-2*x + 2*y + 2) + dfdy = sign*(2*x - 4*y) + return np.array([dfdx, dfdy], float) + + def f_eqcon(self, x, sign=1.0): + """ Equality constraint """ + return np.array([x[0] - x[1]]) + + def f_ieqcon(self, x, sign=1.0): + """ Inequality constraint """ + return np.array([x[0] - x[1] - 1.0]) + + def f_ieqcon2(self, x): + """ Vector inequality constraint """ + return np.asarray(x) + + def fprime_ieqcon2(self, x): + """ Vector inequality constraint, derivative """ + return np.identity(x.shape[0]) + + # minimize + def test_dual_warm_start_unconstrained_without(self): + # unconstrained, without warm start. + res = minimize_ipopt(self.fun, [-1.0, 1.0], args=(-1.0, ), + jac=self.jac, method=None, options=self.opts) + assert_(res['success'], res['message']) + assert_allclose(res.x, [2, 1]) + + @pytest.mark.xfail(raises=(ValueError,), reason="Initial guesses for dual variables have wrong shape") + def test_dual_warm_start_unconstrained_with(self): + # unconstrained, with warm start. + res = minimize_ipopt(self.fun, [-1.0, 1.0], args=(-1.0, ), + jac=self.jac, method=None, options=self.opts, mult_g=[1, 1]) + assert_(res['success'], res['message']) + assert_allclose(res.x, [2, 1]) + + def test_dual_warm_start_equality_without(self): + # equality constraint, without warm start. + res = minimize_ipopt(self.fun, [-1.0, 1.0], jac=self.jac, + method=None, args=(-1.0,), + constraints={'type': 'eq', 'fun':self.f_eqcon, + 'args': (-1.0, )}, + options=self.opts) + assert_(res['success'], res['message']) + assert_allclose(res.x, [1, 1]) + + def test_dual_warm_start_equality_with_right(self): + # equality constraint, with right warm start. + res = minimize_ipopt(self.fun, [-1.0, 1.0], jac=self.jac, + method=None, args=(-1.0,), + constraints={'type': 'eq', 'fun':self.f_eqcon, + 'args': (-1.0, )}, + options=self.opts, mult_g=[1], mult_x_L=[1, 1], mult_x_U=[-1, -1]) + assert_(res['success'], res['message']) + assert_allclose(res.x, [1, 1]) + + @pytest.mark.xfail(raises=(ValueError,), reason="Initial guesses for dual variables have wrong shape") + def test_dual_warm_start_equality_with_wrong_shape(self): + # equality constraint, with wrong warm start shape. + res = minimize_ipopt(self.fun, [-1.0, 1.0], jac=self.jac, + method=None, args=(-1.0,), + constraints={'type': 'eq', 'fun':self.f_eqcon, + 'args': (-1.0, )}, + options=self.opts, mult_g=[1], mult_x_U=[1]) + assert_(res['success'], res['message']) + assert_allclose(res.x, [1, 1]) + + @pytest.mark.xfail(raises=(TypeError,), reason="Initial guesses for dual variables have wrong type") + def test_dual_warm_start_equality_with_wrong_type(self): + # equality constraint, with wrong warm start type. + res = minimize_ipopt(self.fun, [-1.0, 1.0], jac=self.jac, + method=None, args=(-1.0,), + constraints={'type': 'eq', 'fun':self.f_eqcon, + 'args': (-1.0, )}, + options=self.opts, mult_x_L=[1, 1], mult_x_U=np.array([1, 1])) + assert_(res['success'], res['message']) + assert_allclose(res.x, [1, 1]) + + def test_dual_warm_start_inequality_with_right(self): + # inequality constraint, with right warm start. + res = minimize_ipopt(self.fun, [-1.0, 1.0], method=None, + jac=self.jac, args=(-1.0, ), + constraints={'type': 'ineq', + 'fun': self.f_ieqcon, + 'args': (-1.0, )}, + options=self.opts, mult_x_L=[-1, 1], mult_x_U=[1, 1]) + assert_(res['success'], res['message']) + assert_allclose(res.x, [2, 1], atol=1e-3) + + @pytest.mark.xfail(raises=(ValueError,), reason="Initial guesses for dual variables have wrong shape") + def test_dual_warm_start_inequality_vec_with_wrong_shape(self): + # vector inequality constraint, with wrong warm start shape. + res = minimize_ipopt(self.fun, [-1.0, 1.0], jac=self.jac, + method=None, args=(-1.0,), + constraints={'type': 'ineq', + 'fun': self.f_ieqcon2, + 'jac': self.fprime_ieqcon2}, + options=self.opts, mult_g=[1]) + assert_(res['success'], res['message']) + assert_allclose(res.x, [2, 1]) + + @pytest.mark.xfail(raises=(AssertionError,), reason="Initial guesses for dual variables have wrong type") + def test_dual_warm_start_inequality_vec_with_wrong_element_type(self): + # vector inequality constraint, with wrong warm start element type. + res = minimize_ipopt(self.fun, [-1.0, 1.0], jac=self.jac, + method=None, args=(-1.0,), + constraints={'type': 'ineq', + 'fun': self.f_ieqcon2, + 'jac': self.fprime_ieqcon2}, + options=self.opts, mult_g=['1', 1]) + assert_(res['success'], res['message']) + assert_allclose(res.x, [2, 1]) \ No newline at end of file From d2d1427596a3249b09c4fc6df3d6d64682e37835 Mon Sep 17 00:00:00 2001 From: zzl Date: Sun, 5 Nov 2023 21:17:12 +0800 Subject: [PATCH 6/8] handle bool type value of print_level --- cyipopt/scipy_interface.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cyipopt/scipy_interface.py b/cyipopt/scipy_interface.py index 6488e90..4c03316 100644 --- a/cyipopt/scipy_interface.py +++ b/cyipopt/scipy_interface.py @@ -595,7 +595,12 @@ def minimize_ipopt(fun, # Rename some default scipy options replace_option(options, b'disp', b'print_level') replace_option(options, b'maxiter', b'max_iter') - if b'print_level' not in options: + if b'print_level' in options: + if options[b'print_level'] is True: + options[b'print_level'] = 1 + elif options[b'print_level'] is False: + options[b'print_level'] = 0 + else: options[b'print_level'] = 0 if b'tol' not in options: options[b'tol'] = tol or 1e-8 From 56cf2684929ca455427d27028d663c2bc3bf0415 Mon Sep 17 00:00:00 2001 From: zzl Date: Sun, 5 Nov 2023 23:36:22 +0800 Subject: [PATCH 7/8] change the mutable default arguments into None --- cyipopt/scipy_interface.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/cyipopt/scipy_interface.py b/cyipopt/scipy_interface.py index ef4e575..07a49f8 100644 --- a/cyipopt/scipy_interface.py +++ b/cyipopt/scipy_interface.py @@ -386,9 +386,9 @@ def minimize_ipopt(fun, tol=None, callback=None, options=None, - mult_g=[], - mult_x_L=[], - mult_x_U=[],): + mult_g=None, + mult_x_L=None, + mult_x_U=None,): """ Minimization using Ipopt with an interface like :py:func:`scipy.optimize.minimize`. @@ -630,6 +630,9 @@ def minimize_ipopt(fun, msg = 'Invalid option for IPOPT: {0}: {1} (Original message: "{2}")' raise TypeError(msg.format(option, value, e)) + mult_g = [] if mult_g is None else mult_g + mult_x_L = [] if mult_x_L is None else mult_x_L + mult_x_U = [] if mult_x_U is None else mult_x_U _dual_initial_guess_validation("mult_g", mult_g, len(cl)) _dual_initial_guess_validation("mult_x_L", mult_x_L, len(lb)) _dual_initial_guess_validation("mult_x_U", mult_x_U, len(ub)) From 553bdd92da57d0b47a16b53afbcd064e2c7dc6d7 Mon Sep 17 00:00:00 2001 From: zzl Date: Mon, 27 Nov 2023 17:36:05 +0800 Subject: [PATCH 8/8] fix: skip scipy tests when scipy is not installed --- cyipopt/tests/unit/test_dual_warm_start.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cyipopt/tests/unit/test_dual_warm_start.py b/cyipopt/tests/unit/test_dual_warm_start.py index ce1eef3..29c5f78 100644 --- a/cyipopt/tests/unit/test_dual_warm_start.py +++ b/cyipopt/tests/unit/test_dual_warm_start.py @@ -1,9 +1,12 @@ +import sys import pytest import numpy as np from numpy.testing import assert_, assert_allclose from cyipopt import minimize_ipopt +@pytest.mark.skipif("scipy" not in sys.modules, + reason="Test only valid if Scipy available.") class TestDualWarmStart: atol = 1e-7 @@ -89,7 +92,7 @@ def test_dual_warm_start_equality_with_right(self): options=self.opts, mult_g=[1], mult_x_L=[1, 1], mult_x_U=[-1, -1]) assert_(res['success'], res['message']) assert_allclose(res.x, [1, 1]) - + @pytest.mark.xfail(raises=(ValueError,), reason="Initial guesses for dual variables have wrong shape") def test_dual_warm_start_equality_with_wrong_shape(self): # equality constraint, with wrong warm start shape.