diff --git a/cyipopt/scipy_interface.py b/cyipopt/scipy_interface.py index 238a4e6..0ae3fa4 100644 --- a/cyipopt/scipy_interface.py +++ b/cyipopt/scipy_interface.py @@ -385,7 +385,10 @@ def minimize_ipopt(fun, constraints=(), tol=None, callback=None, - options=None): + options=None, + mult_g=None, + mult_x_L=None, + mult_x_U=None,): """ Minimization using Ipopt with an interface like :py:func:`scipy.optimize.minimize`. @@ -487,6 +490,16 @@ def minimize_ipopt(fun, This argument is ignored by the default `method` (Ipopt). If `method` is one of the SciPy methods, this is a callable that is called once per iteration. See [2]_ for details. + 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 ---------- @@ -596,8 +609,11 @@ 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 + 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: @@ -614,7 +630,14 @@ 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) + 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)) + + x, info = nlp.solve(x0, lagrange=mult_g, zl=mult_x_L, zu=mult_x_U) return OptimizeResult(x=x, success=info['status'] == 0, @@ -691,3 +714,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 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..29c5f78 --- /dev/null +++ b/cyipopt/tests/unit/test_dual_warm_start.py @@ -0,0 +1,151 @@ +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 + + 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