import numbers
import numpy as np
import torch
from scipy.optimize import Bounds
from .constrained.lbfgsb import _minimize_lbfgsb
from .constrained.frankwolfe import _minimize_frankwolfe
from .constrained.trust_constr import _minimize_trust_constr
_tolerance_keys = {
'l-bfgs-b': 'gtol',
'frank-wolfe': 'gtol',
'trust-constr': 'tol',
}
def _maybe_to_number(val):
if isinstance(val, np.ndarray) and val.size == 1:
return val.item()
elif isinstance(val, torch.Tensor) and val.numel() == 1:
return val.item()
else:
return val
def _check_bound(val, x0, numpy=False):
n = x0.numel()
if isinstance(val, numbers.Number):
if numpy:
return np.full(n, val, dtype=float) # TODO: correct dtype
else:
return x0.new_full((n,), val)
if isinstance(val, (list, tuple)):
if numpy:
val = np.array(val, dtype=float) # TODO: correct dtype
else:
val = x0.new_tensor(val)
if isinstance(val, torch.Tensor):
assert val.numel() == n, f'Bound tensor has incorrect size'
val = val.flatten()
if numpy:
val = val.detach().cpu().numpy()
return val
elif isinstance(val, np.ndarray):
assert val.size == n, f'Bound array has incorrect size'
val = val.flatten()
if not numpy:
val = x0.new_tensor(val)
return val
else:
raise ValueError(f'Bound has invalid type: {type(val)}')
def _check_bounds(bounds, x0, method):
if isinstance(bounds, Bounds):
if method == 'trust-constr':
return bounds
else:
bounds = (bounds.lb, bounds.ub)
bounds = tuple(map(_maybe_to_number, bounds))
assert isinstance(bounds, (list, tuple)), \
f'Argument `bounds` must be a list or tuple but got {type(bounds)}'
assert len(bounds) == 2, \
f'Argument `bounds` must have length 2: (min, max)'
lb, ub = bounds
lb = float('-inf') if lb is None else lb
ub = float('inf') if ub is None else ub
numpy = (method == 'trust-constr')
lb = _check_bound(lb, x0, numpy=numpy)
ub = _check_bound(ub, x0, numpy=numpy)
return lb, ub
[docs]def minimize_constr(
f,
x0,
method=None,
constr=None,
bounds=None,
max_iter=None,
tol=None,
options=None,
callback=None,
disp=0,
):
"""Minimize a scalar function of one or more variables subject to
bounds and/or constraints.
.. note::
Method ``'trust-constr'`` is currently a wrapper for SciPy's
`trust-constr <https://docs.scipy.org/doc/scipy/reference/optimize.minimize-trustconstr.html>`_
solver.
Parameters
----------
f : callable
Scalar objective function to minimize.
x0 : Tensor
Initialization point.
method : str, optional
The minimization routine to use. Should be one of the following:
- 'l-bfgs-b'
- 'frank-wolfe'
- 'trust-constr'
If no method is provided, a default method will be selected based
on the criteria of the problem.
constr : dict or string, optional
Constraint specifications. Should either be a string (Frank-Wolfe
method) or a dictionary (trust-constr method) with the following fields:
* fun (callable) - Constraint function
* lb (Tensor or float, optional) - Constraint lower bounds
* ub (Tensor or float, optional) - Constraint upper bounds
One of either `lb` or `ub` must be provided. When `lb` == `ub` it is
interpreted as an equality constraint.
bounds : sequence or `Bounds`, optional
Bounds on variables. There are two ways to specify the bounds:
1. Sequence of ``(min, max)`` pairs for each element in `x`. None
is used to specify no bound.
2. Instance of :class:`scipy.optimize.Bounds` class.
Bounds of `-inf`/`inf` are interpreted as no bound. When `lb` == `ub`
it is interpreted as an equality constraint.
max_iter : int, optional
Maximum number of iterations to perform. If unspecified, this will
be set to the default of the selected method.
tol : float, optional
Tolerance for termination. For detailed control, use solver-specific
options.
options : dict, optional
A dictionary of keyword arguments to pass to the selected minimization
routine.
callback : callable, optional
Function to call after each iteration with the current parameter
state, e.g. ``callback(x)``.
disp : int
Level of algorithm's verbosity:
* 0 : work silently (default).
* 1 : display a termination report.
* 2 : display progress during iterations.
* 3 : display progress during iterations (more complete report).
Returns
-------
result : OptimizeResult
Result of the optimization routine.
"""
if method is None:
if constr is not None:
_frank_wolfe_constraints = {
'tracenorm', 'trace-norm', 'birkhoff', 'birkhoff-polytope'}
if (
isinstance(constr, str)
and constr.lower() in _frank_wolfe_constraints
):
method = 'frank-wolfe'
else:
method = 'trust-constr'
else:
method = 'l-bfgs-b'
assert isinstance(method, str)
method = method.lower()
if bounds is not None:
bounds = _check_bounds(bounds, x0, method)
# TODO: update `_minimize_trust_constr()` accepted bounds format
# and remove this
if method == 'trust-constr':
if isinstance(bounds, Bounds):
bounds = dict(
lb=_maybe_to_number(bounds.lb),
ub=_maybe_to_number(bounds.ub),
keep_feasible=bounds.keep_feasible,
)
else:
bounds = dict(lb=bounds[0], ub=bounds[1])
if options is None:
options = {}
else:
assert isinstance(options, dict)
options = options.copy()
options.setdefault('max_iter', max_iter)
options.setdefault('callback', callback)
options.setdefault('disp', disp)
# options.setdefault('return_all', return_all)
if tol is not None:
options.setdefault(_tolerance_keys[method], tol)
if method == 'l-bfgs-b':
assert constr is None
return _minimize_lbfgsb(f, x0, bounds=bounds, **options)
elif method == 'frank-wolfe':
assert bounds is None
return _minimize_frankwolfe(f, x0, constr=constr, **options)
elif method == 'trust-constr':
return _minimize_trust_constr(
f, x0, constr=constr, bounds=bounds, **options)
else:
raise RuntimeError(f'Invalid method: "{method}".')