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

364 statements  

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

1import numpy as np 

2import scipy.sparse as sps 

3from ._numdiff import approx_derivative, group_columns 

4from ._hessian_update_strategy import HessianUpdateStrategy 

5from scipy.sparse.linalg import LinearOperator 

6 

7 

8FD_METHODS = ('2-point', '3-point', 'cs') 

9 

10 

11class ScalarFunction: 

12 """Scalar function and its derivatives. 

13 

14 This class defines a scalar function F: R^n->R and methods for 

15 computing or approximating its first and second derivatives. 

16 

17 Parameters 

18 ---------- 

19 fun : callable 

20 evaluates the scalar function. Must be of the form ``fun(x, *args)``, 

21 where ``x`` is the argument in the form of a 1-D array and ``args`` is 

22 a tuple of any additional fixed parameters needed to completely specify 

23 the function. Should return a scalar. 

24 x0 : array-like 

25 Provides an initial set of variables for evaluating fun. Array of real 

26 elements of size (n,), where 'n' is the number of independent 

27 variables. 

28 args : tuple, optional 

29 Any additional fixed parameters needed to completely specify the scalar 

30 function. 

31 grad : {callable, '2-point', '3-point', 'cs'} 

32 Method for computing the gradient vector. 

33 If it is a callable, it should be a function that returns the gradient 

34 vector: 

35 

36 ``grad(x, *args) -> array_like, shape (n,)`` 

37 

38 where ``x`` is an array with shape (n,) and ``args`` is a tuple with 

39 the fixed parameters. 

40 Alternatively, the keywords {'2-point', '3-point', 'cs'} can be used 

41 to select a finite difference scheme for numerical estimation of the 

42 gradient with a relative step size. These finite difference schemes 

43 obey any specified `bounds`. 

44 hess : {callable, '2-point', '3-point', 'cs', HessianUpdateStrategy} 

45 Method for computing the Hessian matrix. If it is callable, it should 

46 return the Hessian matrix: 

47 

48 ``hess(x, *args) -> {LinearOperator, spmatrix, array}, (n, n)`` 

49 

50 where x is a (n,) ndarray and `args` is a tuple with the fixed 

51 parameters. Alternatively, the keywords {'2-point', '3-point', 'cs'} 

52 select a finite difference scheme for numerical estimation. Or, objects 

53 implementing `HessianUpdateStrategy` interface can be used to 

54 approximate the Hessian. 

55 Whenever the gradient is estimated via finite-differences, the Hessian 

56 cannot be estimated with options {'2-point', '3-point', 'cs'} and needs 

57 to be estimated using one of the quasi-Newton strategies. 

58 finite_diff_rel_step : None or array_like 

59 Relative step size to use. The absolute step size is computed as 

60 ``h = finite_diff_rel_step * sign(x0) * max(1, abs(x0))``, possibly 

61 adjusted to fit into the bounds. For ``method='3-point'`` the sign 

62 of `h` is ignored. If None then finite_diff_rel_step is selected 

63 automatically, 

64 finite_diff_bounds : tuple of array_like 

65 Lower and upper bounds on independent variables. Defaults to no bounds, 

66 (-np.inf, np.inf). Each bound must match the size of `x0` or be a 

67 scalar, in the latter case the bound will be the same for all 

68 variables. Use it to limit the range of function evaluation. 

69 epsilon : None or array_like, optional 

70 Absolute step size to use, possibly adjusted to fit into the bounds. 

71 For ``method='3-point'`` the sign of `epsilon` is ignored. By default 

72 relative steps are used, only if ``epsilon is not None`` are absolute 

73 steps used. 

74 

75 Notes 

76 ----- 

77 This class implements a memoization logic. There are methods `fun`, 

78 `grad`, hess` and corresponding attributes `f`, `g` and `H`. The following 

79 things should be considered: 

80 

81 1. Use only public methods `fun`, `grad` and `hess`. 

82 2. After one of the methods is called, the corresponding attribute 

83 will be set. However, a subsequent call with a different argument 

84 of *any* of the methods may overwrite the attribute. 

85 """ 

