canonical_constraint.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. import numpy as np
  2. import scipy.sparse as sps
  3. class CanonicalConstraint(object):
  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))
  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. eq_all = []
  85. ineq_all = []
  86. for c in canonical_constraints:
  87. eq, ineq = c.fun(x)
  88. eq_all.append(eq)
  89. ineq_all.append(ineq)
  90. return np.hstack(eq_all), np.hstack(ineq_all)
  91. if sparse_jacobian:
  92. vstack = sps.vstack
  93. else:
  94. vstack = np.vstack
  95. def jac(x):
  96. eq_all = []
  97. ineq_all = []
  98. for c in canonical_constraints:
  99. eq, ineq = c.jac(x)
  100. eq_all.append(eq)
  101. ineq_all.append(ineq)
  102. return vstack(eq_all), vstack(ineq_all)
  103. def hess(x, v_eq, v_ineq):
  104. hess_all = []
  105. index_eq = 0
  106. index_ineq = 0
  107. for c in canonical_constraints:
  108. vc_eq = v_eq[index_eq:index_eq + c.n_eq]
  109. vc_ineq = v_ineq[index_ineq:index_ineq + c.n_ineq]
  110. hess_all.append(c.hess(x, vc_eq, vc_ineq))
  111. index_eq += c.n_eq
  112. index_ineq += c.n_ineq
  113. def matvec(p):
  114. result = np.zeros_like(p)
  115. for h in hess_all:
  116. result += h.dot(p)
  117. return result
  118. n = x.shape[0]
  119. return sps.linalg.LinearOperator((n, n), matvec, dtype=float)
  120. n_eq = sum(c.n_eq for c in canonical_constraints)
  121. n_ineq = sum(c.n_ineq for c in canonical_constraints)
  122. keep_feasible = np.hstack([c.keep_feasible for c in
  123. canonical_constraints])
  124. return cls(n_eq, n_ineq, fun, jac, hess, keep_feasible)
  125. @classmethod
  126. def _equal_to_canonical(cls, cfun, value):
  127. empty_fun = np.empty(0)
  128. n = cfun.n
  129. n_eq = value.shape[0]
  130. n_ineq = 0
  131. keep_feasible = np.empty(0, dtype=bool)
  132. if cfun.sparse_jacobian:
  133. empty_jac = sps.csr_matrix((0, n))
  134. else:
  135. empty_jac = np.empty((0, n))
  136. def fun(x):
  137. return cfun.fun(x) - value, empty_fun
  138. def jac(x):
  139. return cfun.jac(x), empty_jac
  140. def hess(x, v_eq, v_ineq):
  141. return cfun.hess(x, v_eq)
  142. empty_fun = np.empty(0)
  143. n = cfun.n
  144. if cfun.sparse_jacobian:
  145. empty_jac = sps.csr_matrix((0, n))
  146. else:
  147. empty_jac = np.empty((0, n))
  148. return cls(n_eq, n_ineq, fun, jac, hess, keep_feasible)
  149. @classmethod
  150. def _less_to_canonical(cls, cfun, ub, keep_feasible):
  151. empty_fun = np.empty(0)
  152. n = cfun.n
  153. if cfun.sparse_jacobian:
  154. empty_jac = sps.csr_matrix((0, n))
  155. else:
  156. empty_jac = np.empty((0, n))
  157. finite_ub = ub < np.inf
  158. n_eq = 0
  159. n_ineq = np.sum(finite_ub)
  160. if np.all(finite_ub):
  161. def fun(x):
  162. return empty_fun, cfun.fun(x) - ub
  163. def jac(x):
  164. return empty_jac, cfun.jac(x)
  165. def hess(x, v_eq, v_ineq):
  166. return cfun.hess(x, v_ineq)
  167. else:
  168. finite_ub = np.nonzero(finite_ub)[0]
  169. keep_feasible = keep_feasible[finite_ub]
  170. ub = ub[finite_ub]
  171. def fun(x):
  172. return empty_fun, cfun.fun(x)[finite_ub] - ub
  173. def jac(x):
  174. return empty_jac, cfun.jac(x)[finite_ub]
  175. def hess(x, v_eq, v_ineq):
  176. v = np.zeros(cfun.m)
  177. v[finite_ub] = v_ineq
  178. return cfun.hess(x, v)
  179. return cls(n_eq, n_ineq, fun, jac, hess, keep_feasible)
  180. @classmethod
  181. def _greater_to_canonical(cls, cfun, lb, keep_feasible):
  182. empty_fun = np.empty(0)
  183. n = cfun.n
  184. if cfun.sparse_jacobian:
  185. empty_jac = sps.csr_matrix((0, n))
  186. else:
  187. empty_jac = np.empty((0, n))
  188. finite_lb = lb > -np.inf
  189. n_eq = 0
  190. n_ineq = np.sum(finite_lb)
  191. if np.all(finite_lb):
  192. def fun(x):
  193. return empty_fun, lb - cfun.fun(x)
  194. def jac(x):
  195. return empty_jac, -cfun.jac(x)
  196. def hess(x, v_eq, v_ineq):
  197. return cfun.hess(x, -v_ineq)
  198. else:
  199. finite_lb = np.nonzero(finite_lb)[0]
  200. keep_feasible = keep_feasible[finite_lb]
  201. lb = lb[finite_lb]
  202. def fun(x):
  203. return empty_fun, lb - cfun.fun(x)[finite_lb]
  204. def jac(x):
  205. return empty_jac, -cfun.jac(x)[finite_lb]
  206. def hess(x, v_eq, v_ineq):
  207. v = np.zeros(cfun.m)
  208. v[finite_lb] = -v_ineq
  209. return cfun.hess(x, v)
  210. return cls(n_eq, n_ineq, fun, jac, hess, keep_feasible)
  211. @classmethod
  212. def _interval_to_canonical(cls, cfun, lb, ub, keep_feasible):
  213. lb_inf = lb == -np.inf
  214. ub_inf = ub == np.inf
  215. equal = lb == ub
  216. less = lb_inf & ~ub_inf
  217. greater = ub_inf & ~lb_inf
  218. interval = ~equal & ~lb_inf & ~ub_inf
  219. equal = np.nonzero(equal)[0]
  220. less = np.nonzero(less)[0]
  221. greater = np.nonzero(greater)[0]
  222. interval = np.nonzero(interval)[0]
  223. n_less = less.shape[0]
  224. n_greater = greater.shape[0]
  225. n_interval = interval.shape[0]
  226. n_ineq = n_less + n_greater + 2 * n_interval
  227. n_eq = equal.shape[0]
  228. keep_feasible = np.hstack((keep_feasible[less],
  229. keep_feasible[greater],
  230. keep_feasible[interval],
  231. keep_feasible[interval]))
  232. def fun(x):
  233. f = cfun.fun(x)
  234. eq = f[equal] - lb[equal]
  235. le = f[less] - ub[less]
  236. ge = lb[greater] - f[greater]
  237. il = f[interval] - ub[interval]
  238. ig = lb[interval] - f[interval]
  239. return eq, np.hstack((le, ge, il, ig))
  240. def jac(x):
  241. J = cfun.jac(x)
  242. eq = J[equal]
  243. le = J[less]
  244. ge = -J[greater]
  245. il = J[interval]
  246. ig = -il
  247. if sps.issparse(J):
  248. ineq = sps.vstack((le, ge, il, ig))
  249. else:
  250. ineq = np.vstack((le, ge, il, ig))
  251. return eq, ineq
  252. def hess(x, v_eq, v_ineq):
  253. n_start = 0
  254. v_l = v_ineq[n_start:n_start + n_less]
  255. n_start += n_less
  256. v_g = v_ineq[n_start:n_start + n_greater]
  257. n_start += n_greater
  258. v_il = v_ineq[n_start:n_start + n_interval]
  259. n_start += n_interval
  260. v_ig = v_ineq[n_start:n_start + n_interval]
  261. v = np.zeros_like(lb)
  262. v[equal] = v_eq
  263. v[less] = v_l
  264. v[greater] = -v_g
  265. v[interval] = v_il - v_ig
  266. return cfun.hess(x, v)
  267. return cls(n_eq, n_ineq, fun, jac, hess, keep_feasible)
  268. def initial_constraints_as_canonical(n, prepared_constraints, sparse_jacobian):
  269. """Convert initial values of the constraints to the canonical format.
  270. The purpose to avoid one additional call to the constraints at the initial
  271. point. It takes saved values in `PreparedConstraint`, modify and
  272. concatenate them to the the canonical constraint format.
  273. """
  274. c_eq = []
  275. c_ineq = []
  276. J_eq = []
  277. J_ineq = []
  278. for c in prepared_constraints:
  279. f = c.fun.f
  280. J = c.fun.J
  281. lb, ub = c.bounds
  282. if np.all(lb == ub):
  283. c_eq.append(f - lb)
  284. J_eq.append(J)
  285. elif np.all(lb == -np.inf):
  286. finite_ub = ub < np.inf
  287. c_ineq.append(f[finite_ub] - ub[finite_ub])
  288. J_ineq.append(J[finite_ub])
  289. elif np.all(ub == np.inf):
  290. finite_lb = lb > -np.inf
  291. c_ineq.append(lb[finite_lb] - f[finite_lb])
  292. J_ineq.append(-J[finite_lb])
  293. else:
  294. lb_inf = lb == -np.inf
  295. ub_inf = ub == np.inf
  296. equal = lb == ub
  297. less = lb_inf & ~ub_inf
  298. greater = ub_inf & ~lb_inf
  299. interval = ~equal & ~lb_inf & ~ub_inf
  300. c_eq.append(f[equal] - lb[equal])
  301. c_ineq.append(f[less] - ub[less])
  302. c_ineq.append(lb[greater] - f[greater])
  303. c_ineq.append(f[interval] - ub[interval])
  304. c_ineq.append(lb[interval] - f[interval])
  305. J_eq.append(J[equal])
  306. J_ineq.append(J[less])
  307. J_ineq.append(-J[greater])
  308. J_ineq.append(J[interval])
  309. J_ineq.append(-J[interval])
  310. c_eq = np.hstack(c_eq) if c_eq else np.empty(0)
  311. c_ineq = np.hstack(c_ineq) if c_ineq else np.empty(0)
  312. if sparse_jacobian:
  313. vstack = sps.vstack
  314. empty = sps.csr_matrix((0, n))
  315. else:
  316. vstack = np.vstack
  317. empty = np.empty((0, n))
  318. J_eq = vstack(J_eq) if J_eq else empty
  319. J_ineq = vstack(J_ineq) if J_ineq else empty
  320. return c_eq, c_ineq, J_eq, J_ineq