problem.py 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296
  1. from contextlib import suppress
  2. from inspect import signature
  3. import copy
  4. import numpy as np
  5. from scipy.optimize import (
  6. Bounds,
  7. LinearConstraint,
  8. NonlinearConstraint,
  9. OptimizeResult,
  10. )
  11. from scipy.optimize._constraints import PreparedConstraint
  12. from .settings import PRINT_OPTIONS, BARRIER
  13. from .utils import CallbackSuccess, get_arrays_tol
  14. from .utils import exact_1d_array
  15. class ObjectiveFunction:
  16. """
  17. Real-valued objective function.
  18. """
  19. def __init__(self, fun, verbose, debug, *args):
  20. """
  21. Initialize the objective function.
  22. Parameters
  23. ----------
  24. fun : {callable, None}
  25. Function to evaluate, or None.
  26. ``fun(x, *args) -> float``
  27. where ``x`` is an array with shape (n,) and `args` is a tuple.
  28. verbose : bool
  29. Whether to print the function evaluations.
  30. debug : bool
  31. Whether to make debugging tests during the execution.
  32. *args : tuple
  33. Additional arguments to be passed to the function.
  34. """
  35. if debug:
  36. assert fun is None or callable(fun)
  37. assert isinstance(verbose, bool)
  38. assert isinstance(debug, bool)
  39. self._fun = fun
  40. self._verbose = verbose
  41. self._args = args
  42. self._n_eval = 0
  43. def __call__(self, x):
  44. """
  45. Evaluate the objective function.
  46. Parameters
  47. ----------
  48. x : array_like, shape (n,)
  49. Point at which the objective function is evaluated.
  50. Returns
  51. -------
  52. float
  53. Function value at `x`.
  54. """
  55. x = np.array(x, dtype=float)
  56. if self._fun is None:
  57. f = 0.0
  58. else:
  59. f = float(np.squeeze(self._fun(x, *self._args)))
  60. self._n_eval += 1
  61. if self._verbose:
  62. with np.printoptions(**PRINT_OPTIONS):
  63. print(f"{self.name}({x}) = {f}")
  64. return f
  65. @property
  66. def n_eval(self):
  67. """
  68. Number of function evaluations.
  69. Returns
  70. -------
  71. int
  72. Number of function evaluations.
  73. """
  74. return self._n_eval
  75. @property
  76. def name(self):
  77. """
  78. Name of the objective function.
  79. Returns
  80. -------
  81. str
  82. Name of the objective function.
  83. """
  84. name = ""
  85. if self._fun is not None:
  86. try:
  87. name = self._fun.__name__
  88. except AttributeError:
  89. name = "fun"
  90. return name
  91. class BoundConstraints:
  92. """
  93. Bound constraints ``xl <= x <= xu``.
  94. """
  95. def __init__(self, bounds):
  96. """
  97. Initialize the bound constraints.
  98. Parameters
  99. ----------
  100. bounds : scipy.optimize.Bounds
  101. Bound constraints.
  102. """
  103. self._xl = np.array(bounds.lb, float)
  104. self._xu = np.array(bounds.ub, float)
  105. # Remove the ill-defined bounds.
  106. self.xl[np.isnan(self.xl)] = -np.inf
  107. self.xu[np.isnan(self.xu)] = np.inf
  108. self.is_feasible = (
  109. np.all(self.xl <= self.xu)
  110. and np.all(self.xl < np.inf)
  111. and np.all(self.xu > -np.inf)
  112. )
  113. self.m = np.count_nonzero(self.xl > -np.inf) + np.count_nonzero(
  114. self.xu < np.inf
  115. )
  116. self.pcs = PreparedConstraint(bounds, np.ones(bounds.lb.size))
  117. @property
  118. def xl(self):
  119. """
  120. Lower bound.
  121. Returns
  122. -------
  123. `numpy.ndarray`, shape (n,)
  124. Lower bound.
  125. """
  126. return self._xl
  127. @property
  128. def xu(self):
  129. """
  130. Upper bound.
  131. Returns
  132. -------
  133. `numpy.ndarray`, shape (n,)
  134. Upper bound.
  135. """
  136. return self._xu
  137. def maxcv(self, x):
  138. """
  139. Evaluate the maximum constraint violation.
  140. Parameters
  141. ----------
  142. x : array_like, shape (n,)
  143. Point at which the maximum constraint violation is evaluated.
  144. Returns
  145. -------
  146. float
  147. Maximum constraint violation at `x`.
  148. """
  149. x = np.asarray(x, dtype=float)
  150. return self.violation(x)
  151. def violation(self, x):
  152. # shortcut for no bounds
  153. if self.is_feasible:
  154. return np.array([0])
  155. else:
  156. return self.pcs.violation(x)
  157. def project(self, x):
  158. """
  159. Project a point onto the feasible set.
  160. Parameters
  161. ----------
  162. x : array_like, shape (n,)
  163. Point to be projected.
  164. Returns
  165. -------
  166. `numpy.ndarray`, shape (n,)
  167. Projection of `x` onto the feasible set.
  168. """
  169. return np.clip(x, self.xl, self.xu) if self.is_feasible else x
  170. class LinearConstraints:
  171. """
  172. Linear constraints ``a_ub @ x <= b_ub`` and ``a_eq @ x == b_eq``.
  173. """
  174. def __init__(self, constraints, n, debug):
  175. """
  176. Initialize the linear constraints.
  177. Parameters
  178. ----------
  179. constraints : list of LinearConstraint
  180. Linear constraints.
  181. n : int
  182. Number of variables.
  183. debug : bool
  184. Whether to make debugging tests during the execution.
  185. """
  186. if debug:
  187. assert isinstance(constraints, list)
  188. for constraint in constraints:
  189. assert isinstance(constraint, LinearConstraint)
  190. assert isinstance(debug, bool)
  191. self._a_ub = np.empty((0, n))
  192. self._b_ub = np.empty(0)
  193. self._a_eq = np.empty((0, n))
  194. self._b_eq = np.empty(0)
  195. for constraint in constraints:
  196. is_equality = np.abs(
  197. constraint.ub - constraint.lb
  198. ) <= get_arrays_tol(constraint.lb, constraint.ub)
  199. if np.any(is_equality):
  200. self._a_eq = np.vstack((self.a_eq, constraint.A[is_equality]))
  201. self._b_eq = np.concatenate(
  202. (
  203. self.b_eq,
  204. 0.5
  205. * (
  206. constraint.lb[is_equality]
  207. + constraint.ub[is_equality]
  208. ),
  209. )
  210. )
  211. if not np.all(is_equality):
  212. self._a_ub = np.vstack(
  213. (
  214. self.a_ub,
  215. constraint.A[~is_equality],
  216. -constraint.A[~is_equality],
  217. )
  218. )
  219. self._b_ub = np.concatenate(
  220. (
  221. self.b_ub,
  222. constraint.ub[~is_equality],
  223. -constraint.lb[~is_equality],
  224. )
  225. )
  226. # Remove the ill-defined constraints.
  227. self.a_ub[np.isnan(self.a_ub)] = 0.0
  228. self.a_eq[np.isnan(self.a_eq)] = 0.0
  229. undef_ub = np.isnan(self.b_ub) | np.isinf(self.b_ub)
  230. undef_eq = np.isnan(self.b_eq)
  231. self._a_ub = self.a_ub[~undef_ub, :]
  232. self._b_ub = self.b_ub[~undef_ub]
  233. self._a_eq = self.a_eq[~undef_eq, :]
  234. self._b_eq = self.b_eq[~undef_eq]
  235. self.pcs = [
  236. PreparedConstraint(c, np.ones(n)) for c in constraints if c.A.size
  237. ]
  238. @property
  239. def a_ub(self):
  240. """
  241. Left-hand side matrix of the linear inequality constraints.
  242. Returns
  243. -------
  244. `numpy.ndarray`, shape (m, n)
  245. Left-hand side matrix of the linear inequality constraints.
  246. """
  247. return self._a_ub
  248. @property
  249. def b_ub(self):
  250. """
  251. Right-hand side vector of the linear inequality constraints.
  252. Returns
  253. -------
  254. `numpy.ndarray`, shape (m, n)
  255. Right-hand side vector of the linear inequality constraints.
  256. """
  257. return self._b_ub
  258. @property
  259. def a_eq(self):
  260. """
  261. Left-hand side matrix of the linear equality constraints.
  262. Returns
  263. -------
  264. `numpy.ndarray`, shape (m, n)
  265. Left-hand side matrix of the linear equality constraints.
  266. """
  267. return self._a_eq
  268. @property
  269. def b_eq(self):
  270. """
  271. Right-hand side vector of the linear equality constraints.
  272. Returns
  273. -------
  274. `numpy.ndarray`, shape (m, n)
  275. Right-hand side vector of the linear equality constraints.
  276. """
  277. return self._b_eq
  278. @property
  279. def m_ub(self):
  280. """
  281. Number of linear inequality constraints.
  282. Returns
  283. -------
  284. int
  285. Number of linear inequality constraints.
  286. """
  287. return self.b_ub.size
  288. @property
  289. def m_eq(self):
  290. """
  291. Number of linear equality constraints.
  292. Returns
  293. -------
  294. int
  295. Number of linear equality constraints.
  296. """
  297. return self.b_eq.size
  298. def maxcv(self, x):
  299. """
  300. Evaluate the maximum constraint violation.
  301. Parameters
  302. ----------
  303. x : array_like, shape (n,)
  304. Point at which the maximum constraint violation is evaluated.
  305. Returns
  306. -------
  307. float
  308. Maximum constraint violation at `x`.
  309. """
  310. return np.max(self.violation(x), initial=0.0)
  311. def violation(self, x):
  312. if len(self.pcs):
  313. return np.concatenate([pc.violation(x) for pc in self.pcs])
  314. return np.array([])
  315. class NonlinearConstraints:
  316. """
  317. Nonlinear constraints ``c_ub(x) <= 0`` and ``c_eq(x) == b_eq``.
  318. """
  319. def __init__(self, constraints, verbose, debug):
  320. """
  321. Initialize the nonlinear constraints.
  322. Parameters
  323. ----------
  324. constraints : list
  325. Nonlinear constraints.
  326. verbose : bool
  327. Whether to print the function evaluations.
  328. debug : bool
  329. Whether to make debugging tests during the execution.
  330. """
  331. if debug:
  332. assert isinstance(constraints, list)
  333. for constraint in constraints:
  334. assert isinstance(constraint, NonlinearConstraint)
  335. assert isinstance(verbose, bool)
  336. assert isinstance(debug, bool)
  337. self._constraints = constraints
  338. self.pcs = []
  339. self._verbose = verbose
  340. # map of indexes for equality and inequality constraints
  341. self._map_ub = None
  342. self._map_eq = None
  343. self._m_ub = self._m_eq = None
  344. def __call__(self, x):
  345. """
  346. Calculates the residual (slack) for the constraints.
  347. Parameters
  348. ----------
  349. x : array_like, shape (n,)
  350. Point at which the constraints are evaluated.
  351. Returns
  352. -------
  353. `numpy.ndarray`, shape (m_nonlinear_ub,)
  354. Nonlinear inequality constraint slack values.
  355. `numpy.ndarray`, shape (m_nonlinear_eq,)
  356. Nonlinear equality constraint slack values.
  357. """
  358. if not len(self._constraints):
  359. self._m_eq = self._m_ub = 0
  360. return np.array([]), np.array([])
  361. x = np.array(x, dtype=float)
  362. # first time around the constraints haven't been prepared
  363. if not len(self.pcs):
  364. self._map_ub = []
  365. self._map_eq = []
  366. self._m_eq = 0
  367. self._m_ub = 0
  368. for constraint in self._constraints:
  369. if not callable(constraint.jac):
  370. # having a callable constraint function prevents
  371. # constraint.fun from being evaluated when preparing
  372. # constraint
  373. c = copy.copy(constraint)
  374. c.jac = lambda x0: x0
  375. c.hess = lambda x0, v: 0.0
  376. pc = PreparedConstraint(c, x)
  377. else:
  378. pc = PreparedConstraint(constraint, x)
  379. # we're going to be using the same x value again immediately
  380. # after this initialisation
  381. pc.fun.f_updated = True
  382. self.pcs.append(pc)
  383. idx = np.arange(pc.fun.m)
  384. # figure out equality and inequality maps
  385. lb, ub = pc.bounds[0], pc.bounds[1]
  386. arr_tol = get_arrays_tol(lb, ub)
  387. is_equality = np.abs(ub - lb) <= arr_tol
  388. self._map_eq.append(idx[is_equality])
  389. self._map_ub.append(idx[~is_equality])
  390. # these values will be corrected to their proper values later
  391. self._m_eq += np.count_nonzero(is_equality)
  392. self._m_ub += np.count_nonzero(~is_equality)
  393. c_ub = []
  394. c_eq = []
  395. for i, pc in enumerate(self.pcs):
  396. val = pc.fun.fun(x)
  397. if self._verbose:
  398. with np.printoptions(**PRINT_OPTIONS):
  399. with suppress(AttributeError):
  400. fun_name = self._constraints[i].fun.__name__
  401. print(f"{fun_name}({x}) = {val}")
  402. # separate violations into c_eq and c_ub
  403. eq_idx = self._map_eq[i]
  404. ub_idx = self._map_ub[i]
  405. ub_val = val[ub_idx]
  406. if len(ub_idx):
  407. xl = pc.bounds[0][ub_idx]
  408. xu = pc.bounds[1][ub_idx]
  409. # calculate slack within lower bound
  410. finite_xl = xl > -np.inf
  411. _v = xl[finite_xl] - ub_val[finite_xl]
  412. c_ub.append(_v)
  413. # calculate slack within lower bound
  414. finite_xu = xu < np.inf
  415. _v = ub_val[finite_xu] - xu[finite_xu]
  416. c_ub.append(_v)
  417. # equality constraints taken from midpoint between lb and ub
  418. eq_val = val[eq_idx]
  419. if len(eq_idx):
  420. midpoint = 0.5 * (pc.bounds[1][eq_idx] + pc.bounds[0][eq_idx])
  421. eq_val -= midpoint
  422. c_eq.append(eq_val)
  423. if self._m_eq:
  424. c_eq = np.concatenate(c_eq)
  425. else:
  426. c_eq = np.array([])
  427. if self._m_ub:
  428. c_ub = np.concatenate(c_ub)
  429. else:
  430. c_ub = np.array([])
  431. self._m_ub = c_ub.size
  432. self._m_eq = c_eq.size
  433. return c_ub, c_eq
  434. @property
  435. def m_ub(self):
  436. """
  437. Number of nonlinear inequality constraints.
  438. Returns
  439. -------
  440. int
  441. Number of nonlinear inequality constraints.
  442. Raises
  443. ------
  444. ValueError
  445. If the number of nonlinear inequality constraints is unknown.
  446. """
  447. if self._m_ub is None:
  448. raise ValueError(
  449. "The number of nonlinear inequality constraints is unknown."
  450. )
  451. else:
  452. return self._m_ub
  453. @property
  454. def m_eq(self):
  455. """
  456. Number of nonlinear equality constraints.
  457. Returns
  458. -------
  459. int
  460. Number of nonlinear equality constraints.
  461. Raises
  462. ------
  463. ValueError
  464. If the number of nonlinear equality constraints is unknown.
  465. """
  466. if self._m_eq is None:
  467. raise ValueError(
  468. "The number of nonlinear equality constraints is unknown."
  469. )
  470. else:
  471. return self._m_eq
  472. @property
  473. def n_eval(self):
  474. """
  475. Number of function evaluations.
  476. Returns
  477. -------
  478. int
  479. Number of function evaluations.
  480. """
  481. if len(self.pcs):
  482. return self.pcs[0].fun.nfev
  483. else:
  484. return 0
  485. def maxcv(self, x, cub_val=None, ceq_val=None):
  486. """
  487. Evaluate the maximum constraint violation.
  488. Parameters
  489. ----------
  490. x : array_like, shape (n,)
  491. Point at which the maximum constraint violation is evaluated.
  492. cub_val : array_like, shape (m_nonlinear_ub,), optional
  493. Values of the nonlinear inequality constraints. If not provided,
  494. the nonlinear inequality constraints are evaluated at `x`.
  495. ceq_val : array_like, shape (m_nonlinear_eq,), optional
  496. Values of the nonlinear equality constraints. If not provided,
  497. the nonlinear equality constraints are evaluated at `x`.
  498. Returns
  499. -------
  500. float
  501. Maximum constraint violation at `x`.
  502. """
  503. return np.max(
  504. self.violation(x, cub_val=cub_val, ceq_val=ceq_val), initial=0.0
  505. )
  506. def violation(self, x, cub_val=None, ceq_val=None):
  507. return np.concatenate([pc.violation(x) for pc in self.pcs])
  508. class Problem:
  509. """
  510. Optimization problem.
  511. """
  512. def __init__(
  513. self,
  514. obj,
  515. x0,
  516. bounds,
  517. linear,
  518. nonlinear,
  519. callback,
  520. feasibility_tol,
  521. scale,
  522. store_history,
  523. history_size,
  524. filter_size,
  525. debug,
  526. ):
  527. """
  528. Initialize the nonlinear problem.
  529. The problem is preprocessed to remove all the variables that are fixed
  530. by the bound constraints.
  531. Parameters
  532. ----------
  533. obj : ObjectiveFunction
  534. Objective function.
  535. x0 : array_like, shape (n,)
  536. Initial guess.
  537. bounds : BoundConstraints
  538. Bound constraints.
  539. linear : LinearConstraints
  540. Linear constraints.
  541. nonlinear : NonlinearConstraints
  542. Nonlinear constraints.
  543. callback : {callable, None}
  544. Callback function.
  545. feasibility_tol : float
  546. Tolerance on the constraint violation.
  547. scale : bool
  548. Whether to scale the problem according to the bounds.
  549. store_history : bool
  550. Whether to store the function evaluations.
  551. history_size : int
  552. Maximum number of function evaluations to store.
  553. filter_size : int
  554. Maximum number of points in the filter.
  555. debug : bool
  556. Whether to make debugging tests during the execution.
  557. """
  558. if debug:
  559. assert isinstance(obj, ObjectiveFunction)
  560. assert isinstance(bounds, BoundConstraints)
  561. assert isinstance(linear, LinearConstraints)
  562. assert isinstance(nonlinear, NonlinearConstraints)
  563. assert isinstance(feasibility_tol, float)
  564. assert isinstance(scale, bool)
  565. assert isinstance(store_history, bool)
  566. assert isinstance(history_size, int)
  567. if store_history:
  568. assert history_size > 0
  569. assert isinstance(filter_size, int)
  570. assert filter_size > 0
  571. assert isinstance(debug, bool)
  572. self._obj = obj
  573. self._linear = linear
  574. self._nonlinear = nonlinear
  575. if callback is not None:
  576. if not callable(callback):
  577. raise TypeError("The callback must be a callable function.")
  578. self._callback = callback
  579. # Check the consistency of the problem.
  580. x0 = exact_1d_array(x0, "The initial guess must be a vector.")
  581. n = x0.size
  582. if bounds.xl.size != n:
  583. raise ValueError(f"The bounds must have {n} elements.")
  584. if linear.a_ub.shape[1] != n:
  585. raise ValueError(
  586. f"The left-hand side matrices of the linear constraints must "
  587. f"have {n} columns."
  588. )
  589. # Check which variables are fixed.
  590. tol = get_arrays_tol(bounds.xl, bounds.xu)
  591. self._fixed_idx = (bounds.xl <= bounds.xu) & (
  592. np.abs(bounds.xl - bounds.xu) < tol
  593. )
  594. self._fixed_val = 0.5 * (
  595. bounds.xl[self._fixed_idx] + bounds.xu[self._fixed_idx]
  596. )
  597. self._fixed_val = np.clip(
  598. self._fixed_val,
  599. bounds.xl[self._fixed_idx],
  600. bounds.xu[self._fixed_idx],
  601. )
  602. # Set the bound constraints.
  603. self._orig_bounds = bounds
  604. self._bounds = BoundConstraints(
  605. Bounds(bounds.xl[~self._fixed_idx], bounds.xu[~self._fixed_idx])
  606. )
  607. # Set the initial guess.
  608. self._x0 = self._bounds.project(x0[~self._fixed_idx])
  609. # Set the linear constraints.
  610. b_eq = linear.b_eq - linear.a_eq[:, self._fixed_idx] @ self._fixed_val
  611. self._linear = LinearConstraints(
  612. [
  613. LinearConstraint(
  614. linear.a_ub[:, ~self._fixed_idx],
  615. -np.inf,
  616. linear.b_ub
  617. - linear.a_ub[:, self._fixed_idx] @ self._fixed_val,
  618. ),
  619. LinearConstraint(linear.a_eq[:, ~self._fixed_idx], b_eq, b_eq),
  620. ],
  621. self.n,
  622. debug,
  623. )
  624. # Scale the problem if necessary.
  625. scale = (
  626. scale
  627. and self._bounds.is_feasible
  628. and np.all(np.isfinite(self._bounds.xl))
  629. and np.all(np.isfinite(self._bounds.xu))
  630. )
  631. if scale:
  632. self._scaling_factor = 0.5 * (self._bounds.xu - self._bounds.xl)
  633. self._scaling_shift = 0.5 * (self._bounds.xu + self._bounds.xl)
  634. self._bounds = BoundConstraints(
  635. Bounds(-np.ones(self.n), np.ones(self.n))
  636. )
  637. b_eq = self._linear.b_eq - self._linear.a_eq @ self._scaling_shift
  638. self._linear = LinearConstraints(
  639. [
  640. LinearConstraint(
  641. self._linear.a_ub @ np.diag(self._scaling_factor),
  642. -np.inf,
  643. self._linear.b_ub
  644. - self._linear.a_ub @ self._scaling_shift,
  645. ),
  646. LinearConstraint(
  647. self._linear.a_eq @ np.diag(self._scaling_factor),
  648. b_eq,
  649. b_eq,
  650. ),
  651. ],
  652. self.n,
  653. debug,
  654. )
  655. self._x0 = (self._x0 - self._scaling_shift) / self._scaling_factor
  656. else:
  657. self._scaling_factor = np.ones(self.n)
  658. self._scaling_shift = np.zeros(self.n)
  659. # Set the initial filter.
  660. self._feasibility_tol = feasibility_tol
  661. self._filter_size = filter_size
  662. self._fun_filter = []
  663. self._maxcv_filter = []
  664. self._x_filter = []
  665. # Set the initial history.
  666. self._store_history = store_history
  667. self._history_size = history_size
  668. self._fun_history = []
  669. self._maxcv_history = []
  670. self._x_history = []
  671. def __call__(self, x, penalty=0.0):
  672. """
  673. Evaluate the objective and nonlinear constraint functions.
  674. Parameters
  675. ----------
  676. x : array_like, shape (n,)
  677. Point at which the functions are evaluated.
  678. penalty : float, optional
  679. Penalty parameter used to select the point in the filter to forward
  680. to the callback function.
  681. Returns
  682. -------
  683. float
  684. Objective function value.
  685. `numpy.ndarray`, shape (m_nonlinear_ub,)
  686. Nonlinear inequality constraint function values.
  687. `numpy.ndarray`, shape (m_nonlinear_eq,)
  688. Nonlinear equality constraint function values.
  689. Raises
  690. ------
  691. `cobyqa.utils.CallbackSuccess`
  692. If the callback function raises a ``StopIteration``.
  693. """
  694. # Evaluate the objective and nonlinear constraint functions.
  695. x = np.asarray(x, dtype=float)
  696. x_full = self.build_x(x)
  697. fun_val = self._obj(x_full)
  698. cub_val, ceq_val = self._nonlinear(x_full)
  699. maxcv_val = self.maxcv(x, cub_val, ceq_val)
  700. if self._store_history:
  701. self._fun_history.append(fun_val)
  702. self._maxcv_history.append(maxcv_val)
  703. self._x_history.append(x)
  704. if len(self._fun_history) > self._history_size:
  705. self._fun_history.pop(0)
  706. self._maxcv_history.pop(0)
  707. self._x_history.pop(0)
  708. # Add the point to the filter if it is not dominated by any point.
  709. if np.isnan(fun_val) and np.isnan(maxcv_val):
  710. include_point = len(self._fun_filter) == 0
  711. elif np.isnan(fun_val):
  712. include_point = all(
  713. np.isnan(fun_filter)
  714. and maxcv_val < maxcv_filter
  715. or np.isnan(maxcv_filter)
  716. for fun_filter, maxcv_filter in zip(
  717. self._fun_filter,
  718. self._maxcv_filter,
  719. )
  720. )
  721. elif np.isnan(maxcv_val):
  722. include_point = all(
  723. np.isnan(maxcv_filter)
  724. and fun_val < fun_filter
  725. or np.isnan(fun_filter)
  726. for fun_filter, maxcv_filter in zip(
  727. self._fun_filter,
  728. self._maxcv_filter,
  729. )
  730. )
  731. else:
  732. include_point = all(
  733. fun_val < fun_filter or maxcv_val < maxcv_filter
  734. for fun_filter, maxcv_filter in zip(
  735. self._fun_filter,
  736. self._maxcv_filter,
  737. )
  738. )
  739. if include_point:
  740. self._fun_filter.append(fun_val)
  741. self._maxcv_filter.append(maxcv_val)
  742. self._x_filter.append(x)
  743. # Remove the points in the filter that are dominated by the new
  744. # point. We must iterate in reverse order to avoid problems when
  745. # removing elements from the list.
  746. for k in range(len(self._fun_filter) - 2, -1, -1):
  747. if np.isnan(fun_val):
  748. remove_point = np.isnan(self._fun_filter[k])
  749. elif np.isnan(maxcv_val):
  750. remove_point = np.isnan(self._maxcv_filter[k])
  751. else:
  752. remove_point = (
  753. np.isnan(self._fun_filter[k])
  754. or np.isnan(self._maxcv_filter[k])
  755. or fun_val <= self._fun_filter[k]
  756. and maxcv_val <= self._maxcv_filter[k]
  757. )
  758. if remove_point:
  759. self._fun_filter.pop(k)
  760. self._maxcv_filter.pop(k)
  761. self._x_filter.pop(k)
  762. # Keep only the most recent points in the filter.
  763. if len(self._fun_filter) > self._filter_size:
  764. self._fun_filter.pop(0)
  765. self._maxcv_filter.pop(0)
  766. self._x_filter.pop(0)
  767. # Evaluate the callback function after updating the filter to ensure
  768. # that the current point can be returned by the method.
  769. if self._callback is not None:
  770. sig = signature(self._callback)
  771. try:
  772. x_best, fun_best, _ = self.best_eval(penalty)
  773. x_best = self.build_x(x_best)
  774. if set(sig.parameters) == {"intermediate_result"}:
  775. intermediate_result = OptimizeResult(
  776. x=x_best,
  777. fun=fun_best,
  778. # maxcv=maxcv_best,
  779. )
  780. self._callback(intermediate_result=intermediate_result)
  781. else:
  782. self._callback(x_best)
  783. except StopIteration as exc:
  784. raise CallbackSuccess from exc
  785. # Apply the extreme barriers and return.
  786. if np.isnan(fun_val):
  787. fun_val = BARRIER
  788. cub_val[np.isnan(cub_val)] = BARRIER
  789. ceq_val[np.isnan(ceq_val)] = BARRIER
  790. fun_val = max(min(fun_val, BARRIER), -BARRIER)
  791. cub_val = np.maximum(np.minimum(cub_val, BARRIER), -BARRIER)
  792. ceq_val = np.maximum(np.minimum(ceq_val, BARRIER), -BARRIER)
  793. return fun_val, cub_val, ceq_val
  794. @property
  795. def n(self):
  796. """
  797. Number of variables.
  798. Returns
  799. -------
  800. int
  801. Number of variables.
  802. """
  803. return self.x0.size
  804. @property
  805. def n_orig(self):
  806. """
  807. Number of variables in the original problem (with fixed variables).
  808. Returns
  809. -------
  810. int
  811. Number of variables in the original problem (with fixed variables).
  812. """
  813. return self._fixed_idx.size
  814. @property
  815. def x0(self):
  816. """
  817. Initial guess.
  818. Returns
  819. -------
  820. `numpy.ndarray`, shape (n,)
  821. Initial guess.
  822. """
  823. return self._x0
  824. @property
  825. def n_eval(self):
  826. """
  827. Number of function evaluations.
  828. Returns
  829. -------
  830. int
  831. Number of function evaluations.
  832. """
  833. return self._obj.n_eval
  834. @property
  835. def fun_name(self):
  836. """
  837. Name of the objective function.
  838. Returns
  839. -------
  840. str
  841. Name of the objective function.
  842. """
  843. return self._obj.name
  844. @property
  845. def bounds(self):
  846. """
  847. Bound constraints.
  848. Returns
  849. -------
  850. BoundConstraints
  851. Bound constraints.
  852. """
  853. return self._bounds
  854. @property
  855. def linear(self):
  856. """
  857. Linear constraints.
  858. Returns
  859. -------
  860. LinearConstraints
  861. Linear constraints.
  862. """
  863. return self._linear
  864. @property
  865. def m_bounds(self):
  866. """
  867. Number of bound constraints.
  868. Returns
  869. -------
  870. int
  871. Number of bound constraints.
  872. """
  873. return self.bounds.m
  874. @property
  875. def m_linear_ub(self):
  876. """
  877. Number of linear inequality constraints.
  878. Returns
  879. -------
  880. int
  881. Number of linear inequality constraints.
  882. """
  883. return self.linear.m_ub
  884. @property
  885. def m_linear_eq(self):
  886. """
  887. Number of linear equality constraints.
  888. Returns
  889. -------
  890. int
  891. Number of linear equality constraints.
  892. """
  893. return self.linear.m_eq
  894. @property
  895. def m_nonlinear_ub(self):
  896. """
  897. Number of nonlinear inequality constraints.
  898. Returns
  899. -------
  900. int
  901. Number of nonlinear inequality constraints.
  902. Raises
  903. ------
  904. ValueError
  905. If the number of nonlinear inequality constraints is not known.
  906. """
  907. return self._nonlinear.m_ub
  908. @property
  909. def m_nonlinear_eq(self):
  910. """
  911. Number of nonlinear equality constraints.
  912. Returns
  913. -------
  914. int
  915. Number of nonlinear equality constraints.
  916. Raises
  917. ------
  918. ValueError
  919. If the number of nonlinear equality constraints is not known.
  920. """
  921. return self._nonlinear.m_eq
  922. @property
  923. def fun_history(self):
  924. """
  925. History of objective function evaluations.
  926. Returns
  927. -------
  928. `numpy.ndarray`, shape (n_eval,)
  929. History of objective function evaluations.
  930. """
  931. return np.array(self._fun_history, dtype=float)
  932. @property
  933. def maxcv_history(self):
  934. """
  935. History of maximum constraint violations.
  936. Returns
  937. -------
  938. `numpy.ndarray`, shape (n_eval,)
  939. History of maximum constraint violations.
  940. """
  941. return np.array(self._maxcv_history, dtype=float)
  942. @property
  943. def type(self):
  944. """
  945. Type of the problem.
  946. The problem can be either 'unconstrained', 'bound-constrained',
  947. 'linearly constrained', or 'nonlinearly constrained'.
  948. Returns
  949. -------
  950. str
  951. Type of the problem.
  952. """
  953. try:
  954. if self.m_nonlinear_ub > 0 or self.m_nonlinear_eq > 0:
  955. return "nonlinearly constrained"
  956. elif self.m_linear_ub > 0 or self.m_linear_eq > 0:
  957. return "linearly constrained"
  958. elif self.m_bounds > 0:
  959. return "bound-constrained"
  960. else:
  961. return "unconstrained"
  962. except ValueError:
  963. # The number of nonlinear constraints is not known. It may be zero
  964. # if the user provided a nonlinear inequality and/or equality
  965. # constraint function that returns an empty array. However, as this
  966. # is not known before the first call to the function, we assume
  967. # that the problem is nonlinearly constrained.
  968. return "nonlinearly constrained"
  969. @property
  970. def is_feasibility(self):
  971. """
  972. Whether the problem is a feasibility problem.
  973. Returns
  974. -------
  975. bool
  976. Whether the problem is a feasibility problem.
  977. """
  978. return self.fun_name == ""
  979. def build_x(self, x):
  980. """
  981. Build the full vector of variables from the reduced vector.
  982. Parameters
  983. ----------
  984. x : array_like, shape (n,)
  985. Reduced vector of variables.
  986. Returns
  987. -------
  988. `numpy.ndarray`, shape (n_orig,)
  989. Full vector of variables.
  990. """
  991. x_full = np.empty(self.n_orig)
  992. x_full[self._fixed_idx] = self._fixed_val
  993. x_full[~self._fixed_idx] = (x * self._scaling_factor
  994. + self._scaling_shift)
  995. return self._orig_bounds.project(x_full)
  996. def maxcv(self, x, cub_val=None, ceq_val=None):
  997. """
  998. Evaluate the maximum constraint violation.
  999. Parameters
  1000. ----------
  1001. x : array_like, shape (n,)
  1002. Point at which the maximum constraint violation is evaluated.
  1003. cub_val : array_like, shape (m_nonlinear_ub,), optional
  1004. Values of the nonlinear inequality constraints. If not provided,
  1005. the nonlinear inequality constraints are evaluated at `x`.
  1006. ceq_val : array_like, shape (m_nonlinear_eq,), optional
  1007. Values of the nonlinear equality constraints. If not provided,
  1008. the nonlinear equality constraints are evaluated at `x`.
  1009. Returns
  1010. -------
  1011. float
  1012. Maximum constraint violation at `x`.
  1013. """
  1014. violation = self.violation(x, cub_val=cub_val, ceq_val=ceq_val)
  1015. if np.count_nonzero(violation):
  1016. return np.max(violation, initial=0.0)
  1017. else:
  1018. return 0.0
  1019. def violation(self, x, cub_val=None, ceq_val=None):
  1020. violation = []
  1021. if not self.bounds.is_feasible:
  1022. b = self.bounds.violation(x)
  1023. violation.append(b)
  1024. if len(self.linear.pcs):
  1025. lc = self.linear.violation(x)
  1026. violation.append(lc)
  1027. if len(self._nonlinear.pcs):
  1028. nlc = self._nonlinear.violation(x, cub_val, ceq_val)
  1029. violation.append(nlc)
  1030. if len(violation):
  1031. return np.concatenate(violation)
  1032. def best_eval(self, penalty):
  1033. """
  1034. Return the best point in the filter and the corresponding objective and
  1035. nonlinear constraint function evaluations.
  1036. Parameters
  1037. ----------
  1038. penalty : float
  1039. Penalty parameter
  1040. Returns
  1041. -------
  1042. `numpy.ndarray`, shape (n,)
  1043. Best point.
  1044. float
  1045. Corresponding objective function value.
  1046. float
  1047. Corresponding maximum constraint violation.
  1048. """
  1049. # If the filter is empty, i.e., if no function evaluation has been
  1050. # performed, we evaluate the objective and nonlinear constraint
  1051. # functions at the initial guess.
  1052. if len(self._fun_filter) == 0:
  1053. self(self.x0)
  1054. # Find the best point in the filter.
  1055. fun_filter = np.array(self._fun_filter)
  1056. maxcv_filter = np.array(self._maxcv_filter)
  1057. x_filter = np.array(self._x_filter)
  1058. finite_idx = np.isfinite(maxcv_filter)
  1059. if np.any(finite_idx):
  1060. # At least one point has a finite maximum constraint violation.
  1061. feasible_idx = maxcv_filter <= self._feasibility_tol
  1062. if np.any(feasible_idx) and not np.all(
  1063. np.isnan(fun_filter[feasible_idx])
  1064. ):
  1065. # At least one point is feasible and has a well-defined
  1066. # objective function value. We select the point with the least
  1067. # objective function value. If there is a tie, we select the
  1068. # point with the least maximum constraint violation. If there
  1069. # is still a tie, we select the most recent point.
  1070. fun_min_idx = feasible_idx & (
  1071. fun_filter <= np.nanmin(fun_filter[feasible_idx])
  1072. )
  1073. if np.count_nonzero(fun_min_idx) > 1:
  1074. fun_min_idx &= maxcv_filter <= np.min(
  1075. maxcv_filter[fun_min_idx]
  1076. )
  1077. i = np.flatnonzero(fun_min_idx)[-1]
  1078. elif np.any(feasible_idx):
  1079. # At least one point is feasible but no feasible point has a
  1080. # well-defined objective function value. We select the most
  1081. # recent feasible point.
  1082. i = np.flatnonzero(feasible_idx)[-1]
  1083. else:
  1084. # No point is feasible. We first compute the merit function
  1085. # value for each point.
  1086. merit_filter = np.full_like(fun_filter, np.nan)
  1087. merit_filter[finite_idx] = (
  1088. fun_filter[finite_idx] + penalty * maxcv_filter[finite_idx]
  1089. )
  1090. if np.all(np.isnan(merit_filter)):
  1091. # No point has a well-defined merit function value. In
  1092. # other words, among the points with a well-defined maximum
  1093. # constraint violation, none has a well-defined objective
  1094. # function value. We select the point with the least
  1095. # maximum constraint violation. If there is a tie, we
  1096. # select the most recent point.
  1097. min_maxcv_idx = maxcv_filter <= np.nanmin(maxcv_filter)
  1098. i = np.flatnonzero(min_maxcv_idx)[-1]
  1099. else:
  1100. # At least one point has a well-defined merit function
  1101. # value. We select the point with the least merit function
  1102. # value. If there is a tie, we select the point with the
  1103. # least maximum constraint violation. If there is still a
  1104. # tie, we select the point with the least objective
  1105. # function value. If there is still a tie, we select the
  1106. # most recent point.
  1107. merit_min_idx = merit_filter <= np.nanmin(merit_filter)
  1108. if np.count_nonzero(merit_min_idx) > 1:
  1109. merit_min_idx &= maxcv_filter <= np.min(
  1110. maxcv_filter[merit_min_idx]
  1111. )
  1112. if np.count_nonzero(merit_min_idx) > 1:
  1113. merit_min_idx &= fun_filter <= np.min(
  1114. fun_filter[merit_min_idx]
  1115. )
  1116. i = np.flatnonzero(merit_min_idx)[-1]
  1117. elif not np.all(np.isnan(fun_filter)):
  1118. # No maximum constraint violation is well-defined but at least one
  1119. # point has a well-defined objective function value. We select the
  1120. # point with the least objective function value. If there is a tie,
  1121. # we select the most recent point.
  1122. fun_min_idx = fun_filter <= np.nanmin(fun_filter)
  1123. i = np.flatnonzero(fun_min_idx)[-1]
  1124. else:
  1125. # No point has a well-defined maximum constraint violation or
  1126. # objective function value. We select the most recent point.
  1127. i = len(fun_filter) - 1
  1128. return (
  1129. self.bounds.project(x_filter[i, :]),
  1130. fun_filter[i],
  1131. maxcv_filter[i],
  1132. )