86 def __init__(self, fun, x0, args, grad, hess, finite_diff_rel_step, 

87 finite_diff_bounds, epsilon=None): 

88 if not callable(grad) and grad not in FD_METHODS: 

89 raise ValueError( 

90 f"`grad` must be either callable or one of {FD_METHODS}." 

91 ) 

92 

93 if not (callable(hess) or hess in FD_METHODS 

94 or isinstance(hess, HessianUpdateStrategy)): 

95 raise ValueError( 

96 f"`hess` must be either callable, HessianUpdateStrategy" 

97 f" or one of {FD_METHODS}." 

98 ) 

99 

100 if grad in FD_METHODS and hess in FD_METHODS: 

101 raise ValueError("Whenever the gradient is estimated via " 

102 "finite-differences, we require the Hessian " 

103 "to be estimated using one of the " 

104 "quasi-Newton strategies.") 

105 

106 # the astype call ensures that self.x is a copy of x0 

107 self.x = np.atleast_1d(x0).astype(float) 

108 self.n = self.x.size 

109 self.nfev = 0 

110 self.ngev = 0 

111 self.nhev = 0 

112 self.f_updated = False 

113 self.g_updated = False 

114 self.H_updated = False 

115 

116 self._lowest_x = None 

117 self._lowest_f = np.inf 

118 

119 finite_diff_options = {} 

120 if grad in FD_METHODS: 

121 finite_diff_options["method"] = grad 

122 finite_diff_options["rel_step"] = finite_diff_rel_step 

123 finite_diff_options["abs_step"] = epsilon 

124 finite_diff_options["bounds"] = finite_diff_bounds 

125 if hess in FD_METHODS: 

126 finite_diff_options["method"] = hess 

127 finite_diff_options["rel_step"] = finite_diff_rel_step 

128 finite_diff_options["abs_step"] = epsilon 

129 finite_diff_options["as_linear_operator"] = True 

130 

131 # Function evaluation 

132 def fun_wrapped(x): 

133 self.nfev += 1 

134 # Send a copy because the user may overwrite it. 

135 # Overwriting results in undefined behaviour because 

136 # fun(self.x) will change self.x, with the two no longer linked. 

137 fx = fun(np.copy(x), *args) 

138 # Make sure the function returns a true scalar 

139 if not np.isscalar(fx): 

140 try: 

141 fx = np.asarray(fx).item() 

142 except (TypeError, ValueError) as e: 

143 raise ValueError( 

144 "The user-provided objective function " 

145 "must return a scalar value." 

146 ) from e 

147 

148 if fx < self._lowest_f: 

149 self._lowest_x = x 

150 self._lowest_f = fx 

151 

152 return fx 

153 

154 def update_fun(): 

155 self.f = fun_wrapped(self.x) 

156 

157 self._update_fun_impl = update_fun 

158 self._update_fun() 

159 

160 # Gradient evaluation 

161 if callable(grad): 

162 def grad_wrapped(x): 

163 self.ngev += 1 

164 return np.atleast_1d(grad(np.copy(x), *args)) 

165 

166 def update_grad(): 

167 self.g = grad_wrapped(self.x) 

168 

169 elif grad in FD_METHODS: 

170 def update_grad(): 

171 self._update_fun() 

172 self.ngev += 1 

173 self.g = approx_derivative(fun_wrapped, self.x, f0=self.f, 

174 **finite_diff_options) 

175 

176 self._update_grad_impl = update_grad 

177 self._update_grad() 

178 

179 # Hessian Evaluation 

180 if callable(hess): 

181 self.H = hess(np.copy(x0), *args) 

182 self.H_updated = True 

183 self.nhev += 1 

