_constraints.py 23 KB

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