selectx.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. '''
  2. This module provides subroutines that ensure the returned X is optimal among all the calculated
  3. points in the sense that no other point achieves both lower function value and lower constraint
  4. violation at the same time. This module is needed only in the constrained case.
  5. Translated from Zaikun Zhang's modern-Fortran reference implementation in PRIMA.
  6. Dedicated to late Professor M. J. D. Powell FRS (1936--2015).
  7. Python translation by Nickolai Belakovski.
  8. '''
  9. import numpy as np
  10. import numpy.typing as npt
  11. from .consts import DEBUGGING, EPS, CONSTRMAX, REALMAX, FUNCMAX
  12. from .present import present
  13. def isbetter(f1: float, c1: float, f2: float, c2: float, ctol: float) -> bool:
  14. '''
  15. This function compares whether FC1 = (F1, C1) is (strictly) better than FC2 = (F2, C2), which
  16. basically means that (F1 < F2 and C1 <= C2) or (F1 <= F2 and C1 < C2).
  17. It takes care of the cases where some of these values are NaN or Inf, even though some cases
  18. should never happen due to the moderated extreme barrier.
  19. At return, BETTER = TRUE if and only if (F1, C1) is better than (F2, C2).
  20. Here, C means constraint violation, which is a nonnegative number.
  21. '''
  22. # Preconditions
  23. if DEBUGGING:
  24. assert not any(np.isnan([f1, c1]) | np.isposinf([f2, c2]))
  25. assert not any(np.isnan([f2, c2]) | np.isposinf([f2, c2]))
  26. assert c1 >= 0 and c2 >= 0
  27. assert ctol >= 0
  28. #====================#
  29. # Calculation starts #
  30. #====================#
  31. is_better = False
  32. # Even though NaN/+Inf should not occur in FC1 or FC2 due to the moderated extreme barrier, for
  33. # security and robustness, the code below does not make this assumption.
  34. is_better = is_better or (any(np.isnan([f1, c1]) | np.isposinf([f1, c1])) and not any(np.isnan([f2, c2]) | np.isposinf([f2, c2])))
  35. is_better = is_better or (f1 < f2 and c1 <= c2)
  36. is_better = is_better or (f1 <= f2 and c1 < c2)
  37. # If C1 <= CTOL and C2 is significantly larger/worse than CTOL, i.e., C2 > MAX(CTOL,CREF),
  38. # then FC1 is better than FC2 as long as F1 < REALMAX. Normally CREF >= CTOL so MAX(CTOL, CREF)
  39. # is indeed CREF. However, this may not be true if CTOL > 1E-1*CONSTRMAX.
  40. cref = 10 * max(EPS, min(ctol, 1.0E-2 * CONSTRMAX)) # The MIN avoids overflow.
  41. is_better = is_better or (f1 < REALMAX and c1 <= ctol and (c2 > max(ctol, cref) or np.isnan(c2)))
  42. #==================#
  43. # Calculation ends #
  44. #==================#
  45. # Postconditions
  46. if DEBUGGING:
  47. assert not (is_better and f1 >= f2 and c1 >= c2)
  48. assert is_better or not (f1 <= f2 and c1 < c2)
  49. assert is_better or not (f1 < f2 and c1 <= c2)
  50. return is_better
  51. def savefilt(cstrv, ctol, cweight, f, x, nfilt, cfilt, ffilt, xfilt, constr=None, confilt=None):
  52. '''
  53. This subroutine saves X, F, and CSTRV in XFILT, FFILT, and CFILT (and CONSTR in CONFILT
  54. if they are present), unless a vector in XFILT[:, :NFILT] is better than X.
  55. If X is better than some vectors in XFILT[:, :NFILT] then these vectors will be
  56. removed. If X is not better than any of XFILT[:, :NFILT], but NFILT == MAXFILT,
  57. then we remove a column from XFILT according to the merit function
  58. PHI = FFILT + CWEIGHT * max(CFILT - CTOL, 0)
  59. N.B.:
  60. 1. Only XFILT[:, :NFILT] and FFILT[:, :NFILT] etc contains valid information,
  61. while XFILT[:, NFILT+1:MAXFILT] and FFILT[:, NFILT+1:MAXFILT] etc are not
  62. initialized yet.
  63. 2. We decide whether and X is better than another by the ISBETTER function
  64. '''
  65. # Sizes
  66. if present(constr):
  67. num_constraints = len(constr)
  68. else:
  69. num_constraints = 0
  70. num_vars = len(x)
  71. maxfilt = len(ffilt)
  72. # Preconditions
  73. if DEBUGGING:
  74. # Check the size of X.
  75. assert num_vars >= 1
  76. # Check CWEIGHT and CTOL
  77. assert cweight >= 0
  78. assert ctol >= 0
  79. # Check NFILT
  80. assert nfilt >= 0 and nfilt <= maxfilt
  81. # Check the sizes of XFILT, FFILT, CFILT.
  82. assert maxfilt >= 1
  83. assert np.size(xfilt, 0) == num_vars and np.size(xfilt, 1) == maxfilt
  84. assert np.size(cfilt) == maxfilt
  85. # Check the values of XFILT, FFILT, CFILT.
  86. assert not (np.isnan(xfilt[:, :nfilt])).any()
  87. assert not any(np.isnan(ffilt[:nfilt]) | np.isposinf(ffilt[:nfilt]))
  88. assert not any(cfilt[:nfilt] < 0 | np.isnan(cfilt[:nfilt]) | np.isposinf(cfilt[:nfilt]))
  89. # Check the values of X, F, CSTRV.
  90. # X does not contain NaN if X0 does not and the trust-region/geometry steps are proper.
  91. assert not any(np.isnan(x))
  92. # F cannot be NaN/+Inf due to the moderated extreme barrier.
  93. assert not (np.isnan(f) | np.isposinf(f))
  94. # CSTRV cannot be NaN/+Inf due to the moderated extreme barrier.
  95. assert not (cstrv < 0 | np.isnan(cstrv) | np.isposinf(cstrv))
  96. # Check CONSTR and CONFILT.
  97. assert present(constr) == present(confilt)
  98. if present(constr):
  99. # CONSTR cannot contain NaN/-Inf due to the moderated extreme barrier.
  100. assert not any(np.isnan(constr) | np.isneginf(constr))
  101. assert np.size(confilt, 0) == num_constraints and np.size(confilt, 1) == maxfilt
  102. assert not (np.isnan(confilt[:, :nfilt]) | np.isneginf(confilt[:, :nfilt])).any()
  103. #====================#
  104. # Calculation starts #
  105. #====================#
  106. # Return immediately if any column of XFILT is better than X.
  107. if any((isbetter(ffilt_i, cfilt_i, f, cstrv, ctol) for ffilt_i, cfilt_i in zip(ffilt[:nfilt], cfilt[:nfilt]))) or \
  108. any(np.logical_and(ffilt[:nfilt] <= f, cfilt[:nfilt] <= cstrv)):
  109. return nfilt, cfilt, ffilt, xfilt, confilt
  110. # Decide which columns of XFILT to keep.
  111. keep = np.logical_not([isbetter(f, cstrv, ffilt_i, cfilt_i, ctol) for ffilt_i, cfilt_i in zip(ffilt[:nfilt], cfilt[:nfilt])])
  112. # If NFILT == MAXFILT and X is not better than any column of XFILT, then we remove the worst column
  113. # of XFILT according to the merit function PHI = FFILT + CWEIGHT * MAX(CFILT - CTOL, ZERO).
  114. if sum(keep) == maxfilt: # In this case, NFILT = SIZE(KEEP) = COUNT(KEEP) = MAXFILT > 0.
  115. cfilt_shifted = np.maximum(cfilt - ctol, 0)
  116. if cweight <= 0:
  117. phi = ffilt
  118. elif np.isposinf(cweight):
  119. phi = cfilt_shifted
  120. # We should not use CFILT here; if MAX(CFILT_SHIFTED) is attained at multiple indices, then
  121. # we will check FFILT to exhaust the remaining degree of freedom.
  122. else:
  123. phi = np.maximum(ffilt, -REALMAX)
  124. phi = np.nan_to_num(phi, nan=-REALMAX) # Replace NaN with -REALMAX and +/- inf with large numbers
  125. phi += cweight * cfilt_shifted
  126. # We select X to maximize PHI. In case there are multiple maximizers, we take the one with the
  127. # largest CSTRV_SHIFTED; if there are more than one choices, we take the one with the largest F;
  128. # if there are several candidates, we take the one with the largest CSTRV; if the last comparison
  129. # still leads to more than one possibilities, then they are equally bad and we choose the first.
  130. # N.B.:
  131. # 1. This process is the opposite of selecting KOPT in SELECTX.
  132. # 2. In finite-precision arithmetic, PHI_1 == PHI_2 and CSTRV_SHIFTED_1 == CSTRV_SHIFTED_2 do
  133. # not ensure that F_1 == F_2!
  134. phimax = max(phi)
  135. cref = max(cfilt_shifted[phi >= phimax])
  136. fref = max(ffilt[cfilt_shifted >= cref])
  137. kworst = np.ma.array(cfilt, mask=(ffilt > fref)).argmax()
  138. if kworst < 0 or kworst >= len(keep): # For security. Should not happen.
  139. kworst = 0
  140. keep[kworst] = False
  141. # Keep the good xfilt values and remove all the ones that are strictly worse than the new x.
  142. nfilt = sum(keep)
  143. index_to_keep = np.where(keep)[0]
  144. xfilt[:, :nfilt] = xfilt[:, index_to_keep]
  145. ffilt[:nfilt] = ffilt[index_to_keep]
  146. cfilt[:nfilt] = cfilt[index_to_keep]
  147. if confilt is not None and constr is not None:
  148. confilt[:, :nfilt] = confilt[:, index_to_keep]
  149. # Once we have removed all the vectors that are strictly worse than x,
  150. # we add x to the filter.
  151. xfilt[:, nfilt] = x
  152. ffilt[nfilt] = f
  153. cfilt[nfilt] = cstrv
  154. if confilt is not None and constr is not None:
  155. confilt[:, nfilt] = constr
  156. nfilt += 1 # In Python we need to increment the index afterwards
  157. #==================#
  158. # Calculation ends #
  159. #==================#
  160. # Postconditions
  161. if DEBUGGING:
  162. # Check NFILT and the sizes of XFILT, FFILT, CFILT.
  163. assert nfilt >= 1 and nfilt <= maxfilt
  164. assert np.size(xfilt, 0) == num_vars and np.size(xfilt, 1) == maxfilt
  165. assert np.size(ffilt) == maxfilt
  166. assert np.size(cfilt) == maxfilt
  167. # Check the values of XFILT, FFILT, CFILT.
  168. assert not (np.isnan(xfilt[:, :nfilt])).any()
  169. assert not any(np.isnan(ffilt[:nfilt]) | np.isposinf(ffilt[:nfilt]))
  170. assert not any(cfilt[:nfilt] < 0 | np.isnan(cfilt[:nfilt]) | np.isposinf(cfilt[:nfilt]))
  171. # Check that no point in the filter is better than X, and X is better than no point.
  172. assert not any([isbetter(ffilt_i, cfilt_i, f, cstrv, ctol) for ffilt_i, cfilt_i in zip(ffilt[:nfilt], cfilt[:nfilt])])
  173. assert not any([isbetter(f, cstrv, ffilt_i, cfilt_i, ctol) for ffilt_i, cfilt_i in zip(ffilt[:nfilt], cfilt[:nfilt])])
  174. # Check CONFILT.
  175. if present(confilt):
  176. assert np.size(confilt, 0) == num_constraints and np.size(confilt, 1) == maxfilt
  177. assert not (np.isnan(confilt[:, :nfilt]) | np.isneginf(confilt[:, :nfilt])).any()
  178. return nfilt, cfilt, ffilt, xfilt, confilt
  179. def selectx(fhist: npt.NDArray, chist: npt.NDArray, cweight: float, ctol: float):
  180. '''
  181. This subroutine selects X according to FHIST and CHIST, which represents (a part of) history
  182. of F and CSTRV. Normally, FHIST and CHIST are not the full history but only a filter, e.g. ffilt
  183. and CFILT generated by SAVEFILT. However, we name them as FHIST and CHIST because the [F, CSTRV]
  184. in a filter should not dominate each other, but this subroutine does NOT assume such a property.
  185. N.B.: CTOL is the tolerance of the constraint violation (CSTRV). A point is considered feasible if
  186. its constraint violation is at most CTOL. Not that CTOL is absolute, not relative.
  187. '''
  188. # Sizes
  189. nhist = len(fhist)
  190. # Preconditions
  191. if DEBUGGING:
  192. assert nhist >= 1
  193. assert np.size(chist) == nhist
  194. assert not any(np.isnan(fhist) | np.isposinf(fhist))
  195. assert not any(chist < 0 | np.isnan(chist) | np.isposinf(chist))
  196. assert cweight >= 0
  197. assert ctol >= 0
  198. #====================#
  199. # Calculation starts #
  200. #====================#
  201. # We select X among the points with F < FREF and CSTRV < CREF.
  202. # Do NOT use F <= FREF, because F == FREF (FUNCMAX or REALMAX) may mean F == INF in practice!
  203. if any(np.logical_and(fhist < FUNCMAX, chist < CONSTRMAX)):
  204. fref = FUNCMAX
  205. cref = CONSTRMAX
  206. elif any(np.logical_and(fhist < REALMAX, chist < CONSTRMAX)):
  207. fref = REALMAX
  208. cref = CONSTRMAX
  209. elif any(np.logical_and(fhist < FUNCMAX, chist < REALMAX)):
  210. fref = FUNCMAX
  211. cref = REALMAX
  212. else:
  213. fref = REALMAX
  214. cref = REALMAX
  215. if not any(np.logical_and(fhist < fref, chist < cref)):
  216. kopt = nhist - 1
  217. else:
  218. # Shift the constraint violations by ctol, so that cstrv <= ctol is regarded as no violation.
  219. chist_shifted = np.maximum(chist - ctol, 0)
  220. # cmin is the minimal shift constraint violation attained in the history.
  221. cmin = np.min(chist_shifted[fhist < fref])
  222. # We consider only the points whose shifted constraint violations are at most the cref below.
  223. # N.B.: Without taking np.maximum(EPS, .), cref would be 0 if cmin = 0. In that case, asking for
  224. # cstrv_shift < cref would be WRONG!
  225. cref = np.maximum(EPS, 2*cmin)
  226. # We use the following phi as our merit function to select X.
  227. if cweight <= 0:
  228. phi = fhist
  229. elif np.isposinf(cweight):
  230. phi = chist_shifted
  231. # We should not use chist here; if np.minimum(chist_shifted) is attained at multiple indices, then
  232. # we will check fhist to exhaust the remaining degree of freedom.
  233. else:
  234. phi = np.maximum(fhist, -REALMAX) + cweight * chist_shifted
  235. # np.maximum(fhist, -REALMAX) makes sure that phi will not contain NaN (unless there is a bug).
  236. # We select X to minimize phi subject to f < fref and cstrv_shift <= cref (see the comments
  237. # above for the reason of taking "<" and "<=" in these two constraints). In case there are
  238. # multiple minimizers, we take the one with the least cstrv_shift; if there is more than one
  239. # choice, we take the one with the least f; if there are several candidates, we take the one
  240. # with the least cstrv; if the last comparison still leads to more than one possibility, then
  241. # they are equally good and we choose the first.
  242. # N.B.:
  243. # 1. This process is the opposite of selecting kworst in savefilt
  244. # 2. In finite-precision arithmetic, phi_2 == phi_2 and cstrv_shift_1 == cstrv_shifted_2 do
  245. # not ensure thatn f_1 == f_2!
  246. phimin = np.min(phi[np.logical_and(fhist < fref, chist_shifted <= cref)])
  247. cref = np.min(chist_shifted[np.logical_and(fhist < fref, phi <= phimin)])
  248. fref = np.min(fhist[chist_shifted <= cref])
  249. # Can't use argmin here because using it with a mask throws off the index
  250. kopt = np.ma.array(chist, mask=(fhist > fref)).argmin()
  251. #==================#
  252. # Calculation ends #
  253. #==================#
  254. # Postconditions
  255. if DEBUGGING:
  256. assert kopt >= 0 and kopt < nhist
  257. assert not any([isbetter(fhisti, chisti, fhist[kopt], chist[kopt], ctol) for fhisti, chisti in zip(fhist[:nhist], chist[:nhist])])
  258. return kopt