Skip to content

Commit

Permalink
Merge pull request #228 from mdhaber/eps_option
Browse files Browse the repository at this point in the history
ENH: expose 'eps' option for finite difference step size
  • Loading branch information
moorepants authored Nov 5, 2023
2 parents 9b9d98d + 8f03bde commit 97c9ec7
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 16 deletions.
44 changes: 29 additions & 15 deletions cyipopt/scipy_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,9 @@ class IpoptProblemWrapper(object):
Extra keyword arguments passed to the objective function and its
derivatives (``fun``, ``jac``, ``hess``).
jac : callable, optional
The Jacobian of the objective function: ``jac(x, *args, **kwargs) ->
ndarray, shape(n, )``. If ``None``, SciPy's ``approx_fprime`` is used.
The Jacobian (gradient) of the objective function:
``jac(x, *args, **kwargs) -> ndarray, shape(n, )``.
If ``None``, SciPy's ``approx_fprime`` is used.
hess : callable, optional
If ``None``, the Hessian is computed using IPOPT's numerical methods.
Explicitly defined Hessians are not yet supported for this class.
Expand All @@ -74,7 +75,8 @@ class IpoptProblemWrapper(object):
`(con_val, con_jac)` consisting of the evaluated constraint `con_val`
and the evaluated jacobian `con_jac`.
eps : float, optional
Epsilon used in finite differences.
Step size used in finite difference approximations of the objective
function gradient and constraint Jacobian.
con_dims : array_like, optional
Dimensions p_1, ..., p_m of the m constraint functions
g_1, ..., g_m : R^n -> R^(p_i).
Expand Down Expand Up @@ -436,8 +438,9 @@ def minimize_ipopt(fun,
If unspecified (default), Ipopt is used.
:py:func:`scipy.optimize.minimize` methods can also be used.
jac : callable, optional
The Jacobian of the objective function: ``jac(x, *args, **kwargs) ->
ndarray, shape(n, )``. If ``None``, SciPy's ``approx_fprime`` is used.
The Jacobian (gradient) of the objective function:
``jac(x, *args, **kwargs) -> ndarray, shape(n, )``.
If ``None``, SciPy's ``approx_fprime`` is used.
hess : callable, optional
The Hessian of the objective function:
``hess(x) -> ndarray, shape(n, )``.
Expand Down Expand Up @@ -469,18 +472,28 @@ def minimize_ipopt(fun,
The desired relative convergence tolerance, passed as an option to
Ipopt. See [1]_ for details.
options : dict, optional
A dictionary of solver options. The options ``disp`` and ``maxiter``
are automatically mapped to their Ipopt equivalents ``print_level``
and ``max_iter``. All other options are passed directly to Ipopt. See
[1]_ for details.
A dictionary of solver options.
When `method` is unspecified (default: Ipopt), the options
``disp`` and ``maxiter`` are automatically mapped to their Ipopt
equivalents ``print_level`` and ``max_iter``, and ``eps`` is used to
control the step size of finite difference gradient and constraint
Jacobian approximations. All other options are passed directly
to Ipopt. See [1]_ for details.
For other values of `method`, `options` is passed to the SciPy solver.
See [2]_ for details.
callback : callable, optional
This parameter is ignored unless `method` is one of the SciPy
methods.
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.
References
----------
.. [1] COIN-OR Project. "Ipopt: Ipopt Options".
https://coin-or.github.io/Ipopt/OPTIONS.html
.. [2] The SciPy Developers. "scipy.optimize.minimize".
https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html
Examples
--------
Expand Down Expand Up @@ -552,22 +565,23 @@ def minimize_ipopt(fun,
sparse_jacs, jac_nnz_row, jac_nnz_col = _get_sparse_jacobian_structure(
constraints, x0)

if options is None:
options = {}
eps = options.pop('eps', 1e-8)

problem = IpoptProblemWrapper(fun,
args=args,
kwargs=kwargs,
jac=jac,
hess=hess,
hessp=hessp,
constraints=constraints,
eps=1e-8,
eps=eps,
con_dims=con_dims,
sparse_jacs=sparse_jacs,
jac_nnz_row=jac_nnz_row,
jac_nnz_col=jac_nnz_col)

if options is None:
options = {}

nlp = cyipopt.Problem(n=len(x0),
m=len(cl),
problem_obj=problem,
Expand Down
8 changes: 7 additions & 1 deletion cyipopt/tests/unit/test_scipy_ipopt_from_scipy.py
Original file line number Diff line number Diff line change
Expand Up @@ -1125,7 +1125,13 @@ def nci(x):
ref = minimize(fun=obj, x0=x0, method='slsqp',
bounds=bnds, constraints=nlcs)
assert ref.success
assert res.fun <= ref.fun
assert res.fun <= ref.fun # Ipopt legitimately does better than slsqp here

# If we give SLSQP a good guess, it agrees with Ipopt
ref2 = minimize(fun=obj, x0=res.x, method='slsqp',
bounds=bnds, constraints=nlcs)
assert ref2.success
assert_allclose(ref2.fun, res.fun)


@pytest.mark.skipif("scipy" not in sys.modules,
Expand Down
22 changes: 22 additions & 0 deletions cyipopt/tests/unit/test_scipy_optional.py
Original file line number Diff line number Diff line change
Expand Up @@ -575,3 +575,25 @@ def test_minimize_late_binding_bug():
assert res.success
np.testing.assert_allclose(res.x, ref.x)
np.testing.assert_allclose(res.fun, ref.fun)


@pytest.mark.skipif("scipy" not in sys.modules,
reason="Test only valid if Scipy available.")
def test_gh115_eps_option():
# gh-115 requested that the `eps` argument be exposed as an option. Verify
# that it is working as advertised (at least for the objective function).
def f(x):
if f.x is None:
f.x = x
elif f.dx is None:
f.dx = x - f.x
return x ** 2

f.x, f.dx = None, None
cyipopt.minimize_ipopt(f, x0=0)
np.testing.assert_equal(f.dx, 1e-8)

f.x, f.dx = None, None
eps = 1e-9
cyipopt.minimize_ipopt(f, x0=0, options={'eps': eps})
np.testing.assert_equal(f.dx, eps)

0 comments on commit 97c9ec7

Please sign in to comment.