update.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. '''
  2. This module contains subroutines concerning the update of the interpolation set.
  3. Translated from Zaikun Zhang's modern-Fortran reference implementation in PRIMA.
  4. Dedicated to late Professor M. J. D. Powell FRS (1936--2015).
  5. Python translation by Nickolai Belakovski.
  6. '''
  7. from ..common.consts import DEBUGGING
  8. from ..common.infos import DAMAGING_ROUNDING, INFO_DEFAULT
  9. from ..common.linalg import isinv, matprod, outprod, inprod, inv, primasum
  10. import numpy as np
  11. def updatexfc(jdrop, constr, cpen, cstrv, d, f, conmat, cval, fval, sim, simi):
  12. '''
  13. This function revises the simplex by updating the elements of SIM, SIMI, FVAL, CONMAT, and CVAL
  14. '''
  15. # Local variables
  16. itol = 1
  17. # Sizes
  18. num_constraints = np.size(constr)
  19. num_vars = np.size(sim, 0)
  20. # Preconditions
  21. if DEBUGGING:
  22. assert num_constraints >= 0
  23. assert num_vars >= 1
  24. assert jdrop >= 0 and jdrop <= num_vars + 1
  25. assert not any(np.isnan(constr) | np.isneginf(constr))
  26. assert not (np.isnan(cstrv) | np.isposinf(cstrv))
  27. assert np.size(d) == num_vars and all(np.isfinite(d))
  28. assert not (np.isnan(f) | np.isposinf(f))
  29. assert np.size(conmat, 0) == num_constraints and np.size(conmat, 1) == num_vars + 1
  30. assert not (np.isnan(conmat) | np.isneginf(conmat)).any()
  31. assert np.size(cval) == num_vars + 1 and not any(cval < 0 | np.isnan(cval) | np.isposinf(cval))
  32. assert np.size(fval) == num_vars + 1 and not any(np.isnan(fval) | np.isposinf(fval))
  33. assert np.size(sim, 0) == num_vars and np.size(sim, 1) == num_vars + 1
  34. assert np.isfinite(sim).all()
  35. assert all(primasum(abs(sim[:, :num_vars]), axis=0) > 0)
  36. assert np.size(simi, 0) == num_vars and np.size(simi, 1) == num_vars
  37. assert np.isfinite(simi).all()
  38. assert isinv(sim[:, :num_vars], simi, itol)
  39. #====================#
  40. # Calculation starts #
  41. #====================#
  42. # Do nothing when JDROP is None. This can only happen after a trust-region step.
  43. if jdrop is None: # JDROP is None is impossible if the input is correct.
  44. return conmat, cval, fval, sim, simi, INFO_DEFAULT
  45. sim_old = sim
  46. simi_old = simi
  47. if jdrop < num_vars:
  48. sim[:, jdrop] = d
  49. simi_jdrop = simi[jdrop, :] / inprod(simi[jdrop, :], d)
  50. simi -= outprod(matprod(simi, d), simi_jdrop)
  51. simi[jdrop, :] = simi_jdrop
  52. else: # jdrop == num_vars
  53. sim[:, num_vars] += d
  54. sim[:, :num_vars] -= np.tile(d, (num_vars, 1)).T
  55. simid = matprod(simi, d)
  56. sum_simi = primasum(simi, axis=0)
  57. simi += outprod(simid, sum_simi / (1 - sum(simid)))
  58. # Check whether SIMI is a poor approximation to the inverse of SIM[:, :NUM_VARS]
  59. # Calculate SIMI from scratch if the current one is damaged by rounding errors.
  60. itol = 1
  61. erri = np.max(abs(matprod(simi, sim[:, :num_vars]) - np.eye(num_vars))) # np.max returns NaN if any input is NaN
  62. if erri > 0.1 * itol or np.isnan(erri):
  63. simi_test = inv(sim[:, :num_vars])
  64. erri_test = np.max(abs(matprod(simi_test, sim[:, :num_vars]) - np.eye(num_vars)))
  65. if erri_test < erri or (np.isnan(erri) and not np.isnan(erri_test)):
  66. simi = simi_test
  67. erri = erri_test
  68. # If SIMI is satisfactory, then update FVAL, CONMAT, CVAL, and the pole position. Otherwise restore
  69. # SIM and SIMI, and return with INFO = DAMAGING_ROUNDING.
  70. if erri <= itol:
  71. fval[jdrop] = f
  72. conmat[:, jdrop] = constr
  73. cval[jdrop] = cstrv
  74. # Switch the best vertex to the pole position SIM[:, NUM_VARS] if it is not there already
  75. conmat, cval, fval, sim, simi, info = updatepole(cpen, conmat, cval, fval, sim, simi)
  76. else:
  77. info = DAMAGING_ROUNDING
  78. sim = sim_old
  79. simi = simi_old
  80. #==================#
  81. # Calculation ends #
  82. #==================#
  83. # Postconditions
  84. if DEBUGGING:
  85. assert np.size(conmat, 0) == num_constraints and np.size(conmat, 1) == num_vars + 1
  86. assert not (np.isnan(conmat) | np.isneginf(conmat)).any()
  87. assert np.size(cval) == num_vars + 1 and not any(cval < 0 | np.isnan(cval) | np.isposinf(cval))
  88. assert np.size(fval) == num_vars + 1 and not any(np.isnan(fval) | np.isposinf(fval))
  89. assert np.size(sim, 0) == num_vars and np.size(sim, 1) == num_vars + 1
  90. assert np.isfinite(sim).all()
  91. assert all(primasum(abs(sim[:, :num_vars]), axis=0) > 0)
  92. assert np.size(simi, 0) == num_vars and np.size(simi, 1) == num_vars
  93. assert np.isfinite(simi).all()
  94. assert isinv(sim[:, :num_vars], simi, itol) or info == DAMAGING_ROUNDING
  95. return sim, simi, fval, conmat, cval, info
  96. def findpole(cpen, cval, fval):
  97. '''
  98. This subroutine identifies the best vertex of the current simplex with respect to the merit
  99. function PHI = F + CPEN * CSTRV.
  100. '''
  101. # Size
  102. num_vars = np.size(fval) - 1
  103. # Preconditions
  104. if DEBUGGING:
  105. assert cpen > 0
  106. assert np.size(cval) == num_vars + 1 and not any(cval < 0 | np.isnan(cval) | np.isposinf(cval))
  107. assert np.size(fval) == num_vars + 1 and not any(np.isnan(fval) | np.isposinf(fval))
  108. #====================#
  109. # Calculation starts #
  110. #====================#
  111. # Identify the optimal vertex of the current simplex
  112. jopt = np.size(fval) - 1
  113. phi = fval + cpen * cval
  114. phimin = min(phi)
  115. # Essentially jopt = np.argmin(phi). However, we keep jopt = num_vars unless there
  116. # is a strictly better choice. When there are multiple choices, we choose the jopt
  117. # with the smallest value of cval.
  118. if phimin < phi[jopt] or any((cval < cval[jopt]) & (phi <= phi[jopt])):
  119. # While we could use argmin(phi), there may be two places where phi achieves
  120. # phimin, and in that case we should choose the one with the smallest cval.
  121. jopt = np.ma.array(cval, mask=(phi > phimin)).argmin()
  122. #==================#
  123. # Calculation ends #
  124. #==================#
  125. # Postconditions
  126. if DEBUGGING:
  127. assert jopt >= 0 and jopt < num_vars + 1
  128. assert jopt == num_vars or phi[jopt] < phi[num_vars] or (phi[jopt] <= phi[num_vars] and cval[jopt] < cval[num_vars])
  129. return jopt
  130. def updatepole(cpen, conmat, cval, fval, sim, simi):
  131. #--------------------------------------------------------------------------------------------------!
  132. # This subroutine identifies the best vertex of the current simplex with respect to the merit
  133. # function PHI = F + CPEN * CSTRV, and then switch this vertex to SIM[:, NUM_VARS], which Powell called
  134. # the "pole position" in his comments. CONMAT, CVAL, FVAL, and SIMI are updated accordingly.
  135. #
  136. # N.B. 1: In precise arithmetic, the following two procedures produce the same results:
  137. # 1) apply UPDATEPOLE to SIM twice, first with CPEN = CPEN1 and then with CPEN = CPEN2;
  138. # 2) apply UPDATEPOLE to SIM with CPEN = CPEN2.
  139. # In finite-precision arithmetic, however, they may produce different results unless CPEN1 = CPEN2.
  140. #
  141. # N.B. 2: When JOPT == N+1, the best vertex is already at the pole position, so there is nothing to
  142. # switch. However, as in Powell's code, the code below will check whether SIMI is good enough to
  143. # work as the inverse of SIM(:, 1:N) or not. If not, Powell's code would invoke an error return of
  144. # COBYLB; our implementation, however, will try calculating SIMI from scratch; if the recalculated
  145. # SIMI is still of poor quality, then UPDATEPOLE will return with INFO = DAMAGING_ROUNDING,
  146. # informing COBYLB that SIMI is poor due to damaging rounding errors.
  147. #
  148. # N.B. 3: UPDATEPOLE should be called when and only when FINDPOLE can potentially returns a value
  149. # other than N+1. The value of FINDPOLE is determined by CPEN, CVAL, and FVAL, the latter two being
  150. # decided by SIM. Thus UPDATEPOLE should be called after CPEN or SIM changes. COBYLA updates CPEN at
  151. # only two places: the beginning of each trust-region iteration, and when REDRHO is called;
  152. # SIM is updated only by UPDATEXFC, which itself calls UPDATEPOLE internally. Therefore, we only
  153. # need to call UPDATEPOLE after updating CPEN at the beginning of each trust-region iteration and
  154. # after each invocation of REDRHO.
  155. # Local variables
  156. itol = 1
  157. # Sizes
  158. num_constraints = conmat.shape[0]
  159. num_vars = sim.shape[0]
  160. # Preconditions
  161. if DEBUGGING:
  162. assert num_constraints >= 0
  163. assert num_vars >= 1
  164. assert cpen > 0
  165. assert np.size(conmat, 0) == num_constraints and np.size(conmat, 1) == num_vars + 1
  166. assert not (np.isnan(conmat) | np.isneginf(conmat)).any()
  167. assert np.size(cval) == num_vars + 1 and not any(cval < 0 | np.isnan(cval) | np.isposinf(cval))
  168. assert np.size(fval) == num_vars + 1 and not any(np.isnan(fval) | np.isposinf(fval))
  169. assert np.size(sim, 0) == num_vars and np.size(sim, 1) == num_vars + 1
  170. assert np.isfinite(sim).all()
  171. assert all(primasum(abs(sim[:, :num_vars]), axis=0) > 0)
  172. assert np.size(simi, 0) == num_vars and np.size(simi, 1) == num_vars
  173. assert np.isfinite(simi).all()
  174. assert isinv(sim[:, :num_vars], simi, itol)
  175. #====================#
  176. # Calculation starts #
  177. #====================#
  178. # INFO must be set, as it is an output.
  179. info = INFO_DEFAULT
  180. # Identify the optimal vertex of the current simplex.
  181. jopt = findpole(cpen, cval, fval)
  182. # Switch the best vertex to the pole position SIM[:, NUM_VARS] if it is not there already and update
  183. # SIMI. Before the update, save a copy of SIM and SIMI. If the update is unsuccessful due to
  184. # damaging rounding errors, we restore them and return with INFO = DAMAGING_ROUNDING.
  185. sim_old = sim.copy()
  186. simi_old = simi.copy()
  187. if 0 <= jopt < num_vars:
  188. # Unless there is a bug in FINDPOLE it is guaranteed that JOPT >= 0
  189. # When JOPT == NUM_VARS, there is nothing to switch; in addition SIMI[JOPT, :] will be illegal.
  190. # fval[[jopt, -1]] = fval[[-1, jopt]]
  191. # conmat[:, [jopt, -1]] = conmat[:, [-1, jopt]] # Exchange CONMAT[:, JOPT] AND CONMAT[:, -1]
  192. # cval[[jopt, -1]] = cval[[-1, jopt]]
  193. sim[:, num_vars] += sim[:, jopt]
  194. sim_jopt = sim[:, jopt].copy()
  195. sim[:, jopt] = 0 # np.zeros(num_constraints)?
  196. sim[:, :num_vars] -= np.tile(sim_jopt, (num_vars, 1)).T
  197. # The above update is equivalent to multiplying SIM[:, :NUM_VARS] from the right side by a matrix whose
  198. # JOPT-th row is [-1, -1, ..., -1], while all the other rows are the same as those of the
  199. # identity matrix. It is easy to check that the inverse of this matrix is itself. Therefore,
  200. # SIMI should be updated by a multiplication with this matrix (i.e. its inverse) from the left
  201. # side, as is done in the following line. The JOPT-th row of the updated SIMI is minus the sum
  202. # of all rows of the original SIMI, whereas all the other rows remain unchanged.
  203. # NDB 20250114: In testing the cutest problem 'SYNTHES2' between the Python implementation and
  204. # the Fortran bindings, I saw a difference between the following for loop and the
  205. # np.sum command. The differences were small, on the order of 1e-16, i.e. epsilon.
  206. # According to numpy documentation, np.sum sometimes uses partial pairwise summation,
  207. # depending on the memory layout of the array and the axis specified.
  208. # for i in range(simi.shape[1]):
  209. # simi[jopt, i] = -sum(simi[:, i])
  210. simi[jopt, :] = -primasum(simi, axis=0)
  211. # Check whether SIMI is a poor approximation to the inverse of SIM[:, :NUM_VARS]
  212. # Calculate SIMI from scratch if the current one is damaged by rounding errors.
  213. erri = np.max(abs(matprod(simi, sim[:, :num_vars]) - np.eye(num_vars))) # np.max returns NaN if any input is NaN
  214. itol = 1
  215. if erri > 0.1 * itol or np.isnan(erri):
  216. simi_test = inv(sim[:, :num_vars])
  217. erri_test = np.max(abs(matprod(simi_test, sim[:, :num_vars]) - np.eye(num_vars)))
  218. if erri_test < erri or (np.isnan(erri) and not np.isnan(erri_test)):
  219. simi = simi_test
  220. erri = erri_test
  221. # If SIMI is satisfactory, then update FVAL, CONMAT, and CVAL. Otherwise restore SIM and SIMI, and
  222. # return with INFO = DAMAGING_ROUNDING.
  223. if erri <= itol:
  224. if 0 <= jopt < num_vars:
  225. fval[[jopt, num_vars]] = fval[[num_vars, jopt]]
  226. conmat[:, [jopt, num_vars]] = conmat[:, [num_vars, jopt]]
  227. cval[[jopt, num_vars]] = cval[[num_vars, jopt]]
  228. else: # erri > itol or erri is NaN
  229. info = DAMAGING_ROUNDING
  230. sim = sim_old
  231. simi = simi_old
  232. #==================#
  233. # Calculation ends #
  234. #==================#
  235. # Postconditions
  236. if DEBUGGING:
  237. assert findpole(cpen, cval, fval) == num_vars or info == DAMAGING_ROUNDING
  238. assert np.size(conmat, 0) == num_constraints and np.size(conmat, 1) == num_vars + 1
  239. assert not (np.isnan(conmat) | np.isneginf(conmat)).any()
  240. assert np.size(cval) == num_vars + 1 and not any(cval < 0 | np.isnan(cval) | np.isposinf(cval))
  241. assert np.size(fval) == num_vars + 1 and not any(np.isnan(fval) | np.isposinf(fval))
  242. assert np.size(sim, 0) == num_vars and np.size(sim, 1) == num_vars + 1
  243. assert np.isfinite(sim).all()
  244. assert all(primasum(abs(sim[:, :num_vars]), axis=0) > 0)
  245. assert np.size(simi, 0) == num_vars and np.size(simi, 1) == num_vars
  246. assert np.isfinite(simi).all()
  247. # Do not check SIMI = SIM[:, :num_vars]^{-1}, as it may not be true due to damaging rounding.
  248. assert isinv(sim[:, :num_vars], simi, itol) or info == DAMAGING_ROUNDING
  249. return conmat, cval, fval, sim, simi, info