_constraints.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  1. """Constraints definition for minimize."""
  2. import numpy as np
  3. from ._hessian_update_strategy import BFGS
  4. from ._differentiable_functions import (
  5. VectorFunction, LinearVectorFunction, IdentityVectorFunction)
  6. from ._optimize import OptimizeWarning
  7. from warnings import warn, catch_warnings, simplefilter
  8. from numpy.testing import suppress_warnings
  9. from scipy.sparse import issparse
  10. def _arr_to_scalar(x):
  11. # If x is a numpy array, return x.item(). This will
  12. # fail if the array has more than one element.
  13. return x.item() if isinstance(x, np.ndarray) else x
  14. class NonlinearConstraint:
  15. """Nonlinear constraint on the variables.
  16. The constraint has the general inequality form::
  17. lb <= fun(x) <= ub
  18. Here the vector of independent variables x is passed as ndarray of shape
  19. (n,) and ``fun`` returns a vector with m components.
  20. It is possible to use equal bounds to represent an equality constraint or
  21. infinite bounds to represent a one-sided constraint.
  22. Parameters
  23. ----------
  24. fun : callable
  25. The function defining the constraint.
  26. The signature is ``fun(x) -> array_like, shape (m,)``.
  27. lb, ub : array_like
  28. Lower and upper bounds on the constraint. Each array must have the
  29. shape (m,) or be a scalar, in the latter case a bound will be the same
  30. for all components of the constraint. Use ``np.inf`` with an
  31. appropriate sign to specify a one-sided constraint.
  32. Set components of `lb` and `ub` equal to represent an equality
  33. constraint. Note that you can mix constraints of different types:
  34. interval, one-sided or equality, by setting different components of
  35. `lb` and `ub` as necessary.
  36. jac : {callable, '2-point', '3-point', 'cs'}, optional
  37. Method of computing the Jacobian matrix (an m-by-n matrix,
  38. where element (i, j) is the partial derivative of f[i] with
  39. respect to x[j]). The keywords {'2-point', '3-point',
  40. 'cs'} select a finite difference scheme for the numerical estimation.
  41. A callable must have the following signature:
  42. ``jac(x) -> {ndarray, sparse matrix}, shape (m, n)``.
  43. Default is '2-point'.
  44. hess : {callable, '2-point', '3-point', 'cs', HessianUpdateStrategy, None}, optional
  45. Method for computing the Hessian matrix. The keywords
  46. {'2-point', '3-point', 'cs'} select a finite difference scheme for
  47. numerical estimation. Alternatively, objects implementing
  48. `HessianUpdateStrategy` interface can be used to approximate the
  49. Hessian. Currently available implementations are:
  50. - `BFGS` (default option)
  51. - `SR1`
  52. A callable must return the Hessian matrix of ``dot(fun, v)`` and
  53. must have the following signature:
  54. ``hess(x, v) -> {LinearOperator, sparse matrix, array_like}, shape (n, n)``.
  55. Here ``v`` is ndarray with shape (m,) containing Lagrange multipliers.
  56. keep_feasible : array_like of bool, optional
  57. Whether to keep the constraint components feasible throughout
  58. iterations. A single value set this property for all components.
  59. Default is False. Has no effect for equality constraints.
  60. finite_diff_rel_step: None or array_like, optional
  61. Relative step size for the finite difference approximation. Default is
  62. None, which will select a reasonable value automatically depending
  63. on a finite difference scheme.
  64. finite_diff_jac_sparsity: {None, array_like, sparse matrix}, optional
  65. Defines the sparsity structure of the Jacobian matrix for finite
  66. difference estimation, its shape must be (m, n). If the Jacobian has
  67. only few non-zero elements in *each* row, providing the sparsity
  68. structure will greatly speed up the computations. A zero entry means
  69. that a corresponding element in the Jacobian is identically zero.
  70. If provided, forces the use of 'lsmr' trust-region solver.
  71. If None (default) then dense differencing will be used.
  72. Notes
  73. -----
  74. Finite difference schemes {'2-point', '3-point', 'cs'} may be used for
  75. approximating either the Jacobian or the Hessian. We, however, do not allow
  76. its use for approximating both simultaneously. Hence whenever the Jacobian
  77. is estimated via finite-differences, we require the Hessian to be estimated
  78. using one of the quasi-Newton strategies.
  79. The scheme 'cs' is potentially the most accurate, but requires the function
  80. to correctly handles complex inputs and be analytically continuable to the
  81. complex plane. The scheme '3-point' is more accurate than '2-point' but
  82. requires twice as many operations.
  83. Examples
  84. --------
  85. Constrain ``x[0] < sin(x[1]) + 1.9``
  86. >>> from scipy.optimize import NonlinearConstraint
  87. >>> import numpy as np
  88. >>> con = lambda x: x[0] - np.sin(x[1])
  89. >>> nlc = NonlinearConstraint(con, -np.inf, 1.9)
  90. """
  91. def __init__(self, fun, lb, ub, jac='2-point', hess=BFGS(),
  92. keep_feasible=False, finite_diff_rel_step=None,
  93. finite_diff_jac_sparsity=None):
  94. self.fun = fun
  95. self.lb = lb
  96. self.ub = ub
  97. self.finite_diff_rel_step = finite_diff_rel_step
  98. self.finite_diff_jac_sparsity = finite_diff_jac_sparsity
  99. self.jac = jac
  100. self.hess = hess
  101. self.keep_feasible = keep_feasible
  102. class LinearConstraint:
  103. """Linear constraint on the variables.
  104. The constraint has the general inequality form::
  105. lb <= A.dot(x) <= ub
  106. Here the vector of independent variables x is passed as ndarray of shape
  107. (n,) and the matrix A has shape (m, n).
  108. It is possible to use equal bounds to represent an equality constraint or
  109. infinite bounds to represent a one-sided constraint.
  110. Parameters
  111. ----------
  112. A : {array_like, sparse matrix}, shape (m, n)
  113. Matrix defining the constraint.
  114. lb, ub : array_like, optional
  115. Lower and upper limits on the constraint. Each array must have the
  116. shape (m,) or be a scalar, in the latter case a bound will be the same
  117. for all components of the constraint. Use ``np.inf`` with an
  118. appropriate sign to specify a one-sided constraint.
  119. Set components of `lb` and `ub` equal to represent an equality
  120. constraint. Note that you can mix constraints of different types:
  121. interval, one-sided or equality, by setting different components of
  122. `lb` and `ub` as necessary. Defaults to ``lb = -np.inf``
  123. and ``ub = np.inf`` (no limits).
  124. keep_feasible : array_like of bool, optional
  125. Whether to keep the constraint components feasible throughout
  126. iterations. A single value set this property for all components.
  127. Default is False. Has no effect for equality constraints.
  128. """
  129. def _input_validation(self):
  130. if self.A.ndim != 2:
  131. message = "`A` must have exactly two dimensions."
  132. raise ValueError(message)
  133. try:
  134. shape = self.A.shape[0:1]
  135. self.lb = np.broadcast_to(self.lb, shape)
  136. self.ub = np.broadcast_to(self.ub, shape)
  137. self.keep_feasible = np.broadcast_to(self.keep_feasible, shape)
  138. except ValueError:
  139. message = ("`lb`, `ub`, and `keep_feasible` must be broadcastable "
  140. "to shape `A.shape[0:1]`")
  141. raise ValueError(message)
  142. def __init__(self, A, lb=-np.inf, ub=np.inf, keep_feasible=False):
  143. if not issparse(A):
  144. # In some cases, if the constraint is not valid, this emits a
  145. # VisibleDeprecationWarning about ragged nested sequences
  146. # before eventually causing an error. `scipy.optimize.milp` would
  147. # prefer that this just error out immediately so it can handle it
  148. # rather than concerning the user.
  149. with catch_warnings():
  150. simplefilter("error")
  151. self.A = np.atleast_2d(A).astype(np.float64)
  152. else:
  153. self.A = A
  154. self.lb = np.atleast_1d(lb).astype(np.float64)
  155. self.ub = np.atleast_1d(ub).astype(np.float64)
  156. self.keep_feasible = np.atleast_1d(keep_feasible).astype(bool)
  157. self._input_validation()
  158. def residual(self, x):
  159. """
  160. Calculate the residual between the constraint function and the limits
  161. For a linear constraint of the form::
  162. lb <= A@x <= ub
  163. the lower and upper residuals between ``A@x`` and the limits are values
  164. ``sl`` and ``sb`` such that::
  165. lb + sl == A@x == ub - sb
  166. When all elements of ``sl`` and ``sb`` are positive, all elements of
  167. the constraint are satisfied; a negative element in ``sl`` or ``sb``
  168. indicates that the corresponding element of the constraint is not
  169. satisfied.
  170. Parameters
  171. ----------
  172. x: array_like
  173. Vector of independent variables
  174. Returns
  175. -------
  176. sl, sb : array-like
  177. The lower and upper residuals
  178. """
  179. return self.A@x - self.lb, self.ub - self.A@x
  180. class Bounds:
  181. """Bounds constraint on the variables.
  182. The constraint has the general inequality form::
  183. lb <= x <= ub
  184. It is possible to use equal bounds to represent an equality constraint or
  185. infinite bounds to represent a one-sided constraint.
  186. Parameters
  187. ----------
  188. lb, ub : array_like, optional
  189. Lower and upper bounds on independent variables. `lb`, `ub`, and
  190. `keep_feasible` must be the same shape or broadcastable.
  191. Set components of `lb` and `ub` equal
  192. to fix a variable. Use ``np.inf`` with an appropriate sign to disable
  193. bounds on all or some variables. Note that you can mix constraints of
  194. different types: interval, one-sided or equality, by setting different
  195. components of `lb` and `ub` as necessary. Defaults to ``lb = -np.inf``
  196. and ``ub = np.inf`` (no bounds).
  197. keep_feasible : array_like of bool, optional
  198. Whether to keep the constraint components feasible throughout
  199. iterations. Must be broadcastable with `lb` and `ub`.
  200. Default is False. Has no effect for equality constraints.
  201. """
  202. def _input_validation(self):
  203. try:
  204. res = np.broadcast_arrays(self.lb, self.ub, self.keep_feasible)
  205. self.lb, self.ub, self.keep_feasible = res
  206. except ValueError:
  207. message = "`lb`, `ub`, and `keep_feasible` must be broadcastable."
  208. raise ValueError(message)
  209. def __init__(self, lb=-np.inf, ub=np.inf, keep_feasible=False):
  210. self.lb = np.atleast_1d(lb)
  211. self.ub = np.atleast_1d(ub)
  212. self.keep_feasible = np.atleast_1d(keep_feasible).astype(bool)
  213. self._input_validation()
  214. def __repr__(self):
  215. start = f"{type(self).__name__}({self.lb!r}, {self.ub!r}"
  216. if np.any(self.keep_feasible):
  217. end = f", keep_feasible={self.keep_feasible!r})"
  218. else:
  219. end = ")"
  220. return start + end
  221. def residual(self, x):
  222. """Calculate the residual (slack) between the input and the bounds
  223. For a bound constraint of the form::
  224. lb <= x <= ub
  225. the lower and upper residuals between `x` and the bounds are values
  226. ``sl`` and ``sb`` such that::
  227. lb + sl == x == ub - sb
  228. When all elements of ``sl`` and ``sb`` are positive, all elements of
  229. ``x`` lie within the bounds; a negative element in ``sl`` or ``sb``
  230. indicates that the corresponding element of ``x`` is out of bounds.
  231. Parameters
  232. ----------
  233. x: array_like
  234. Vector of independent variables
  235. Returns
  236. -------
  237. sl, sb : array-like
  238. The lower and upper residuals
  239. """
  240. return x - self.lb, self.ub - x
  241. class PreparedConstraint:
  242. """Constraint prepared from a user defined constraint.
  243. On creation it will check whether a constraint definition is valid and
  244. the initial point is feasible. If created successfully, it will contain
  245. the attributes listed below.
  246. Parameters
  247. ----------
  248. constraint : {NonlinearConstraint, LinearConstraint`, Bounds}
  249. Constraint to check and prepare.
  250. x0 : array_like
  251. Initial vector of independent variables.
  252. sparse_jacobian : bool or None, optional
  253. If bool, then the Jacobian of the constraint will be converted
  254. to the corresponded format if necessary. If None (default), such
  255. conversion is not made.
  256. finite_diff_bounds : 2-tuple, optional
  257. Lower and upper bounds on the independent variables for the finite
  258. difference approximation, if applicable. Defaults to no bounds.
  259. Attributes
  260. ----------
  261. fun : {VectorFunction, LinearVectorFunction, IdentityVectorFunction}
  262. Function defining the constraint wrapped by one of the convenience
  263. classes.
  264. bounds : 2-tuple
  265. Contains lower and upper bounds for the constraints --- lb and ub.
  266. These are converted to ndarray and have a size equal to the number of
  267. the constraints.
  268. keep_feasible : ndarray
  269. Array indicating which components must be kept feasible with a size
  270. equal to the number of the constraints.
  271. """
  272. def __init__(self, constraint, x0, sparse_jacobian=None,
  273. finite_diff_bounds=(-np.inf, np.inf)):
  274. if isinstance(constraint, NonlinearConstraint):
  275. fun = VectorFunction(constraint.fun, x0,
  276. constraint.jac, constraint.hess,
  277. constraint.finite_diff_rel_step,
  278. constraint.finite_diff_jac_sparsity,
  279. finite_diff_bounds, sparse_jacobian)
  280. elif isinstance(constraint, LinearConstraint):
  281. fun = LinearVectorFunction(constraint.A, x0, sparse_jacobian)
  282. elif isinstance(constraint, Bounds):
  283. fun = IdentityVectorFunction(x0, sparse_jacobian)
  284. else:
  285. raise ValueError("`constraint` of an unknown type is passed.")
  286. m = fun.m
  287. lb = np.asarray(constraint.lb, dtype=float)
  288. ub = np.asarray(constraint.ub, dtype=float)
  289. keep_feasible = np.asarray(constraint.keep_feasible, dtype=bool)
  290. lb = np.broadcast_to(lb, m)
  291. ub = np.broadcast_to(ub, m)
  292. keep_feasible = np.broadcast_to(keep_feasible, m)
  293. if keep_feasible.shape != (m,):
  294. raise ValueError("`keep_feasible` has a wrong shape.")
  295. mask = keep_feasible & (lb != ub)
  296. f0 = fun.f
  297. if np.any(f0[mask] < lb[mask]) or np.any(f0[mask] > ub[mask]):
  298. raise ValueError("`x0` is infeasible with respect to some "
  299. "inequality constraint with `keep_feasible` "
  300. "set to True.")
  301. self.fun = fun
  302. self.bounds = (lb, ub)
  303. self.keep_feasible = keep_feasible
  304. def violation(self, x):
  305. """How much the constraint is exceeded by.
  306. Parameters
  307. ----------
  308. x : array-like
  309. Vector of independent variables
  310. Returns
  311. -------
  312. excess : array-like
  313. How much the constraint is exceeded by, for each of the
  314. constraints specified by `PreparedConstraint.fun`.
  315. """
  316. with suppress_warnings() as sup:
  317. sup.filter(UserWarning)
  318. ev = self.fun.fun(np.asarray(x))
  319. excess_lb = np.maximum(self.bounds[0] - ev, 0)
  320. excess_ub = np.maximum(ev - self.bounds[1], 0)
  321. return excess_lb + excess_ub
  322. def new_bounds_to_old(lb, ub, n):
  323. """Convert the new bounds representation to the old one.
  324. The new representation is a tuple (lb, ub) and the old one is a list
  325. containing n tuples, ith containing lower and upper bound on a ith
  326. variable.
  327. If any of the entries in lb/ub are -np.inf/np.inf they are replaced by
  328. None.
  329. """
  330. lb = np.broadcast_to(lb, n)
  331. ub = np.broadcast_to(ub, n)
  332. lb = [float(x) if x > -np.inf else None for x in lb]
  333. ub = [float(x) if x < np.inf else None for x in ub]
  334. return list(zip(lb, ub))
  335. def old_bound_to_new(bounds):
  336. """Convert the old bounds representation to the new one.
  337. The new representation is a tuple (lb, ub) and the old one is a list
  338. containing n tuples, ith containing lower and upper bound on a ith
  339. variable.
  340. If any of the entries in lb/ub are None they are replaced by
  341. -np.inf/np.inf.
  342. """
  343. lb, ub = zip(*bounds)
  344. # Convert occurrences of None to -inf or inf, and replace occurrences of
  345. # any numpy array x with x.item(). Then wrap the results in numpy arrays.
  346. lb = np.array([float(_arr_to_scalar(x)) if x is not None else -np.inf
  347. for x in lb])
  348. ub = np.array([float(_arr_to_scalar(x)) if x is not None else np.inf
  349. for x in ub])
  350. return lb, ub
  351. def strict_bounds(lb, ub, keep_feasible, n_vars):
  352. """Remove bounds which are not asked to be kept feasible."""
  353. strict_lb = np.resize(lb, n_vars).astype(float)
  354. strict_ub = np.resize(ub, n_vars).astype(float)
  355. keep_feasible = np.resize(keep_feasible, n_vars)
  356. strict_lb[~keep_feasible] = -np.inf
  357. strict_ub[~keep_feasible] = np.inf
  358. return strict_lb, strict_ub
  359. def new_constraint_to_old(con, x0):
  360. """
  361. Converts new-style constraint objects to old-style constraint dictionaries.
  362. """
  363. if isinstance(con, NonlinearConstraint):
  364. if (con.finite_diff_jac_sparsity is not None or
  365. con.finite_diff_rel_step is not None or
  366. not isinstance(con.hess, BFGS) or # misses user specified BFGS
  367. con.keep_feasible):
  368. warn("Constraint options `finite_diff_jac_sparsity`, "
  369. "`finite_diff_rel_step`, `keep_feasible`, and `hess`"
  370. "are ignored by this method.", OptimizeWarning)
  371. fun = con.fun
  372. if callable(con.jac):
  373. jac = con.jac
  374. else:
  375. jac = None
  376. else: # LinearConstraint
  377. if np.any(con.keep_feasible):
  378. warn("Constraint option `keep_feasible` is ignored by this "
  379. "method.", OptimizeWarning)
  380. A = con.A
  381. if issparse(A):
  382. A = A.toarray()
  383. fun = lambda x: np.dot(A, x)
  384. jac = lambda x: A
  385. # FIXME: when bugs in VectorFunction/LinearVectorFunction are worked out,
  386. # use pcon.fun.fun and pcon.fun.jac. Until then, get fun/jac above.
  387. pcon = PreparedConstraint(con, x0)
  388. lb, ub = pcon.bounds
  389. i_eq = lb == ub
  390. i_bound_below = np.logical_xor(lb != -np.inf, i_eq)
  391. i_bound_above = np.logical_xor(ub != np.inf, i_eq)
  392. i_unbounded = np.logical_and(lb == -np.inf, ub == np.inf)
  393. if np.any(i_unbounded):
  394. warn("At least one constraint is unbounded above and below. Such "
  395. "constraints are ignored.", OptimizeWarning)
  396. ceq = []
  397. if np.any(i_eq):
  398. def f_eq(x):
  399. y = np.array(fun(x)).flatten()
  400. return y[i_eq] - lb[i_eq]
  401. ceq = [{"type": "eq", "fun": f_eq}]
  402. if jac is not None:
  403. def j_eq(x):
  404. dy = jac(x)
  405. if issparse(dy):
  406. dy = dy.toarray()
  407. dy = np.atleast_2d(dy)
  408. return dy[i_eq, :]
  409. ceq[0]["jac"] = j_eq
  410. cineq = []
  411. n_bound_below = np.sum(i_bound_below)
  412. n_bound_above = np.sum(i_bound_above)
  413. if n_bound_below + n_bound_above:
  414. def f_ineq(x):
  415. y = np.zeros(n_bound_below + n_bound_above)
  416. y_all = np.array(fun(x)).flatten()
  417. y[:n_bound_below] = y_all[i_bound_below] - lb[i_bound_below]
  418. y[n_bound_below:] = -(y_all[i_bound_above] - ub[i_bound_above])
  419. return y
  420. cineq = [{"type": "ineq", "fun": f_ineq}]
  421. if jac is not None:
  422. def j_ineq(x):
  423. dy = np.zeros((n_bound_below + n_bound_above, len(x0)))
  424. dy_all = jac(x)
  425. if issparse(dy_all):
  426. dy_all = dy_all.toarray()
  427. dy_all = np.atleast_2d(dy_all)
  428. dy[:n_bound_below, :] = dy_all[i_bound_below]
  429. dy[n_bound_below:, :] = -dy_all[i_bound_above]
  430. return dy
  431. cineq[0]["jac"] = j_ineq
  432. old_constraints = ceq + cineq
  433. if len(old_constraints) > 1:
  434. warn("Equality and inequality constraints are specified in the same "
  435. "element of the constraint list. For efficient use with this "
  436. "method, equality and inequality constraints should be specified "
  437. "in separate elements of the constraint list. ", OptimizeWarning)
  438. return old_constraints
  439. def old_constraint_to_new(ic, con):
  440. """
  441. Converts old-style constraint dictionaries to new-style constraint objects.
  442. """
  443. # check type
  444. try:
  445. ctype = con['type'].lower()
  446. except KeyError as e:
  447. raise KeyError('Constraint %d has no type defined.' % ic) from e
  448. except TypeError as e:
  449. raise TypeError(
  450. 'Constraints must be a sequence of dictionaries.'
  451. ) from e
  452. except AttributeError as e:
  453. raise TypeError("Constraint's type must be a string.") from e
  454. else:
  455. if ctype not in ['eq', 'ineq']:
  456. raise ValueError("Unknown constraint type '%s'." % con['type'])
  457. if 'fun' not in con:
  458. raise ValueError('Constraint %d has no function defined.' % ic)
  459. lb = 0
  460. if ctype == 'eq':
  461. ub = 0
  462. else:
  463. ub = np.inf
  464. jac = '2-point'
  465. if 'args' in con:
  466. args = con['args']
  467. fun = lambda x: con['fun'](x, *args)
  468. if 'jac' in con:
  469. jac = lambda x: con['jac'](x, *args)
  470. else:
  471. fun = con['fun']
  472. if 'jac' in con:
  473. jac = con['jac']
  474. return NonlinearConstraint(fun, lb, ub, jac)