Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/scipy/optimize/_trustregion.py: 16%

154 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-12 06:31 +0000

1"""Trust-region optimization.""" 

2import math 

3import warnings 

4 

5import numpy as np 

6import scipy.linalg 

7from ._optimize import (_check_unknown_options, _status_message, 

8 OptimizeResult, _prepare_scalar_function) 

9from scipy.optimize._hessian_update_strategy import HessianUpdateStrategy 

10from scipy.optimize._differentiable_functions import FD_METHODS 

11__all__ = [] 

12 

13 

14def _wrap_function(function, args): 

15 # wraps a minimizer function to count number of evaluations 

16 # and to easily provide an args kwd. 

17 ncalls = [0] 

18 if function is None: 

19 return ncalls, None 

20 

21 def function_wrapper(x, *wrapper_args): 

22 ncalls[0] += 1 

23 # A copy of x is sent to the user function (gh13740) 

24 return function(np.copy(x), *(wrapper_args + args)) 

25 

26 return ncalls, function_wrapper 

27 

28 

29class BaseQuadraticSubproblem: 

30 """ 

31 Base/abstract class defining the quadratic model for trust-region 

32 minimization. Child classes must implement the ``solve`` method. 

33 

34 Values of the objective function, Jacobian and Hessian (if provided) at 

35 the current iterate ``x`` are evaluated on demand and then stored as 

36 attributes ``fun``, ``jac``, ``hess``. 

37 """ 

38 

39 def __init__(self, x, fun, jac, hess=None, hessp=None): 

40 self._x = x 

41 self._f = None 

42 self._g = None 

43 self._h = None 

44 self._g_mag = None 

45 self._cauchy_point = None 

46 self._newton_point = None 

47 self._fun = fun 

48 self._jac = jac 

49 self._hess = hess 

50 self._hessp = hessp 

51 

52 def __call__(self, p): 

53 return self.fun + np.dot(self.jac, p) + 0.5 * np.dot(p, self.hessp(p)) 

54 

55 @property 

56 def fun(self): 

57 """Value of objective function at current iteration.""" 

58 if self._f is None: 

59 self._f = self._fun(self._x) 

60 return self._f 

61 

62 @property 

63 def jac(self): 

64 """Value of Jacobian of objective function at current iteration.""" 

65 if self._g is None: 

66 self._g = self._jac(self._x) 

67 return self._g 

68 

69 @property 

70 def hess(self): 

71 """Value of Hessian of objective function at current iteration.""" 

72 if self._h is None: 

73 self._h = self._hess(self._x) 

74 return self._h 

75 

76 def hessp(self, p): 

77 if self._hessp is not None: 

78 return self._hessp(self._x, p) 

79 else: 

80 return np.dot(self.hess, p) 

81 

82 @property 

83 def jac_mag(self): 

84 """Magnitude of jacobian of objective function at current iteration.""" 

85 if self._g_mag is None: 

86 self._g_mag = scipy.linalg.norm(self.jac) 

87 return self._g_mag 

88 

89 def get_boundaries_intersections(self, z, d, trust_radius): 

90 """ 

91 Solve the scalar quadratic equation ||z + t d|| == trust_radius. 

92 This is like a line-sphere intersection. 

93 Return the two values of t, sorted from low to high. 

94 """ 

95 a = np.dot(d, d) 

96 b = 2 * np.dot(z, d) 

97 c = np.dot(z, z) - trust_radius**2 

98 sqrt_discriminant = math.sqrt(b*b - 4*a*c) 

99 

100 # The following calculation is mathematically 

101 # equivalent to: 

102 # ta = (-b - sqrt_discriminant) / (2*a) 

103 # tb = (-b + sqrt_discriminant) / (2*a) 

104 # but produce smaller round off errors. 

105 # Look at Matrix Computation p.97 

106 # for a better justification. 

107 aux = b + math.copysign(sqrt_discriminant, b) 

108 ta = -aux / (2*a) 

109 tb = -2*c / aux 

110 return sorted([ta, tb]) 

111 

112 def solve(self, trust_radius): 

113 raise NotImplementedError('The solve method should be implemented by ' 

114 'the child class') 

115 

116 

117def _minimize_trust_region(fun, x0, args=(), jac=None, hess=None, hessp=None, 

118 subproblem=None, initial_trust_radius=1.0, 

119 max_trust_radius=1000.0, eta=0.15, gtol=1e-4, 

120 maxiter=None, disp=False, return_all=False, 

121 callback=None, inexact=True, **unknown_options): 

122 """ 

123 Minimization of scalar function of one or more variables using a 

124 trust-region algorithm. 

125 

126 Options for the trust-region algorithm are: 

127 initial_trust_radius : float 

128 Initial trust radius. 

129 max_trust_radius : float 

130 Never propose steps that are longer than this value. 

131 eta : float 

132 Trust region related acceptance stringency for proposed steps. 

133 gtol : float 

134 Gradient norm must be less than `gtol` 

135 before successful termination. 

136 maxiter : int 

137 Maximum number of iterations to perform. 

138 disp : bool 

139 If True, print convergence message. 

140 inexact : bool 

141 Accuracy to solve subproblems. If True requires less nonlinear 

142 iterations, but more vector products. Only effective for method 

143 trust-krylov. 

144 

145 This function is called by the `minimize` function. 

146 It is not supposed to be called directly. 

147 """ 

148 _check_unknown_options(unknown_options) 

149 

150 if jac is None: 

151 raise ValueError('Jacobian is currently required for trust-region ' 

152 'methods') 

153 if hess is None and hessp is None: 