184 

185 if sps.issparse(self.H): 

186 def hess_wrapped(x): 

187 self.nhev += 1 

188 return sps.csr_matrix(hess(np.copy(x), *args)) 

189 self.H = sps.csr_matrix(self.H) 

190 

191 elif isinstance(self.H, LinearOperator): 

192 def hess_wrapped(x): 

193 self.nhev += 1 

194 return hess(np.copy(x), *args) 

195 

196 else: 

197 def hess_wrapped(x): 

198 self.nhev += 1 

199 return np.atleast_2d(np.asarray(hess(np.copy(x), *args))) 

200 self.H = np.atleast_2d(np.asarray(self.H)) 

201 

202 def update_hess(): 

203 self.H = hess_wrapped(self.x) 

204 

205 elif hess in FD_METHODS: 

206 def update_hess(): 

207 self._update_grad() 

208 self.H = approx_derivative(grad_wrapped, self.x, f0=self.g, 

209 **finite_diff_options) 

210 return self.H 

211 

212 update_hess() 

213 self.H_updated = True 

214 elif isinstance(hess, HessianUpdateStrategy): 

215 self.H = hess 

216 self.H.initialize(self.n, 'hess') 

217 self.H_updated = True 

218 self.x_prev = None 

219 self.g_prev = None 

220 

221 def update_hess(): 

222 self._update_grad() 

223 self.H.update(self.x - self.x_prev, self.g - self.g_prev) 

224 

225 self._update_hess_impl = update_hess 

226 

227 if isinstance(hess, HessianUpdateStrategy): 

228 def update_x(x): 

229 self._update_grad() 

230 self.x_prev = self.x 

231 self.g_prev = self.g 

232 # ensure that self.x is a copy of x. Don't store a reference 

233 # otherwise the memoization doesn't work properly. 

234 self.x = np.atleast_1d(x).astype(float) 

235 self.f_updated = False 

236 self.g_updated = False 

237 self.H_updated = False 

238 self._update_hess() 

239 else: 

240 def update_x(x): 

241 # ensure that self.x is a copy of x. Don't store a reference 

242 # otherwise the memoization doesn't work properly. 

243 self.x = np.atleast_1d(x).astype(float) 

244 self.f_updated = False 

245 self.g_updated = False 

246 self.H_updated = False 

247 self._update_x_impl = update_x 

248 

249 def _update_fun(self): 

250 if not self.f_updated: 

251 self._update_fun_impl() 

252 self.f_updated = True 

253 

254 def _update_grad(self): 

255 if not self.g_updated: 

256 self._update_grad_impl() 

257 self.g_updated = True 

258 

259 def _update_hess(self): 

260 if not self.H_updated: 

261 self._update_hess_impl() 

262 self.H_updated = True 

263 

264 def fun(self, x): 

265 if not np.array_equal(x, self.x): 

266 self._update_x_impl(x) 

267 self._update_fun() 

268 return self.f 

269 

270 def grad(self, x): 

271 if not np.array_equal(x, self.x): 

272 self._update_x_impl(x) 

273 self._update_grad() 

274 return self.g 

275 

276 def hess(self, x): 

277 if not np.array_equal(x, self.x): 

278 self._update_x_impl(x) 

279 self._update_hess() 

280 return self.H 

281 

282 def fun_and_grad(self, x): 

283 if not np.array_equal(x, self.x): 

284 self._update_x_impl(x) 

285 self._update_fun() 

286 self._update_grad() 

287 return self.f, self.g 

288 

289 

290class VectorFunction: 

291 """Vector function and its derivatives. 

292 

293 This class defines a vector function F: R^n->R^m and methods for 

294 computing or approximating its first and second derivatives. 

295 

296 Notes 

297 ----- 

298 This class implements a memoization logic. There are methods `fun`, 

299 `jac`, hess` and corresponding attributes `f`, `J` and `H`. The following 

300 things should be considered: 

301 

302 1. Use only public methods `fun`, `jac` and `hess`. 

303 2. After one of the methods is called, the corresponding attribute 

304 will be set. However, a subsequent call with a different argument 

305 of *any* of the methods may overwrite the attribute. 

306 """ 

