-
Notifications
You must be signed in to change notification settings - Fork 56
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
FIX: allow for print_level greater than 1 and ENH: allow warm start for dual variables #239
Changes from 7 commits
dade9fc
d460d8c
e49fdcf
2d7cdb6
811e392
d2d1427
5963c45
56cf268
cda2ea6
553bdd9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -385,7 +385,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`. | ||||||||||||||||||||||||||||||||
|
@@ -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 | ||||||||||||||||||||||||||||||||
Comment on lines
-599
to
+616
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This change doesn't appear to make any difference to the way the code functions, except increasing the number of lines of code to do the same thing. Am I missing something? If so, please can you explain why this change is needed? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ipopt print levels are integers between 1 and 12, with the default being 5: https://coin-or.github.io/Ipopt/OPTIONS.html#OPT_print_level There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should use something like the following. This will ensure that if a
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought it this way: The original version would force the
About the default |
||||||||||||||||||||||||||||||||
else: | ||||||||||||||||||||||||||||||||
options[b'print_level'] = 0 | ||||||||||||||||||||||||||||||||
if b'tol' not in options: | ||||||||||||||||||||||||||||||||
|
@@ -614,7 +630,11 @@ 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) | ||||||||||||||||||||||||||||||||
_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, | ||||||||||||||||||||||||||||||||
|
@@ -689,3 +709,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.') | ||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why does this have to be a list? Couldn't any iterable or sequence work? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since cyipopt/cyipopt/cython/ipopt_wrapper.pyx Line 628 in 2048826
|
||||||||||||||||||||||||||||||||
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}`.') |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that using mutable default arguments in function signatures can cause bugs if the function is called more than once. See the "Mutable Default Arguments" section of this article for an explanation and suggestion on how to avoid. A simple way to avoid this foot gun is to use
None
as the default arguments in the function signature and then manually create a new instance of an empty list within the function body if the value of the variable in question isNone
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your patience! I did neglect this.