154 raise ValueError('Either the Hessian or the Hessian-vector product ' 

155 'is currently required for trust-region methods') 

156 if subproblem is None: 

157 raise ValueError('A subproblem solving strategy is required for ' 

158 'trust-region methods') 

159 if not (0 <= eta < 0.25): 

160 raise Exception('invalid acceptance stringency') 

161 if max_trust_radius <= 0: 

162 raise Exception('the max trust radius must be positive') 

163 if initial_trust_radius <= 0: 

164 raise ValueError('the initial trust radius must be positive') 

165 if initial_trust_radius >= max_trust_radius: 

166 raise ValueError('the initial trust radius must be less than the ' 

167 'max trust radius') 

168 

169 # force the initial guess into a nice format 

170 x0 = np.asarray(x0).flatten() 

171 

172 # A ScalarFunction representing the problem. This caches calls to fun, jac, 

173 # hess. 

174 sf = _prepare_scalar_function(fun, x0, jac=jac, hess=hess, args=args) 

175 fun = sf.fun 

176 jac = sf.grad 

177 if callable(hess): 

178 hess = sf.hess 

179 elif callable(hessp): 

180 # this elif statement must come before examining whether hess 

181 # is estimated by FD methods or a HessianUpdateStrategy 

182 pass 

183 elif (hess in FD_METHODS or isinstance(hess, HessianUpdateStrategy)): 

184 # If the Hessian is being estimated by finite differences or a 

185 # Hessian update strategy then ScalarFunction.hess returns a 

186 # LinearOperator or a HessianUpdateStrategy. This enables the 

187 # calculation/creation of a hessp. BUT you only want to do this 

188 # if the user *hasn't* provided a callable(hessp) function. 

189 hess = None 

190 

191 def hessp(x, p, *args): 

192 return sf.hess(x).dot(p) 

193 else: 

194 raise ValueError('Either the Hessian or the Hessian-vector product ' 

195 'is currently required for trust-region methods') 

196 

197 # ScalarFunction doesn't represent hessp 

198 nhessp, hessp = _wrap_function(hessp, args) 

199 

200 # limit the number of iterations 

201 if maxiter is None: 

202 maxiter = len(x0)*200 

203 

204 # init the search status 

205 warnflag = 0 

206 

207 # initialize the search 

208 trust_radius = initial_trust_radius 

209 x = x0 

210 if return_all: 

211 allvecs = [x] 

212 m = subproblem(x, fun, jac, hess, hessp) 

213 k = 0 

214 

215 # search for the function min 

216 # do not even start if the gradient is small enough 

217 while m.jac_mag >= gtol: 

218 

219 # Solve the sub-problem. 

220 # This gives us the proposed step relative to the current position 

221 # and it tells us whether the proposed step 

222 # has reached the trust region boundary or not. 

223 try: 

224 p, hits_boundary = m.solve(trust_radius) 

225 except np.linalg.LinAlgError: 

226 warnflag = 3 

227 break 

228 

229 # calculate the predicted value at the proposed point 

230 predicted_value = m(p) 

231 

232 # define the local approximation at the proposed point 

233 x_proposed = x + p 

234 m_proposed = subproblem(x_proposed, fun, jac, hess, hessp) 

235 

236 # evaluate the ratio defined in equation (4.4) 

237 actual_reduction = m.fun - m_proposed.fun 

238 predicted_reduction = m.fun - predicted_value 

239 if predicted_reduction <= 0: 

240 warnflag = 2 

241 break 

242 rho = actual_reduction / predicted_reduction 

243 

244 # update the trust radius according to the actual/predicted ratio 

245 if rho < 0.25: 

246 trust_radius *= 0.25 

247 elif rho > 0.75 and hits_boundary: 

248 trust_radius = min(2*trust_radius, max_trust_radius) 

249 

250 # if the ratio is high enough then accept the proposed step 

251 if rho > eta: 

252 x = x_proposed 

253 m = m_proposed 

254 

255 # append the best guess, call back, increment the iteration count 

256 if return_all: 

257 allvecs.append(np.copy(x)) 

258 if callback is not None: 

259 callback(np.copy(x)) 

260 k += 1 

261 

262 # check if the gradient is small enough to stop 

263 if m.jac_mag < gtol: 

264 warnflag = 0 

265 break 

266 

267 # check if we have looked at enough iterations 

268 if k >= maxiter: 

269 warnflag = 1 

270 break 

271 

272 # print some stuff if requested 

273 status_messages = ( 

274 _status_message['success'], 

275 _status_message['maxiter'], 

276 'A bad approximation caused failure to predict improvement.', 

277 'A linalg error occurred, such as a non-psd Hessian.', 

278 ) 

279 if disp: 

280 if warnflag == 0: 

281 print(status_messages[warnflag]) 

282 else: 

283 warnings.warn(status_messages[warnflag], RuntimeWarning, 3) 

284 print(" Current function value: %f" % m.fun) 

285 print(" Iterations: %d" % k) 

286 print(" Function evaluations: %d" % sf.nfev) 

287 print(" Gradient evaluations: %d" % sf.ngev) 

288 print(" Hessian evaluations: %d" % (sf.nhev + nhessp[0])) 

289 

290 result = OptimizeResult(x=x, success=(warnflag == 0), status=warnflag, 

291 fun=m.fun, jac=m.jac, nfev=sf.nfev, njev=sf.ngev, 

292 nhev=sf.nhev + nhessp[0], nit=k, 

293 message=status_messages[warnflag]) 

294 

295 if hess is not None: 

296 result['hess'] = m.hess 

297 

298 if return_all: 

299 result['allvecs'] = allvecs 

300 

301 return result