307 def __init__(self, fun, x0, jac, hess, 

308 finite_diff_rel_step, finite_diff_jac_sparsity, 

309 finite_diff_bounds, sparse_jacobian): 

310 if not callable(jac) and jac not in FD_METHODS: 

311 raise ValueError("`jac` must be either callable or one of {}." 

312 .format(FD_METHODS)) 

313 

314 if not (callable(hess) or hess in FD_METHODS 

315 or isinstance(hess, HessianUpdateStrategy)): 

316 raise ValueError("`hess` must be either callable," 

317 "HessianUpdateStrategy or one of {}." 

318 .format(FD_METHODS)) 

319 

320 if jac in FD_METHODS and hess in FD_METHODS: 

321 raise ValueError("Whenever the Jacobian is estimated via " 

322 "finite-differences, we require the Hessian to " 

323 "be estimated using one of the quasi-Newton " 

324 "strategies.") 

325 

326 self.x = np.atleast_1d(x0).astype(float) 

327 self.n = self.x.size 

328 self.nfev = 0 

329 self.njev = 0 

330 self.nhev = 0 

331 self.f_updated = False 

332 self.J_updated = False 

333 self.H_updated = False 

334 

335 finite_diff_options = {} 

336 if jac in FD_METHODS: 

337 finite_diff_options["method"] = jac 

338 finite_diff_options["rel_step"] = finite_diff_rel_step 

339 if finite_diff_jac_sparsity is not None: 

340 sparsity_groups = group_columns(finite_diff_jac_sparsity) 

341 finite_diff_options["sparsity"] = (finite_diff_jac_sparsity, 

342 sparsity_groups) 

343 finite_diff_options["bounds"] = finite_diff_bounds 

344 self.x_diff = np.copy(self.x) 

345 if hess in FD_METHODS: 

346 finite_diff_options["method"] = hess 

347 finite_diff_options["rel_step"] = finite_diff_rel_step 

348 finite_diff_options["as_linear_operator"] = True 

349 self.x_diff = np.copy(self.x) 

350 if jac in FD_METHODS and hess in FD_METHODS: 

351 raise ValueError("Whenever the Jacobian is estimated via " 

352 "finite-differences, we require the Hessian to " 

353 "be estimated using one of the quasi-Newton " 

354 "strategies.") 

355 

356 # Function evaluation 

357 def fun_wrapped(x): 

358 self.nfev += 1 

359 return np.atleast_1d(fun(x)) 

360 

361 def update_fun(): 

362 self.f = fun_wrapped(self.x) 

363 

364 self._update_fun_impl = update_fun 

365 update_fun() 

366 

367 self.v = np.zeros_like(self.f) 

368 self.m = self.v.size 

369 

370 # Jacobian Evaluation 

371 if callable(jac): 

372 self.J = jac(self.x) 

373 self.J_updated = True 

374 self.njev += 1 

375 

376 if (sparse_jacobian or 

377 sparse_jacobian is None and sps.issparse(self.J)): 

378 def jac_wrapped(x): 

379 self.njev += 1 

380 return sps.csr_matrix(jac(x)) 

381 self.J = sps.csr_matrix(self.J) 

382 self.sparse_jacobian = True 

383 

384 elif sps.issparse(self.J): 

385 def jac_wrapped(x): 

386 self.njev += 1 

387 return jac(x).toarray() 

388 self.J = self.J.toarray() 

389 self.sparse_jacobian = False 

390 

391 else: 

392 def jac_wrapped(x): 

393 self.njev += 1 

394 return np.atleast_2d(jac(x)) 

