_spectral.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. """
  2. Spectral Algorithm for Nonlinear Equations
  3. """
  4. import collections
  5. import numpy as np
  6. from scipy.optimize import OptimizeResult
  7. from scipy.optimize._optimize import _check_unknown_options
  8. from ._linesearch import _nonmonotone_line_search_cruz, _nonmonotone_line_search_cheng
  9. class _NoConvergence(Exception):
  10. pass
  11. def _root_df_sane(func, x0, args=(), ftol=1e-8, fatol=1e-300, maxfev=1000,
  12. fnorm=None, callback=None, disp=False, M=10, eta_strategy=None,
  13. sigma_eps=1e-10, sigma_0=1.0, line_search='cruz', **unknown_options):
  14. r"""
  15. Solve nonlinear equation with the DF-SANE method
  16. Options
  17. -------
  18. ftol : float, optional
  19. Relative norm tolerance.
  20. fatol : float, optional
  21. Absolute norm tolerance.
  22. Algorithm terminates when ``||func(x)|| < fatol + ftol ||func(x_0)||``.
  23. fnorm : callable, optional
  24. Norm to use in the convergence check. If None, 2-norm is used.
  25. maxfev : int, optional
  26. Maximum number of function evaluations.
  27. disp : bool, optional
  28. Whether to print convergence process to stdout.
  29. eta_strategy : callable, optional
  30. Choice of the ``eta_k`` parameter, which gives slack for growth
  31. of ``||F||**2``. Called as ``eta_k = eta_strategy(k, x, F)`` with
  32. `k` the iteration number, `x` the current iterate and `F` the current
  33. residual. Should satisfy ``eta_k > 0`` and ``sum(eta, k=0..inf) < inf``.
  34. Default: ``||F||**2 / (1 + k)**2``.
  35. sigma_eps : float, optional
  36. The spectral coefficient is constrained to ``sigma_eps < sigma < 1/sigma_eps``.
  37. Default: 1e-10
  38. sigma_0 : float, optional
  39. Initial spectral coefficient.
  40. Default: 1.0
  41. M : int, optional
  42. Number of iterates to include in the nonmonotonic line search.
  43. Default: 10
  44. line_search : {'cruz', 'cheng'}
  45. Type of line search to employ. 'cruz' is the original one defined in
  46. [Martinez & Raydan. Math. Comp. 75, 1429 (2006)], 'cheng' is
  47. a modified search defined in [Cheng & Li. IMA J. Numer. Anal. 29, 814 (2009)].
  48. Default: 'cruz'
  49. References
  50. ----------
  51. .. [1] "Spectral residual method without gradient information for solving
  52. large-scale nonlinear systems of equations." W. La Cruz,
  53. J.M. Martinez, M. Raydan. Math. Comp. **75**, 1429 (2006).
  54. .. [2] W. La Cruz, Opt. Meth. Software, 29, 24 (2014).
  55. .. [3] W. Cheng, D.-H. Li. IMA J. Numer. Anal. **29**, 814 (2009).
  56. """
  57. _check_unknown_options(unknown_options)
  58. if line_search not in ('cheng', 'cruz'):
  59. raise ValueError(f"Invalid value {line_search!r} for 'line_search'")
  60. nexp = 2
  61. if eta_strategy is None:
  62. # Different choice from [1], as their eta is not invariant
  63. # vs. scaling of F.
  64. def eta_strategy(k, x, F):
  65. # Obtain squared 2-norm of the initial residual from the outer scope
  66. return f_0 / (1 + k)**2
  67. if fnorm is None:
  68. def fnorm(F):
  69. # Obtain squared 2-norm of the current residual from the outer scope
  70. return f_k**(1.0/nexp)
  71. def fmerit(F):
  72. return np.linalg.norm(F)**nexp
  73. nfev = [0]
  74. f, x_k, x_shape, f_k, F_k, is_complex = _wrap_func(func, x0, fmerit,
  75. nfev, maxfev, args)
  76. k = 0
  77. f_0 = f_k
  78. sigma_k = sigma_0
  79. F_0_norm = fnorm(F_k)
  80. # For the 'cruz' line search
  81. prev_fs = collections.deque([f_k], M)
  82. # For the 'cheng' line search
  83. Q = 1.0
  84. C = f_0
  85. converged = False
  86. message = "too many function evaluations required"
  87. while True:
  88. F_k_norm = fnorm(F_k)
  89. if disp:
  90. print(f"iter {k}: ||F|| = {F_k_norm:g}, sigma = {sigma_k:g}")
  91. if callback is not None:
  92. callback(x_k, F_k)
  93. if F_k_norm < ftol * F_0_norm + fatol:
  94. # Converged!
  95. message = "successful convergence"
  96. converged = True
  97. break
  98. # Control spectral parameter, from [2]
  99. if abs(sigma_k) > 1/sigma_eps:
  100. sigma_k = 1/sigma_eps * np.sign(sigma_k)
  101. elif abs(sigma_k) < sigma_eps:
  102. sigma_k = sigma_eps
  103. # Line search direction
  104. d = -sigma_k * F_k
  105. # Nonmonotone line search
  106. eta = eta_strategy(k, x_k, F_k)
  107. try:
  108. if line_search == 'cruz':
  109. alpha, xp, fp, Fp = _nonmonotone_line_search_cruz(f, x_k, d, prev_fs,
  110. eta=eta)
  111. elif line_search == 'cheng':
  112. alpha, xp, fp, Fp, C, Q = _nonmonotone_line_search_cheng(f, x_k, d, f_k,
  113. C, Q, eta=eta)
  114. except _NoConvergence:
  115. break
  116. # Update spectral parameter
  117. s_k = xp - x_k
  118. y_k = Fp - F_k
  119. sigma_k = np.vdot(s_k, s_k) / np.vdot(s_k, y_k)
  120. # Take step
  121. x_k = xp
  122. F_k = Fp
  123. f_k = fp
  124. # Store function value
  125. if line_search == 'cruz':
  126. prev_fs.append(fp)
  127. k += 1
  128. x = _wrap_result(x_k, is_complex, shape=x_shape)
  129. F = _wrap_result(F_k, is_complex)
  130. result = OptimizeResult(x=x, success=converged,
  131. message=message,
  132. fun=F, nfev=nfev[0], nit=k, method="df-sane")
  133. return result
  134. def _wrap_func(func, x0, fmerit, nfev_list, maxfev, args=()):
  135. """
  136. Wrap a function and an initial value so that (i) complex values
  137. are wrapped to reals, and (ii) value for a merit function
  138. fmerit(x, f) is computed at the same time, (iii) iteration count
  139. is maintained and an exception is raised if it is exceeded.
  140. Parameters
  141. ----------
  142. func : callable
  143. Function to wrap
  144. x0 : ndarray
  145. Initial value
  146. fmerit : callable
  147. Merit function fmerit(f) for computing merit value from residual.
  148. nfev_list : list
  149. List to store number of evaluations in. Should be [0] in the beginning.
  150. maxfev : int
  151. Maximum number of evaluations before _NoConvergence is raised.
  152. args : tuple
  153. Extra arguments to func
  154. Returns
  155. -------
  156. wrap_func : callable
  157. Wrapped function, to be called as
  158. ``F, fp = wrap_func(x0)``
  159. x0_wrap : ndarray of float
  160. Wrapped initial value; raveled to 1-D and complex
  161. values mapped to reals.
  162. x0_shape : tuple
  163. Shape of the initial value array
  164. f : float
  165. Merit function at F
  166. F : ndarray of float
  167. Residual at x0_wrap
  168. is_complex : bool
  169. Whether complex values were mapped to reals
  170. """
  171. x0 = np.asarray(x0)
  172. x0_shape = x0.shape
  173. F = np.asarray(func(x0, *args)).ravel()
  174. is_complex = np.iscomplexobj(x0) or np.iscomplexobj(F)
  175. x0 = x0.ravel()
  176. nfev_list[0] = 1
  177. if is_complex:
  178. def wrap_func(x):
  179. if nfev_list[0] >= maxfev:
  180. raise _NoConvergence()
  181. nfev_list[0] += 1
  182. z = _real2complex(x).reshape(x0_shape)
  183. v = np.asarray(func(z, *args)).ravel()
  184. F = _complex2real(v)
  185. f = fmerit(F)
  186. return f, F
  187. x0 = _complex2real(x0)
  188. F = _complex2real(F)
  189. else:
  190. def wrap_func(x):
  191. if nfev_list[0] >= maxfev:
  192. raise _NoConvergence()
  193. nfev_list[0] += 1
  194. x = x.reshape(x0_shape)
  195. F = np.asarray(func(x, *args)).ravel()
  196. f = fmerit(F)
  197. return f, F
  198. return wrap_func, x0, x0_shape, fmerit(F), F, is_complex
  199. def _wrap_result(result, is_complex, shape=None):
  200. """
  201. Convert from real to complex and reshape result arrays.
  202. """
  203. if is_complex:
  204. z = _real2complex(result)
  205. else:
  206. z = result
  207. if shape is not None:
  208. z = z.reshape(shape)
  209. return z
  210. def _real2complex(x):
  211. return np.ascontiguousarray(x, dtype=float).view(np.complex128)
  212. def _complex2real(z):
  213. return np.ascontiguousarray(z, dtype=complex).view(np.float64)