canonical_constraint.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. import numpy as np
  2. import scipy.sparse as sps
  3. class CanonicalConstraint:
  4. """Canonical constraint to use with trust-constr algorithm.
  5. It represents the set of constraints of the form::
  6. f_eq(x) = 0
  7. f_ineq(x) <= 0
  8. where ``f_eq`` and ``f_ineq`` are evaluated by a single function, see
  9. below.
  10. The class is supposed to be instantiated by factory methods, which
  11. should prepare the parameters listed below.
  12. Parameters
  13. ----------
  14. n_eq, n_ineq : int
  15. Number of equality and inequality constraints respectively.
  16. fun : callable
  17. Function defining the constraints. The signature is
  18. ``fun(x) -> c_eq, c_ineq``, where ``c_eq`` is ndarray with `n_eq`
  19. components and ``c_ineq`` is ndarray with `n_ineq` components.
  20. jac : callable
  21. Function to evaluate the Jacobian of the constraint. The signature
  22. is ``jac(x) -> J_eq, J_ineq``, where ``J_eq`` and ``J_ineq`` are
  23. either ndarray of csr_matrix of shapes (n_eq, n) and (n_ineq, n),
  24. respectively.
  25. hess : callable
  26. Function to evaluate the Hessian of the constraints multiplied
  27. by Lagrange multipliers, that is
  28. ``dot(f_eq, v_eq) + dot(f_ineq, v_ineq)``. The signature is
  29. ``hess(x, v_eq, v_ineq) -> H``, where ``H`` has an implied
  30. shape (n, n) and provide a matrix-vector product operation
  31. ``H.dot(p)``.
  32. keep_feasible : ndarray, shape (n_ineq,)
  33. Mask indicating which inequality constraints should be kept feasible.
  34. """
  35. def __init__(self, n_eq, n_ineq, fun, jac, hess, keep_feasible):
  36. self.n_eq = n_eq
  37. self.n_ineq = n_ineq
  38. self.fun = fun
  39. self.jac = jac
  40. self.hess = hess
  41. self.keep_feasible = keep_feasible
  42. @classmethod
  43. def from_PreparedConstraint(cls, constraint):
  44. """Create an instance from `PreparedConstrained` object."""
  45. lb, ub = constraint.bounds
  46. cfun = constraint.fun
  47. keep_feasible = constraint.keep_feasible
  48. if np.all(lb == -np.inf) and np.all(ub == np.inf):
  49. return cls.empty(cfun.n)
  50. if np.all(lb == -np.inf) and np.all(ub == np.inf):
  51. return cls.empty(cfun.n)
  52. elif np.all(lb == ub):
  53. return cls._equal_to_canonical(cfun, lb)
  54. elif np.all(lb == -np.inf):
  55. return cls._less_to_canonical(cfun, ub, keep_feasible)
  56. elif np.all(ub == np.inf):
  57. return cls._greater_to_canonical(cfun, lb, keep_feasible)
  58. else:
  59. return cls._interval_to_canonical(cfun, lb, ub, keep_feasible)
  60. @classmethod
  61. def empty(cls, n):
  62. """Create an "empty" instance.
  63. This "empty" instance is required to allow working with unconstrained
  64. problems as if they have some constraints.
  65. """
  66. empty_fun = np.empty(0)
  67. empty_jac = np.empty((0, n))
  68. empty_hess = sps.csr_matrix((n, n))
  69. def fun(x):
  70. return empty_fun, empty_fun
  71. def jac(x):
  72. return empty_jac, empty_jac
  73. def hess(x, v_eq, v_ineq):
  74. return empty_hess
  75. return cls(0, 0, fun, jac, hess, np.empty(0, dtype=np.bool_))
  76. @classmethod
  77. def concatenate(cls, canonical_constraints, sparse_jacobian):
  78. """Concatenate multiple `CanonicalConstraint` into one.
  79. `sparse_jacobian` (bool) determines the Jacobian format of the
  80. concatenated constraint. Note that items in `canonical_constraints`
  81. must have their Jacobians in the same format.
  82. """
  83. def fun(x):
  84. if canonical_constraints:
  85. eq_all, ineq_all = zip(
  86. *[c.fun(x) for c in canonical_constraints])
  87. else:
  88. eq_all, ineq_all = [], []
  89. return np.hstack(eq_all), np.hstack(ineq_all)
  90. if sparse_jacobian:
  91. vstack = sps.vstack
  92. else:
  93. vstack = np.vstack
  94. def jac(x):
  95. if canonical_constraints:
  96. eq_all, ineq_all = zip(
  97. *[c.jac(x) for c in canonical_constraints])
  98. else:
  99. eq_all, ineq_all = [], []
  100. return vstack(eq_all), vstack(ineq_all)
  101. def hess(x, v_eq, v_ineq):
  102. hess_all = []
  103. index_eq = 0
  104. index_ineq = 0
  105. for c in canonical_constraints:
  106. vc_eq = v_eq[index_eq:index_eq + c.n_eq]
  107. vc_ineq = v_ineq[index_ineq:index_ineq + c.n_ineq]
  108. hess_all.append(c.hess(x, vc_eq, vc_ineq))
  109. index_eq += c.n_eq
  110. index_ineq += c.n_ineq
  111. def matvec(p):
  112. result = np.zeros_like(p)
  113. for h in hess_all:
  114. result += h.dot(p)
  115. return result
  116. n = x.shape[0]
  117. return sps.linalg.LinearOperator((n, n), matvec, dtype=float)
  118. n_eq = sum(c.n_eq for c in canonical_constraints)
  119. n_ineq = sum(c.n_ineq for c in canonical_constraints)
  120. keep_feasible = np.hstack([c.keep_feasible for c in
  121. canonical_constraints])
  122. return cls(n_eq, n_ineq, fun, jac, hess, keep_feasible)
  123. @classmethod
  124. def _equal_to_canonical(cls, cfun, value):
  125. empty_fun = np.empty(0)
  126. n = cfun.n
  127. n_eq = value.shape[0]
  128. n_ineq = 0
  129. keep_feasible = np.empty(0, dtype=bool)
  130. if cfun.sparse_jacobian:
  131. empty_jac = sps.csr_matrix((0, n))
  132. else:
  133. empty_jac = np.empty((0, n))
  134. def fun(x):
  135. return cfun.fun(x) - value, empty_fun
  136. def jac(x):
  137. return cfun.jac(x), empty_jac
  138. def hess(x, v_eq, v_ineq):
  139. return cfun.hess(x, v_eq)
  140. empty_fun = np.empty(0)
  141. n = cfun.n
  142. if cfun.sparse_jacobian:
  143. empty_jac = sps.csr_matrix((0, n))
  144. else:
  145. empty_jac = np.empty((0, n))
  146. return cls(n_eq, n_ineq, fun, jac, hess, keep_feasible)
  147. @classmethod
  148. def _less_to_canonical(cls, cfun, ub, keep_feasible):
  149. empty_fun = np.empty(0)
  150. n = cfun.n
  151. if cfun.sparse_jacobian:
  152. empty_jac = sps.csr_matrix((0, n))
  153. else:
  154. empty_jac = np.empty((0, n))
  155. finite_ub = ub < np.inf
  156. n_eq = 0
  157. n_ineq = np.sum(finite_ub)
  158. if np.all(finite_ub):
  159. def fun(x):
  160. return empty_fun, cfun.fun(x) - ub
  161. def jac(x):
  162. return empty_jac, cfun.jac(x)
  163. def hess(x, v_eq, v_ineq):
  164. return cfun.hess(x, v_ineq)
  165. else:
  166. finite_ub = np.nonzero(finite_ub)[0]
  167. keep_feasible = keep_feasible[finite_ub]
  168. ub = ub[finite_ub]
  169. def fun(x):
  170. return empty_fun, cfun.fun(x)[finite_ub] - ub
  171. def jac(x):
  172. return empty_jac, cfun.jac(x)[finite_ub]
  173. def hess(x, v_eq, v_ineq):
  174. v = np.zeros(cfun.m)
  175. v[finite_ub] = v_ineq
  176. return cfun.hess(x, v)
  177. return cls(n_eq, n_ineq, fun, jac, hess, keep_feasible)
  178. @classmethod
  179. def _greater_to_canonical(cls, cfun, lb, keep_feasible):
  180. empty_fun = np.empty(0)
  181. n = cfun.n
  182. if cfun.sparse_jacobian:
  183. empty_jac = sps.csr_matrix((0, n))
  184. else:
  185. empty_jac = np.empty((0, n))
  186. finite_lb = lb > -np.inf
  187. n_eq = 0
  188. n_ineq = np.sum(finite_lb)
  189. if np.all(finite_lb):
  190. def fun(x):
  191. return empty_fun, lb - cfun.fun(x)
  192. def jac(x):
  193. return empty_jac, -cfun.jac(x)
  194. def hess(x, v_eq, v_ineq):
  195. return cfun.hess(x, -v_ineq)
  196. else:
  197. finite_lb = np.nonzero(finite_lb)[0]
  198. keep_feasible = keep_feasible[finite_lb]
  199. lb = lb[finite_lb]
  200. def fun(x):
  201. return empty_fun, lb - cfun.fun(x)[finite_lb]
  202. def jac(x):
  203. return empty_jac, -cfun.jac(x)[finite_lb]
  204. def hess(x, v_eq, v_ineq):
  205. v = np.zeros(cfun.m)
  206. v[finite_lb] = -v_ineq
  207. return cfun.hess(x, v)
  208. return cls(n_eq, n_ineq, fun, jac, hess, keep_feasible)
  209. @classmethod
  210. def _interval_to_canonical(cls, cfun, lb, ub, keep_feasible):
  211. lb_inf = lb == -np.inf
  212. ub_inf = ub == np.inf
  213. equal = lb == ub
  214. less = lb_inf & ~ub_inf
  215. greater = ub_inf & ~lb_inf
  216. interval = ~equal & ~lb_inf & ~ub_inf
  217. equal = np.nonzero(equal)[0]
  218. less = np.nonzero(less)[0]
  219. greater = np.nonzero(greater)[0]
  220. interval = np.nonzero(interval)[0]
  221. n_less = less.shape[0]
  222. n_greater = greater.shape[0]
  223. n_interval = interval.shape[0]
  224. n_ineq = n_less + n_greater + 2 * n_interval
  225. n_eq = equal.shape[0]
  226. keep_feasible = np.hstack((keep_feasible[less],
  227. keep_feasible[greater],
  228. keep_feasible[interval],
  229. keep_feasible[interval]))
  230. def fun(x):
  231. f = cfun.fun(x)
  232. eq = f[equal] - lb[equal]
  233. le = f[less] - ub[less]
  234. ge = lb[greater] - f[greater]
  235. il = f[interval] - ub[interval]
  236. ig = lb[interval] - f[interval]
  237. return eq, np.hstack((le, ge, il, ig))
  238. def jac(x):
  239. J = cfun.jac(x)
  240. eq = J[equal]
  241. le = J[less]
  242. ge = -J[greater]
  243. il = J[interval]
  244. ig = -il
  245. if sps.issparse(J):
  246. ineq = sps.vstack((le, ge, il, ig))
  247. else:
  248. ineq = np.vstack((le, ge, il, ig))
  249. return eq, ineq
  250. def hess(x, v_eq, v_ineq):
  251. n_start = 0
  252. v_l = v_ineq[n_start:n_start + n_less]
  253. n_start += n_less
  254. v_g = v_ineq[n_start:n_start + n_greater]
  255. n_start += n_greater
  256. v_il = v_ineq[n_start:n_start + n_interval]
  257. n_start += n_interval
  258. v_ig = v_ineq[n_start:n_start + n_interval]
  259. v = np.zeros_like(lb)
  260. v[equal] = v_eq
  261. v[less] = v_l
  262. v[greater] = -v_g
  263. v[interval] = v_il - v_ig
  264. return cfun.hess(x, v)
  265. return cls(n_eq, n_ineq, fun, jac, hess, keep_feasible)
  266. def initial_constraints_as_canonical(n, prepared_constraints, sparse_jacobian):
  267. """Convert initial values of the constraints to the canonical format.
  268. The purpose to avoid one additional call to the constraints at the initial
  269. point. It takes saved values in `PreparedConstraint`, modififies and
  270. concatenates them to the canonical constraint format.
  271. """
  272. c_eq = []
  273. c_ineq = []
  274. J_eq = []
  275. J_ineq = []
  276. for c in prepared_constraints:
  277. f = c.fun.f
  278. J = c.fun.J
  279. lb, ub = c.bounds
  280. if np.all(lb == ub):
  281. c_eq.append(f - lb)
  282. J_eq.append(J)
  283. elif np.all(lb == -np.inf):
  284. finite_ub = ub < np.inf
  285. c_ineq.append(f[finite_ub] - ub[finite_ub])
  286. J_ineq.append(J[finite_ub])
  287. elif np.all(ub == np.inf):
  288. finite_lb = lb > -np.inf
  289. c_ineq.append(lb[finite_lb] - f[finite_lb])
  290. J_ineq.append(-J[finite_lb])
  291. else:
  292. lb_inf = lb == -np.inf
  293. ub_inf = ub == np.inf
  294. equal = lb == ub
  295. less = lb_inf & ~ub_inf
  296. greater = ub_inf & ~lb_inf
  297. interval = ~equal & ~lb_inf & ~ub_inf
  298. c_eq.append(f[equal] - lb[equal])
  299. c_ineq.append(f[less] - ub[less])
  300. c_ineq.append(lb[greater] - f[greater])
  301. c_ineq.append(f[interval] - ub[interval])
  302. c_ineq.append(lb[interval] - f[interval])
  303. J_eq.append(J[equal])
  304. J_ineq.append(J[less])
  305. J_ineq.append(-J[greater])
  306. J_ineq.append(J[interval])
  307. J_ineq.append(-J[interval])
  308. c_eq = np.hstack(c_eq) if c_eq else np.empty(0)
  309. c_ineq = np.hstack(c_ineq) if c_ineq else np.empty(0)
  310. if sparse_jacobian:
  311. vstack = sps.vstack
  312. empty = sps.csr_matrix((0, n))
  313. else:
  314. vstack = np.vstack
  315. empty = np.empty((0, n))
  316. J_eq = vstack(J_eq) if J_eq else empty
  317. J_ineq = vstack(J_ineq) if J_ineq else empty
  318. return c_eq, c_ineq, J_eq, J_ineq