395 self.J = np.atleast_2d(self.J) 

396 self.sparse_jacobian = False 

397 

398 def update_jac(): 

399 self.J = jac_wrapped(self.x) 

400 

401 elif jac in FD_METHODS: 

402 self.J = approx_derivative(fun_wrapped, self.x, f0=self.f, 

403 **finite_diff_options) 

404 self.J_updated = True 

405 

406 if (sparse_jacobian or 

407 sparse_jacobian is None and sps.issparse(self.J)): 

408 def update_jac(): 

409 self._update_fun() 

410 self.J = sps.csr_matrix( 

411 approx_derivative(fun_wrapped, self.x, f0=self.f, 

412 **finite_diff_options)) 

413 self.J = sps.csr_matrix(self.J) 

414 self.sparse_jacobian = True 

415 

416 elif sps.issparse(self.J): 

417 def update_jac(): 

418 self._update_fun() 

419 self.J = approx_derivative(fun_wrapped, self.x, f0=self.f, 

420 **finite_diff_options).toarray() 

421 self.J = self.J.toarray() 

422 self.sparse_jacobian = False 

423 

424 else: 

425 def update_jac(): 

426 self._update_fun() 

427 self.J = np.atleast_2d( 

428 approx_derivative(fun_wrapped, self.x, f0=self.f, 

429 **finite_diff_options)) 

430 self.J = np.atleast_2d(self.J) 

431 self.sparse_jacobian = False 

432 

433 self._update_jac_impl = update_jac 

434 

435 # Define Hessian 

436 if callable(hess): 

437 self.H = hess(self.x, self.v) 

438 self.H_updated = True 

439 self.nhev += 1 

440 

441 if sps.issparse(self.H): 

442 def hess_wrapped(x, v): 

443 self.nhev += 1 

444 return sps.csr_matrix(hess(x, v)) 

445 self.H = sps.csr_matrix(self.H) 

446 

447 elif isinstance(self.H, LinearOperator): 

448 def hess_wrapped(x, v): 

449 self.nhev += 1 

450 return hess(x, v) 

451 

452 else: 

453 def hess_wrapped(x, v): 

454 self.nhev += 1 

455 return np.atleast_2d(np.asarray(hess(x, v))) 

456 self.H = np.atleast_2d(np.asarray(self.H)) 

457 

458 def update_hess(): 

459 self.H = hess_wrapped(self.x, self.v) 

460 elif hess in FD_METHODS: 

461 def jac_dot_v(x, v): 

462 return jac_wrapped(x).T.dot(v) 

463 

464 def update_hess(): 

465 self._update_jac() 

466 self.H = approx_derivative(jac_dot_v, self.x, 

467 f0=self.J.T.dot(self.v), 

468 args=(self.v,), 

469 **finite_diff_options) 

470 update_hess() 

471 self.H_updated = True 

472 elif isinstance(hess, HessianUpdateStrategy): 

473 self.H = hess 

474 self.H.initialize(self.n, 'hess') 

475 self.H_updated = True 

476 self.x_prev = None 

477 self.J_prev = None 

478 

479 def update_hess(): 

480 self._update_jac() 

481 # When v is updated before x was updated, then x_prev and 

482 # J_prev are None and we need this check. 

483 if self.x_prev is not None and self.J_prev is not None: 

484 delta_x = self.x - self.x_prev 

485 delta_g = self.J.T.dot(self.v) - self.J_prev.T.dot(self.v) 

486 self.H.update(delta_x, delta_g) 

487 

488 self._update_hess_impl = update_hess 

489 

490 if isinstance(hess, HessianUpdateStrategy): 

491 def update_x(x): 

492 self._update_jac() 

493 self.x_prev = self.x 

494 self.J_prev = self.J 

495 self.x = np.atleast_1d(x).astype(float) 

496 self.f_updated = False 

497 self.J_updated = False 

498 self.H_updated = False 

499 self._update_hess() 

500 else: 

501 def update_x(x): 

502 self.x = np.atleast_1d(x).astype(float) 

503 self.f_updated = False 

504 self.J_updated = False 

505 self.H_updated = False 

506 

507 self._update_x_impl = update_x 

508 

509 def _update_v(self, v): 

510 if not np.array_equal(v, self.v): 

511 self.v = v 

512 self.H_updated = False 

513 

514 def _update_x(self, x): 

515 if not np.array_equal(x, self.x): 

516 self._update_x_impl(x) 

517 

518 def _update_fun(self): 

519 if not self.f_updated: 

520 self._update_fun_impl() 

521 self.f_updated = True 

522 

523 def _update_jac(self): 

524 if not self.J_updated: 

525 self._update_jac_impl() 

526 self.J_updated = True 

527 

528 def _update_hess(self): 

529 if not self.H_updated: 

530 self._update_hess_impl() 

531 self.H_updated = True 

532 

533 def fun(self, x): 

534 self._update_x(x) 

535 self._update_fun() 

536 return self.f 

537 

538 def jac(self, x): 

539 self._update_x(x) 

540 self._update_jac() 

541 return self.J 

542 

543 def hess(self, x, v): 

544 # v should be updated before x. 

545 self._update_v(v) 

546 self._update_x(x) 

547 self._update_hess() 

548 return self.H 

549 

550 

551class LinearVectorFunction: 

552 """Linear vector function and its derivatives. 

553 

554 Defines a linear function F = A x, where x is N-D vector and 

555 A is m-by-n matrix. The Jacobian is constant and equals to A. The Hessian 

556 is identically zero and it is returned as a csr matrix. 

557 """ 

558 def __init__(self, A, x0, sparse_jacobian): 

559 if sparse_jacobian or sparse_jacobian is None and sps.issparse(A): 

560 self.J = sps.csr_matrix(A) 

561 self.sparse_jacobian = True 

562 elif sps.issparse(A): 

563 self.J = A.toarray() 

564 self.sparse_jacobian = False 

565 else: 

566 # np.asarray makes sure A is ndarray and not matrix 

567 self.J = np.atleast_2d(np.asarray(A)) 

568 self.sparse_jacobian = False 

569 

570 self.m, self.n = self.J.shape 

571 

572 self.x = np.atleast_1d(x0).astype(float) 

573 self.f = self.J.dot(self.x) 

574 self.f_updated = True 

575 

576 self.v = np.zeros(self.m, dtype=float) 

577 self.H = sps.csr_matrix((self.n, self.n)) 

578 

579 def _update_x(self, x): 

580 if not np.array_equal(x, self.x): 

581 self.x = np.atleast_1d(x).astype(float) 

582 self.f_updated = False 

583 

584 def fun(self, x): 

585 self._update_x(x) 

586 if not self.f_updated: 

587 self.f = self.J.dot(x) 

588 self.f_updated = True 

589 return self.f 

590 

591 def jac(self, x): 

592 self._update_x(x) 

593 return self.J 

594 

595 def hess(self, x, v): 

596 self._update_x(x) 

597 self.v = v 

598 return self.H 

599 

600 

601class IdentityVectorFunction(LinearVectorFunction): 

602 """Identity vector function and its derivatives. 

603 

604 The Jacobian is the identity matrix, returned as a dense array when 

605 `sparse_jacobian=False` and as a csr matrix otherwise. The Hessian is 

606 identically zero and it is returned as a csr matrix. 

607 """ 

608 def __init__(self, x0, sparse_jacobian): 

609 n = len(x0) 

610 if sparse_jacobian or sparse_jacobian is None: 

611 A = sps.eye(n, format='csr') 

612 sparse_jacobian = True 

613 else: 

614 A = np.eye(n) 

615 sparse_jacobian = False 

616 super().__init__(A, x0, sparse_jacobian)